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].
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:
gatewayis 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_ipis 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.certificateis 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.emergencyis 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 carriespolicy pull,policy push,list-nodes,list-workloads,list-resources,set-resourceandremove-resource, and refuses everything else by name. See Admin guide → Lock-out and recovery.storeis not a general bypass. It is accepted only byauthority audit list, which reads the local audit rows without a network hop;verify-auditis also local but has no route flag. The command rejectsstorefor 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.pemandca.pemare all readable;cert.pemparses, 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.pemstill carries the CA the cluster serves with, without which the admin could not verify Authority on the next handshake;key.pemis the private half ofcert.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.
| 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 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 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 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 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 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.
--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.
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.
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 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.
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, sodb-orders_prodyields 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,llmandweb-egresshave 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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
--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:
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.
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.
| 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.
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.
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-Smigrates it, aspushdoes. The status bar carries a line of its own naming the vocabulary and listing what the migration will change, the same listpolicy validateprints 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:
Tabopens 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 offersgetTag/hasTag. - Elsewhere: entity types and actions from the schema, Cedar keywords, live
resource ids from
gdsgate ls.
- Inside a
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 authority recordings list [filters]¶
Lists recent session-close events, newest first, with the recording's cast URI.
Gated by the Cedar viewRecordings action.
| 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.