Skip to content

Security configuration

The flat configuration hub remains the compatibility route. This page is the canonical source-checked field reference for its family, including nested tables and list rows.

Paths in the tables are relative to [security], [security.integrity], [mfa] or [ca_rotation]. Each row gives the serde type and source default or unset meaning; keys and credentials belong in protected files. Check parsing and security cross-field rules with gdsgate doctor --config <PATH> --json, then follow the CA rotation guide.

[security]

Used by: every role.

Key Type Default Purpose
profile string (optional) unset The deployment profile this cluster is held to at start-up: dev, pilot, or production.
[security]
profile = "production"

Leaving profile unset changes nothing a running cluster already did: no new refusal, no new warning, no banner. A cluster deployed before this key existed keeps starting exactly as it did. Unset is not read as dev; gdsgate doctor reports the omission instead.

dev refuses nothing. A start prints one line naming the deployment a development one, once.

pilot and production both refuse the same faults, and a refusal names every fault present at once rather than stopping at the first one found:

Role Refused
Authority store_url does not name a PostgreSQL store. [authority].enroll_cert_file names no certificate for the registration listener.
Proxy The client-facing listener would run in the clear: no [proxy].public_cert_file pair, and no enrolled identity to present one from. The connector-facing listeners would run without mutual TLS. A configured [enroll].endpoint is not https://.
Connector The node holds no enrolled identity. A configured [enroll].endpoint is not https://.
Any role, started as gdsgate all Authority, the Proxy, and a connector run in one process off a certificate authority held in that process's memory.

[audit].export_path unset warns and starts anyway under both profiles: the audit chain in the database stays sealed and verified regardless, and what is missing is the copy a collector would carry away.

production refuses more than pilot does. Authority is refused unless [oidc].issuer and [oidc].client_id are both set: it is the one role that verifies a person's identity token, and with either half missing it verifies tokens against the built-in development issuer, whose secret every copy of gdsgate carries. No other role is asked for a provider. The Proxy's browser entry runs the sign-in itself, then hands the token it obtained to Authority to verify. [oidc].allow_insecure_http set to true is refused wherever the running role reads it: Authority validating identity tokens against a configured provider, or the Proxy serving the clientless browser entry. Authority is refused unless the two [security.integrity] keyrings are both named, kept apart, and owner-only, and unless every one of the five [audit].*_key_path files that still names a file on disk is owner-only too; see [security.integrity].

Once Authority has opened its store, production checks two more things against the store's own rows before it serves anything. Every Authority replica that is still confirming has to hold the same keys of each keyring: a fingerprint some replicas hold and others do not is refused when a row is already sealed under it, and left as a warning when no row is, which is what a keyring partway through a rolling rotation looks like. And the newest sealed row of every table has to name a key one of the two keyrings here holds, or a key whose loss was declared with keys accept-loss; a fingerprint neither answers for is most often the wrong keyring file, or a restore that took the database and left the ring behind. See Administration → gdsgate authority keys status and keys accept-loss.

[security.integrity]

Used by: Authority only.

The two keyrings a cluster's sealed rows are put under. Each field accepts the secret-reference notation, including the ${kms.<name>+...} wrapper over any of its sources; a bare value is a file.

Key Type Default Purpose
control_keyring string (optional) unset Keyring approvals, revocations, delegations, and inventory receipts are sealed under.
audit_keyring string (optional) unset Keyring the audit chain's anchors are sealed under.
[security.integrity]
control_keyring = "/var/lib/gdsgate/control.keys"
audit_keyring   = "/var/lib/gdsgate/audit.keys"

The two are kept apart because they are kept for different lengths of time: the audit ring seals a journal that outlives the decisions it records and may be handed to a retention system or a customer, while the control ring seals what the access path itself reads. One keyring for both means one leak forges the decisions and the record of them together, so naming the same file for both, directly or through a second path to it, is refused under production. pilot accepts a shared keyring and warns instead: naming a second one is left for the operator to arrange before the cluster is one people depend on.

Neither field is required to start a cluster. Unset, Authority goes on reading the five [audit].*_key_path parameters it always read (see Operations → [audit]). production requires both named, kept apart, and owner-only; pilot and dev do not check them.

gdsgate authority keys migrate-legacy brings the keys those five parameters name into the two keyrings above, as records that verify existing rows and seal nothing new. Each key keeps the fingerprint its own module already computed, so every row it sealed keeps verifying without being re-sealed; new rows keep going under the five key files until a keyring holds an active key of its own, so the five parameters stay in the configuration and keep being read. Once a keyring is named, gdsgate doctor reports which of the five files have not yet been brought in. See Administration → gdsgate authority keys migrate-legacy.

[mfa]

Used by: Authority.

Gateway step-up MFA factors. The TOTP factor (gdsgate mfa enroll / verify) needs no configuration. The WebAuthn (passkey) factor needs a relying party, configured under [mfa.webauthn]; without it the WebAuthn factor is off and the webauthn_satisfied Cedar channel stays false.

Key Type Default Purpose
webauthn.rp_id string unset The WebAuthn relying-party id, the domain a passkey is bound to. Each origin's host must be rp_id or a subdomain of it.
webauthn.origins list [] The browser origins (scheme://host[:port]) the ceremony may run against. Every origin must be https:// unless it is a localhost dev opt-in (below).
webauthn.allow_localhost bool false Dev only. Permit http://localhost origins. Binds passkeys to the workstation rather than a gateway host.
webauthn.allow_any_port bool false Dev only. Accept any port on the origin host, since the loopback ceremony picks a fresh port per run. Requires allow_localhost.
webauthn.rp_origin string unset Legacy single-origin alias, folded into origins.

In production the ceremony runs on the gateway's own browser origin (set [proxy].public_origin/web_addr), so a passkey is scoped to the gateway domain:

[mfa.webauthn]
rp_id = "gate.example.com"
origins = ["https://gate.example.com"]
Users register a passkey with gdsgate mfa register-passkey and step up with gdsgate step-up <resource>; both open the gateway's confirmation page. See Policy → Two MFA channels.

Development: localhost loopback

Without a gateway web origin the CLI runs the ceremony on a loopback page (http://localhost:<port>), which binds the passkey to localhost rather than one gateway host. That is weaker, so it is rejected at startup unless opted into:

[mfa.webauthn]
rp_id = "localhost"
origins = ["http://localhost"]
allow_localhost = true   # dev only: binds passkeys to the workstation
allow_any_port = true    # dev only: the loopback port varies per run

Migrating off localhost

A passkey registered for localhost cannot be used against a gateway-domain origin, because the relying-party id differs. To move a deployment to production: set [proxy].public_origin and web_addr, drop allow_localhost and allow_any_port, and have each user re-run gdsgate mfa register-passkey against the gateway origin. The old localhost passkeys stop working, and there is nothing to revoke.

[ca_rotation]

Used by: Authority.

Scheduled rotation of the User SSH CA. The same controller settings are consulted for manual Onward SSH, HostTls, SPIFFE, and JWT-signing rotations. Off by default; the five manual CA commands are gdsgate authority rotate-ca, rotate-onward-ca, rotate-host-tls-ca, rotate-spiffe-ca, and rotate-jwt-ca, and work regardless of enabled. Only the User SSH CA auto-rotates on a schedule. The other four are manual-only: Onward SSH and HostTls because operators time the propagation window themselves (for HostTls, ensure it covers a full mTLS-renew cycle so every active node refreshes its trust bundle before the signer swap); JWT-signing because what has to catch up inside propagation_secs is not this cluster's own nodes but however long the applications behind the web lane hold onto the published key set, see Telling the application who came.

Key Type Default Purpose
enabled bool false Master switch for the scheduled rotation.
interval_secs u64 2592000 (30 days) Cadence. 0 disables the schedule.
proactive_secs u64 604800 (7 days) Also rotate when the active generation has less than this much validity left. 0 disables the safety net.
propagation_secs u64 300 (5 min) After publishing the candidate, wait this long for verifiers (Proxy, connectors) to refresh their trust bundle before promoting it to the signer.
retire_secs u64 3600 (1 h) After promotion, wait this long before retiring the old CA. Set this at or above the issued-certificate TTL so existing certificates expire naturally before their signer is removed.
check_secs u64 3600 How often the controller wakes up to check whether a rotation is due.
[ca_rotation]
enabled          = true
interval_secs    = 2592000   # 30 days
proactive_secs   = 604800    # 7 days
propagation_secs = 300
retire_secs      = 3600
check_secs       = 3600