# Provisioning

> **Related:** [Administration: admin console and Admin REST API](https://issuerd.org/administration.md) · [Agentic IAM: securing MCP tool calls with DPoP and token exchange](https://issuerd.org/agentic-iam-mcp.md) · [Backup, restore, and upgrade](https://issuerd.org/backup-and-upgrade.md) · [Human step-up approval for agent actions: CIBA](https://issuerd.org/ciba-step-up.md) · [Client integration](https://issuerd.org/client-integration.md) · [Clustering — Multi-Node Issuerd](https://issuerd.org/clustering.md) · [Configuration](https://issuerd.org/configuration.md) · [Deployment](https://issuerd.org/deployment.md) · [Issuerd operations documentation](https://issuerd.org/docs-overview.md) · [Getting started: installation and first login](https://issuerd.org/getting-started.md) · [Identity brokering](https://issuerd.org/identity-brokering.md) · [LDAP Group Mapping and Token Claims](https://issuerd.org/ldap-group-mapping.md) · [Monitoring: health probes, metrics, logging, and audit events](https://issuerd.org/monitoring.md) · [Issuerd Performance](https://issuerd.org/performance.md) · [Security](https://issuerd.org/security.md) · [Issuerd Trademark Policy](https://issuerd.org/trademark.md) · [Troubleshooting](https://issuerd.org/troubleshooting.md) · [User federation: LDAP, Kerberos, and Active Directory](https://issuerd.org/user-federation.md)
> **Also:** [HTML version](https://issuerd.org/provisioning) · [Docs index](https://issuerd.org/llms.txt)

---
This document describes Issuerd's declarative provisioning: a YAML, TOML, or JSON
file ("provision file") that seeds realms, roles, groups, clients, users, identity
providers, and authentication flow configs into a fresh — or partially seeded —
deployment. It is written for system operators who install and maintain Issuerd.
For the server configuration file that points at a provision file, see
[configuration.md](/configuration/); for a guided first boot, see
[getting-started.md](/getting-started/).

## Contents

- [What provisioning is](#what-provisioning-is)
- [Running the provisioner](#running-the-provisioner)
- [File format](#file-format)
- [Entity reference](#entity-reference)
- [Bootstrap interactions](#bootstrap-interactions)
- [Re-runs and idempotency](#re-runs-and-idempotency)
- [Complete worked example](#complete-worked-example)
- [Common pitfalls](#common-pitfalls)

## What provisioning is

Provisioning applies a declarative seed file to Issuerd's storage **exactly once**.
A durable marker recorded in storage tracks whether a given file has already been
applied, so restarts and concurrent first boots never apply it twice, and
administrator changes made afterwards are never overwritten by the file.

Use provisioning for:

- **First-boot seeding** — create the `master` realm, an admin user, and the console
  clients when bringing up a new environment (mandatory on PostgreSQL; see
  [Bootstrap interactions](#bootstrap-interactions)).
- **GitOps-friendly realm baselines** — keep the initial shape of application realms
  (roles, groups, clients, service users) in version control next to the deployment.

Use the **Admin API** (see [administration.md](/administration/)) for everything
after the first boot: provisioning never updates, deletes, or drifts — it only
creates entities that do not exist yet, and only while its marker is unclaimed.

What provisioning deliberately does **not** cover (use the Admin API instead):
password policies, brute-force settings, events configuration, client roles,
protocol mappers, and any change to an entity that already exists. Per-realm
SMTP overrides are the one exception: they are realm attributes (`smtpServer.*`
keys), so they can be seeded through a realm's free-form `attributes` map.

The implementation lives in `crates/issuerd-server/src/provisioner.rs`; the file schema
is defined by the `Provision*` structs in `crates/issuerd-core/src/provision.rs`.
Reference files shipped in the repository:

- `examples/provision.example.yaml` — fully populated example (generated by
  `issuerd example provision-config`, kept in sync by a unit test).
- `examples/provision.federation.yaml` — multi-realm federation lab (LDAP/Kerberos).
- `examples/provision.demo.yaml` — the local demo stack's seed file.
- `cluster/provision.yaml` — the two-node cluster demo's seed file.

## Running the provisioner

There are two entry points. Both load the file through the same code path
(`Provisioner::from_file` + `apply_once`) and share identical marker semantics.

### At daemon startup: `provision = "..."`

Point the server configuration at a provision file:

```toml
# issuerd.toml
provision = "provision.yaml"   # relative paths resolve against the working directory
```

During startup (in `ServerState::from_config`, `crates/issuerd-server/src/state.rs`),
after storage initialization and after the automatic master-realm bootstrap (for the
in-memory/JSON backends), the daemon loads and applies the file once. On subsequent
restarts the marker is already present and the file is ignored entirely — leaving
the `provision` key in place is harmless.

> **Warning:** Provisioning problems are **not fatal** at daemon startup. A missing
> or unparsable file logs `failed to load provision config` at **ERROR** level; a
> failure during application logs `provision failed` — both carry the error as a
> structured field, not interpolated into the message. Boot then continues. Always
> check the startup log after a first boot, especially on PostgreSQL where a failed
> provision can leave you without a `master` realm.

### As a one-shot command: `issuerd provision`

```bash
issuerd provision -c issuerd.toml --file provision.yaml
```

The command reads the server configuration only to learn which storage to connect
to (`-c` defaults to `issuerd.toml` in the working directory), then:

1. Connects to the configured storage — for PostgreSQL it also runs pending schema
   migrations first (`src/main.rs`).
2. Applies the provision file exactly once (same marker mechanism as the daemon).
3. Exits. Unlike the daemon, errors propagate: the process exits non-zero, so it is
   safe to gate deployments on it in scripts and CI.

Use the CLI when you want to seed the database **before** the daemon ever starts
(e.g. a deployment pipeline step), or to apply an additional baseline to an existing
database under a *new* marker without changing the daemon's configuration (see
[Re-runs and idempotency](#re-runs-and-idempotency)). Use the config key when the
file should travel with the environment and be picked up automatically on first
boot — this is how the Docker demo stack mounts `examples/provision.demo.yaml` at
`/etc/issuerd/provision.yaml` (`docker-compose.yml`).

## File format

The file format is detected by extension: `.yaml`/`.yml` → YAML, `.json` → JSON,
anything else → TOML. YAML is the canonical form used throughout the repository.
Generate a fully populated starting point with:

```bash
issuerd example provision-config -o provision.yaml
```

> **Note:** `issuerd example` defaults its output to
> `examples/issuerd.example.toml` for *both* example kinds — always pass `-o`
> explicitly when generating a provision file.

The top level consists of an optional `marker` key plus one list per entity type.
Every list is optional and defaults to empty:

```yaml
marker: my-deployment-v1        # optional; defaults to "default"

realms:             [ ... ]
roles:              [ ... ]
groups:             [ ... ]
clients:            [ ... ]
users:              [ ... ]
identity_providers: [ ... ]
flow_configs:       [ ... ]
```

### Environment variable substitution

After parsing, `${VAR}` placeholders in string values are expanded from the process
environment — including client `secret`, user `password`, and identity-provider
`config` values such as `bindCredential`. If any referenced variable is unset, the
file fails to load entirely (`environment substitution failed: ${VAR}, ...`).

```yaml
clients:
  - realm: myrealm
    client_id: billing-api
    secret: ${BILLING_API_CLIENT_SECRET}   # keeps secrets out of version control
```

This applies equally to daemon startup provisioning and the CLI, and is the
recommended way to keep credentials out of the checked-in file.

### Application order and per-entity skips

Entities are applied in a fixed order regardless of how the lists are arranged in
the file (`crates/issuerd-server/src/provisioner.rs`):

```
realms → roles → clients → groups → users → identity_providers → flow_configs
```

Groups are applied **after clients** so that group `client_roles` mappings can
resolve the clients they reference.

Every entity is skipped with a log warning if it already exists — realms by name,
roles and groups by name within the realm, clients by `client_id` within the realm,
users by username, identity providers by alias, flow configs by alias. Provisioning
**creates missing entities only; it never updates existing ones.**

### Built-in clients per realm

Every provisioned realm automatically receives two built-in public clients (the same
ones the Admin API creates on realm creation), with redirect URIs derived from the
server's `issuer_url`:

- `admin-cli` — redirects `{issuer_url}/`, `{issuer_url}/admin/console/callback`,
  `{issuer_url}/swagger-ui/oauth2-redirect.html`.
- `account-console` — redirects `{issuer_url}/`,
  `{issuer_url}/realms/{realm}/account`.

Because these are created while the realm itself is provisioned, explicit
`clients:` entries named `admin-cli` or `account-console` are skipped as duplicates.
If you need different redirect URIs on them, adjust them afterwards via the Admin
API. Realm creation also seeds the built-in browser/registration authentication
flows and the built-in client scopes via the storage layer.

## Entity reference

All cross-references between entities are **by realm name** (never the internal
UUID) and resolve against realms that exist when the entity is applied — either
declared earlier in the same file or already present in storage. A reference to a
nonexistent realm aborts the run with `realm '<name>' not found`.

### realms

| Field | Type | Default | Description |
|---|---|---|---|
| `name` | string | required | Realm name. Embedded verbatim in URLs (`/realms/{name}/...`) and the issuer, so it must be ASCII, ≤ 255 chars, with no whitespace, control, or URL-reserved characters (`/ \ ? # % + & =`). |
| `display_name` | string | none | Human-readable name shown on login pages. |
| `enabled` | bool | `true` | Disabled realms reject logins. |
| `ssl_required` | string | `"external"` | Only `"none"` and `"all"` are recognized; **any other value (including a typo) maps to `external`**. |
| `login_theme` | string | none | Login theme name (from the `[themes] dir` directory). |
| `email_theme` | string | none | Email theme name. |
| `admin_theme` | string | none | Admin console theme name. |
| `default_role` | string | none | Name of a realm role automatically assigned to users on self-registration and first broker login. Stored verbatim; not validated at provision time — declare the role in `roles:`. |
| `access_token_lifespan` | seconds | `300` | Must be non-zero; `0` is rejected with an error. |
| `refresh_token_lifespan` | seconds | `1800` | Must be non-zero. |
| `sso_session_idle_timeout` | seconds | `1800` | Must be non-zero. |
| `sso_session_max_lifespan` | seconds | `36000` | Must be non-zero. |
| `offline_session_idle_timeout` | seconds | `2592000` (30 days) | Must be non-zero. |
| `registration_enabled` | bool | `false` | Allow user self-registration. |
| `verify_email_enabled` | bool | `false` | Require email verification. |
| `reset_password_allowed` | bool | `false` | Allow users to reset their password. |
| `remember_me_enabled` | bool | `false` | Show "remember me" on the login page. |
| `login_with_email_allowed` | bool | `true` | Allow the email address as login username. |
| `browser_flow` | string | system default | Alias of the realm's browser login flow binding. Stored verbatim; not validated at provision time. |
| `registration_flow` | string | system default | Alias of the registration flow binding. |
| `attributes` | map\<string, string\> | `{}` | Free-form realm attributes (some features read per-realm toggles from attributes). |

Not settable here: password policy, brute-force detection, events
configuration — all take realm defaults and are managed via the Admin API.
Per-realm SMTP overrides are realm attributes (`smtpServer.*` keys) and can be
seeded through the free-form `attributes` map above.

```yaml
realms:
  - name: myrealm
    display_name: My Realm
    ssl_required: external
    default_role: user
    registration_enabled: true
    reset_password_allowed: true
    access_token_lifespan: 300
    sso_session_max_lifespan: 36000
    attributes:
      custom_attr: value
```

### roles

Creates **realm roles only** — provisioning never creates client roles.

| Field | Type | Default | Description |
|---|---|---|---|
| `realm` | string | required | Owning realm name. |
| `name` | string | required | Role name. |
| `description` | string | none | Free-text description. |

```yaml
roles:
  - realm: myrealm
    name: admin
    description: Administrator role
  - realm: myrealm
    name: user
    description: Standard user role
```

### groups

| Field | Type | Default | Description |
|---|---|---|---|
| `realm` | string | required | Owning realm name. |
| `name` | string | required | Group name. |
| `path` | string | `/{name}` | Group path. |
| `attributes` | map\<string, list\<string\>\> | `{}` | Multi-valued group attributes. |
| `realm_roles` | list\<string\> | `[]` | Realm role names assigned to the group; members inherit them. Unknown roles are skipped with a warning. |
| `client_roles` | map\<string, list\<string\>\> | `{}` | Client role names keyed by `client_id`. Unknown clients and unknown client roles are skipped with a warning. |

Groups are applied after roles, clients, and before users, so `realm_roles` can
reference roles declared in the same file and `client_roles` can reference clients
declared in the same file. **Client roles themselves cannot be provisioned** — they
must already exist (create them via the Admin API first), otherwise the mapping is
silently skipped (warning in the log).

```yaml
groups:
  - realm: myrealm
    name: developers
    path: /developers
    realm_roles:
      - user
    client_roles:
      my-app:            # client_id
        - editor         # must already exist as a client role on my-app
```

### clients

| Field | Type | Default | Description |
|---|---|---|---|
| `realm` | string | required | Owning realm name. |
| `client_id` | string | required | The OAuth2/OIDC `client_id`. |
| `name` | string | none | Display name. |
| `description` | string | none | Free-text description. |
| `enabled` | bool | `true` | Disabled clients cannot authenticate. |
| `public_client` | bool | `false` | `true` = public client (no secret; use PKCE). |
| `bearer_only` | bool | `false` | `true` = the client only validates bearer tokens, never initiates logins. |
| `secret` | string | generated | Explicit client secret. If omitted: confidential clients get a **randomly generated** secret; public clients get none. |
| `redirect_uris` | list\<string\> | `[]` | Valid redirect URIs. Entries that fail validation are dropped with a warning. |
| `web_origins` | list\<string\> | `[]` | Allowed CORS origins for the client. Invalid entries are dropped with a warning. |
| `default_scopes` | list\<string\> | `[openid, profile]` | Scopes granted when the request has no `scope` parameter. Only when the list is non-empty does it override the default. |
| `optional_scopes` | list\<string\> | `[]` | Scopes the client may request optionally. |
| `consent_required` | bool | `false` | Require a user consent screen. |
| `full_scope_allowed` | bool | `true` | When `true`, tokens contain the user's full role set; when `false`, only scope mappings apply. |

The protocol is always `openid-connect`. Not settable here: service accounts,
protocol mappers, client roles, client-scope assignments beyond the two scope
lists — use the Admin API.

```yaml
clients:
  - realm: myrealm
    client_id: my-app
    name: My Application
    public_client: false
    secret: ${MY_APP_CLIENT_SECRET}   # or omit to have one generated
    redirect_uris:
      - https://app.example.com/callback
    web_origins:
      - https://app.example.com
    default_scopes: [openid, profile]
    optional_scopes: [email]
    consent_required: false
    full_scope_allowed: true
```

> **Note:** A confidential client provisioned without `secret` ends up with a random
> secret that appears nowhere in your configuration — retrieve or rotate it via the
> Admin API, or set the secret explicitly (ideally through `${ENV}` substitution).

### users

| Field | Type | Default | Description |
|---|---|---|---|
| `realm` | string | required | Owning realm name. |
| `username` | string | required | Login username. |
| `email` | string | none | Email address. |
| `email_verified` | bool | `false` | Mark the address as verified. |
| `first_name` | string | none | Given name. |
| `last_name` | string | none | Family name. |
| `enabled` | bool | `true` | Disabled users cannot log in. |
| `password` | string | none | **Plain-text** password, hashed with Argon2id at apply time and never stored as-is. Omitted = user has no password credential (e.g. federation-only accounts). |
| `realm_roles` | list\<string\> | `[]` | Realm role names to assign. Unknown roles are skipped with a warning. |
| `groups` | list\<string\> | `[]` | Group names to join. Unknown groups are skipped with a warning. |
| `attributes` | map\<string, list\<string\>\> | `{}` | Multi-valued user attributes. |

```yaml
users:
  - realm: myrealm
    username: alice
    email: alice@example.com
    email_verified: true
    first_name: Alice
    last_name: Anderson
    password: ${ALICE_INITIAL_PASSWORD}
    realm_roles: [admin, user]
    groups: [developers]
    attributes:
      department: [engineering]
```

### identity_providers

Registers an identity broker or user-federation provider on a realm. The `config`
map is passed through to the provider verbatim — **all values are strings**, so
quote numbers and booleans (`pagination: "true"`, `batchSize: "5000"`).

| Field | Type | Default | Description |
|---|---|---|---|
| `realm` | string | required | Owning realm name. |
| `alias` | string | required | Provider alias; appears in broker URLs (`/realms/{realm}/broker/{alias}/...`). |
| `provider_id` | string | required | `ldap`, `kerberos`, `oidc`, `saml`, or `social`; any other value registers a custom provider id. |
| `enabled` | bool | `true` | Disabled providers are hidden and inactive. |
| `config` | map\<string, string\> | `{}` | Provider-specific settings. |

For `ldap` providers the config keys follow Keycloak's LDAP mapper vocabulary —
`connectionUrl`, `bindDn`, `bindCredential`, `usersDn`, `baseDn`,
`usernameLdapAttribute`, `rdnLdapAttribute`, `uuidLdapAttribute`, `userObjectClasses`,
`vendor` (`ACTIVE_DIRECTORY`, `SAMBA`, `GENERIC`, ...), `searchScope`, `editMode`,
`pagination`, `batchSize`, `priority`, plus `groupsDn` (and friends) for LDAP group
synchronization. `kerberos` providers take `kerberosRealm`, `serverPrincipal`,
`keyTab`, `allowKerberosAuthentication`, `allowPasswordAuthentication`,
`updateProfileFirstLogin`. See `examples/provision.federation.yaml` for working
AD/Samba/OpenLDAP/Kerberos examples, [user-federation.md](/user-federation/) for
the full key reference and sync semantics, and
[identity-brokering.md](/identity-brokering/) for external OIDC/social providers.

```yaml
identity_providers:
  - realm: myrealm
    alias: corporate-ldap
    provider_id: ldap
    enabled: true
    config:
      connectionUrl: ldap://ldap.example.com:389
      bindDn: cn=admin,dc=example,dc=com
      bindCredential: ${LDAP_BIND_CREDENTIAL}
      usersDn: ou=users,dc=example,dc=com
      usernameLdapAttribute: uid
      uuidLdapAttribute: entryUUID
      userObjectClasses: inetOrgPerson,organizationalPerson
      vendor: GENERIC
      editMode: READ_ONLY
      priority: "1"
```

### flow_configs

Registers a custom authentication flow. Built-in flows (browser, registration) are
seeded automatically per realm by the storage layer; this section is for additional
custom flows.

| Field | Type | Default | Description |
|---|---|---|---|
| `realm` | string | required | Owning realm name. |
| `alias` | string | required | Flow alias; referenced by the realm's `browser_flow`/`registration_flow` bindings. |
| `provider_id` | string | `"basic-flow"` | Flow provider type. |
| `top_level` | bool | `true` | `true` = top-level flow; `false` = sub-flow referenced by a stage's `sub_flow_alias`. |
| `built_in` | bool | `false` | Marks the flow as built-in (read-only in the Admin API). |
| `stages` | list | `[]` | Execution stages (below). |

Each stage:

| Field | Type | Default | Description |
|---|---|---|---|
| `id` | string | required | Stage identifier (unique within the flow). |
| `requirement` | string | required | `required`, `alternative`, `optional`, `disabled`, or `conditional`. An unknown value aborts the entire provision run with an error. |
| `authenticator` | string | required | Authenticator alias (e.g. `auth-cookie`, `auth-username-password`). |
| `priority` | int | `0` | Ordering weight within the flow. |
| `sub_flow_alias` | string | none | References a sub-flow for form-style nested executions. |

```yaml
flow_configs:
  - realm: myrealm
    alias: custom-browser
    provider_id: basic-flow
    top_level: true
    stages:
      - id: cookie
        requirement: alternative
        authenticator: auth-cookie
        priority: 10
      - id: username-password
        requirement: required
        authenticator: auth-username-password
        priority: 20
```

Flow configs are applied **last**, and the realm's `browser_flow` binding is stored
unvalidated, so a realm in the same file may bind to a custom flow alias declared in
`flow_configs:`. For deeper flow surgery (executions, per-execution authenticator
config) use the Admin API.

## Bootstrap interactions

The automatic master-realm bootstrap (`bootstrap_master_realm` in
`crates/issuerd-server/src/state.rs`) runs at daemon startup **only** when the storage
backend is in-memory or JSON-file **and** no realms exist yet. It creates:

- the `master` realm with user `admin` / password `admin`,
- the realm-management roles (`manage-realm`, `view-realm`, `manage-users`,
  `view-users`, `manage-clients`, `view-clients`, `impersonation`) assigned to
  `admin`,
- the built-in `admin-cli` and `account-console` clients.

It runs **before** the provisioner, so with the in-memory/JSON backends a provision
file that also defines `master` finds the realm (and possibly the `admin` user)
already present and skips those entries, while still applying any remaining roles,
clients, and users from the file.

> **Warning:** On **PostgreSQL there is no automatic bootstrap**. A fresh deployment
> whose provision file does not define the `master` realm has no admin user and no
> way to log into the admin console. Always define `master`, the realm-management
> roles, and an `admin` user in the provision file — as `examples/provision.demo.yaml`
> (demo stack) and `cluster/provision.yaml` (two-node cluster, all URLs pointing at
> the load balancer; see [CLUSTERING.md](/clustering/)) do.

If the daemon is already running against such a database, fix it out-of-band with
`issuerd provision -c issuerd.toml --file master-provision.yaml`.

## Re-runs and idempotency

The `marker:` key (default `"default"`) names a record in storage that tracks
whether the file has been applied. With PostgreSQL the marker lives in the
`provision_markers` table (`crates/issuerd-storage/migrations/004_provision_markers.sql`);
with JSON-file storage it is part of the snapshot; with in-memory storage it lives
and dies with the process.

- **Marker claimed before applying.** The provisioner atomically claims the marker
  *first*, then applies the entities. This makes concurrent first boots safe (two
  cluster nodes starting against an empty database cannot both provision) — but it
  also means **a failed run leaves the marker claimed**. Re-running with the same
  marker after a failure is a no-op; you must fix the file and either bump the
  marker (e.g. `marker: my-deployment-v2`) or wipe the storage and start over.
- **Daemon restart:** with a durable backend the marker exists, so the `provision`
  file is skipped entirely — admin changes are never overwritten. With the
  **in-memory** backend the marker vanishes on restart, so provisioning re-applies
  on every boot; this is harmless because existing entities are skipped per-entity.
- **Provisioning twice with a different marker:** the file is applied again, but
  every entity that already exists is skipped (warning in the log); only missing
  entities are created. Existing entities are **never updated** — a changed password
  or redirect URI in the file has no effect on an entity that already exists. Use
  this to layer an additional baseline onto a live database; use the Admin API for
  modifications.
- **Deleting entities** is likewise never performed; provisioning is additive only.

To force a complete re-provision of a disposable environment, reset the storage:
for the Docker demo stack, `docker compose down -v` drops the PostgreSQL volume
(markers included) and the next `up` seeds everything again.

## Complete worked example

A compact but realistic baseline: one application realm with two roles, one group,
a confidential and a public client, and two users. On PostgreSQL, add the `master`
realm block as well (see [Bootstrap interactions](#bootstrap-interactions) and
`examples/provision.demo.yaml`).

```yaml
# provision.yaml — applied exactly once; bump the marker to layer new content.
marker: acme-baseline-v1

realms:
  - name: acme
    display_name: Acme Corp
    ssl_required: external
    default_role: user
    reset_password_allowed: true
    remember_me_enabled: true
    access_token_lifespan: 300
    sso_session_max_lifespan: 36000

roles:
  - realm: acme
    name: admin
    description: Application administrator
  - realm: acme
    name: user
    description: Standard user

groups:
  - realm: acme
    name: developers
    path: /developers
    realm_roles: [user]

clients:
  # Confidential server-side web application.
  - realm: acme
    client_id: acme-portal
    name: Acme Portal
    secret: ${ACME_PORTAL_CLIENT_SECRET}
    redirect_uris: [https://portal.acme.example.com/callback]
    web_origins: [https://portal.acme.example.com]
    default_scopes: [openid, profile]
    optional_scopes: [email]

  # Public single-page application (PKCE).
  - realm: acme
    client_id: acme-spa
    name: Acme SPA
    public_client: true
    redirect_uris: [https://spa.acme.example.com/callback]
    web_origins: [https://spa.acme.example.com]
    default_scopes: [openid, profile]

users:
  - realm: acme
    username: alice
    email: alice@acme.example.com
    email_verified: true
    first_name: Alice
    last_name: Admin
    password: ${ALICE_INITIAL_PASSWORD}
    realm_roles: [admin, user]
    groups: [developers]
    attributes:
      department: [engineering]

  - realm: acme
    username: bob
    email: bob@acme.example.com
    email_verified: true
    first_name: Bob
    last_name: Builder
    password: ${BOB_INITIAL_PASSWORD}
    realm_roles: [user]
    groups: [developers]
```

Apply it with either:

```bash
# Out-of-band, before (or without) starting the daemon:
export ACME_PORTAL_CLIENT_SECRET=... ALICE_INITIAL_PASSWORD=... BOB_INITIAL_PASSWORD=...
issuerd provision -c issuerd.toml --file provision.yaml

# …or at first boot, via the server config:  provision = "provision.yaml"
issuerd daemon -c issuerd.toml
```

## Common pitfalls

- **Provisioning "didn't do anything" on the second attempt.** The marker is
  claimed *before* entities are applied, so an aborted or failed first run blocks
  all retries with the same marker. Fix the file, bump `marker:` (or wipe the
  storage), and check the startup log for `provision failed` /
  `failed to load provision config` — the daemon logs these at ERROR level and
  keeps booting, which makes silent half-provisioning easy to miss.
- **Typo in a `realm:` reference.** All cross-references resolve by realm *name*
  (never the UUID). A reference to a nonexistent realm aborts the whole run with
  `realm '<name>' not found` — and because of the marker behavior above, leaves a
  partially provisioned database.
- **Group `client_roles` silently missing.** Groups are applied after clients, but
  the provisioner never creates client roles; a `client_roles` mapping to a client
  role that does not exist yet is skipped with only a log warning. Create client
  roles via the Admin API first (or accept that they cannot be seeded this way).
  The same skip-with-warning behavior applies to unknown `realm_roles` on groups
  and users, and to unknown groups on users.
- **Locked out on PostgreSQL.** Without a provisioned `master` realm there is no
  `admin` user and the admin console is unusable. Provision `master` (realm +
  realm-management roles + admin user) as in `examples/provision.demo.yaml`, or repair
  out-of-band with `issuerd provision --file ...`.
- **Expecting the file to manage drift.** Editing the provision file after first
  boot changes nothing — not even with a new marker, because existing entities are
  skipped. Provisioning seeds; the Admin API manages. See
  [administration.md](/administration/).
- **`${VAR}` not set in the daemon's environment.** The whole file fails to load
  (the daemon continues without provisioning). Export the variables into the
  service's environment (e.g. systemd `EnvironmentFile`, container env) — and
  remember the CLI reads *its own* shell environment.
- **Auto-generated secrets.** A confidential client without `secret:` gets a random
  one that exists only in storage. Set secrets explicitly (via `${ENV}`) for
  reproducible deployments.
- **`ssl_required` typos.** Only the exact values `none` and `all` are special;
  anything else — including `External` or `required` — silently means `external`.
- **Zero token lifespans.** A lifespan of `0` is rejected with an error; omit the
  key to get the default instead.
- **Redefining `admin-cli` / `account-console`.** Every provisioned realm gets
  these built-in clients automatically (redirect URIs derived from `issuer_url`);
  file entries with the same `client_id` are skipped as duplicates.

For operational diagnosis of a running server (logs, health endpoints, common
failure modes), see [troubleshooting.md](/troubleshooting/).