diff --git a/bin/Makefile.am b/bin/Makefile.am
index 18bf5d1e57ab52e3367ef5b2d35fcf307a2d74f7..87cbb5bc8398b1f34f0e09aaa86e5f584ee26f56 100644
--- a/bin/Makefile.am
+++ b/bin/Makefile.am
@@ -1,4 +1,4 @@
-bin_SCRIPTS = access-check-manager.pl
+bin_SCRIPTS = access-check-manager.pl update-metadata
 
 www_SCRIPTS = access-check-manager.cgi
 
@@ -13,6 +13,13 @@ access-check-manager.pl: Makefile access-check-manager.pl.in
 		< $(srcdir)/$@.in > $@
 	chmod +x $@
 
+update-metadata: Makefile update-metadata.in
+	sed \
+		-e 's|[@]modulesdir[@]|$(modulesdir)|' \
+		-e 's|[@]confdir[@]|$(confdir)|' \
+		< $(srcdir)/$@.in > $@
+	chmod +x $@
+
 access-check-manager.cgi: Makefile access-check-manager.cgi.in
 	sed \
 		-e 's|[@]modulesdir[@]|$(modulesdir)|' \
diff --git a/bin/update-metadata.in b/bin/update-metadata.in
new file mode 100755
index 0000000000000000000000000000000000000000..1e3832b55b92be744d03431e053004e74bc98e05
--- /dev/null
+++ b/bin/update-metadata.in
@@ -0,0 +1,120 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use lib qw(@modulesdir@);
+
+use Config::Tiny;
+use English qw(-no_match_vars);
+use File::Temp;
+use Getopt::Long qw(:config auto_help);
+use List::MoreUtils qw(uniq);
+use LWP::UserAgent;
+use Pod::Usage;
+
+use AccountManager::DB;
+use AccountManager::Metadata;
+use AccountManager::ServiceProvider;
+
+my %options;
+GetOptions(
+    \%options,
+    'configuration=s',
+    'verbose',
+) or pod2usage(
+    -message => "unknown option, aborting\n",
+    -verbose => 0
+);
+
+my $configuration_file =
+    $options{configuration} || '@confdir@/manager.conf';
+my $configuration = Config::Tiny->read($configuration_file);
+if (!$configuration) {
+    die Config::Tiny->errstr() . "\n";
+}
+
+AccountManager::DB->register_db(
+    driver   => $configuration->{database}->{type},
+    database => $configuration->{database}->{name},
+    host     => $configuration->{database}->{host},
+    password => $configuration->{database}->{password},
+    username => $configuration->{database}->{username},
+    options  => [ split(/, */, $configuration->{database}->{options}) ]
+);
+
+my $db = AccountManager::DB->new();
+
+my $ua = LWP::UserAgent->new();
+
+$db->begin_work();
+
+AccountManager::ServiceProvider->delete_service_providers(all => 1);
+
+my %seen;
+foreach my $id (split(/, */, $configuration->{groups}->{list})) {
+    my $spec = $configuration->{$id};
+    next unless $spec->{type} eq 'metadata';
+    print "processing federation $id\n" if $options{verbose};
+    my $file = File::Temp->new();
+
+    print "downloading metadata from url $spec->{url}\n" if $options{verbose};
+    my $response = $ua->get($spec->{url}, ':content_file' => $file->filename());
+    if (!$response->is_success()) {
+        $db->rollback();
+        die "failed to download federation metadata: " . $response->status_line();
+    }
+
+    my $metadata;
+    eval {
+        $metadata = AccountManager::Metadata->new(
+            file => $file
+        );
+    };
+    if ($EVAL_ERROR) {
+        $db->rollback();
+        die "failed to load federation metadata: $EVAL_ERROR";
+    }
+
+    print "parsing metadata from file $file\n" if $options{verbose};
+    my $entities;
+    eval {
+       $entities = $metadata->parse(type => 'sp');
+    };
+    if ($EVAL_ERROR) {
+        $db->rollback();
+        die "failed to parse federation metadata: $EVAL_ERROR";
+    }
+
+    foreach my $entry (@$entities) {
+        # avoid duplicates
+        next if $seen{$entry->{entityid}}++;
+
+        my $entity = AccountManager::ServiceProvider->new(
+            db           => $db,
+            entityid     => $entry->{entityid},
+            displayname  => $entry->{display_name},
+            url          => $entry->{url},
+        );
+
+        $entity->contacts(uniq map { $_->{EmailAddress} } @{$entry->{contacts}})
+            if $entry->{contacts};
+
+        $entity->save();
+    }
+}
+
+$db->commit();
+
+__END__
+
+=head1 NAME
+
+update-metadata - Out-of-band metadata processing
+
+=head1 SYNOPSIS
+
+update-metadata [options]
+
+  Options:
+    --configuration <file>
+    --verbose