Import from a file
--from config imports a realm from a FerrisKey-native description file. The format comes from the extension: .yaml, .yml, or .toml. No external system is involved.
ferris-ctl realm import --from config --file realm.yaml
Preview without writing anything:
ferris-ctl realm import --from config --file realm.yaml --dry-run -o yaml
Format
A description carries a name, plus optional settings, roles, clients, and users sections. The ferriskey-cli repository ships a starting point at examples/realm.yaml.
name: acme
settings:
access_token_lifetime: 300
refresh_token_lifetime: 1800
user_registration_enabled: true
forgot_password_enabled: true
roles:
- name: admin
description: Realm administrators
permissions:
- manage_realm
- manage_users
- name: viewer
description: Read-only access
permissions:
- view_users
- view_clients
clients:
- client_id: web-app
name: Web Application
client_type: public
public_client: true
redirect_uris:
- https://app.acme.test/callback
- https://app.acme.test/*
- client_id: backend
name: Backend Service
client_type: confidential
service_account_enabled: true
direct_access_grants_enabled: true
roles:
- name: service-reader
description: Service-scoped read role
users:
- username: alice
email: alice@acme.test
firstname: Alice
lastname: Doe
email_verified: true
roles:
- admin
- username: bob
email: bob@acme.test
roles:
- viewer
The same keys work in TOML. Sections that hold a list become arrays of tables:
name = "acme"
[settings]
access_token_lifetime = 300
user_registration_enabled = true
[[roles]]
name = "admin"
description = "Realm administrators"
permissions = ["manage_realm"]
[[clients]]
client_id = "web-app"
client_type = "public"
public_client = true
redirect_uris = ["https://app.acme.test/callback"]
[[clients.roles]]
name = "reader"
[[users]]
username = "alice"
email = "alice@acme.test"
roles = ["admin", "web-app:reader"]
A misspelled key is not an error
The importer ignores keys it does not know, without a warning. A typo or a key from another tool leaves the default in place. Run the file through --dry-run -o yaml and read the result: it prints every field the importer understood, with its value.
Settings
Every setting is optional. Anything you leave out keeps the FerrisKey default. Settings are sent again on every run, so they overwrite the realm’s current values each time.
| Field | Description |
|---|---|
default_signing_algorithm | Token signing algorithm |
access_token_lifetime | Access token TTL (seconds) |
refresh_token_lifetime | Refresh token TTL (seconds) |
id_token_lifetime | ID token TTL (seconds) |
temporary_token_lifetime | Temporary token TTL (seconds) |
user_registration_enabled | Allow self-registration |
forgot_password_enabled | Enable the forgot-password flow |
remember_me_enabled | Enable “remember me” |
magic_link_enabled / magic_link_ttl | Magic-link login and its TTL |
passkey_enabled | Enable passkeys |
compass_enabled | Enable the Compass module |
email_verification_enabled | Require email verification |
email_verification_ttl_hours | Email verification link TTL (hours) |
Roles
| Field | Default | Description |
|---|---|---|
name | required | Role name |
description | none | Free-text description |
permissions | [] | Permission names, in snake_case |
Permissions use the names from the permissions reference, so manage_realm, not ManageRealm and not realm:manage.
The identity that runs the import can only grant permissions it holds. Without manage_realm, a role that lists a permission the identity lacks, or a name FerrisKey does not know, is refused with 403 cannot grant a permission you do not hold, and the import stops. With manage_realm, a name FerrisKey does not recognize is accepted and dropped, so the role is created with fewer permissions than the file lists.
Clients
| Field | Default | Description |
|---|---|---|
client_id | required | Client identifier |
name | none | Display name |
client_type | public | public, confidential, or system |
protocol | openid-connect | Protocol used by the client |
enabled | true | Whether the client can be used |
public_client | false | Public client, no secret |
service_account_enabled | false | Create a linked service account user |
direct_access_grants_enabled | false | Allow the password grant |
device_authorization_grant_enabled | false | Allow the device authorization grant, which ferris-ctl login needs |
redirect_uris | [] | Allowed redirect URIs |
post_logout_redirect_uris | [] | Allowed post-logout redirect URIs |
web_origins | [] | Browser origins allowed on this client’s realm-scoped routes |
require_pkce | inherit | Require PKCE on the authorization code flow |
access_token_lifetime | inherit | Override the realm access token TTL, in seconds |
refresh_token_lifetime | inherit | Override the realm refresh token TTL |
id_token_lifetime | inherit | Override the realm ID token TTL |
temporary_token_lifetime | inherit | Override the realm temporary token TTL |
roles | [] | Roles scoped to this client, same shape as realm roles |
A client that already exists is not modified, apart from require_pkce and the lifetimes, which are sent again on every run. Redirect URIs, web origins, and roles missing from it are added. The report lists the secret of each confidential client; see the import report.
Users
| Field | Default | Description |
|---|---|---|
username | required | Username |
id | none | Create the user with this UUID instead of a generated one |
email | none | Email address |
firstname | none | First name |
lastname | none | Last name |
email_verified | none | Mark the email as already verified |
roles | [] | Roles to assign |
credential | none | A password hash to store as the user’s password, see below |
A plain name in roles refers to a realm role. Prefix it with a client id to pick a role scoped to that client:
users:
- username: alice
roles:
- admin # realm role
- backend:service-reader # role of the "backend" client
A client_id:role that no client in the file defines and that the client does not already hold stops the import. A plain name that matches no realm role only produces a warning, and that assignment is skipped.
Passwords
Without a credential, a user arrives with no password. Set one afterwards with ferris-ctl user set-password, from the admin console, or through a recovery flow. To carry an existing password over, give the user the hash it already has:
users:
- username: alice
credential:
algorithm: bcrypt
secret_data: "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
hash_iterations: 10
| Field | Description |
|---|---|
algorithm | bcrypt, argon2id, argon2i, or argon2d |
secret_data | The hash as the source stores it: a $2a$, $2b$, or $2y$ bcrypt string, or an Argon2 PHC string |
hash_iterations | The cost written inside the hash: the bcrypt cost (4 to 14), or the Argon2 time cost t. The server refuses the hash when the two disagree |
The import stores the hash as is and never marks it temporary. FerrisKey re-encodes it as Argon2id the first time the user signs in; Export & Import explains how. A user who already has a password keeps it. A refused hash adds a warning and counts in passwords_failed, and the import carries on.
A file that holds password hashes is as sensitive as the password store it came from. Keep it out of version control. The --dry-run output redacts the hashes, the file does not.
Fixed ids
With id, the user is created with that UUID, which keeps every identifier that points at the user valid. The import stops with an error if the server creates the user under a different id, or if the id already belongs to another user.
Not in release 0.3.0
The credential and id fields on users arrived after release 0.3.0. With 0.3.0 they are ignored like any unknown key. Build from the main branch of ferriskey-cli to use them.