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.

FieldDescription
default_signing_algorithmToken signing algorithm
access_token_lifetimeAccess token TTL (seconds)
refresh_token_lifetimeRefresh token TTL (seconds)
id_token_lifetimeID token TTL (seconds)
temporary_token_lifetimeTemporary token TTL (seconds)
user_registration_enabledAllow self-registration
forgot_password_enabledEnable the forgot-password flow
remember_me_enabledEnable “remember me”
magic_link_enabled / magic_link_ttlMagic-link login and its TTL
passkey_enabledEnable passkeys
compass_enabledEnable the Compass module
email_verification_enabledRequire email verification
email_verification_ttl_hoursEmail verification link TTL (hours)

Roles

FieldDefaultDescription
namerequiredRole name
descriptionnoneFree-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

FieldDefaultDescription
client_idrequiredClient identifier
namenoneDisplay name
client_typepublicpublic, confidential, or system
protocolopenid-connectProtocol used by the client
enabledtrueWhether the client can be used
public_clientfalsePublic client, no secret
service_account_enabledfalseCreate a linked service account user
direct_access_grants_enabledfalseAllow the password grant
device_authorization_grant_enabledfalseAllow 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_pkceinheritRequire PKCE on the authorization code flow
access_token_lifetimeinheritOverride the realm access token TTL, in seconds
refresh_token_lifetimeinheritOverride the realm refresh token TTL
id_token_lifetimeinheritOverride the realm ID token TTL
temporary_token_lifetimeinheritOverride 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

FieldDefaultDescription
usernamerequiredUsername
idnoneCreate the user with this UUID instead of a generated one
emailnoneEmail address
firstnamenoneFirst name
lastnamenoneLast name
email_verifiednoneMark the email as already verified
roles[]Roles to assign
credentialnoneA 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
FieldDescription
algorithmbcrypt, argon2id, argon2i, or argon2d
secret_dataThe hash as the source stores it: a $2a$, $2b$, or $2y$ bcrypt string, or an Argon2 PHC string
hash_iterationsThe 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.