config.yaml — the server configuration catalog
Included from Basic tier
$OPSCHAIN_HOME/server/config.yaml is the server's deploy-authoritative
configuration file: the keys an operator (not the API) owns. It is written
by opschain server configure, read once by opschain server start, and
strict-parsed — an unknown or typo'd key refuses to start the server with
an error naming the accepted keys, rather than being silently dropped.
Overrides above the file are narrower than a blanket rule: exactly four
keys have flag twins (--listen, --store, --tls-cert, --tls-key,
each winning over its file key), and the environment wins over the file
only where a named variable exists — OPSCHAIN_ENABLE_BASIC_AUTH over
enable_basic_auth, and the
keys.env secrets (which the same-named process environment beats).
Everything else in this file has no flag or environment twin. See
opschain docs settings-reference for the full per-key precedence ladders.
This page is the key dictionary. It is the documentation half of the same
catalog that backs GET /api/admin/config (every key with its set/default
state and effective value, secrets redacted), the GUI's Configuration
screen, opschain server configure --template, and the config validator
(opschain admin config validate / the API's validate endpoint, which
reports exactly what server start would refuse, plus the resolved
security-posture notice when the candidate is valid).
Authority: config.yaml vs stored settings
OpsChain configuration has two authorities:
- Deploy-authoritative — this file. Keys here are owned by whoever
operates the host. They are never editable through the API; several
(
enable_basic_auth,security.posture) deliberately override or anchor what the database holds. - DB-authoritative — stored settings, edited via the API/GUI (see
opschain docs settings). Where a key's default below reads (stored setting), the file key is an override layered over the stored value: stored < file < environment variable.
Secrets never belong in config.yaml where an environment alternative
exists: the key-material set (OPSCHAIN_TOKEN_SECRET_KEY,
OPSCHAIN_PRIMARY_KEY, OPSCHAIN_DETERMINISTIC_KEY,
OPSCHAIN_KEY_DERIVATION_SALT) lives in $OPSCHAIN_HOME/server/keys.env
(mode 0600), and database credentials belong there as PG* variables
rather than inline in the store DSN. Keys marked secret below are
redacted in the effective catalog view and in support bundles.
Top-level keys
| Key | Type | Default | Meaning |
|---|---|---|---|
listen | string | 127.0.0.1:8443 | API listen address (host:port; port 0 picks a free one). |
peer_url | string | — | Base URL peer server replicas reach THIS replica at (scheme://host:port — the pod/host address, never the load balancer; OPSCHAIN_PEER_URL wins). The replica-to-replica lane: step-log chunks a peer receives for an attempt this replica owns are forwarded here (one spool owner per step attempt, so a non-sticky load balancer never splits a step's log), cross-replica live log tails fetch in-flight step output from the owning replica, and cross-replica agent routing forwards a request for an agent connected here. Required on every replica of a multi-replica estate: unset, peers cannot forward this replica's attempts (uploads are refused retryably and warn:logs:ingest_forward_failed is recorded), peers' live tails of steps owned here converge at step seal, and peers answer 503 for agents connected here. Single-replica estates leave it unset. |
server_name | string | <hostname> | This node's STABLE server name — the key of the per-node quiesce posture (opschain admin server node quiesce <name>), surviving restarts and binary rolls where the per-boot replica id cannot. OPSCHAIN_SERVER_NAME wins over the file. Set it explicitly wherever hostnames are not stable (Kubernetes Deployments). |
site | string | — | This node's site identity (e.g. dc1) — the same vocabulary as the reserved location runner-group attribute. Makes claiming site-preferring (a step demanding one location is claimed by that site's replicas first, with an any-site fallback so work never strands — see opschain docs dispatch), keys the site quiesce posture (opschain admin server site quiesce <site>) this node honours (held-at-boot included; the site-scoped offer gate refuses, rather than serves, when the posture cannot be read), and appears on opschain admin status. OPSCHAIN_SITE wins over the file. Single-site estates leave it unset; multi-site estates set it on every replica. |
store | string | <opschain home>/opschain.db | Data store: a postgres:// DSN (parameters absent from the DSN fall back to the standard PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE/PGSSLMODE environment variables, so credentials belong in keys.env, not here), or a SQLite database file path. |
blob_root | string | <opschain home>/blobs | Blobstore directory (the blobstore.kind: fs backend's root; unused with kind: s3). |
tls_cert | string | — | TLS certificate file — set with tls_key to serve HTTPS; unset means plain HTTP. |
tls_key | string | — | TLS private key file. |
enable_basic_auth | bool | true | Whether HTTP basic authentication is accepted (deploy-authoritative over the stored setting; OPSCHAIN_ENABLE_BASIC_AUTH wins). |
blobstore: — the blob backend
Which backend holds step-log segments, artifacts, pack bundles and
retention archives — and what the /readyz blobstore check probes. Omit
the block (or set kind: fs) for the local filesystem store under
blob_root: the single-server default. Running more than one server
replica requires kind: s3: a shared S3-compatible store every replica
reaches. On cloud estates point every replica (every site) at ONE shared
bucket and let the provider's durability and geo-redundancy do the
replication; on-prem estates run a per-site S3-compatible store (MinIO is
the reference) with the store's own site replication, and monitor its
replication watermark. Credentials belong in keys.env (or the process
environment) as OPSCHAIN_BLOBSTORE_ACCESS_KEY_ID /
OPSCHAIN_BLOBSTORE_SECRET_ACCESS_KEY, which win over the file keys.
| Key | Type | Default | Meaning |
|---|---|---|---|
blobstore.kind | string | fs | Blob backend: fs (local filesystem under blob_root) or s3 (shared S3-compatible store — required for more than one server replica). |
blobstore.endpoint | string | — | S3 endpoint URL (https://s3.<region>.amazonaws.com, or the MinIO/compatible server's URL). Required with kind: s3. |
blobstore.bucket | string | — | Bucket holding the blobstore objects. Required with kind: s3. |
blobstore.region | string | us-east-1 | SigV4 signing region. |
blobstore.access_key_id | string | — | Access key id (OPSCHAIN_BLOBSTORE_ACCESS_KEY_ID wins over the file). |
blobstore.secret_access_key | string secret | — | Secret access key (OPSCHAIN_BLOBSTORE_SECRET_ACCESS_KEY wins over the file; prefer that over writing it here). |
blobstore.prefix | string | — | Key prefix namespacing this OpsChain instance inside a shared bucket. |
blobstore.path_style | bool | true | Path-style object addressing (<endpoint>/<bucket>/<key> — MinIO and most self-hosted stores). false addresses the bucket as a virtual host (AWS S3). |
shutdown: — graceful-shutdown tuning
What a rolling restart choreographs against: shutdown first flips
/readyz to 503 (the drain signal), lets in-flight requests finish for
the drain grace, unparks runner long-polls (runners re-poll and land on a
peer), detaches run drives (runs stay durable and resume on the next
boot), and abandons whatever still runs when the overall timeout expires.
Size the platform's termination grace (for example the pod's
terminationGracePeriodSeconds) above timeout_seconds plus any preStop
sleep.
| Key | Type | Default | Meaning |
|---|---|---|---|
shutdown.drain_grace_seconds | int | 5 | How long shutdown lets in-flight requests finish before parked runner long-polls are unparked — the load balancer's drain-observation window. |
shutdown.timeout_seconds | int | 60 | Overall graceful-shutdown budget: work still running after this many seconds is abandoned (run drives stay durable and resume on the next boot). |
ldap: — RETIRED
The ldap: block and its OPSCHAIN_LDAP_* environment overlay were
removed completely: a directory is a
kind: ldap identity provider connection managed through the
IdentityProviderService admin verbs — per-instance configuration, an
encrypted bind credential, a test verb, per-connection health and a sweep
verb (opschain docs identity). An existing block is still parsed for one
purpose: on the first boot after the upgrade it is consumed, with the
stored ldap.* settings and the environment variables, into the seeded
connection — after which the boot notice tells you to delete it.
vault: — platform crypto only (the provider keys are retired)
The vault: block's provider keys are removed completely: type,
address, auth_method, token, username, password, secret_id,
mount_path, password_length, cipher_stacking and client_options
have no runtime consumer. The secrets backend is a SecretsProvider
connection managed through the API/GUI (SecretsProviderService,
opschain admin secrets providers): per-instance configuration with an
encrypted write-only credential, a test verb, per-connection health,
audited writes, and the single-enabled default
(opschain docs cli/admin-secrets-providers).
On the first boot after the upgrade, an existing block (and any
OPSCHAIN_VAULT_* in the server's process environment, each file key
overriding its env twin) is consumed ONCE to seed that connection
(code vault, credential encrypted at rest, kind recorded from the old
vault.type — inference included, so it retires with the key). After the
seed, a config.yaml still carrying provider keys refuses to start with
an error naming them — delete them. Lingering OPSCHAIN_VAULT_* server
variables only draw a boot notice. With no provider registered the local
AGE dev store resolves references best-effort (the old no-block default).
What stays in the block is the platform crypto material — it keys the
server's secret codec for {AES2}/T cipher-stacked values and was never a
connection property:
| Key | Type | Default | Meaning |
|---|---|---|---|
vault.transportable_key | string, secret | — | Transportable encryption key inline (server-side only; overrides transportable_key_path). |
vault.transportable_key_path | string | OPSCHAIN_TRANSPORTABLE_KEY_PATH, then ~/.limepoint/localKey | File holding the transportable key (server-side only). |
Any extra set key still renders in the effective catalog as "set but not
in the documented catalog" (secret-named ones redacted), so nothing stored
is invisible. One long-retired key keeps its own refusal:
vault.encrypt_secrets was renamed to vault.cipher_stacking (v5.0.0, no
alias) and fails strict parsing with the migration hint.
metrics: — the Prometheus endpoint
| Key | Type | Default | Meaning |
|---|---|---|---|
metrics.enabled | bool | true | Mount GET /metrics. |
metrics.require_token | bool | true | Require admin/bearer authorisation on scrapes. |
gui: — the embedded GUI
The binary embeds the GUI (a vendored snapshot of the sibling GUI
repository, provenance reported on GET /api/admin/status and the
overview) and serves it at the root origin: the app at /, the API under
/api on the same port. The API, health (/healthz, /readyz), metrics
and RPC paths always take precedence; any other unknown path serves the app
so client-side routes deep-link. Default on in every posture; disable it only where the GUI is fronted separately — unknown root
paths then 404 as before.
| Key | Type | Default | Meaning |
|---|---|---|---|
gui.enabled | bool | true | Serve the embedded GUI at /. |
api: — v2 API rate limits
The api.rate_limit block tunes the built-in per-principal rate limits on
the v2 surface (/api/v2): a per-principal request
budget (token bucket; anonymous callers are keyed by client address) and a
cap on concurrent server streams (FollowLogs, WatchEvents). An
absent block means enforced with the generous defaults below. Every v2
response carries the standard RateLimit-Limit/RateLimit-Remaining/
RateLimit-Reset headers; a refusal answers the typed resource_exhausted
(ERROR_REASON_RATE_LIMITED) with Retry-After. Budgets are per server
process — under HA each replica grants each principal its own budget. A
deployment whose API gateway owns rate limiting sets enabled: false (or
individual keys to 0) — effectively-off, per the gateway contract.
| Key | Type | Default | Meaning |
|---|---|---|---|
api.rate_limit.enabled | bool | true | Enforce the built-in v2 rate limits. false turns both limits off (and the headers with them). |
api.rate_limit.requests_per_minute | int | 600 | Per-principal steady-state v2 request budget. 0 turns the request budget off. |
api.rate_limit.burst | int | 600 | Request-bucket capacity: how many requests a quiet principal may issue at once before the steady rate governs. |
api.rate_limit.max_concurrent_streams | int | 25 | Concurrent v2 server streams one principal may hold open. 0 turns the cap off. |
api.maintenance_badge_horizon_minutes | int | 60 | How far ahead node reads badge a scheduled maintenance window covering the node (the imminent maintenance badge). 0 badges active windows only. |
logging: — file logging and rotation
| Key | Type | Default | Meaning |
|---|---|---|---|
logging.file | string | — | Log file path; setting it engages file mode with built-in rotation. |
logging.max_size_mb | int | 100 | Rotate when the file exceeds this many MB. |
logging.max_age_days | int | 30 | Delete rotated backups older than this many days. |
logging.max_backups | int | 10 | Keep at most this many rotated backups. |
logging.compress | bool | false | gzip rotated backups. |
retention: — age-out policy
The retention job itself is documented at opschain docs retention; every
*_days class defaults to 0 — never age out — except
chat_transcripts_days, whose default is 365, and
evidence_ledger_days, whose default is 730.
| Key | Type | Default | Meaning |
|---|---|---|---|
retention.enabled | bool | true when the block is present | Run the retention job. |
retention.interval_minutes | int | 60 | Minutes between sweeps on the leader. |
retention.batch_size | int | 500 | Rows per delete batch. |
retention.archive_dir | string | — | Retired: export-before-delete archives are sealed into the blobstore as verified tar objects (one per batch, re-read and verified before anything is deleted), so every replica can reach them. The key is accepted and ignored. |
retention.step_logs_days | int | 0 (never) | Age out step log segments. |
retention.chat_transcripts_days | int | 365 | Age out chat and adapter transcript segments; 0 means never, and the default only applies while retention as a whole is enabled (an absent retention: block still disables retention entirely). Exchanges cited as transcript evidence on a legal-held ledger entry are exempt until unflagged. |
retention.events_days | int | 0 (never) | Age out events. |
retention.step_transitions_days | int | 0 (never) | Age out step transition audit rows. |
retention.scheduler_done_days | int | 0 (never) | Age out scheduler completion bookkeeping (no export). |
retention.artifacts_days | int | 0 (never) | Tombstone artifacts and release their blob bytes. |
retention.recommendations_open_days | int | 0 (never) | Expire agent recommendations still open after this many days. |
retention.evidence_ledger_days | int | 730 | The evidence ledger's hot window: rows older than this are sealed into verified blob-store archive segments and only then deleted — never a bare purge, and the hash chain verifies across the boundary (opschain docs retention). 0 means never, the default only applies while retention as a whole is enabled, and legal-held rows are never archived out from under their hold. |
artifacts.max_artifact_mb | int | 512 | Per-artifact step-artifact collection budget in MB (total matched file bytes); an over-budget collection is refused (opschain docs artifacts). |
artifacts.max_run_mb | int | 1024 | Per-run step-artifact collection budget in MB; an over-budget collection is refused. |
agents: — agent-governance knobs
| Key | Type | Default | Meaning |
|---|---|---|---|
agents.approvers | []string | — | Fallback usernames who approve SUPERVISED agent-created work. |
agents.release_stage | string | — | Minimum AgentRelease promotion stage required at enrolment: a rung of the estate's stage ladder (dev, test or prod on the default ladder; opschain docs agent-releases). Empty requires none — a declared release still verifies. A value the ladder lacks refuses every declared enrolment by name. |
agents.assignment_floor | string | advise | What an assets-under-management assignment confers with no matching grant: advise or deny. |
agents.owner_property_key | string | metadata.asset_owner | Properties key holding an asset's designated owner. |
security: — the identity-security posture
security.posture selects the baseline (evaluation or hardened); the
other keys override individual controls, except the ⚓-anchored ones, which
the hardened posture refuses to relax. Deploy-authoritative throughout —
never editable through the API. See opschain docs identity.
| Key | Type | Default | Meaning |
|---|---|---|---|
security.posture | string | evaluation | Identity-security posture: evaluation or hardened (deploy-authoritative, never editable through the API). |
security.require_bootstrap_password | bool | hardened true, evaluation false | ⚓ anchored in hardened: first boot requires OPSCHAIN_BOOTSTRAP_ADMIN_PASSWORD and rotate-on-first-login. |
security.local_user_mode | string | hardened break_glass_only, evaluation full | full, or break_glass_only (refuse creating non-break-glass local users). |
security.password_min_length | int | hardened 14, evaluation 0 | Minimum local password length. |
security.bcrypt_cost | int | hardened 12, evaluation 10 | bcrypt work factor for local passwords (4-18). |
security.password_history | int | hardened 5, evaluation 0 | Break-glass accounts refuse reusing their last N passwords. |
security.password_max_age_days | int | 0 (off) | Force rotation of local passwords older than N days. |
security.break_glass_containment | bool | hardened true, evaluation false | Break-glass logins get 1h access tokens, no refresh token, no api_key minting. |
security.max_break_glass_accounts | int | hardened 2, evaluation 0 (unlimited) | Refuse creating/designating break-glass accounts beyond this count. |
security.collision_defence | bool | hardened true, evaluation false | Refuse local usernames shadowing directory accounts; warn on provider-collision logins. |
security.directory_reconciliation | bool | hardened true, evaluation false | Periodically disable directory identities whose entry the directory no longer returns (per ldap-kind connection, keyed by entry id), revoking their tokens and sessions. |
security.directory_reconciliation_hours | int | 24 | Reconciliation cadence in hours. |
auth: — network controls
| Key | Type | Default | Meaning |
|---|---|---|---|
auth.break_glass_networks | []string | (any source) | CIDR allowlist: break-glass logins from outside it are refused. |
server: — reverse-proxy trust
The client address OpsChain records about a request — in the request log's
remote_ip, the per-source login throttle, the session row's
source_address/last_client_address and the audit trail's actor object
(opschain docs events) — is the TCP peer unless that peer is a trusted
proxy. Behind a load balancer, an ingress or Cloudflare the peer is the
proxy, so list the proxies' CIDRs here and the server resolves the real
client from the headers only a listed peer is allowed to set: when the peer
is listed, the client is CF-Connecting-IP when present, otherwise the
right-most X-Forwarded-For hop that is not itself listed (the standard
right-to-left walk — every hop left of the first untrusted one is the
client's own claim and ignored). An unlisted peer's forwarding headers are
ignored entirely, so a direct caller can never spoof its recorded address.
An entry that is not a valid CIDR refuses to start the server by name
(server.trusted_proxies: "x" is not a valid CIDR), and the validator
reports the same.
| Key | Type | Default | Meaning |
|---|---|---|---|
server.trusted_proxies | []string | (none — the TCP peer is the client) | CIDRs of the reverse proxies whose X-Forwarded-For / CF-Connecting-IP the server honours when resolving a request's client address. |
How long the recorded origin is kept: events age out under
retention.events_days (default never), sessions live until they are
purged, and the request log under logging.max_age_days — see
opschain docs security-audit, "What the audit trail holds about origin".
eula: — first-start acceptance
| Key | Type | Default | Meaning |
|---|---|---|---|
eula.accepted | bool | false | Accept the LimePoint EULA on a fresh installation's first start (the config twin of server start --confirm-eula; read it with opschain eula). Inert once the acceptance is recorded. |
database: — scheduled database maintenance
The database.pool block caps each server replica's PostgreSQL
connections — three pools share one database, and the caps keep an
N-replica estate inside the server's max_connections budget (the
defaults total 25 per replica, so four replicas fit the PostgreSQL
default of 100).
| Key | Type | Default | Meaning |
|---|---|---|---|
database.pool.store_max_conns | int | 10 | Connection cap for the primary store pool. |
database.pool.api_max_conns | int | 10 | Connection cap for the shared API/auth read pool. |
database.pool.scheduler_max_conns | int | 5 | Connection cap for the scheduler's pool. |
The database.maintenance block tunes the dbmaint subsystem
(opschain docs database-maintenance). Unlike retention:, an absent
block means enabled with the defaults below; enabled: false is the
explicit opt-out for estates where a DBA owns maintenance.
| Key | Type | Default | Meaning |
|---|---|---|---|
database.maintenance.enabled | bool | true | Run the scheduled database maintenance lanes (per-table ANALYZE daily and after retention sweeps; weekly VACUUM (ANALYZE) in the off-peak window; SQLite PRAGMA optimize). false hands maintenance to a DBA — status and on-demand runs stay available. |
database.maintenance.analyze_interval_minutes | int | 1440 | Minutes between scheduled ANALYZE passes on the leader. |
database.maintenance.vacuum_window | string | 02:00-04:00 | Server-local HH:MM-HH:MM window the weekly VACUUM (ANALYZE) may start in. |
database.maintenance.vacuum_day | string | sunday | Weekday the VACUUM (ANALYZE) window applies on. |
database.maintenance.tables | []string | (the built-in churn set) | Override the maintained table set (default: events, runs, steps, workflow steps, log tables, queue and bookkeeping tables). |
Failure modes
- An unknown key refuses to start the server. The strict parser names
the accepted keys at each level in its error, so a typo (
metrics.enbled) fails loudly instead of leaving an ignored password behind an unfindable LDAP error 49. - A missing config.yaml is not an error — the server starts with every key at its default (zero config).
- An invalid candidate never half-applies: the validator strict-parses
and resolves the security posture without applying anything, so what it
reports is exactly what
server startwould refuse with.
Related pages
opschain docs settings— the DB-authoritative half: stored settings documents and their inheritance down the node tree.opschain docs settings-reference— the per-key dictionary across all three authorities, with the full precedence ladders.opschain docs retention,opschain docs database-maintenance,opschain docs identity,opschain docs storage— the subsystems these blocks tune.