Skip to content

Administration commands

This is the canonical Authority command reference. Common global flags, output channels, environment overlays, and exit codes remain on the CLI hub. The command and nested-action inventories below are source-derived from crates/gdsgate/src/main.rs; use the hub only for compatibility anchors.

authority proxy connector all connect join login machine-id up down status completion ls request-access approve requests delegate delegations revoke delegation mfa step-up ssh ssh-config proxy-ssh db kube tcp mcp web ca session doctor

auto gateway certificate emergency store

gdsgate authority

Run the Authority service: control-plane gRPC listener (mutual TLS once the cluster is registered), bootstrap registration listener (plaintext, with the join token as the proof), policy engine, certificate authorities, audit chain.

Reads: store_url, [endpoints], [policy], [oidc], [ca_rotation], [ha], [discovery], [approvals].

gdsgate --config authority.toml authority

Authority has sub-actions in three execution groups, with four possible administrative routes (gateway, certificate, emergency, and the direct store route used only by audit listing).

The administrative ones (create-token, the five rotate-*-ca commands, list-nodes, list-offered, revoke-node, delete-node, import-node, list-workloads, revoke-workload, list-resources, set-resource, remove-resource) are gRPC calls Cedar decides, and each has two forms: a person through the gateway, or the control-plane certificate. Which one a run takes is --route.

The person's own ones (the policy family that talks to a running cluster, and recordings) are gRPC calls too, and Authority resolves their caller from an identity token alone, so there is no certificate form to choose. policy pull and policy push also answer on the emergency socket, which is what a locked-out cluster is repaired with.

The direct-store ones (bootstrap-admin, init-anchor, init-revocation-key, init-approval-key, init-inventory-key, init-delegation-key, verify-audit, audit list, migrate, onward-ca-pub / onward-ca-pubs) run on the Authority host itself, against store_url, and take no route at all; so do policy validate and policy template, which touch neither the store nor the network. bootstrap-provisioner is in none of the three: it needs no store credentials and no identity to start from, obtaining one at the enrollment listener and renewing it over mTLS afterwards.

Two identities may hold [admin].identity_dir: gdsgate-admin, issued by bootstrap-admin, and the narrower gdsgate-provisioner, issued by bootstrap-provisioner. The commands are identical under both; what differs is what the policy grants each group, so a sub-action may be refused under one identity and allowed under the other. See Policy → The starting policy.

--route <auto|gateway|certificate|emergency|store>

Selects the route an administrative sub-action uses to reach Authority. Accepted on every authority sub-action, before or after it, and refused by name on the ones that route does not serve.

Value The call travels The caller is The audit row names
gateway the gateway's client-facing listener you, by the identity token your provider issued you, and the address you called from
certificate a direct dial to [admin].endpoint the control-plane certificate in [admin].identity_dir that certificate's subject
emergency the socket on the Authority host whoever can open the socket gdsgate-break-glass, with admin_channel=emergency-socket
store the local persistent store the operating-system account that can open it no audit row; only audit list accepts this route

The first three are network or socket routes; store is a narrow local route:

  • gateway is the everyday route for a person. Cedar decides over you and the groups your provider gives you, so a rule can name your team rather than a machine; context.src_ip is the address you called from, so a rule admitting an action only from a corporate network applies; and losing your account at the provider loses the access with it. It needs a signed-in identity (gdsgate login) and a policy that grants your group the action; the starting policy grants four of the six.
  • certificate is the everyday route for a machine. A control plane reconciling a cluster on a timer has no person behind it and no account at that cluster's identity provider, so a certificate is the only identity it can hold. For a person it is the way in when the gateway is unreachable, which is why the lock-out ladder has it on the top rung.
  • emergency is not an everyday route. It is for the one situation where the policy admits neither of the others, and it exists only where Authority runs. It carries policy pull, policy push, list-nodes, list-workloads, list-resources, set-resource and remove-resource, and refuses everything else by name. See Admin guide → Lock-out and recovery.
  • store is not a general bypass. It is accepted only by authority audit list, which reads the local audit rows without a network hop; verify-audit is also local but has no route flag. The command rejects store for every other sub-action.

auto, the default, takes certificate wherever the configuration this command read names a control-plane identity, an [admin] section, and gateway where it does not. An operator workstation and a provisioning control plane therefore keep the route they have always taken with no flag and no edit, and a machine configured only as a client (gdsgate connect) takes the route meant for a person.

# a person, on their own machine: through the gateway, as themselves
gdsgate authority list-nodes

# a control plane, on the workstation configuration: straight at Authority
gdsgate --config provisioner.toml authority list-nodes

# either configuration, saying which route out loud
gdsgate --config provisioner.toml authority --route gateway list-nodes

The policy family that talks to a running cluster (pull, push, edit, approve-edit, pending-edits) and the recordings viewer are the exception: Authority resolves their caller from the identity token and from nothing else, so they have no certificate form. Under auto they take the gateway whatever else is configured, and --route certificate on one is refused saying so. Of those, the emergency socket carries policy pull and policy push.

--operator <NAME>

Names the person an administrative call is made on behalf of, on --route certificate:

gdsgate --config provisioner.toml authority --operator anna.ivanova@provider.example \
    revoke-node nodes-abc123

It is for a system that drives these commands for somebody else: a provisioning control plane holding CN=gdsgate-provisioner, whose own log knows which person asked while the cluster's log would otherwise see only the machine identity. Run by hand against your own cluster there is nobody to name, since the audit row already carries the identity that acted.

It is accepted only on that route. Through the gateway the caller is already named, by an identity the cluster verified, and on the emergency socket by being able to open a file on the Authority host. A second, unverified name beside a verified one is the confusion this field exists to prevent, so the command line refuses it there, and so does Authority if something else sends the pair.

The cluster records the value as asserted. It cannot check the name, only that a control-plane identity supplied it, so the name is stored in a field of its own, printed marked as unverified, and decides nothing. The useful value is therefore the identifier your own log names the person by, which is what joins the two logs.

Up to 128 bytes of ASCII letters, digits, space, or . _ - @ + ', with no leading or trailing space, and outside the gdsgate- namespace the cluster names its own identities in, since a name of that shape would land in the audit row beside a verified actor of the same shape. Case makes no difference there. Anything else is refused (InvalidArgument) and the call does nothing, because a name quietly rewritten is a different name. A peer that is not a control-plane identity is refused outright (PermissionDenied) rather than having the field ignored.

The flag reaches the sub-actions that write an audit row: create-token, the five rotate-*-ca commands, revoke-node, delete-node, revoke-workload, import-node, list-resources, set-resource, remove-resource and audit list. On any other sub-action there is no row for a name to land on, and the command refuses instead of accepting a name it would drop.

audit list is the one listing on that list, and the difference is the shape of its row rather than its importance. Reading the chain is audited on both outcomes, since it is the one action whose whole effect is that somebody now knows something, so a name always has somewhere to land. list-nodes and list-offered write a row only when they are refused; a name given to them would reach the chain on the calls that failed and be dropped on the ones that worked. --route store takes no name either: it reads the database and records nothing.

gdsgate authority init --profile <dev|pilot|production> [--output <PATH>] [--store-url <URL>] [--control-keyring <REFERENCE>] [--audit-keyring <REFERENCE>] [--kms-key <NAME=KIND:COMMAND>] [--if-missing]

The first run of an Authority: writes the configuration, puts the two integrity keyrings in place, and prints one JSON object to stdout. The form is non-interactive: a provisioning control plane calls it directly and reads the summary as data.

gdsgate authority init --profile pilot \
    --output /etc/gdsgate/authority.toml \
    --store-url postgres://gdsgate@db.example.internal/gdsgate
{
  "configuration": "/etc/gdsgate/authority.toml",
  "outcome": "written",
  "profile": "pilot",
  "transport_ca": { "fingerprint": "3f9c1a2b4d5e6f708192a3b4c5d6e7f8" },
  "keyrings": [
    {
      "keyring": "control",
      "source": "file /etc/gdsgate/control.keys",
      "created": true,
      "keys": ["a1b2c3d4e5f60718293a4b5c6d7e8f90"]
    },
    {
      "keyring": "audit",
      "source": "file /etc/gdsgate/audit.keys",
      "created": true,
      "keys": ["d4e5f60718293a4b5c6d7e8f90a1b2c3"]
    }
  ]
}

No key material appears in the summary, in any field: only fingerprints, the references as the operator wrote them, the profile, and a path.

Flag Default Purpose
--profile <dev\|pilot\|production> required What this deployment is held to. See [security].
--output <PATH> /etc/gdsgate/authority.toml Where the configuration is written; the shipped unit reads this path.
--store-url <URL> unset The cluster's state, written into store_url. A postgres:// URL for anything but a laptop; a password belongs in store_password_file, not in this string.
--control-keyring <REFERENCE> unset A control keyring already in place, in secret-reference notation. Without it, init creates one beside the configuration, unless --profile production is given.
--audit-keyring <REFERENCE> unset An audit keyring, likewise. Kept apart from the control one because the audit journal outlives the decisions it records.
--kms-key <NAME=KIND:COMMAND> none An external key, written into [[kms_keys]]. Repeatable. See below.
--if-missing off Report the configuration that is already there and exit 3, instead of refusing. Nothing is overwritten in either mode.

Without --if-missing, a configuration that already exists is a refusal, exit code 1. With it, init prints the summary read off the existing file and exits 3: a provisioning wave that finds the cluster already installed gets the same fingerprints as the wave that installed it.

Under --profile production, init refuses to create a keyring itself: a keyring minted here would be key material generated on disk, in the clear, on this host. Name one already in place with --control-keyring or --audit-keyring. dev and pilot create a keyring where none exists yet, each with one active root key.

--kms-key <NAME=KIND:COMMAND> declares an external key, written into [[kms_keys]]; repeat the flag for each key. <name> is what a ${kms.<name>+file:PATH} reference passed to --control-keyring or --audit-keyring points at. exec is the only kind this flag writes: <name>=exec:<program> <argument...>, split on whitespace, so an argument containing a space has to be a wrapper script that takes none. A native key service is declared in the configuration file afterwards instead, where each of its values gets a field of its own; see [[kms_keys]] for the full field set and the native kinds.

This is what lets --profile production create a keyring instead of refusing to: point --control-keyring or --audit-keyring at ${kms.<name>+file:PATH} with the key declared here, and the material is made inside the key service instead of on this host, in the clear.

init opens store_url and brings its schema up to date, the same as authority migrate, so it can resume or persist the cluster's transport CA and publish its fingerprint. With --store-url left unset, transport_ca.fingerprint is null and the object names the reason instead.

gdsgate authority set-profile --profile <dev|pilot|production> --changed-by <NAME>

Changes the deployment profile of a cluster that is already installed: rewrites [security].profile in the configuration this run loaded, and records the change on the audit chain. Only that one line is rewritten; the file's comments and ordering are left alone.

gdsgate --config authority.toml authority set-profile \
    --profile pilot --changed-by anna.ivanova
# authority.toml: `[security].profile` is now "pilot" (was "dev").
# Restart Authority for it to apply. The change is on the audit chain.
Flag Default Purpose
--profile <dev\|pilot\|production> required The profile to declare.
--changed-by <NAME> required Who the change is made for, on the audit row. See below.

The profile decides whether the cluster's state may be a file, whether a listener may be plaintext, whether the all-in-one process starts, and whether keys accept-loss is allowed at all; see [security]. The row is written to the chain before the file is rewritten, so a change that reached the chain and then failed to write is visible, never the reverse. The new profile applies at the next restart; this command does not perform one. Exits 3 when the declared profile is already the one asked for.

--changed-by names the person the change is made for. The row records it as asserted: set-profile reaches no cluster and presents no identity, so nothing here can check who typed it. It takes its own flag rather than the global --operator, which belongs to calls made over --route certificate, where a machine identity presents a control-plane certificate; no certificate is presented here.

gdsgate authority bootstrap-admin --output-dir <DIR> [--ttl <secs>] [--if-missing]

On the first run, an empty directory is bootstrapped: gdsgate opens the store directly, mints an admin-role provisioning token, runs it through the standard registration flow, and writes cert.pem, key.pem and ca.pem to --output-dir. This is the only remaining direct-store admin path. When the directory already holds a live identity, the command renews that identity over its own mTLS connection to [admin].endpoint; it does not mint a second admin identity or consume another provisioning token.

gdsgate --config authority.toml authority bootstrap-admin \
    --output-dir /var/lib/gdsgate/admin --ttl 300

Copy the three files to the operator workstation and point its config at them ([admin].identity_dir).

Flag Default Purpose
--output-dir <DIR> required Where cert.pem, key.pem and ca.pem are written. Required.
--ttl <secs> 300 Validity of the bootstrap provisioning token. It is consumed straight away, so this matters only if the registration hop is interrupted.
--if-missing off On an existing identity, leave it untouched when this cluster would accept it and report exit 3; no token is minted, no renewal runs, and no file is written. Without it, a live identity is renewed over mTLS.

With --if-missing, re-applying a tenant's desired state is safe to repeat:

gdsgate --config authority.toml authority bootstrap-admin \
    --output-dir /var/lib/gdsgate/admin --if-missing
# admin identity already present at /var/lib/gdsgate/admin (ttl 21h) — nothing to do

Without --if-missing, that same directory takes the renewal path whenever its three files still describe a usable identity. If the material is missing, expired, no longer trusted by this cluster, or otherwise unusable, gdsgate reports the reason and issues a fresh identity through the bootstrap path. Once an identity is usable, an mTLS renewal failure is returned as an error and is never silently converted into a second enrollment; remove the directory explicitly when a new identity is intended.

--if-missing keeps an identity only if this cluster, as it stands now, would accept it. All of the following have to hold; anything else prints what disqualified it and issues a new identity:

  • cert.pem, key.pem and ca.pem are all readable;
  • cert.pem parses, and its validity window covers now;
  • more than a quarter of the certificate's own lifetime is still ahead. The headroom is a fraction of the lifetime rather than a fixed span, so a provisioner that reconciles more often than every quarter of it always hands the next run a live identity;
  • the certificate was issued by a CA this cluster trusts now, which is the check a re-provisioned store cannot slip past, because its new CA never signed the certificate on disk;
  • ca.pem still carries the CA the cluster serves with, without which the admin could not verify Authority on the next handshake;
  • key.pem is the private half of cert.pem. The three files are written one after another, so a run killed in the middle leaves a set that passes every check above and still cannot complete a handshake.

The identity this command issues lives 24 hours by default and follows [transport].cert_ttl_secs where that is set, the same setting that governs the identities a running Authority issues. A cluster that never sets the key sees the same day it always did.

gdsgate authority bootstrap-provisioner --output-dir <DIR> [--token <TOKEN>] [--if-missing]

Obtains the provisioning control plane's mTLS identity on the first run and renews it on every run after that, writing cert.pem, key.pem and ca.pem to --output-dir, the same three files [admin].identity_dir reads. Every authority sub-action then runs as CN=gdsgate-provisioner and gets what the policy grants Group::"gdsgate-provisioner".

Unlike bootstrap-admin this never opens the cluster's store. The first run spends a one-time --role provisioner token at the enrollment listener; every later run renews the identity already in the directory over Authority.RenewIdentity. A control plane that manages a cluster it does not host therefore needs no credentials to that cluster's database.

# First run, on the provisioning control plane.
gdsgate --config provisioner.toml authority bootstrap-provisioner \
    --output-dir /var/lib/gdsgate/provisioner \
    --token "$(gdsgate --config operator-workstation.toml \
        authority create-token --role provisioner --ttl 300)"

# Every run after that — no token is read.
gdsgate --config provisioner.toml authority bootstrap-provisioner \
    --output-dir /var/lib/gdsgate/provisioner --if-missing
Flag Default Purpose
--output-dir <DIR> required Where cert.pem, key.pem and ca.pem are written and read back. Required.
--token <TOKEN> [enroll].token One-time token from authority create-token --role provisioner, for the first run only. Not read once --output-dir holds an identity.
--if-missing off Leave an identity that is still well inside its validity window as it is and exit 3, instead of renewing it.

The endpoints come from the same settings every node uses: [enroll].endpoint for the first run's registration, and [enroll].renew_endpoint, or [endpoints].authority when it is unset, for the renewals. With nothing in --output-dir and no enrollment endpoint, the run fails, saying so.

A held identity is renewed, never re-enrolled. Once --output-dir holds one, a failed renewal is reported as a failure instead of answered by spending a token, which would mint a second live provisioner identity that nothing revokes and that outlives the run that created it. To recover an identity beyond repair, remove --output-dir and run again with a fresh token.

Two control planes applying the same cluster at the same time therefore end up with one identity between them: the losing registration finds the one-time token already spent.

--if-missing keeps an identity with more than five minutes left to run, or more than a quarter of its own lifetime where that is less. Without the flag every run renews, which is harmless: a renewal replaces the identity rather than adding one, and the registry row moves with it.

The command prints where it wrote the identity, how long it is good for, and the [admin] identity_dir line to put in this host's config.

gdsgate authority create-token --role {proxy|connector|admin|workload|provisioner} [--ttl <secs>]

Generates a one-time node-registration token authorising one standalone gdsgate proxy or gdsgate connector to register. Prints the token to stdout; logs to stderr.

gdsgate --config operator-workstation.toml authority create-token --role connector --ttl 3600
Flag Default Purpose
--role required proxy, connector, admin, workload or provisioner. Required. admin is normally minted by bootstrap-admin instead.
--ttl <secs> 3600 Validity window before the token expires.

Calls the running Authority's CreateProvisionToken admin RPC; the operator config must define [admin].endpoint and [admin].identity_dir.

The call is decided by Cedar issueJoinToken, and the resource it decides over is the identity the token would bring into being: EnrolledNode::"gdsgate-connector", EnrolledNode::"gdsgate-proxy", EnrolledNode::"gdsgate-admin", and so on. A token is a licence to become that identity, so a policy grants the roles it means to hand out by name; the starting policy gives the provisioner connector, proxy and workload and refuses the rest. A refusal is permission denied: policy denied admin action, and exits non-zero. See Policy → Administrative actions.

gdsgate authority rotate-ca

Requests a User SSH CA rotation through admin gRPC. The running Authority, the leader in HA, records the request and runs the paced double-signing rotation. Works regardless of [ca_rotation].enabled.

gdsgate --config operator-workstation.toml authority rotate-ca

gdsgate authority rotate-onward-ca

Requests an Onward SSH CA rotation: the CA Authority uses to sign the OpenSSH user certificates a connector presents to a downstream sshd in the model-B jump-host path. Same paced rotation as rotate-ca. After rotation finishes, every downstream sshd needs the new public key added to its TrustedUserCAKeys.

gdsgate --config operator-workstation.toml authority rotate-onward-ca

gdsgate authority rotate-host-tls-ca

Requests a HostTls (transport) CA rotation. Same paced double-signing controller as rotate-ca and rotate-onward-ca. After the candidate is published, nodes pick up the new trust bundle on their next mTLS renew; set [ca_rotation].propagation_secs to at least a typical renew interval so every active node refreshes before the signer is swapped. There is no auto-schedule for HostTls; the cadence is operator-timed.

gdsgate --config operator-workstation.toml authority rotate-host-tls-ca

gdsgate authority rotate-spiffe-ca

Requests a SPIFFE CA rotation: the CA that signs workload certificates. Same paced double-signing controller as the other rotate-*-ca commands; connectors and proxies pick up the new SPIFFE bundle with the rest of the trust material on their next mTLS renew.

gdsgate --config operator-workstation.toml authority rotate-spiffe-ca

gdsgate authority rotate-jwt-ca

Requests a JWT-signing CA rotation: the CA that signs the Gdsgate-Identity assertion the web lane hands an application when a kind = "web" backend sets identity = "signed". Same paced double-signing controller as the other rotate-*-ca commands. Manual only, because what propagation_secs has to clear here is not this cluster's own nodes but however long an application behind the web lane caches the key set the Proxy publishes at /.well-known/gdsgate/jwks.json, a figure only the operator knows. See Configuration → Telling the application who came.

gdsgate --config operator-workstation.toml authority rotate-jwt-ca

gdsgate authority list-nodes

Prints every enrolled node, active and revoked, one per line:

<id>  <role>  <cn>  fp=<short-fingerprint>  enrolled=<unix>  [revoked=<unix>]
    calls itself <name> (claimed by the node, unverified)

The second line appears only for a node that set [enroll].node_name. It is indented and labelled rather than given a column of its own, because the columns are what this cluster verified and the name is what the node said about itself. See Naming a node.

Use the <id> from this output with authority revoke-node. Calls the running Authority's ListEnrolledNodes admin RPC.

gdsgate --config operator-workstation.toml authority list-nodes

--health instead shows the live health each node last reported, the in-memory board fed by the periodic check (see [doctor]): per-severity check counts, how long ago the node reported, and its degraded checks. A node absent from --health has not reported since Authority last started.

gdsgate --config operator-workstation.toml authority list-nodes --health

Both forms are decided by Cedar viewNodes over EnrolledNode::"*", the registry as a whole, since a listing names no single node. The same action gates authority list-workloads. A refusal is permission denied: policy denied admin action and exits non-zero; only refusals reach the audit chain, because these are reads a provisioner and the health board repeat on a timer.

gdsgate authority list-offered

Prints what connectors say they serve, one per line:

<node-id>  <resource-id>  kind=<kind>  offered=<unix>  declared=<yes|no>
    offered by <name> (claimed by the node, unverified)

A connector states the resources it serves when its reverse tunnel opens; this is that statement, kept. declared=no is the state that needs an operator: the connector serves the resource, the catalog has no row for it, and the cluster routes nothing to it until one is declared with authority set-resource.

The resource id and its kind are the connector's own words, since a provision token is issued for a role instead of for a machine, so they are claimed rather than verified, exactly like the node name on the indented line. What the cluster contributes is the node id, the timestamp, and declared.

A connector takes the list it declares from the memory of its running process, so a backend added to [[connector.backends]] appears here only after that connector restarts. Offers nothing has restated for ten minutes are left out: a connector re-declares on every tunnel it opens, so silence means a process that stopped.

gdsgate --config operator-workstation.toml authority list-offered

Decided by Cedar viewNodes, the same action as authority list-nodes, since this is that registry read from the other side. Calls the running Authority's ListOfferedResources admin RPC.

gdsgate authority revoke-node <id>

Revokes the enrolled node so it can no longer renew its transport leaf over mTLS. Idempotent: prints revoked on first success, already revoked on a re-run, unknown (and exits non-zero) if no such row exists. The revocation is sealed with the cluster's HMAC revocation key (see init-revocation-key) so a database rollback cannot un-revoke the node.

Revocation also cuts the node off immediately, without waiting for it to reconnect: the revocation is announced to every proxy, which drops that node's reverse tunnel and terminates the sessions running over it. Resources it served stop being routable, and its next registration attempt is refused outright.

The already-issued leaf itself stays chain-valid until its not_after: it is the tunnel that is cut, not the certificate. That matters only where a leaf is presented to something other than a gdsgate listener; for a fleet-wide sweep of the certificates themselves, rotate the HostTls CA.

NODE_ID="<id-from-list-nodes>"
gdsgate --config operator-workstation.toml authority revoke-node "$NODE_ID"

gdsgate authority delete-node <id>

Removes a long-revoked node's registry row, so the registry says what is deployed rather than everything that ever was. Prints deleted on success; anything else exits non-zero:

Printed Meaning
deleted The row is gone.
not revoked The node is still active; run revoke-node first.
too recent Revoked, but less than seven days ago. The message names the second from which the row may go.
unknown No such row, which is also what a re-run of a successful delete prints.

The node's HMAC revocation receipt is kept, so the id keeps answering "revoked" for good: removing a row can never un-revoke a node. The removal is written to the audit chain as identity.delete, with the reason when it was refused, and the join and revocation rows stay where they were, so the chain still verifies.

Gated by Cedar revokeNode on the node's EnrolledNode entity, the same action as revoke-node: ending a node's identity and forgetting its row take the same standing, so no policy has to change to use this. A policy cannot currently grant one without the other.

NODE_ID="<id-from-list-nodes>"
gdsgate --config operator-workstation.toml authority delete-node "$NODE_ID"

gdsgate authority import-node --role <ROLE> --cert <PATH>

One-shot migration of a node that was already issued a transport leaf before the per-node revocation registry existed, in any deployment upgraded from a release older than the registry rollout. The CLI reads the node's current transport leaf PEM, verifies its CN matches the expected gdsgate-{role}, computes the SHA-256 of its SubjectPublicKeyInfo, and calls the running Authority's ImportEnrolledNode RPC under the admin identity from [admin].identity_dir.

Idempotent: re-running with the same cert prints existing <id> and exits 0, adding no duplicate row, while the first successful run prints imported <id>. Audited as identity.import (AdminAction) with the verified admin CN as the actor.

After import, the node renews normally through RenewIdentity. Without import, renewals from any pre-registry leaf are refused, since the registry is strict and performs no silent backfill.

gdsgate --config operator-workstation.toml authority import-node \
    --role connector \
    --cert /var/lib/gdsgate/connector.cert.pem

Every freshly enrolled node, through enroll, is already in the registry from t=0. This command is for the migration window only.

gdsgate authority list-workloads

Prints every registered workload, the provision-token machines tracked for token-less renewal, one per line:

<spiffe-id>  owner=<owner|->  fp=<short-fingerprint>  joined=<unix>  <active, cert until <unix> | REVOKED at <unix>>

Platform-token workloads are stateless and do not appear here. Calls the running Authority's ListWorkloadIdentities admin RPC, decided by Cedar viewNodes over EnrolledNode::"*", the same action as authority list-nodes.

gdsgate --config operator-workstation.toml authority list-workloads

gdsgate authority revoke-workload <spiffe-id>

Revokes a registered workload. Idempotent: prints revoked on first success, and a re-run or an unknown id reports the no-op. The revocation is sealed with the cluster's HMAC revocation key (see init-revocation-key), so a database rollback cannot un-revoke it.

Revocation cuts the workload off immediately, without waiting for its certificate to lapse: the revocation is announced to every proxy, which ends the sessions that identity has open. Its next connection is refused on every lane, and so is its next renewal.

A workload holds no tunnel of its own, riding the tunnel of whichever connector serves the resource, so only its own sessions end. Other workloads and people on the same tunnel are untouched.

The already-issued certificate itself stays chain-valid until its short not_after: it is the workload's access that is cut, not the certificate. For a fleet-wide sweep of the certificates themselves, rotate the SPIFFE CA.

Workloads bootstrapped from a platform token keep no registry row and are not revocable this way; cut them off with policy instead.

WORKLOAD_SPIFFE_ID="spiffe://gds.example/provision/8f3c1234"  # from `list-workloads`
gdsgate --config operator-workstation.toml authority revoke-workload \
    "$WORKLOAD_SPIFFE_ID"

gdsgate authority list-resources

Prints the resource catalog whole, one line per declared resource, in id order:

<id>  <kind>  project=<project>  env=<environment>  [hostname=<name>]  [aliases=<name>,…]  [port=<n>]  [min_approvers=<n>]  [labels=<key>=<value>,…]  [parent=<id>]

Everything after env= is printed only for the rows that carry it. Labels are sorted by key; aliases keep the order they were declared in. An empty catalog prints catalog is empty.

A row that declares no name and no port prints neither: the line shows what was declared rather than what it resolves to. The defaults behind an omitted declaration are described under set-resource, and gdsgate ls shows a caller the resolved name and port.

This is the provisioning view: what was declared, including resources nobody may reach. gdsgate ls answers the other question, what the caller may open a session to. Calls the running Authority's ListResourcesAdmin admin RPC.

gdsgate --config operator-workstation.toml authority list-resources
# db-orders_prod  postgres  project=payments  env=prod  hostname=orders  aliases=orders-db  port=5433  min_approvers=2  labels=tier=gold  parent=host-db1
# host-db1  ssh  project=payments  env=prod

--json prints one object per resource instead, for anything reading the listing instead of looking at it. Absence is absence there: a row with no port carries no port field rather than a zero.

gdsgate --config operator-workstation.toml authority list-resources --json
# {"id":"host-db1","kind":"ssh","project":"payments","environment":"prod","serving":"yes","port":22}

serving says whether a connector's tunnel is registered for that resource at the moment of the listing. The gateway is what knows this, so the field is there on the route that goes through the gateway (--route gateway) and absent on the route that reaches Authority directly, since Authority holds the catalog and no routing table. An absent serving means nobody answered the question rather than that nothing is serving the resource.

The three catalog commands, list-resources, set-resource and remove-resource, all go through admin gRPC under the identity from [admin], are decided by Cedar manageCatalog over the resource's own entity, and land an audit row whether they were allowed or refused. A refusal names the manageCatalog action and the policy to permit it in, and exits non-zero.

gdsgate authority list-serving

Prints the resources a connector's tunnel is registered for at that moment, one id per line. An empty answer prints nothing.

It asks the gateway instead of Authority, since only the gateway has a routing table. The call dials the gateway's internal listener ([endpoints].proxy_internal, overridable with --endpoint) with the control-plane certificate from [admin].identity_dir, and the gateway refuses any peer whose CN is not one of this cluster's own control-plane roles.

The internal listener rather than the client-facing one, because this call is authenticated by a certificate the cluster issued: only that listener's TLS is under the same authority. A deployment publishing its gateway under a public name carries a public certificate there, which is not something a control plane can verify against the transport CA.

gdsgate --config provisioner.toml authority list-serving
# gitlabwww

The same answer is in list-resources --route gateway --json, under serving. That route carries an identity token, so it is for a person; this one is for a control plane, which has a certificate and nobody to sign in as.

gdsgate authority set-resource <id> --kind <KIND>

Declares a resource in the catalog, or rewrites the metadata of one that is already declared. Prints created or updated followed by the row as written, in the shape list-resources prints. Calls the running Authority's UpsertResource admin RPC.

A rewrite is whole: the project, environment, labels, parent, threshold and addressing given on the command line replace what the row carried rather than merging into it, so a label or an alias left off the second run is gone from the row.

gdsgate --config operator-workstation.toml authority set-resource db-orders_prod \
    --kind postgres --project payments --env prod \
    --hostname orders --alias orders-db --port 5433 \
    --label tier=gold --parent host-db1 --min-approvers 2
# created db-orders_prod  postgres  project=payments  env=prod  hostname=orders  aliases=orders-db  port=5433  min_approvers=2  labels=tier=gold  parent=host-db1
Flag Default Purpose
--kind required ssh, postgres, mysql, kubernetes (k8s is accepted for it), tcp, mcp, llm, web, web-egress. cockroach is a connector backend kind accepted by [[discovery.resources]] and [[connector.backends]], but its catalog resource kind is postgres and this CLI flag intentionally does not expose it.
--project default Owning project.
--environment / --env dev Deployment environment, which the [approvals] per-environment cascade and environment policies match on.
--label <KEY=VALUE> not set Label to attach; repeat the flag for more. Labels become the resource's Cedar entity tags, so what is written here is exactly what label-based policy reads.
--parent <ID> not set Parent resource id, for example a database's host server. Omitted, the resource is top-level.
--min-approvers <N> 0 Resource-local JIT approval threshold, the narrowest level of the approvals cascade. 0 leaves the row declaring none, so the [approvals] settings decide instead.
--hostname <NAME> not set Name clients address the resource by. Omitted, the resource id is the name, where the id is itself a usable one.
--alias <NAME> not set A further name for the same resource, under the same rules; repeat the flag for more.
--port <N> 0 Port clients dial. 0 leaves the row declaring none, so the well-known port of the kind stands in.
--pin-tool-descriptors off mcp only. Pins the server's tool descriptors; see Pinning tool descriptors. It lives on the resource instead of on the connector's backend entry because the connector is the party the guarantee constrains; a write that omits the flag turns pinning off, like every other column here. Set on any other kind, the write is refused.
--native-name <DOMAIN> not set web only. The domain the application itself answers on (app.example.com), as [[connector.backends]] url spells it. It is a different field from --hostname, which is a single label this cluster hands out and a client resolves inside its own zone; this is a whole domain gdsgate does not own. A whole name of at least two labels, unique in the same namespace as --hostname and --alias. Declaring it publishes nothing by itself: a resident client answers to it only where the person running it has also named it in their own [client].intercept_names. Set on any other kind, the write is refused. See Resident client → An application's own name.

The addressing flags are the API side of what a [[discovery.resources]] entry declares as hostname, aliases and port, and they are held to the same rules:

  • A name is one DNS label: 1 to 63 bytes of letters, digits and -, not starting or ending with -, case folded to lowercase. A declared name that is not is refused rather than dropped.
  • A name defaults from the resource id, where the id is itself a legal label. An id may carry _ or . and a name may not, so db-orders_prod yields no default name and has to declare one to be addressable.
  • Names are unique across the catalog. hostnames, aliases, native_names and the names defaulted from ids all compete for the same namespace, because a client resolves them through one map. Asking for a name another resource already answers to is refused, naming that resource. Moving a name is therefore two writes: release it on the one holding it, then claim it.
  • The port follows the kind (ssh 22, postgres 5432, cockroach 26257, mysql 3306, kubernetes 6443, web 80) unless the row declares one, which overrides it. tcp, mcp, llm and web-egress have no well-known port and must declare one; without it the write is refused. This is the port clients connect on, not the address the connector's backend listens on.

A kind outside that list is refused before anything is sent, and so is a label that is not key=value, has an empty key or an empty value, or repeats a key already given. A --parent naming the resource itself is refused by Authority.

A resource that is also declared in [[discovery.resources]] is written back over from the file at the next Authority start, so keep [discovery] empty on a cluster whose catalog is managed this way.

gdsgate authority remove-resource <id>

Removes a resource from the catalog. Idempotent: prints removed <id> when a row was there and no such resource <id> when none was, and exits 0 either way, so a teardown script can run twice. Calls the running Authority's DeleteResource admin RPC.

Removal closes access to the resource and sweeps its connector binding and published inventory in the same transaction; see Admin guide → What removing a resource does. A resource still declared in [[discovery.resources]] comes back at the next Authority start.

gdsgate --config operator-workstation.toml authority remove-resource db-orders_prod

gdsgate authority onward-ca-pub

Prints the active Onward SSH CA public key in OpenSSH wire format, one line, the form TrustedUserCAKeys expects.

gdsgate --config operator-workstation.toml authority onward-ca-pub \
    > /etc/ssh/gdsgate-onward-ca.pub

gdsgate authority onward-ca-pubs

Prints every persisted Onward SSH CA public key, active, retiring, and any candidate, one OpenSSH line each, newest generation first. Use this for the downstream sshd's TrustedUserCAKeys so that certificates signed by a still-trusted retiring generation keep verifying during the rotation window.

gdsgate --config operator-workstation.toml authority onward-ca-pubs \
    > /etc/ssh/gdsgate-onward-ca-bundle.pub

gdsgate authority keys

Nine sub-actions over the two integrity keyrings and the CA signing seeds kept beside them: what the rings hold, whether every key in them is the key its fingerprint claims, what a backup needs beyond the database, the public keys the audit chain's signed checkpoints are checked under, recording that a key is gone, moving a ring on to a new key, bringing the five legacy [audit].*_key_path keys into a ring, putting the CA signing seeds already in the store under a wrapping key, and removing the CA generations nothing will use again. None of them prints key material; each prints fingerprints, states, counts, public keys, and the references an operator wrote themselves.

gdsgate authority keys migrate-legacy

Brings the keys named by the five [audit].*_key_path parameters into the two keyrings [security.integrity] names, as records that verify rows and never seal them.

gdsgate --config authority.toml authority keys migrate-legacy

Nothing is re-sealed and no key is generated. Each key keeps the fingerprint its own module computes, which is the fingerprint the rows it sealed already carry, so every one of those rows goes on verifying. Until a root is added to a ring, new rows are still sealed under the key files, so the five parameters stay where they are.

Idempotent: a second run finds the same fingerprints in the rings, adds nothing, and leaves the files untouched.

A key whose ring is not named is refused rather than skipped, so a run that reports success has brought in everything this deployment had. A ring named by ${env:...} or ${exec:...} is refused too: it is assembled wherever it comes from, and there is no file here to write.

gdsgate authority keys wrap-ca-seeds

Puts every CA signing seed already in the store under the wrapping key [authority].ca_kms_key names.

gdsgate --config authority.toml authority keys wrap-ca-seeds
3 of 3 CA signing key(s) wrapped under `ca`.

Naming the key is what makes every seed written from then on ciphertext; the generations already in the table stay as they were written, because nothing rewrites a private key behind an operator's back. This command is the step that rewrites them. Run it against a cluster that is serving: each row is converted on its own, and a run that stops halfway leaves some rows wrapped and some in the clear, which the table reads correctly either way, because a row states in its own seed column which it is.

Idempotent: a second run finds every row already under the key and changes nothing.

the store holds no CA signing keys yet, so there is nothing to wrap.

Refused when [authority].ca_kms_key names no key. Also refused when a row is wrapped under a key other than the one this run holds: moving a seed from key to key means opening it with the first, and this command holds only the second.

gdsgate authority keys prune-ca-keys [--delete]

Removes the private keys of CA generations nothing will use again.

gdsgate --config authority.toml authority keys prune-ca-keys
2 CA generation(s) can be removed:
  user-ssh generation 1 (window closed 412d ago)
  user-ssh generation 2 (window closed 380d ago)
Nothing was removed. Run this again with `--delete` to remove them; there is no
command that puts one back.

Nothing else thins ca_private_key. A committed rotation rewrites the public registry and leaves the retired generation's private row where it is, so a cluster carries every generation it has ever had, and under [authority].ca_kms_key each of those rows is a call to the key service on every start.

A generation is listed only when all four hold: its own validity window closed more than thirty days ago, no rotation role names it, it is neither of its authority's two newest generations, and the trust registry no longer publishes it. The active signer fails three of the four, so no run reaches it.

Without --delete the command lists and removes nothing. With it, each removal is written to the audit journal as ca.key_pruned (CAS-0201) before the row goes, naming the authority, the generation and when its window closed. There is no command that restores one, and a backup taken after the run does not hold it.

Idempotent: a second run finds nothing left to remove.

gdsgate authority keys status

Prints what each keyring holds: every key by fingerprint, what it may still be used for, and how many rows in the store still name it. No key material.

gdsgate --config authority.toml authority keys status
control  file /etc/gdsgate/control.keys
  active       3f9c1a2b4d5e6f708192a3b4c5d6e7f8  root, every domain derived from it
               4 rows name it: access_request 2, step_up_grant 2
               2 of 2 replicas hold it
  verify_only  a1b2c3d4e5f60718293a4b5c6d7e8f90  approval key, taken as it stands
               no row names it
  replicas: auth-1 confirmed 12s ago, auth-2 confirmed 47s ago
audit  file /etc/gdsgate/audit.keys
  active       d4e5f60718293a4b5c6d7e8f90a1b2c3  root, every domain derived from it
               812 rows name it: audit_anchor 812
               2 of 2 replicas hold it
  replicas: auth-1 confirmed 12s ago, auth-2 confirmed 47s ago

A key is a root, deriving every domain's working key, or a single domain's key taken as it stands, the shape a key keeps after keys migrate-legacy. The row count is read from the key_id column that every sealed table already carries: audit_anchor, access_request, step_up_grant, revocation_receipt, owner_revocation, delegation_grant, backend_inventory, and mcp_tool_pin. It is the count a key is retired on: a key leaves a ring once nothing names it any more.

Each Authority instance confirms the fingerprints it holds once a minute, so the line under a key prints only once at least one replica has said anything about that ring; a deployment where no replica has confirmed yet reads replicas: nothing published (no Authority has recorded the fingerprints it holds). A replica that stops confirming for ten minutes drops out of the count and is instead named on the replicas: line as last confirmed some time ago; it comes back once it confirms again. This is the same set keys rotate --activate waits on before it switches a ring's active key, and what a production start-up checks the replicas against.

A key declared lost with keys accept-loss (below) adds a trailing declared lost: block, one line per key: its fingerprint, which keyring it belonged to, when the loss was recorded, who accepted it, and the same row count.

gdsgate authority keys verify

Checks that every key in every keyring is the key its fingerprint names, by recomputing the fingerprint from the material and comparing it, and that no fingerprint is held by both keyrings at once. Opens no store: the check is over the keyring files alone, so it still answers with the store unreachable, which is when a keyring is most likely to be checked by hand.

gdsgate --config authority.toml authority keys verify
control  file /etc/gdsgate/control.keys
  3f9c1a2b4d5e6f708192a3b4c5d6e7f8  root, every domain derived from it its own key
audit  file /etc/gdsgate/audit.keys
  d4e5f60718293a4b5c6d7e8f90a1b2c3  root, every domain derived from it its own key
every key is the key its fingerprint names.

A keyring file is edited by hand, and material replaced under an unchanged fingerprint is the one fault nothing else notices: rows sealed by the original key go on naming that fingerprint and are checked against the new material, so the deployment reads its own history as forged. verify exits non-zero and lists every failing record, so a ring is repaired in one pass instead of one run per fault.

gdsgate authority keys backup-manifest

Prints what has to travel with a database backup, and why, without opening the store: this is read in the minute before a restore, when there may be no store to open.

gdsgate --config authority.toml authority keys backup-manifest
control  file /etc/gdsgate/control.keys
  covered by: nothing (the source holds the material as it stands)
  3f9c1a2b4d5e6f708192a3b4c5d6e7f8  active  root, every domain derived from it, created at 1755000000
audit  file /etc/gdsgate/audit.keys
  covered by: nothing (the source holds the material as it stands)
  d4e5f60718293a4b5c6d7e8f90a1b2c3  active  root, every domain derived from it, created at 1755000000
a database restored without these keyrings cannot be verified: every sealed row names the key that sealed it, and a key nothing holds cannot answer for its rows.
a keyring in the same backup as the database protects nothing: whoever holds that backup holds the rows and the key that seals them, and can write rows that verify.
the public keys above are the exception: publish them with the dump. They check the chain's signed checkpoints and seal nothing, so whoever holds them can verify the journal and forge no part of it.

Nothing wraps a keyring's material today, which "covered by: nothing" says outright. The three closing lines print on every run: keeping a keyring out of the database's own backup, keeping the two keyrings apart from each other, and publishing the one thing that is safe to hand a reviewer instead, is what this command exists to remind the reader of at the one moment that matters. See Admin guide → Keeping keyrings out of the database backup.

gdsgate authority keys accept-loss --key-id <FINGERPRINT> --accepted-by <NAME>

Records that one key is gone, so the rows it sealed are read as unverifiable rather than as forged. The statement is itself sealed, by the ring's current active key, and lands on the audit chain.

gdsgate --config authority.toml authority keys accept-loss \
    --key-id 3f9c1a2b4d5e6f708192a3b4c5d6e7f8 --accepted-by anna.ivanova
Flag Default Purpose
--key-id <FINGERPRINT> required The fingerprint that is gone, exactly as a row carries it: 32 hex characters. There is no form that accepts every unverifiable key at once; keys status prints the fingerprint of each key a ring holds.
--accepted-by <NAME> required Who is accepting the loss, on the audit row. See below.

Refused under [security].profile = "production". Accepting a loss turns rows nothing can verify into rows that are acted on, which a production cluster only does after stepping down from that profile with set-profile, itself an event on the chain. Also refused when the named fingerprint is still held by a keyring (it is not lost), when no row names it (nothing became unverifiable), or when rows of both keyrings name it (one key cannot have sealed both).

--accepted-by is recorded as asserted, the same as set-profile --changed-by: this command reaches no cluster and presents no identity, so nothing here can check who typed it.

What changes, by table:

Row After the loss is accepted
revocation_receipt (and the equivalent workload-identity row) Still reads as revoked. A receipt sealed under a key now declared lost is no longer read as tampering.
access_request, step_up_grant, delegation_grant Refused, as any unverifiable row is; the request, step-up, or delegation has to be made again.
backend_inventory, mcp_tool_pin Discarded, as any unverifiable row is; the connector republishes them.
audit_anchor Left in place. verify-audit reports the stretch of chain it cannot read instead of stopping at the first one.

See Admin guide → When a keyring is lost for the procedure, including what to do when the ring itself, not just one key in it, is gone.

gdsgate authority keys rotate <control|audit> <--prepare|--activate|--retire>

Moves a keyring on to a new key, in three steps. --prepare puts a new key in the ring as prepared and leaves the active one alone; the operator then distributes the ring and restarts every Authority, since a ring is read once at start-up. --activate makes the prepared key the one new rows are sealed under, and is refused while any replica that is still confirming has not published that it holds it. --retire takes the old key out once no row names it, and is refused for the audit ring always: an anchor is named for as long as the journal is kept, so that count never reaches zero.

--prepare --kind signing mints the Ed25519 key the audit chain's checkpoints are signed with instead of the key rows are sealed under; the default is --kind hmac. The two rotate on their own schedules, and each --activate stands down the active key of its own kind and leaves the other where it is. The control ring signs nothing, so it takes no signing key.

See Admin guide → Rotating a keyring for the procedure end to end, including the restart a ring change requires.

gdsgate authority keys checkpoint-keys

Prints the public keys the audit chain's signed checkpoints are checked under. One line per signing key the ring holds, whatever state each is in, because a checkpoint outlives the key that signed it. Nothing here is secret: put the output beside the database dump.

gdsgate --config authority.toml authority keys checkpoint-keys > checkpoint-keys.txt

With that file and a dump, verify-audit --checkpoint-keys is a complete verification of the journal that needs no key material and cannot produce a row that verifies. See Admin guide → Verifying the journal without a keyring for the procedure a reviewer outside the deployment runs.

gdsgate authority init-anchor [--path <PATH>] [--if-missing]

Generates a fresh 32-byte HMAC anchor key and writes it, hex-encoded, mode 0o600, to --path, or to [audit].anchor_key_path if --path is omitted. Refuses to overwrite an existing file, since silently invalidating every existing anchor row would make the chain unverifiable.

gdsgate --config authority.toml authority init-anchor --path /var/lib/gdsgate/anchor.key

After running, set [audit].anchor_key_path to the resulting path in Authority's config and back the file up: losing it means existing anchored events can no longer be verified.

Flag Default Purpose
--path <PATH> [audit].anchor_key_path Where to write the key file.
--if-missing off A key file that is already there is left alone and reported (exit 3), not refused (exit 1). Nothing is overwritten in either mode.

--if-missing is the form a provisioner runs: applying the same desired state again finds the key the first run wrote and says so instead of failing.

gdsgate --config authority.toml authority init-anchor \
    --path /var/lib/gdsgate/anchor.key --if-missing
# HMAC anchor key already present at /var/lib/gdsgate/anchor.key — nothing to do

The refusal stays the default: for a person at a terminal "it is already there" is the answer worth stopping on. Two provisioners racing on the same path do not clobber each other either, since the file is created exclusively, so the one that loses the race reports the same "already present" as one that simply found the file.

Both --path and the field it defaults to name a file to write. Where the field instead names a variable or a command, through the secret-reference notation, the command refuses: a key delivered from the environment or by a helper is created where it lives, not by this command. Pass --path to write a file regardless of what the field holds.

The five commands below take the same two flags and behave the same way.

gdsgate authority init-revocation-key [--path <PATH>] [--if-missing]

Generates a fresh 32-byte HMAC revocation-receipt key, as init-anchor does. Wire [audit].revocation_key_path to it so revoke-node writes a sealed receipt that survives database rollback.

gdsgate --config authority.toml authority init-revocation-key \
    --path /var/lib/gdsgate/revocation.key

gdsgate authority init-approval-key [--path <PATH>] [--if-missing]

Generates a fresh 32-byte HMAC key for the just-in-time access-request flow. Wire [audit].approval_key_path to it so an approved request row is integrity-sealed against direct database tampering.

gdsgate --config authority.toml authority init-approval-key \
    --path /var/lib/gdsgate/approvals.key

gdsgate authority init-inventory-key [--path <PATH>] [--if-missing]

Generates a fresh 32-byte HMAC key for live-discovery inventory receipts, as init-anchor does. Wire [audit].inventory_key_path to it so each published backend inventory carries an integrity receipt that survives a direct rewrite of the row. Needed only where live resource discovery is used.

gdsgate --config authority.toml authority init-inventory-key \
    --path /var/lib/gdsgate/inventory.key

gdsgate authority init-delegation-key [--path <PATH>] [--if-missing]

Generates a fresh 32-byte HMAC key that seals each delegation grant, as init-anchor does. Wire [audit].delegation_key_path to it so a database-write-only attacker cannot forge a grant. Needed only where delegation is used; unset, gdsgate delegate is refused fail-closed.

gdsgate --config authority.toml authority init-delegation-key \
    --path /var/lib/gdsgate/delegation.key

gdsgate authority verify-audit [--checkpoint-keys <FILE>] [--keyring <REFERENCE>]

Off-band verifier: opens the store directly, replays the hash chain, and cross-checks every row in audit_anchor against audit_log under the operator's anchor key ([audit].anchor_key_path). Non-zero exit on any integrity failure. Run it from a workstation that has the key file but is not the live Authority host, so tampering is detectable even when Authority itself is suspected of compromise.

gdsgate --config authority.toml authority verify-audit
# audit chain ok (chain + anchors + checkpoints): 12483 rows, 4127 anchors, 61 checkpoints

--checkpoint-keys <FILE> is the mode for somebody who is not this deployment. Given the file keys checkpoint-keys printed, the run replays the chain locally and checks each signed checkpoint by signature: no keyring, no anchor key, nothing that could forge a row. Without the flag, the checkpoints are checked under the audit keyring's own published halves when this host names one.

gdsgate --config reviewer.toml authority verify-audit --checkpoint-keys checkpoint-keys.txt
# audit chain ok (chain + checkpoints): 12483 rows, 0 anchors, 61 checkpoints

--keyring <REFERENCE> is the mode for an operator who holds the audit keyring, typically on a workstation that is not the Authority host, which is where a verification worth having is run from. It reads a reference in the same secret-reference notation [security.integrity].audit_keyring accepts, including a wrapped ${kms.<name>+file:PATH} whose key [[kms_keys]] declares, and replaces that setting for this run. When given, the ring answers both halves of the check: anchors are cross-checked against the keys it holds, standing in for [audit].anchor_key_path, and checkpoints are checked against the public halves it publishes, ahead of the configured audit_keyring.

gdsgate --config reviewer.toml authority verify-audit --keyring /media/usb/audit.keys

--checkpoint-keys still decides the checkpoint half on its own when both flags are given: the file names the public keys checkpoints are checked under, and the ring named by --keyring still answers for the anchors. A reviewer outside the deployment holds no secret and needs only --checkpoint-keys; an operator with the keyring in hand runs a complete verification with --keyring alone. Only the audit ring is accepted; a file that names itself the control ring is refused.

After keys accept-loss, an anchor stretch sealed under the lost key is reported instead of failing the run:

not verified: serials 4..=9 (6 anchors) were sealed under key 3f9c1a2b4d5e6f708192a3b4c5d6e7f8, declared lost at 1755000000

The summary line comes first either way; everything outside a reported stretch was verified.

gdsgate authority audit list [filters]

Prints audit chain rows, newest first, through whichever of the routes --route names. Over the gateway or the control-plane certificate the read is decided by Cedar (viewAudit) and is itself written to the chain; --route store opens this host's database directly, the way verify-audit does, and needs store_url rather than a running Authority. Unstated, store is taken where store_url is set and the gateway otherwise.

Two output shapes. By default, a row of seven columns (time in Unix seconds, event class, action, code, actor, target, outcome) with anything else hanging under it on indented branch lines. --json prints one JSON object per row instead, for anything that reads the listing instead of looks at it.

gdsgate --config authority.toml authority audit list --class admin-action --limit 3
# # time      class        action          code      actor(verified)      target                             outcome
# 1753783204  AdminAction  catalog.manage  CAT-0101  gdsgate-provisioner  Database::"db-orders_prod"         Success
#     |- operation=create resource_kind=postgres
#     `- claimed operator (asserted by the caller, unverified): anna.ivanova@provider.example
# 1753783190  AdminAction  revokeNode                gdsgate-admin        EnrolledNode::"gdsgate-connector"  Success
# 1753782001  CaOperation  ca.rotate       CAS-0101  gdsgate-admin        CertificateAuthority::"user-ssh"   Allow

Every column is verified: the actor comes from a certificate or a token, never from the request body.

CODE is a stable event identifier intended for filtering, dashboards and runbooks: SES-0101 is a session opening whatever the action column spells in a given release. Codes are never reused and never renamed, and --code filters on them exactly. The cell is empty for a row whose writer records no code and for every row sealed before codes existed.

Column widths are computed per page, heading included, and each page carries its own heading. Reading this host's own store (--route store) is one page.

Variable event-specific fields are printed below the main row rather than as fixed columns: a ClientAssertion row names nine sandbox properties, a database row carries the statement that ran, while the fixed columns are a timestamp, two identifiers and two words.

A detail stored as an object is printed as key=value pairs separated by spaces, one branch line per row regardless of field count, without the braces and quotes of its stored form. A detail with no fields gets no branch line. --json carries the field exactly as the chain holds it.

A row on which a control plane named the person behind it gets a branch line marking that name unverified, because the columns hold what the cluster checked. A row with neither a detail nor a claimed operator prints one line.

On a colour terminal the branch lines are dimmed. Redirected to a file or a pipe, or with NO_COLOR set, the output carries no escape sequences.

--json is the complete way to take the detail back out: it carries the detail unescaped, and truncated says whether the server cut it.

gdsgate authority audit list --limit 200 --json |
  jq -r 'select(.detail != "") | [.time_unix_ms, .action, .detail] | @tsv'

From the text listing, a branch line is four spaces, `- or |-, and then the field, so sed reaches it:

gdsgate authority audit list --limit 200 | sed -n 's/^    [|`]- //p'

The columns themselves are padded to the page rather than separated by tabs, so cut -f does not read them; --json does.

The ClientAssertion class holds statements a client made about its own machine that the cluster cannot verify: currently the sandbox a delegated run reports. The actor is verified; the assertion is not, and the class separates the two for a reader who filters by class.

CaOperation holds operations whose object is a certificate authority, a rotation and a refusal of one, and nothing else. The certificates an authority signs are recorded under the class of the thing signed.

Flag Default Purpose
--class <CLASS> every class One of authentication, authorization, ca-operation, session-activity, admin-action, capability-grant, client-assertion, resource-integrity. Selects the class a record was sealed with: records written before a class was split keep the class they had, so a filter that must span both epochs uses --code.
--actor <ID> any Exact match on the verified actor.
--action <PREFIX> any Action prefix: enroll., session., policy.. Rows sealed under an earlier spelling keep it, so a prefix that names an epoch finds one epoch; --code finds both.
--code <CODE> any Exact match on the event code, for example SES-0101. Exact instead of a prefix, since a code names one event. Rows carrying no code match nothing.
--target <ID> any Exact match on the target.
--since-ms <MS> no floor Inclusive floor on event time (Unix milliseconds).
--limit <N> 50 Cap on rows; 0 prints the whole chain, which only --route store will do. Over a network it is a page size, and pages are followed until the cap is met.
--json off One JSON object per row instead of the columns.

The column layout is for reading, not for parsing: columns are padded to the page rather than delimited, a cell is empty where the row carries no value, and the detail and a claimed operator are branch lines rather than fields. Use --json for programmatic access:

gdsgate --config authority.toml authority audit list --json --limit 1
# {"serial":682,"time_unix_ms":1785974336000,"class":"AdminAction","action":"audit.view",
#  "code":"AUD-0101","actor_verified":"anna.ivanova@provider.example",
#  "target":"AuditLog::\"chain\"","outcome":"Allow","detail":"{\"page_size\":1}",
#  "truncated":false,"submitter":"gdsgate-authority"}

The object is the same on every route. A class or outcome code this build does not know is preserved as class(200) instead of dropped. A detail the server cut is reported as "truncated": true. A field with no value is an absent key rather than an empty one, so code is absent on a row that carries none. Hashes are not included: a listing reports what the chain says, and verify-audit is what proves it.

There is no filter on the asserted operator name, which is optional and unverifiable, so filtering on it would silently exclude every row where nobody was named. Filter on --actor, which is verified.

This command reads the chain and does not verify it. Pair it with verify-audit.

gdsgate authority migrate [--check]

Brings the store's schema up to date and exits. Opens the store directly, so no running Authority is needed; point it at the same persistent store_url Authority uses. Idempotent: a second run applies nothing.

gdsgate --config authority.toml authority migrate
# applied 3 migration(s); store schema up to date

gdsgate --config authority.toml authority migrate
# store schema up to date — nothing to apply
Flag Default Purpose
--check off Report only: changes nothing and exits non-zero if the store has pending migrations. The form to run before a rollout.
gdsgate --config authority.toml authority migrate --check
# store schema has 2 pending migration(s): m0036_owner_revocation, m0037_delegation_grant_root_owner

Pair it with store_auto_migrate = false to keep schema changes a step of the rollout rather than a side effect of a service start.

A store that carries migrations this binary does not know about was migrated by a newer build. migrate refuses it in both modes, as do a service start and gdsgate doctor; run the newer build on that host.

gdsgate authority policy template

Prints the canonical starting policy for a new cluster on stdout. Runs offline, reading neither the store nor the network, and needs no running Authority.

gdsgate authority policy template > /etc/gdsgate/policy.cedar

The template grants the control-plane admin identity (Group::"gdsgate-admin", the CN of the admin mTLS certificate) the six provisioning actions: account lifecycle, CA rotation, the catalogue, join tokens and the node registry. It grants the provisioning control plane (Group::"gdsgate-provisioner", CN=gdsgate-provisioner) the catalogue, the registry and join tokens for three named roles; and one group of people from the identity provider (Group::"admins") the day-to-day rights (listing, sessions, databases, MCP tools, approvals, recordings, policy editing) plus the catalogue and the registries, so an administrator runs the cluster through the gateway under their own identity. It also carries the break-glass rule every later push is checked against. Everything else, a second group for a team, SSH -L / -R, onward certificates, is a commented example.

A cluster with no external provisioning system deletes the Group::"gdsgate-provisioner" block: the certificate is then never issued.

Rename admins to a group your identity provider emits, name the file in [policy].path and start Authority: the file is seeded as policy version 1 while the store holds no policy version. See Policy → The starting policy.

gdsgate authority policy validate [<FILE>] [-o <FILE>] [--rule-ids]

Strict-validates a Cedar policy against gdsgate's schema and exits. Non-zero on any error, so it works as a CI gate. Runs offline and does not contact the running cluster.

gdsgate --config operator-workstation.toml authority policy validate
gdsgate --config operator-workstation.toml authority policy validate ./new-policy.cedar

Without <FILE>, validates [policy].path from the config.

A policy written in the vocabulary v0.8.23 retired (sshConnect, dbConnect, tcpConnect, k8sAccess, k8sReadOnly, the entity type Server) does not validate as it stands, and the cluster takes it anyway: it migrates the document when it is pushed and when it is loaded. This command therefore migrates a copy, reports policy OK after migration, exits zero, and lists every change and everything a person should read. --output <FILE> writes the migrated text, which is how the policy in your repository gets brought up to date.

One substitution needs the catalog and so cannot happen here: before the rename an MCP backend's session was decided over TcpService::"<id>", and only the catalog says which ids those were. Such a rule is left as it stands and named in the output; the cluster completes it on push.

Anything else invalid still fails.

--rule-ids also lists the document's rules under the names its decisions are recorded with. A policy.decision record names the rules it came out of in deciding, and a rule is named by a digest over its own text, so the name survives a statement being added or removed elsewhere in the document, and this listing is how such a name is read back as a rule:

policy OK: ./policy.cedar
  34 rules, as an audit row names them:
  02726249cc2326ce  policy0    permit(principal in Group::"gdsgate-break-glass", action == Action::"editPolicy", resource);
  48bc8dac906e102c  policy1    permit(principal in Group::"gdsgate-admin", action == Action::"enrollNode", resource);

The middle column is the same policy0 the record carries in deciding_at. Run the listing against the version of the document the record was decided under; the version is on the chain, under policy.edit. For a document that needed migrating, the rules listed are the migrated ones, because those are the rules the cluster loads.

Flag Default Purpose
-o, --output <FILE> not set Write the migrated policy here. Written only when the document needed migrating.
--rule-ids off Also list every rule under the name its decisions are recorded with.

gdsgate authority policy pull [-o <FILE>]

Fetches the active authorization policy from the cluster, through Proxy to Authority, and prints it to stdout, or writes it to --output <file>. The call is gated by the Cedar editPolicy action over the caller's verified identity, so only authorized admins can read it back. When the store has no policy yet, prints nothing and logs no policy stored yet (Authority is on the deny-all bootstrap) on stderr.

gdsgate --config client.toml authority policy pull -o policy.cedar
Flag Default Purpose
-o, --output <FILE> not set Write the policy here instead of stdout.

gdsgate authority policy push <FILE>

Strict-validates <FILE> locally as a fail-fast review lint, then uploads it as the new active policy through the cluster. The server strict-validates again, persists a new version, and hot-reloads it across every Authority instance, with no restart. Gated by the Cedar editPolicy action.

gdsgate --config client.toml authority policy push ./policy.cedar

Policy lock-out risk

The policy you push must keep an editPolicy permit for your group, or no one can push again. Recovery is on the Authority host: the emergency administrative socket if one is configured (policy push --route emergency, no restart), or [policy.editor].break_glass_policy_file, a policy file a restart applies past the editPolicy gate. Restarting Authority against the [policy] file does not recover a lock-out: that file seeds a store with no policy version, and a locked-out cluster has one. See Admin guide → Lock-out and recovery.

gdsgate authority policy edit

Opens the active policy in a built-in full-screen terminal editor. On entry the policy is pulled; on Ctrl-S it is validated locally and pushed, hot-reloading cluster-wide. Both calls are gated by the Cedar editPolicy action. The editor needs an interactive TTY.

gdsgate --config client.toml authority policy edit

The editor combines a buffer engine (tui-textarea) with a custom ratatui renderer for Cedar-aware presentation:

  • Live lint: every keystroke re-runs the same strict Cedar validator as policy validate. The first error message shows in the status bar, and the offending line is painted with a red background in the buffer.

    A document written in the vocabulary v0.8.23 retired is not an error: Ctrl-S migrates it, as push does. The status bar carries a line of its own naming the vocabulary and listing what the migration will change, the same list policy validate prints for the same text.

  • Syntax highlighting for keywords (permit, forbid, when, unless, in, has, like, if, then, else, true, false), quoted strings, line comments (// …) and capitalised entity-type names (User, Group, Action, SshHost).

  • Context-aware completion: Tab opens a popup of candidates scoped to where the cursor is.
    • Inside a Type::"…" literal: only ids of that type from the schema and live catalog (Action::"<action>", SshHost::"<id>", Database::"<id>").
    • Right after resource. / principal. / context.: the attributes Cedar declares for that base, and resource also offers getTag / hasTag.
    • Elsewhere: entity types and actions from the schema, Cedar keywords, live resource ids from gdsgate ls.

The popup narrows as you keep typing, and Backspace widens it; Enter or Tab accepts; Esc or any other key cancels. An empty prefix shows every candidate in the current context. - Search: Ctrl-F opens a status-bar prompt with a live match counter; Enter jumps to the next match, substring and wrapping, and matches get a yellow background. Esc exits. - Format: Ctrl-L runs the official cedar-policy-formatter. A pre-format snapshot is taken so a single Ctrl-Z right after Ctrl-L restores the original, in one step. - Selection and clipboard: Shift with arrow keys, or Ctrl-A, select; Ctrl-C copies, Ctrl-X cuts, Ctrl-V pastes, through an internal yank buffer rather than the system clipboard; Ctrl-Z undoes and Ctrl-R redoes. - Navigation: arrows move by character and line; Page Up and Page Down move a screen and take the screen with them; Home and End go to the ends of the line and Ctrl-Home and Ctrl-End to the ends of the document. Held with Shift, each of them extends the selection. Scrolling follows the cursor both ways, and the active line gets a faint background and a highlighted line number. - Deploy and quit: Ctrl-S validates and pushes; on success the editor exits and prints policy deployed — now active as version <N>. Ctrl-Q quits, and if the buffer is dirty a compact confirmation modal appears, where Y discards and any other key keeps editing.

The status bar shows the lint state (✓ green, ✗ red with the first error), the migration notice when there is one, the keybindings, and the result of the most recent deploy attempt.

Key Action
Type / arrows Edit and move.
Home / End Start / end of the line.
PgUp / PgDn One screen up / down.
Ctrl-Home / Ctrl-End Start / end of the document.
Shift + any of the above Extend selection.
Tab Open context-aware completion.
Ctrl-A Select all.
Ctrl-C / Ctrl-X / Ctrl-V Copy / cut / paste (internal).
Ctrl-Z / Ctrl-R Undo / redo.
Ctrl-F Search; Enter for the next match, Esc to exit.
Ctrl-L Format the buffer.
Ctrl-S Validate and deploy (push, then hot-reload).
Ctrl-Q Quit; confirms when dirty.

gdsgate authority policy pending-edits

Lists every policy proposal waiting for approval. Use it when [policy.editor].min_approvers is 2 or more, so a push lands as a pending proposal instead of activating immediately. Gated by the Cedar approvePolicyEdit action.

gdsgate authority policy approve-edit <version>

Casts one approval vote on a pending proposal; the <version> comes from pending-edits. When the proposal reaches the configured quorum of distinct approvers, it becomes the active policy and hot-reloads cluster-wide. Gated by the Cedar approvePolicyEdit action.

gdsgate --config client.toml authority policy approve-edit 7

gdsgate authority recordings list [filters]

Lists recent session-close events, newest first, with the recording's cast URI. Gated by the Cedar viewRecordings action.

gdsgate --config client.toml authority recordings list --target prod-db --limit 20
Flag Default Purpose
--actor <name> not set Filter by the certificate subject that ran the session.
--target <id> not set Filter by resource id.
--since-ms <unix-ms> 0 Only events at or after this time (Unix milliseconds).
--limit <n> 0 Cap the number of rows; 0 uses the server default.

gdsgate authority recordings show <session_uid>

Prints a finished recording to stdout, rendered for reading. A terminal recording comes out as its cast bytes, which pipe into asciinema play - to replay. An exchange recording, a web or model session, is rendered instead: what was asked, what came back, and what was not written down. See Configuration → Recording the exchange. --raw gets the artifact's own bytes unchanged, whatever it is. Gated by viewRecordings.

SESSION_UID="<session_uid>"
gdsgate --config client.toml authority recordings show "$SESSION_UID" | asciinema play -
gdsgate --config client.toml authority recordings show "$SESSION_UID"          # a web or model exchange, rendered
gdsgate --config client.toml authority recordings show "$SESSION_UID" --raw    # the artifact's own bytes

gdsgate authority recordings live <session_uid>

Tails an in-progress recording, printing asciicast chunks as the session produces them. Take the <session_uid> from gdsgate session list. Interrupt to stop. Gated by viewRecordings.

create-token rotate-ca rotate-onward-ca rotate-host-tls-ca rotate-spiffe-ca rotate-jwt-ca onward-ca-pub onward-ca-pubs policy recordings audit list-nodes list-offered revoke-node delete-node list-workloads revoke-workload import-node list-serving list-resources set-resource remove-resource bootstrap-admin bootstrap-provisioner init set-profile init-anchor init-approval-key init-revocation-key init-delegation-key init-inventory-key keys verify-audit migrate

list show live

list

validate pull push edit approve-edit pending-edits template

Source contract

The nested enums in crates/gdsgate/src/main.rs are the source of truth for Authority, policy, audit, and recording actions. Each command section above retains its exact syntax, defaults, conflicts, route, identity, output, audit, error, and recovery contract.