Repository navigation
Admin: export and import the configuration as a password-encrypted, schema-validated file (repositories, trusted publishers, realms, service accounts) #699
Description
An administrator has no way to copy Forge's configuration to another instance, or to keep a copy of it, short of dumping the whole Postgres database. Setting up a second instance (staging, a migration, disaster recovery) means re-creating every repository, realm and service account by hand.
Forge should export its configuration to one file and import it again: a mini backup of everything an administrator set up, without the users and without the packages.
What the file holds
- Repositories. Hosted, proxy and group repositories: their format,
config, group members, retention policy, and a proxy's upstream credentials (upstream_username, the password sealed inupstream_password_enc). - Trusted publishers of each repository (
/admin/repositories/{name}/trusted-publishers). - Authentication realms. Their order,
configand secrets (auth_realm.secrets_enc), including the built-inlocalrealm's state. - Service accounts. Description, realm, roles, and their API tokens (see the open questions).
- Roles, grants and content selectors, which service accounts and realms refer to. User role assignments are left out.
- Settings. The sign-in policy (
/admin/settings/auth), the retention settings and the email settings with their secrets (email_settings.secrets_enc).
What it leaves out
- Users. User accounts (any
auth_sourcebutservice), their passwords, tokens, sessions, external identities, invitations and role assignments. - Content. Packages, blobs, images and proxy caches.
- Runtime state. Audit logs, retention runs, Nexus imports, workload API keys, OIDC flows and CLI logins.
- The encryption keys. The export opens each secret with the envelope service and writes it into the file, which the password protects. The import seals each secret again with the target's envelope service, bound to the new row. The
encryption_keyrows and the root key never leave the instance.
The file
- Encrypted with a password. The administrator chooses a password when exporting, and the whole document is encrypted with a key derived from it. For example: Argon2id with a stored salt and parameters, then AES-256-GCM or XChaCha20-Poly1305.
- A small plaintext header (magic, file-format version, KDF parameters, nonce) is authenticated as AAD.
- Without the password nothing else is readable, not even the repository names.
- A wrong password or a tampered file is refused with a clear error, and nothing is imported.
- A password below a minimum length is refused.
- Validated with JSON schemas. The decrypted payload is a JSON document naming its schema version.
- Each section (repositories, trusted publishers, realms, service accounts, roles, settings) has a JSON schema.
- A repository's
configis validated against its format's schema, which each format undercrates/formats/provides. - The schemas are generated from the Rust types (for example with
schemars) so they cannot drift, and are checked into the repository, for example underdocs/schemas/config/v1/. A test fails when the checked-in schemas differ from the generated ones. - Any JSON Schema validator can check a decoded file.
- References by name, never by id. Ids differ between instances, so objects refer to each other by name: repository, realm, role and selector names.
- Versioned. An import reads every file-format and schema version up to its own, and refuses a newer one.
Import
- Validate first. The import decrypts the file and validates it against the schemas before it touches the database. A file that does not validate is refused with every error (JSON pointer and message).
- Dry run. A dry run lists what the import would create, change and leave alone, and any conflicts, without applying anything.
- Conflicts. The administrator chooses what happens when a name already exists: skip it (the default) or replace it. Built-in roles and the
localrealm are always updated in place, never created. - All or nothing. The import runs in one transaction.
- Checks against the target. A repository whose format the target does not have is refused, as are references to objects that are neither in the file nor on the target.
- Logged. Each export and import is logged with who ran it and what it covered. The password is never logged.
Admin API and web UI
- API. For example
POST /admin/config/export(the password and, optionally, the sections to export) answers the file.POST /admin/config/importtakes the file, the password, the conflict choice anddry_run, and answers the report. Both are admin-only. - Web UI. A screen in the admin pages, for example under Admin → System, exports, previews the dry run and imports (AGENTS.md: an admin API comes with its screen).
forge CLI
Once the forge CLI exists (#167), it can decode a file with its password and without a server. For example, forge config decode <file> prompts for the password (or reads it from stdin or an environment variable) and writes the JSON document, and forge config validate checks it against the schemas.
The file format is documented, so other tools can decode it too.
Docs and tests
- Docs. A docs page covers what the file holds and leaves out, the encryption, the schemas, and how to move a configuration to another instance.
- Round trip. An export imported into an empty instance yields the same configuration. Its secrets open on the target, sealed under the target's keys.
- Refusals. A wrong password, a tampered file, a schema violation and an unknown format are each refused, and nothing is written.
- Conflicts. A test covers skip and replace on an instance that has some of the objects already.
Open questions
- Service account tokens. Should the file carry the tokens' hashes (
token.token_hashis a plain SHA-256), so the CI secrets in use keep working on the new instance, or leave them out so they are issued again? A backup wants them, but then the same token works on two instances. One option is to carry them by default and offer to leave them out when exporting. - Email OAuth tokens. Refresh tokens may be bound to the old instance's redirect URL. Should they be carried, or should the target re-authorize?
- Encryption format. Use our own, or an existing one such as age in passphrase mode? With age,
age -dcould decode a file before the forge CLI exists. - Plaintext header. Should the header also show the Forge version, the export time and the sections without the password, so the UI can show them before asking for it?
Activity