diff --git a/api/funkwhale_api/federation/dynamic_preferences_registry.py b/api/funkwhale_api/federation/dynamic_preferences_registry.py
new file mode 100644
index 0000000000000000000000000000000000000000..83d0285be263d228ffbb564a9bfc9f898fe77dbf
--- /dev/null
+++ b/api/funkwhale_api/federation/dynamic_preferences_registry.py
@@ -0,0 +1,34 @@
+from django.forms import widgets
+
+from dynamic_preferences import types
+from dynamic_preferences.registries import global_preferences_registry
+
+federation = types.Section('federation')
+
+
+@global_preferences_registry.register
+class FederationPrivateKey(types.StringPreference):
+    show_in_api = False
+    section = federation
+    name = 'private_key'
+    default = ''
+    help_text = (
+        'Instance private key, used for signing federation HTTP requests'
+    )
+    verbose_name = (
+        'Instance private key (keep it secret, do not change it)'
+    )
+
+
+@global_preferences_registry.register
+class FederationPublicKey(types.StringPreference):
+    show_in_api = False
+    section = federation
+    name = 'public_key'
+    default = ''
+    help_text = (
+        'Instance public key, used for signing federation HTTP requests'
+    )
+    verbose_name = (
+        'Instance public key (do not change it)'
+    )
diff --git a/api/funkwhale_api/federation/management/__init__.py b/api/funkwhale_api/federation/management/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/federation/management/commands/__init__.py b/api/funkwhale_api/federation/management/commands/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/federation/management/commands/generate_keys.py b/api/funkwhale_api/federation/management/commands/generate_keys.py
new file mode 100644
index 0000000000000000000000000000000000000000..eafe9aae3477753a7b61cbc854152e3d95e26e59
--- /dev/null
+++ b/api/funkwhale_api/federation/management/commands/generate_keys.py
@@ -0,0 +1,53 @@
+from django.core.management.base import BaseCommand, CommandError
+from django.db import transaction
+
+from dynamic_preferences.registries import global_preferences_registry
+
+from funkwhale_api.federation import keys
+
+
+class Command(BaseCommand):
+    help = (
+        'Generate a public/private key pair for your instance,'
+        ' for federation purposes. If a key pair already exists, does nothing.'
+    )
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '--replace',
+            action='store_true',
+            dest='replace',
+            default=False,
+            help='Replace existing key pair, if any',
+        )
+        parser.add_argument(
+            '--noinput', '--no-input', action='store_false', dest='interactive',
+            help="Do NOT prompt the user for input of any kind.",
+        )
+
+    @transaction.atomic
+    def handle(self, *args, **options):
+        preferences = global_preferences_registry.manager()
+        existing_public = preferences['federation__public_key']
+        existing_private = preferences['federation__public_key']
+
+        if existing_public or existing_private and not options['replace']:
+            raise CommandError(
+                'Keys are already present! '
+                'Replace them with --replace if you know what you are doing.')
+
+        if options['interactive']:
+            message = (
+                'Are you sure you want to do this?\n\n'
+                "Type 'yes' to continue, or 'no' to cancel: "
+            )
+            if input(''.join(message)) != 'yes':
+                raise CommandError("Operation cancelled.")
+        private, public = keys.get_key_pair()
+        preferences['federation__public_key'] = public.decode('utf-8')
+        preferences['federation__private_key'] = private.decode('utf-8')
+
+        self.stdout.write(
+            'Your new key pair was generated.'
+            'Your public key is now:\n\n{}'.format(public.decode('utf-8'))
+        )
diff --git a/api/tests/federation/__init__.py b/api/tests/federation/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/tests/federation/test_commands.py b/api/tests/federation/test_commands.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c533306821a24a664c260089c0f15201c5a9870
--- /dev/null
+++ b/api/tests/federation/test_commands.py
@@ -0,0 +1,14 @@
+from django.core.management import call_command
+
+
+def test_generate_instance_key_pair(preferences, mocker):
+    mocker.patch(
+        'funkwhale_api.federation.keys.get_key_pair',
+        return_value=(b'private', b'public'))
+    assert preferences['federation__public_key'] == ''
+    assert preferences['federation__private_key'] == ''
+
+    call_command('generate_keys', interactive=False)
+
+    assert preferences['federation__private_key'] == 'private'
+    assert preferences['federation__public_key'] == 'public'