Release history¶
This page is the canonical, complete reverse-chronological release history. Historical bodies are retained as published, including stale command spellings; current equivalents belong in the upgrade guide.
The current workspace release is 0.8.23-rc.2. Channel status, artifact verification, rollout classes, and the recovery boundary are in the upgrade guide.
v0.8.23-rc.2¶
Release identity and rollback boundary
v0.8.23-rc.2 is the exact manifest/tag identity. The release adds four
store migrations, m0049 through m0052. Store migrations are
forward-only: a binary older than this release refuses a store they have
been applied to. [security].profile is new. Left unset it changes nothing
a running cluster already did: no new refusal, no new warning, no banner.
Declared as production it adds start-up refusals, and an Authority that
declares it does not start until the configuration answers every one: both
[security.integrity] keyrings named, naming different files, and
owner-only; every Authority replica that is still confirming holding the
same keys of each keyring; the newest sealed row of every table naming a
key one of the keyrings holds or one whose loss has been declared; and
[oidc].issuer with [oidc].client_id both set. The five
[audit].*_key_path parameters go on being read under every profile, and
gdsgate authority keys migrate-legacy brings the keys they name into the
keyrings. Before upgrading, export the audit anchors and take a store
backup. If rollback is required, restore the pre-upgrade store and its
external keys rather than downgrading rows.
Added¶
${file:PATH},${env:NAME}and${exec:COMMAND ARG...}in place of a configuration value that is a secret or names one. Fifteen parameters accept the notation, and a bare value keeps the reading it always had, so a file written before the notation existed is unchanged.exec:runs without a shell, passes nothing in its arguments, and a non-zero exit refuses the start. A listener's certificate key is resolved again at each certificate rotation, not only at start-up. See Configuration → Secret references.store_password_file: the state backend's password, kept out ofstore_url. A path or a secret reference, also set byGDSGATE_STORE_PASSWORD_FILE. PostgreSQL only.[security].profile, the deployment profile a cluster is held to at start-up:dev,pilot, orproduction. Unset changes nothing.devrefuses nothing and prints one line naming the deployment a development one.pilotandproductionrefuse a start that would keep the cluster's state in a file, serve a listener in the clear, or run all three roles off a certificate authority held in one process's memory;productionrefuses more. A refusal names every fault present at once. See Configuration →[security].[security.integrity]withcontrol_keyringandaudit_keyring: the two keyrings a cluster's sealed rows go under, kept apart because the audit journal outlives the decisions the control keyring seals. Both fields accept the secret-reference notation. Unset, Authority goes on reading the five[audit].*_key_pathparameters.gdsgate authority keys, nine sub-actions over the two keyrings and the CA signing seeds kept beside them:status,verify,backup-manifest,accept-loss,rotate,checkpoint-keys,migrate-legacy,wrap-ca-seeds, andprune-ca-keys. None of them prints key material.keys migrate-legacybrings the keys the five[audit].*_key_pathparameters name into the two keyrings, as records that verify existing rows and seal nothing new. Nothing is re-sealed and no key is generated.keys rotate <control|audit> --prepare | --activate | --retiremoves a keyring on to a new key in three steps, with a rolling restart between the first two, since a keyring is read once at start-up.--activateis refused while an Authority replica that is still confirming has not published that it holds the prepared key.--retireis refused while a row still names the key, and always for theauditkeyring.--prepare --kind signingrotates the checkpoint-signing key instead of the key that seals rows.keys accept-loss --key-id --accepted-byrecords that a key is gone.gdsgate authority verify-auditthen reports the anchors it sealed as not verified, naming the serial range, instead of failing the run.- Signed audit checkpoints. Authority signs the chain head into the store every
[audit].checkpoint_interval_secs(3600 by default) whenever theauditkeyring holds a signing key.keys checkpoint-keysprints the public halves, andverify-audit --checkpoint-keys <FILE>replays the chain and checks each checkpoint by signature, so a reviewer outside the deployment can verify the journal while holding no key material.verify-audit --keyring <REFERENCE>answers both halves of the check from a keyring named for that run. gdsgate authority init --profile <dev|pilot|production>: the first run of an Authority. It writes the configuration, puts the two keyrings in place, and prints one JSON object of fingerprints, references and paths, with no key material in any field. Underproductionit refuses to mint a keyring on this host: name one already in place, or a wrapped one whose key--kms-keydeclares.gdsgate authority set-profile --profile --changed-bydeclares a different profile afterwards, writing the change to the audit chain before rewriting the one line in the file.[[kms_keys]]: the external keys this deployment's wrapped material sits under.${kms.<name>+file:PATH}, and the same prefix overenv:andexec:, says the source holds ciphertext and names the key that opens it.provider = "exec"is in every build;vault(the transit engine of Vault and OpenBao),aws, andgcpneed a build carryingkms-vault,kms-aws, orkms-gcp, and a row naming a kind the binary was not built with is refused by name. Credentials come from the process environment. A key answersunwrap,wrapandgenerateand nothing else, and no material crosses in an argument in either direction. See Configuration →[[kms_keys]].[authority].ca_kms_key: the wrapping key CA signing seeds are written under. Every seed written from then on is ciphertext in the store, opened when a generation is loaded and never on the path that signs a certificate.keys wrap-ca-seedsconverts the generations already stored, and a table converted only partway runs either way, because each row states which form its own seed is in.keys prune-ca-keysremoves the private keys of CA generations nothing will use again. Without--deleteit lists them and removes nothing. Each removal is written to the audit chain before the row goes, and no command puts one back.
Changed¶
- A sealed row is verified under the key its own fingerprint names, so a keyring holding several keys verifies every row any of them sealed. Approvals, revocations, delegations, inventory receipts and the audit anchors all select this way.
- New rows go on being sealed under the five
[audit].*_key_pathkeys until a keyring holds an active key of its own, so the five parameters stay in the configuration and keep being read afterkeys migrate-legacy. - Authority leaves closed the signing seed of a CA generation nothing can reach
again: one that is neither of its authority's two newest, that no rotation
role names, and whose validity window closed more than thirty days ago.
Under
[authority].ca_kms_keyevery generation opened at start-up is one call to the key service. gdsgate authority init-anchorand the four otherinit-*-keycommands refuse when the parameter they would write names an environment variable, a command, or a wrapped source instead of a file, since a key delivered that way is created where it lives.--pathnames a file to write.gdsgate doctorreports which form each covered parameter is written in and where its value comes from, without reading the material behind it, and checks permissions only where the parameter names a file on disk. It also reports the declared profile, whether each[[kms_keys]]entry is one this build can use, and which of the five legacy key files a named keyring has not taken in yet.
Fixed¶
- A password left in
store_urlis withheld from every place the connection string is printed: log lines,gdsgate doctoroutput, and the refusals the database driver raises with the string quoted inside them. gdsgate doctorchecks all five audit signing keys and all four listener keys. The delegation seal, the inventory seal, and[proxy].webapps_key_filehad no row.
v0.8.23-rc.1¶
Release identity and rollback boundary
v0.8.23-rc.1 is the exact manifest/tag identity. The release changes
certificate compatibility: a certificate issued under --as is refused by a
connector or proxy older than this release, so roll the fleet together and
upgrade proxies and connectors before the Authority that mints them. Store
migrations are forward-only; an older binary refuses a store migrated by
this release. Before upgrading, export the audit anchors and take a store
backup. If rollback is required, restore the pre-upgrade store and its
external keys rather than downgrading rows.
Added¶
routed_rangeson akind = "web-egress"backend: the address ranges this way out may dial although no registry allocates them to the public internet. Default empty.allow_agent_forwardon akind = "ssh"backend, defaultfalse, and thesshForwardAgentpolicy action.ssh -Arequires both.ssh:forward@<host>andssh:agent@<host>in the capability vocabulary.ssh -Landssh -Rtake the first,ssh -Athe second, and a delegated session carriesssh:connect@<host>alongside either. Mintingssh:forwardrequires the delegator to hold bothsshForwardLocalandsshForwardRemote.- An agent-bound session takes the onward hop of a jump-host backend where its
grant and the policy permit it.
gdsgate delegate --execreached no such backend before. The hop needs a connector of this release or newer.
Changed¶
gdsgate --as <grant>narrows every operation in the session. A grant that namesdb:readrefuses writes; one that names a single MCP tool refuses the others. Revoking the grant or letting it expire refuses the session's next operation.- A certificate issued under
--ascarries the delegation grant in its signed bytes. A connector or proxy older than this release refuses it; update them before the Authority that mints it. A certificate issued without--asis unchanged. - A delegated session forwards only what its grant names.
ssh:connectnames no forward, so a grant that openedssh -L,ssh -Rorssh -Aneeds the new word added to it. - An agent-bound session forwards where its grant and the policy permit it. Such forwards need a connector of this release or newer.
- A cached or held credential answers
--as <grant>only when it is that grant. A credential minted for another grant, or one holding none, is replaced by a fresh certificate. - Controlled web egress dials an address only when it is globally routable
unicast and outside every range the IANA special-purpose registries reserve. A
deployment that relies on reaching a reserved range names it in
routed_ranges. ssh -Ais refused unless the backend enables it and the policy permits it. A refused session receives noSSH_AUTH_SOCK.- A connector registration states the kind of every resource it declares. Upgrade proxies before authorities: a proxy older than v0.8.19 forwards no kinds, and its connectors' tunnels are dropped until it is upgraded.
- A resource stays with the connector holding it. Replacement requires revoking that connector or removing the catalog row.
Fixed¶
- A one-time step-up grant is spent by exactly one operation. Concurrent requests no longer both pass on a single approval.
- Kubernetes watch requests are classified from the parsed query, so the policy question and the audit row name the verb the cluster runs.
?watch=trueon a named object is aget, as the API server reads it.- The Kubernetes path parser reads the legacy
watchandproxyprefixes, treatsHEADasGET, and reads a namespace addressed as an object. - A person's grant is read from the signed field of their certificate, and a
delegation's from its subject. An identity provider whose
subis spelled like a delegation subject no longer places the session in that grant. gdsgate delegation statusno longer fails on an ordinary certificate subject.
v0.8.23-alpha.12¶
Release identity and rollback boundary
v0.8.23-alpha.12 is the exact manifest/tag identity. The release changes
certificate and PostgreSQL-wire compatibility: roll the fleet together and
do not mix this binary with an older connector/proxy that cannot verify its
certificates. Store migrations are forward-only; an older binary refuses a
store migrated by this release. Before upgrading, export the audit anchors
and take a store backup. If rollback is required, restore the pre-upgrade
store and its external keys rather than downgrading rows.
Added¶
- HTTP(S) applications are a lane of their own: a
kind = "web"backend is reverse-proxied with a policy decision on every request, and policy is written over named path families ([[connector.backends.routes]]).gdsgate web proxy <resource> --listenopens one locally, and a resource of this kind also answers by name undergdsgate up. See User guide → Web applications and The web lane. httpRequest, decided for every request on the web lane overWebApporWebEgress. See Policy → HTTP(S) application routes.kind = "web-egress": a forward proxy over a list of names, pointed at a delegated program through$HTTP_PROXY. Addresses outside the public internet are refused.inspect = trueterminates TLS beside the cage and decides every request inside it. See Reading what a way out carries.web:tunnel, the capability verb for a channel the gateway does not read:CONNECTout of the zone and a WebSocket upgrade into an application, neither covered byweb:write.- WebSocket upgrades where a backend sets
websocket = true. The channel ends when the certificate that admitted the session expires. - Recording of the web exchange at a level chosen per direction: nothing, fingerprints, redacted bodies, or bodies verbatim. Credential headers are withheld at every level. See Recording the exchange.
identity = "signed"puts a short-lived signed assertion inGdsgate-Identity, audience-bound to the resource, and stops the caller's ownAuthorizationfrom travelling. Applications verify it against the JWKS at/.well-known/gdsgate/jwks.json, rotated bygdsgate authority rotate-jwt-ca. See Telling the application who came.- Clientless browser entry: with
webapps_zone,webapps_addrand a wildcard certificate for that zone, a browser reaches an application athttps://<application>.<zone>after signing in at the identity provider. The session appears ingdsgate session list. See The clientless browser entry. - The browser entry and the public listener take a client's address from a PROXY
protocol header (versions 1 and 2) from fronts named in
[proxy].webapps_proxy_protocol_fromand[proxy].public_proxy_protocol_from. Both are empty by default; with a list set, a connection from an unlisted address, or from a listed one without a header, is closed. See Behind a front. [transport].front_sni: the name a cluster answers to on a shared front, bound into every TLS-terminating role's leaf and requested by nodes on the reverse tunnel and on renewal. Authority advertises it to joining nodes. See The name a shared front routes on.- A catalogue entry's
native_nameis answered for by the resident client where the local[client].intercept_namesalso names it, with TLS terminated by that machine's own root (gdsgate web trust). See Resident client → An application's own name. context.presentedis"key"or"cookie"onconnectandhttpRequest, and session and decision rows carry it on every lane. See Policy → Who arrived with what.- CockroachDB backends:
kind = "cockroach", default port 26257, served as PostgreSQL is,auth_mode = "session_role"andenforce_query_categoriesincluded.gdsgate doctor's session-role grant probe does not cover it. read_only = trueon a session-role profile, checked bygdsgate doctoragainst every database the session can reach, andsettable_parameters, which widens what a restricted client maySET. See[[connector.backends]].- Three records report a database write that contradicts the gateway's decision:
db.write_in_read_only_session,db.write_refused,db.write_misclassified. See Operations → Database observations and gateway decisions. authority audit list --operator <NAME>records, as asserted, the person a control plane is reading for. Refused on--route store.- One-time step-up grants for
kubeRequest,viewRecordingsandissueJoinToken. Seegdsgate step-up. authority policy pushwarns about aconnectrule that names no kind of resource, andauthority policy validate --output <FILE>writes the migrated policy with every change listed. Seegdsgate authority policy validate.- Raw-TCP sessions are recorded as
session.open/session.closewithkindtcp, and the close record carriesbytes_up,bytes_downandreason. - Audit rows carry an event code, shown as a column by
authority audit list, filtered with--code <CODE>and reported ascodeunder--json. Seegdsgate authority audit list. max_request_bytesbounds the request body aweborweb-egressbackend carries. Default0, no ceiling.
Changed¶
- Roll the fleet together: an access certificate now states what its holder proved, a key or a browser session cookie, and a connector or proxy older than this release cannot verify a certificate issued by it.
- The five session-level Cedar actions are one,
connect, with the resource kind named by its entity type.k8sAccessandk8sReadOnlyarekubeRequest, with the read-only distinction incontext.read_only;ServerisSshHost; a catalogue-wide decision is taken overCatalog::"all"; an MCP backend opens overMcpServer. Existing policies are migrated on push and on read, and the rewriter reports what it will not touch. See Policy → Oneconnect, seven kinds of resource. - A policy decision is recorded as
policy.decision, with the schema action in the detail undercedar_action, so--action kubeRequestbecomes--code POL-0201. Decisions name the rules they came from (deciding,deciding_at) and the session they were taken in (session_uid); the thirteen control-plane actions that write no decision row carry the same fields on the row of the thing they gate. Details are objects with named fields, printed askey=valuepairs on a branch line. See Operations → What lands in audit. - Session records use one pair of names,
session.openandsession.close, with the protocol inkind;k8s.requestiskube.request; Kubernetes requests andsftp.*records carrysession_uid. - Control-plane event names carry a dot:
editPolicyispolicy.edit,manageCatalogiscatalog.manage,rotateCAisca.rotate. Event codes did not move, so a filter on--codespans both spellings. - The
Authorizationclass holds policy decisions only.session.no_routemoved toSessionActivity, and the seven MCP descriptor records to a new class,ResourceIntegrity(--class resource-integrity). A filter written on--class authorizationto catch either must move. Rows already on the chain keep the class they were sealed with. - The CEF export carries the event code as Signature ID, the event class as an
extension key, the row's millisecond as
rt, the running version as Device Version, and the detail as the last field. Severity is ranked from the outcome (Success1,Allow3,Failure5,Deny7). A SIEM rule that read the outcome from the seventh header field must read theoutcomeextension key. See Administration → Audit export. - Policy denies on the Kubernetes, MCP and model lanes, and a refused database
statement, are sealed as
denyrather thanfailure. A backend refusing its own caller stays afailure, as does a gate that could not be asked. Rows already on the chain keep their outcome. See Operations → What an outcome means. - The web lane streams a request body instead of holding it: the 32 MiB limit
that answered
413is gone, replaced bymax_request_bytes. A recorded request with a body is written as ahead, its chunks and anend, making thegdsgate.http.exchangeformat version2;recordings showrenders version1unchanged. - A client whose request body ends before its declared length is answered
400rather than502. - The settings advertised to a joining node no longer carry a client-facing TLS server name.
context.db_roleis present only when the client named a session role; a rule reading it needscontext has db_role.- The model lane refuses a connect-level authorization; every model call is
decided by
llmCall. - A restricted database session may set a fixed set of parameters and no others,
and statements are judged individually, inside a
DObody too. Categories follow what a statement does:SELECT … INTOisddl, a locking clause iswrite,COPY … TO STDOUTisread. See Policy → Per-statement database authorization. authority bootstrap-adminrenews the identity it holds instead of enrolling a second one, keeping its registry row. An empty directory still enrolls, and so does an expired identity.- Listings size their columns to their contents, and an audit row's detail is a
branch line instead of a column; read it back with
--json.
Fixed¶
- A resource served as a protocol the gateway does not know is refused instead of being decided as an SSH host; the refusal names the version mismatch.
- The browser entry refuses a name served as something other than a web
application before the person is sent to their identity provider: a
404, and no decision row. - A connector that serves only Kubernetes now evaluates
kubeRequeston every apiserver call; previously the session-open decision stood for the whole session. This is stricter: calls are denied wherever nokubeRequestrule permits them, so review the cluster's rules before upgrading such a connector. See Policy →kubeRequestandcontext.read_only. - A local forward is judged by policy on every connector. This is stricter:
ssh -Lon a connector that held no other line to the control plane now needs asshForwardLocalrule, which the baseline policy grants to nobody.-Ris unchanged. See Configuration → Forward gating. - A connector serving only raw TCP, only MCP, or only an SSH resource it terminates itself admits workload identities; such sessions were previously refused. People were unaffected. See Policy → Authorizing a workload.
- Client-chosen field lengths no longer decide whether other people's audit rows
are written: request paths, SQL statements and parameters, model, tool and
resource names, an SFTP rename's old name and a pinned MCP revision are cut
before the row is built, and a cut row names the field.
actorandtargetare bounded, and Authority refuses a submitted row over the bound. - A connector no longer retries indefinitely against an Authority that commits
none of a batch: "none accepted" counts as no progress, with an
errorline when the retry budget runs out. - SSH sessions, remote commands and
sftpthrough a jump host answer clients that block until a request is confirmed (Go'sx/crypto/ssh, libssh, paramiko).envandx11-reqare refused rather than ignored. OpenSSH was unaffected. See User guide → SFTP. - Answers reach the caller whole on the model, Kubernetes and SSH lanes,
including
kubectl execoutput. - A Kubernetes request stopped by an unreachable gate is recorded with the status
the client received, and a Kubernetes
execthat opened is recorded as a success. - A recording left on the connector's disk is no longer reported as uploaded. A pushed CA trust bundle is announced by what it changed and warns when no key in it could be read.
gdsgate session killreports only sessions it terminated, a health report Authority turned down is not logged as reported, andgdsgate loginnotifies a resident client, sogdsgate statusreports the new identity at once.authority policy editreads a document in the retired vocabulary the wayauthority policy validatedoes, listing what the migration will change rather than marking every such line an error.Page Up,Page Down,Ctrl-HomeandCtrl-Endmove in it.gdsgate login --browserreports the identity provider's refusal in the provider's own words and answers the browser instead of waiting out the login.- A browser login refused for requesting
offline_accessis retried without it, and the identity it obtains is reported as not renewable.
v0.8.22¶
Added¶
- The administrative catalog listing says whether a connector is serving each resource. It is filled by the gateway as it relays; asked of Authority directly it stays unknown, which is not the same as no.
authority list-resources --json: one object per resource,servingamong them.authority list-servingasks the gateway which resources have a tunnel behind them at that moment, for a control plane that administers a cluster with a certificate and has no person to sign in as.
Changed¶
- The release publishes one binary, the static
…-x86_64-unknown-linux-musl. The glibc build is gone; install the static one in its place. - mimalloc is the global allocator, in every build and for every role.
- Names resolve through
getentwhen the C library does not answer them, so a host's own sources (mDNS, a directory service) are reachable from a static build. - The server image carries the released binary.
sha256sum /usr/local/bin/gdsgateinside the image matches the line in the signedSHA256SUMS.
v0.8.21¶
Added¶
Authority.ListAuditEventsserves the audit chain over the gateway under the caller's own identity, the way the node registry and the resource catalogue are already served, so reading the chain no longer requires access to the database. The action isviewAudit, taken overAuditLog::"chain", and it is granted on its own rather than as part ofviewNodesorviewRecordings. A read of the chain is itself written to the chain, allowed or refused. Pages are keyed on(time, serial), so a chain that grows while it is being read does not repeat or skip a row.gdsgate authority audit listtakes the same--routeas every other administrative command, with the database available as--route storewhere the caller has it.gdsgate authority audit list --json: one JSON object per row, the same object on both--routevalues. The column layout stays the default and is unchanged. A class or outcome code this build does not know is kept asclass(200)and not dropped; a detail the server cut is reported as a boolean; a field nobody filled in is an absent key. Hashes are not included;gdsgate authority verify-auditis what proves the chain.gdsgate doctorreports whether its sandbox readings hold for other runs. The board's properties are measured inside thedoctorprocess, and on a host that restricts unprivileged user namespaces what a caged program gets depends on the confinement profile the run carries. Thesandbox.userns_confinementrow reports which case this host is in: unrestricted, a profile that names this program, a profile inherited from whatever started this one, or a run with no profile at all, whose cage loses the privileges it needs. Where a profile is wanted and missing, the row says what it has to grant.
Changed¶
["*"]in a model backend's allow-lists means every element. It is a value inallowed_request_headersandallowed_query_paramsrather than a name to match, so a backend that has to pass on whatever a particular client sends no longer needs that client's headers enumerated. It goes alone in its list: a list holding both*and names is refused at start-up, naming the list. Under it every header travels except those the lane strips from every call andcredential_headeritself. This widens what reaches the provider: in substitution mode the lane presents the resource's credential in place of the caller's, and under*a credential the caller sends under any other header name travels with the call. The connector states this at start-up, per resource. Where that is not wanted, name the headers instead; the enumerated list behaves as before.
Fixed¶
- An SFTP upload is on the host when the close answers. The server flushes the
file before it acknowledges
SSH_FXP_CLOSE, so a transfer that reported success is one the next reader finds whole. A failed flush is answered as a failure. - Every exit path of
gdsgate joinreads the piped token to the end. A run refused from its arguments and a--printpass drain the standard input before they leave, so a caller piping a token is answered by the exit code rather than by a broken pipe. What is drained is zeroized and reaches nothing that is printed.
v0.8.19¶
Added¶
gdsgate statusreports whether anything is serving a resource, in a newSERVINGcolumn besideACCESS.ACCESS okmeans the cluster would admit you, and says nothing about whether there is anything on the far side.yes: a connector's tunnel was registered for it when the catalog was last read.no: declared, and nothing behind it; the row is dimmed, the line under the table says why, and no command is offered for it.-: nobody answered the question, because the listing did not come through a gateway, which is not a synonym forno. Only a gateway can answer it, since the catalog holds no routing table, so it fillsResourceEntry.servingin as the listing passes through, and Authority answersSERVING_UNKNOWNon its own lane. An unexpectednousually means ordering: a connector states what it serves when its tunnel opens, so a resource declared after it last registered is not among what it offered. Restart the connector. See Resident client → Declared, and nothing serving it.[[delegation_profiles]]saves a delegation under a name: the capabilities, the cage, the paths that agent's toolchain reads, the variable it wants its credential in.--profile <name>takes them from the row.--execmay be given without a value, and the program then comes from the row; without--execthe same row mints the grant and prints its id. The row also carries environment, which is what makes a caged agent usable:--sandbox basicscrubs the environment to an allowlist, and until now there was no way to pass a credential through it. A value is${env:NAME},${file:/path}, or the string itself. The two references keep the secret out of a file that is read by everything which loads the configuration; one that resolves to nothing refuses the run before the grant is minted. See CLI → Saved delegations.- A
--sandbox basiccage carries a/procof its own. It denied/procwholesale, because that is where/proc/<pid>/environlives, and a runtime that reads/proc/self/mapsfor its allocator aborts on start, which is most of what a coding agent is built on. A procfs shows the PID namespace it was mounted in, so the cage mounts one inside its own: the program gets/proc/self, and your processes do not exist in there.own-proc yes|nois on the run's report line. It is not a sandbox property and is not promised by a profile, so a host that will not mount one still deliversbasicin full.
Changed¶
- The
gdsgate statusheader is four lines.identitycarries the identity, the level and when the catalog was last read;namesandaccessare the two settings that can be half working;stateis dimmed and collects the state file, and at thetransparentlevel the interface and what it has refused. Thelevel,catalog,interfaceandrefusedlines are gone as lines of their own. Anerrorline is never dimmed and never folded into another one. - Who on this machine reached a resource moved behind
--explain. It explains a session that already happened and decides nothing about the next one, so it is no longer part of the short board. - The
namesreason under the table states the cause and lists each remedy as its own action. It previously restated the header line and ran cause, symptom and three ways out together in one sentence, so the short board could carry no remedy for it. gdsgate doctornames a socket it can actually open, once per socket. The sandbox section tested that the path existed while promising a socket "whose permissions admit it". On a host where the account is in thedockergroup it named three where there is one:containerd's isroot:root 0660, and/var/run/docker.sockis the same inode as/run/docker.sock, because/var/runis a symlink to/run.
v0.8.18¶
Added¶
gdsgate authority list-offeredreports what your connectors say they serve, and what the catalog does not know about yet. A connector states its backends when its tunnel opens, and that statement is kept and readable. Each line carries the offering node, the resource id, the kind, when the statement last arrived, anddeclared=yes|no.declared=nomeans the connector serves it and this cluster routes nothing to it, until you declare it withauthority set-resource. The resource id and its kind are the connector's own words: a join token is issued for a role and not for a machine, so nothing verified them. The list follows the connector's running process, so a backend added to[[connector.backends]]appears after that connector restarts, and a connector that has been off for ten minutes drops out of the listing. Decided by CedarviewNodes, the same action asauthority list-nodes, and it takes--route gatewaylike the rest of the administrative surface. See CLI →authority list-offered.
v0.8.17¶
Changed¶
- Operations on a certificate authority are audited under
CaOperation. A rotation and a refusal of one go on the chain in the class behindgdsgate authority audit list --class ca-operation, and no longer appear under--class admin-action; a filter or forwarding rule written on the latter to catch rotations has to move. The class holds operations whose object is an authority, and never the certificates an authority signs, which happen on every session and stay with the thing signed. Rows already on the chain keep the class they were sealed with, soverify-auditand existing exports are unaffected. - Enumerations in help text and refusal messages name the model lane. The
set-resourcerefusal for an unknown kind, thestep-uprefusal listing the actions a one-time grant can be minted for, and the help for--actionand--portall namellm.--actionalso states what it does not derive: a Kubernetes gate runs per apiserver request and spends no one-time grant, so that resource needs the flag given explicitly.
Added¶
- The user guide covers model services, in a section beside the other six
lanes: how a model service is reached, and that there is no
gdsgate llmsubcommand, because the caller is a program that already speaks a provider's API and is handed an address.gdsgate lslists model services with everything else, and the decision is taken per call, so a grant can be revoked between two calls of one program. See User guide → Model services. docs/config.example.tomldocuments nine more sections:[identity],[workload],[mcp],[mfa],[audit],[recording],[admin],[policy.editor]and[doctor], each with its keys, its defaults and what leaving it unset costs.
v0.8.16¶
Added¶
- A cage for the programs a connector runs on its own node. A backend's
commandMCP server and itsdecider_commandare programs out of the configuration file, executed on the protected node as the connector's own user.[connector.backends.cage]declares what confines them, in the vocabulary a service manager's unit file uses:private_network,protect_home,private_tmp,read_only_paths,read_write_paths,restrict_address_families,memory_max,tasks_max,cpu_quota. One table covers both programs. The filesystem is an allow-list with no switch to turn it off, so the connector's key, this node's Kubernetes token, every other backend's secrets and/procare not there. Absent, a backend has the posture it has always had, and every start logs which of the two it is in. See Configuration → The cage. report_denied_paths = trueputs everyopenthe program makes to the connector, which warns about the ones outside the cage and names the path, so an allow-list can be filled in by somebody who did not write the server. It decides nothing; the boundary stays Landlock's. It costs a round trip peropen, so it is a diagnostic and not a posture.- Under a cage the server runs in a private PID namespace whose first process is
the connector's, so the whole descendant tree dies with the session, including
a descendant that called
setsid.PDEATHSIGis set on every link of that chain and the run refuses to start without it, which covers a connector killed outright.
Changed¶
- What the cage turned out to be rides the session's own record.
mcp.session.closecarries acage=field besidetransport=, measured inside the program's process rather than read back off the configuration. A promise the host did not keep stops the program and fails the session unless the backend setsallow_degraded. - The model lane is in the lane lists.
kind = "llm"was described on the configuration and AI-agent pages and missing from every place that enumerates what the gateway fronts. The component table, the diagram, the workload lanes and the front page name it, and the per-operation authorization section counts four lanes rather than three.
v0.8.15¶
Added¶
- AI agents: documentation for running an AI agent through the
gateway, covering the two options of
gdsgate delegate --exec, the seven usable combinations of the nine, and one worked example of the same program under each sandbox profile, with the security limitations stated alongside the properties. - Documentation for the model lane:
kind = "llm"in the backend reference with its keys (allowed_models,allowed_request_headers,allowed_query_params,record_request/record_response), the substitution and pass-through modes, mandatory token accounting and the four recording levels. The CedarllmCallaction and theLlmService/LlmModelentity types are in Policy, anddocs/config.example.tomlcarries a commented model backend. - A registry of agent profiles:
gdsgate delegate --exectranslates the endpoints it opens into what a particular agent reads, the environment variable its vendor documents for a model address and a configuration file in the shape its vendor documents for MCP servers. The profile is chosen by the file name the program was invoked under, or named with--agent-profile;--list-agent-profilesprints the ones a run knows. Profiles are[[agent_profiles]]rows in the configuration, read exactly as the shipped rows are, with a configured row replacing a shipped one of the same name, so a renamed vendor variable is corrected without a release. Every row carriessourceandchecked, and a row missing either is refused at startup. A program with no profile is not guessed at: the run lists the endpoints it handed over and where a profile would go. Ships profiles forclaude,codex,opencode,openclawandpi, each checked against that agent's documentation on 2026-08-03. See CLI → Agent profiles. gdsgate delegate --exec --model <gateway|direct|off>states where the program obtains model access: through this gateway as a delegatedllm:call@<service>capability, from a provider of its own with its own credential, or not at all. Left unstated it follows the grant, so a run that worked before still works. Stating it produces refusals instead of changing the run:--model gatewayover a grant that names no model service is refused, as aredirectandoffover a grant that names one.--model directrequires--sandbox none, because a cage has no route to a provider, and--allow-degradeddoes not waive it.--sandbox nonereports what it does not enforce before the program starts, naming what stays reachable: the delegator's own credential by its path, their~/.ssh,~/.awsand~/.pgpass, the environment of every process they run, and a direct network route to the same resources around the gateway. It also reports what the model channel is worth: configured but not enforced, or not configured at all where no profile could hand the program the address. Printed on the error stream.
Changed¶
gdsgate delegate --execno longer writes a credential to disk. The key and the certificate live in thegdsgateprocess that serves the endpoints, and the bundle directory under the system temporary directory is gone.GDSGATE_KEYSTORE_ROOTis no longer set for the program and is removed from its environment.--bind-keystill writes a bundle.--renewable --execis rolled forward bygdsgateitself, since the program has no bundle to roll forward from, so it needs a registration listener: such a run with no[enroll].endpointis refused before the grant is minted.--renewable --bind-keyis unchanged.- A caged
--execrun's scratch directory (the program's$HOMEand$TMPDIR) is named after the grant, so two concurrent runs keep their working state apart.
v0.8.14¶
Added¶
- A model service is a resource of the catalogue.
kind = "llm"stands besidepostgres,mcpand the rest: delegated asllm:call@<service>, decided by Cedar, audited, revoked, discovered bygdsgate lsand recorded.addron this kind is a base URL rather than ahost:port, and what stands behind it is the operator's business: a provider's own API, or a model gateway of their own in front of several. gdsgate does not ship provider adapters.- A decision per call. The connector terminates HTTP on this lane, reads the
model name out of the request body and evaluates the Cedar
llmCallaction before anything leaves for the provider. The model is an entity (LlmModel::"<service>.<model>") under itsLlmService, so a rule can cover a whole service or one model.allowed_modelsis a per-resource ceiling under that decision, the counterpart ofallowed_tools. - Token accounting is not optional. Every call is accounted for in the audit chain, and there is no setting that turns it off. Where the provider reports usage the row carries the input and output counts; where it reports none, or where the answer could not be scanned for them, the row says so rather than showing a zero.
- An
llmbackend refuses to serve a session it cannot decide for.
- A decision per call. The connector terminates HTTP on this lane, reads the
model name out of the request body and evaluates the Cedar
- The lane presents the resource's own credential in place of the caller's. With
credential_fileset it substitutes: the organisation's key goes to the provider, and the caller's headers travel by allow-list,allowed_request_headerson top of the content-negotiation defaults (accept,accept-encoding,content-type), andallowed_query_paramsfor the query string, empty by default, so a call carrying any query string is refused, because some services take a key there.credential_headerandcredential_formatshape what is presented, and a header the lane writes itself is refused at startup. Withcredential_fileunset the lane is pass-through: TLS is terminated, the credential stays the caller's, and the decision and the accounting are still the gateway's. Substitution keeps the provider key out of the sandbox; pass-through does not. - A model whose service does not serve it is refused before the call leaves, and the refusal names the model instead of the connection.
- A model exchange is recorded the way session content is recorded. Prompts and
answers go to the recording store, streamed, with a SHA-256 sealed into the
chain and a
recording://<uid>reference, gated by the sameviewRecordingsaction as an SSH cast. The two directions are configured independently,record_requestandrecord_response, eachoff,digest(the default),redactedthrough the existing secret redactor, orfull, and any composition is valid:off/fullkeeps what a model sent without keeping what it was sent,redacted/offkeeps what left the perimeter with secrets stripped.digestis the default because prompt bodies are the most sensitive content this gateway carries. Neither key touches token accounting. The exchange's fingerprint is over the bytes rather than over the meaning, andgdsgate doctorreports what each model resource writes, as does the connector at startup, as a warning where bodies are stored.
Changed¶
- An inline provider key in the configuration is refused at startup. The key
belongs in a file named by
credential_file.credentialis kept in the schema so that a configuration written that way is refused rather than landing in pass-through, with the caller's own key travelling to the provider.
v0.8.13¶
Added¶
gdsgate delegate --execcan confine the program it runs:--sandbox none | basic | strictbounds what the program can read and reach on the host while it holds the grant. It is a second boundary; policy at the gateway decides what the grant may do and is unaffected, so a program that escapes still cannot exceed its capabilities.noneis the default, and a run that predates the flag behaves as it did.basicpromises seven properties: a deny-by-default filesystem (Landlock, ABI 3 or newer), no IP network beyond the endpoints served to the run (an empty network namespace), an environment rebuilt from an allow-list, inherited descriptors closed on exec,no_new_privs, the program killed ifgdsgatedies (PR_SET_PDEATHSIG), and the program's whole process tree dying with the run through a private PID namespace whose first process isgdsgate's own.setsiddoes not escape it.strictpromises those seven plus two: no socket in an address family the network namespace does not confine (a seccomp filter, which closes the host daemon reachable by socket path underbasic), and memory, process-count and CPU limits over the whole tree (cgroup v2, which needs a subtree delegated to this account).- Each profile refuses to run where the host cannot deliver a property it
promises;
--allow-degradedruns anyway, loudly.gdsgate doctorreports what the host can enforce before a run. - On the filesystem allow-list: the system runtime, a minimal
/etc, the usual character devices, the working directory, a private directory that becomes$HOMEand$TMPDIR, the program's own installation prefix, and anything named with--sandbox-allow. Not on it: the real home directory,/proc, and the credential bundle. - Signal semantics are unchanged, and how the program ended reaches the caller unchanged.
- The resources a grant names are reachable inside the cage and no credential is:
--execserves one loopback endpoint per resource inside the run's own network namespace, reachable from there and nowhere else, and passes the list in$GDSGATE_ENDPOINTS. The key and the certificate stay outside the cage. The endpoints are ports, not socket files on a host path, which every process of the account could reach. - The grant's window bounds the run as well as its requests: a caged
--execrun ends when its grant does. - Every
--execrun reports the boundary it received, and the gateway seals one row against the grant. The row carries what was measured inside the sandbox rather than the profile requested: all nine properties on every row, and whether--allow-degradedlet the run continue.--sandbox nonereports too. It is aClientAssertion(claimSandbox): the gateway verifies that the caller owns the grant being described and nothing more, since it cannot inspect a kernel on another machine, andoutcomeisSuccesswhere the boundary matched the profile requested andFailurewhere it fell short. A run whose row cannot be sealed is refused: the program is killed and the grant revoked. See CLI →--sandbox basicand--sandbox strict.
v0.8.12¶
Added¶
resources/readandprompts/getare decided and recorded:mcpReadResourceagainstMcpResource::"<server>.<uri>"andmcpGetPromptagainstMcpPrompt::"<server>.<name>", both sub-resources of theirMcpServer, both under theenforce_tool_policyopt-in, fail-closed on a policy deny or an unreachable Authority. A resource entity carriesschemebesideuri. Both producemcp.resource_read/mcp.prompt_getwith the same attribution, session id and payload handling as a tool call. New RPCs:AuthorizeMcpResourceRead,AuthorizeMcpPromptGet.- A
resources/readwhose URI is not already canonical is refused rather than rewritten (file:///etc/../etc/shadow,%2e%2e,FILE://,//runs), so policy and the backend cannot resolve different strings. The client is told which rule the URI broke. - A delegation can name a resource or a prompt:
mcp:resource:<name>@<server>andmcp:prompt:<name>@<server>beside the tool form, which may also be writtenmcp:tool:<name>@<server>. Existing grants are unaffected, since a token without a kind means a tool. A resource is named as the server's ownresources/listnames the row instead of by its URI, and a client that reads a URI it never listed is refused. The kind is part of the match, and--canis checked against the delegator's own rights per kind. - Server-to-client asks are gated:
sampling/createMessage,elicitation/createandroots/list, in both the pre-2026-07-28 dialect and theinputRequestsfield the 2026-07-28 revision moved them to. An ask is carried only where the backend'sallowed_input_requestsnames it, and that list is empty by default;allow_elicitation_urlseparately permits a server-chosen link. Every ask is recorded with its payload redacted, capped and escaped. The revision's own rules are enforced first: an ask may ride only on the answer toprompts/get,resources/readortools/call, and only for a capability the client declared on that request. - An MCP server can be a program the connector runs:
command(absolute path plus arguments) in place ofaddr, with optionalcommand_env. Setting both, or neither, is refused at startup. Every gate applies unchanged, andmcp.session.closerecords the transport (stdioorsocket). The program runs on the connector's node as the connector's own user; its environment is cleared and rebuilt fromPATH,HOME,LANG,LC_ALL,LC_CTYPE,TZ,USERpluscommand_env, which scrubs the environment and does not restrict the files it can open. A session that ends closes the child's stdin and signals its process group. See Running the MCP server as a child process. - A remote MCP server can sit behind the gateway:
urlspeaks Streamable HTTP, one HTTP POST per message, mutually exclusive withaddrandcommand. Every gate is the same implementation.Mcp-Method,Mcp-NameandMCP-Protocol-Versionare mirrored out of the body and never invented, andMcp-Param-*headers are rebuilt from thex-mcp-headerannotations in a tool'sinputSchema, so a tool must have been listed before it can be called. Redirects are not followed,httpsis verified against the platform roots plusca_path, and a credential comes fromcredential_file.allowed_backend_hostsconstrains aurlas it constrains anaddr. See A remote MCP server over HTTP. - The MCP endpoint speaks Streamable HTTP, protocol revision
2026-07-28: point a client athttp://127.0.0.1:<port>/. For a client that starts a program instead,gdsgate mcp bridge <host:port>is a byte pipe, and every request through it is authorized at the connector exactly as one sent to the URL. The endpoint refuses any request carrying anOriginheader, and any whoseHostis not its own loopback address, which closes the DNS-rebinding case. Per the revision, a header that disagrees with the body is refused400with JSON-RPC error-32020,GETandDELETEare answered405, and anMcp-Session-Idis ignored. The previous newline-delimited framing still answers on the same port. - MCP tool descriptors can be pinned.
pin_tool_descriptors = trueon an MCP resource takes the first descriptor seen for each tool as its baseline (mcp.descriptor_pinned), records a later one that differs (mcp.descriptor_changed), and refuses thetools/call, not the listing, until the change is re-approved throughgdsgate request-access/gdsgate approve(mcp.descriptor_repinned). The flag lives on the catalog row rather than on the connector's backend entry, and a connector that still sets it under[[connector.backends]]fails to start. The baseline is sealed with the operator's inventory key ([audit].inventory_key_path); with pinning on and no key configured, every call on that resource is refused. Trust is on first use: this detects a server that changed after approval, not one hostile from the start. A server'sinstructionsare pinned the same way and refuse nothing. See Pinning tool descriptors. refuse_unfiltered_tool_listing = trueanswers a caller with a JSON-RPC error when the per-callerviewToolsdecision cannot be obtained, instead of forwarding the listing unfiltered. The default remains to forward and recordmcp.tool_listing_undecidedwith the cause, the posture and the number of tool names at stake; the call gate is fail-closed either way. Requiresenforce_tool_policy. See When Authority cannot answer.- An external decider can be consulted before an MCP call leaves:
decider_url(onePOSTper call) ordecider_command(a program on this node, fed the request on stdin), synchronously, on all three data paths. The contract isallow|block, so it can only add a refusal, and it is never asked about a call another gate already refused. It sees what the chain sees, with the same redaction and size cap.decider_fail_closed(defaulttrue) covers every shape of no answer, and every consultation writesmcp.decider_hook.decider_commandcosts about 1.9 ms per call against 0.35 ms for a loopbackPOST.decider_ca_pathverifies anhttpsdecider against an operator's own CA. See The external decider. - MCP sessions are bracketed by
mcp.session.open/mcp.session.closewith asession_uidthat everymcp.tool_callandmcp.input_requestrow carries. A backend that cannot be reached is recorded as asession.openwith a failure outcome and no paired close. - A permitted tool call is attributed to the caller, with the session
certificate's subject and, for a delegated or workload identity, the owner it
names.
params.argumentsrides in the event detail as JSON, through the same secret redaction, capped at 4 KiB and marked when cut. - A session pins the protocol revision to the first one a request names; a later
request naming another is refused, and the revision is recorded on
mcp.session.close. Client metadata is read fromparams._metaonly. In the 2026-07-28 revision bothio.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilitiesare required, so a request missing either is refused with-32602. - A forwarded line must parse as one JSON-RPC request object: a batch array, a malformed line, and a line repeating a JSON object key are answered with a JSON-RPC error and reach no backend.
- Backends are enumerated in either era: the discovery probe opens with
server/discoverand falls back toinitializeonly when that is refused. A server's handshakeinstructionsare relayed unchanged, and their presence and size are recorded asmcp.instructions.
Changed¶
- Wire change: a JSON-RPC error from the MCP gateway carries code
-31001instead of-32000, which is inside the range JSON-RPC reserves. Anything matching on the number rather than the message needs updating; one code covers both protocol eras. - Wire change:
Authority.AuthorizeMcpViewToolswrites a chain row on a first sighting or a change, so a follower replica answersUnavailableand the connector fails over. Resources that are not pinned are unaffected. A connector that enforces tool policy reports descriptors on every listing.
v0.8.11¶
Added¶
gdsgate authorityadministrative calls take one of three routes, selected with--route:gateway, the Proxy's client-facing listener under the caller's own identity token, with Cedar deciding over that identity and the audit row naming it and the address it called from;certificate, a direct dial to[admin].endpointover the control-plane certificate; oremergency, the socket on the Authority host.auto, the default, takescertificatewhere the configuration names an[admin]section andgatewayotherwise, so an existing workstation keeps its route with no flag.policy pullandpolicy pushalways resolve through the gateway. See Administering from outside the control zone.- Two Cedar actions,
issueJoinTokenandviewNodes, decide minting a join token and reading the node and workload registries; both were previously gated only by a name on the caller's certificate, so no policy could narrow them. Declaring a resource, removing one, reading the catalog whole, ending a node's or workload's identity, forgetting a long-revoked registry row, importing an identity and rotating a certificate authority also answer under an administrator's own identity over the gateway route: twelve routes in total, new on the wire contract. The equivalent calls over the control-plane certificate are unchanged, and one request cannot mix the two. SeemanageCatalog. authority --operator <NAME>records, on the certificate route, the person an administrative call is made for. It is stored in its own audit column, printed and exported as unverified, and read by no authorization decision. A name outside 128 bytes of plain ASCII is refused, and any peer other than the control-plane certificate that sends one is refused.gdsgate authority audit listprints the chain, newest first, filtered by class, actor, action prefix, target or time. See Operations → Actor attribution.- An emergency administrative socket on the Authority host
(
[authority].emergency_socket), off unless a path is named, created owner-only in a directory Authority locks down0700. A call arriving there carries no identity of its own, and a token or asserted operator name is refused. It is decided by Cedar overUser::"gdsgate-break-glass", the principal every accepted policy must keep able to edit the policy, and carriespolicy pull,policy push, the catalog calls and the registry listings, refusing every other method by name.gdsgate doctorreports whether the socket or its directory is reachable by another account. See Lock-out and recovery. - One-time step-up grants can gate a certificate-authority rotation (
rotateCA), an administrative action taken by a person (enrollNode,disableUser,manageCatalog,issueJoinToken,viewNodes) and a proposed policy edit's meta-invariant check, each tied to the rotation, action or proposal it was minted for and spent once.context.step_up_grantcarries{ method, for_action }, so a rule can gate on the action a grant was minted for. See One-time step-up grants.
Changed¶
- The two control-plane groups are renamed:
Group::"admin"isGroup::"gdsgate-admin"andGroup::"provisioner"isGroup::"gdsgate-provisioner". Both live inside agdsgate-namespace an identity token may never carry, since asuborgroupsclaim starting withgdsgate-is refused, so no identity provider can put a person into either. A policy naming a retired group id is refused, on push and on the seed file, with the replacement named. A policy naming a group inside the namespace that the cluster does not populate is refused as well. See Why the machine groups carry thegdsgate-prefix. - The starting policy grants its group of people
manageCatalog,viewNodes,disableUserandissueJoinTokenforEnrolledNode::"gdsgate-connector",EnrolledNode::"gdsgate-proxy"andEnrolledNode::"gdsgate-workload", naming the roles the join-token rule admits.rotateCAandenrollNodestay with the certificate identities and ship as commented examples. See The administrative actions people get. - Every
manageCatalogandviewNodesaudit row, and every row an administrative action leaves behind (identity.revoke,identity.delete,identity.import,enroll.token.create,workload.revoke,rotateCA), carries the address the call was made from assrc_ip=.
Fixed¶
- A one-time step-up grant is spent only by the request or action it names; a request some other rule would have allowed leaves the grant untouched. Which rules read the grant is worked out once when the policy loads.
- An authorization request that opens a session is accepted only for a
connect-level action (
sshConnect,dbConnect,k8sAccess,tcpConnect), paired with the resource entity type the schema binds it to; any other pairing is refused as malformed. - A one-time step-up grant is minted only for an action a gate spends one on. A
Kubernetes session is gated on the reusable step-up window
(
context.step_up_satisfied), andgdsgate step-upon one is refused saying so. - A policy that gates
editPolicyon a step-up can be pushed: the meta-invariant probe checks a candidate against the factors its author presented instead of a blank context. - A policy requiring a second factor for an operator action (
killSession,viewRecordings,editPolicy,approvePolicyEdit,approveRequest, theviewcatalog listing,viewTools,mintOnwardSshCert,sshForwardLocal/sshForwardRemote,revoke --owner, thedbConnectgate) or an administrative action (enrollNode,disableUser,manageCatalog,issueJoinToken,viewNodes,rotateCA) now sees the caller's realmfa_*/step_up_*/totp_*/webauthn_*status; all of them previously built their Cedar context with every step-up channel pinned tofalse, so such a rule refused every call. A policy that mentions no factor is unaffected. The control-plane certificate and the emergency socket keep the conservative context, since neither has a factor to present.
v0.8.10¶
Added¶
- Catalog administration over gRPC:
UpsertResource,DeleteResourceandListResourcesAdmin, each under the control-plane certificate, a Cedar decision, and an audit row on allow and deny. Upsert and delete are idempotent and take effect with no restart. The commands areauthority set-resource,authority remove-resourceandauthority list-resources, which take--hostname, a repeatable--aliasand--portunder the same rules a[[discovery.resources]]entry is held to.[discovery]remains the file-declared path, and the startup seed writes the file over the stored rows and removes nothing. See Admin guide → Resource catalog lifecycle. manageCatalog, the Cedar action for the write side of the catalog, kept apart from the session actions on the same resource types and granted in the starting policy to the control-plane certificate identity alone. The catalog row also carriesmin_approvers(store migrationm0038), the narrowest level of the JIT approval cascade. See Policy → Administrative actions.gdsgate authority policy templateprints the canonical starting policy on stdout, offline, with no store and no running Authority. It is how a first policy reaches a cluster, sincepolicy pushis gated by the policy it would replace. See Policy → The starting policy.- A container image (
packaging/docker/Dockerfile) carrying the binary and a trust store: an unprivileged uid, pre-created state directories, no capabilities. A release tag publishesgdsgate:<tag>, andgdsgate:latestfor a final release, signed by digest with the key that signsSHA256SUMS.packaging/systemd/gdsgate-{auth,proxy,connector}.servicerun each role underDynamicUser=yeswith a read-only filesystem apart from its state directory, andpackaging/config/*.toml.examplecarry the settings a deployment sets. Five HMAC key paths, the audit export, the metrics listener, the OTLP endpoint and the admin identity take an environment override. See Admin guide → As a container and → As a systemd unit. gdsgate ca fingerprintcomputes the transport CA's SHA-256 fingerprint, matching what a connecting client compares against, without shelling out toopenssl.- A second control-plane role,
CN=gdsgate-provisionerspeaking asGroup::"provisioner", which declares catalog resources, mints join tokens and reads the node registry, and holds noenrollNode,disableUser,rotateCAorviewRecordingsand no join token for a control-plane role.gdsgate authority bootstrap-provisionerissues and renews that identity without opening the cluster's database. See Policy → The starting policy. --if-missingongdsgate authority bootstrap-adminand the fiveauthority init-*key commands: what exists is left alone and reported under exit code3. Forbootstrap-admin, "already there" means an identity this cluster would still accept. See CLI → Exit codes.profilelabels every Prometheus series, structured log line and exported trace of the process. Unset by default; must be non-empty, at most 64 bytes, drawn fromA-Z a-z 0-9 . _ -, and a service refuses to start on anything else. See Operations → Metrics and health.- A person's sign-in is recorded the first time each day their identity token
verifies, naming them and the issuer, with a floor of one row per person per
day. The row carries no token, email, group membership,
amror address. See Operations → Authentication identity. - Admission control for the join relay under
[proxy]:join_max_connections,join_accept_rate_per_ip,join_accept_burst,join_peek_timeout_secsandjoin_idle_timeout_secs. Unlike other limits in that section, an omitted key keeps the built-in figure rather than meaning unlimited.gdsgate doctorreports the limits in force and whether a provision token crossing the relay travels in cleartext. gdsgate join <domain>configures and registers a node in one command, reading a one-time token from standard input and writing/etc/gdsgate/connector.tomland the transport identity. Every address comes from the registration answer rather than from a convention, including the route to Authority and the renewal address with its scheme.--if-missingreconciles instead of installing over an existing node and spends no token when the identity is current.packaging/install-connector.shand its--kubernetes --image <ref>form call it for a host or for a Secret plus a single-replica StatefulSet.- A node may claim a name (
[enroll].node_name), recorded and printed as a claim; deduplication stays on the key fingerprint the registration proved.authority delete-noderemoves the row of a node revoked more than a week ago, keeping the revocation receipt, and writes the removal to the audit chain. [proxy].public_joincarries a node's join on the client-facing listener, so a node needs one address for its whole life. Registration is terminated at the gateway and forwarded to Authority; renewal is relayed untouched, because Authority authenticates the node's own certificate. The relayed surface runs two lanes told apart by TLS server name, each under its own ceiling (public_renewal_max_connections,public_admin_max_connections), and[admin].transport = "fronted"puts an administrative dial on that name. A separate call budget covers the two unauthenticated RPCs (public_anonymous_rate_per_ip,public_anonymous_burst).gdsgate authority migratebrings the store's schema up to date and exits, opening the store directly with no running Authority, and applies nothing on a second run;--checkreports only and exits non-zero while migrations are pending.store_auto_migrate(top-level,trueby default) decides whether a starting service may change the schema itself. See Operations → Store schema.
Changed¶
- Removing a resource from the catalog ends access to it: Authority resolves every
request against the catalog before consulting the policy and refuses one with no
row, covering certificate issuance, SSH forwards, the model-B onward
certificate, database connect and its statement categories, Kubernetes requests,
MCP tool calls, and the workload equivalents. The refusal reads apart from a
policy deny (
resource is not in the catalogue), and a live session is not torn down. An emptied catalog no longer reopens the first-provisioning window, which is keyed to a marker written by the first catalog write and never cleared (store migrationm0039). See Admin guide → What removing a resource does. - Policy lock-out recovery is
[policy.editor].break_glass_policy_file, a file on the Authority host that a start applies past theeditPolicygate. A restart against[policy].pathdoes not recover a lock-out: that path only seeds a store holding no policy version. See Admin guide → Lock-out and recovery. - The Proxy presents a publicly trusted certificate on its client-facing listener and verifies a peer's chain against the host's own trust store, so connecting needs no anchor and no fingerprint where a public certificate is in force.
gdsgate doctorreports the store's schema against the binary and applies no migrations. A migration run against PostgreSQL takes a database lock, so two instances starting together do not collide. A store carrying migrations recorded by a newer build stops a service start,authority migrateanddoctoralike.- A first contact that fails names the repair it needs (plaintext, an untrusted
certificate, one for another name, one outside its window, nothing listening, a
connection closed mid-handshake) instead of one transport error. A
--configpath that does not exist is an error for every command; only the discovered~/.gdsgate/config.tomlmay be absent. A listing command that fails exits non-zero. gdsgate doctornames an unrecognised configuration key by its full path and fails on it, instead of the key being dropped by the lenient parser.- A cleartext URL in
[enroll].renew_endpointor[admin].endpointis refused at start-up: both are mutual-TLS calls whose scheme decides whether TLS runs at all.gdsgate doctorcarries a finding for each (config.enroll.renew_scheme,config.admin.endpoint_scheme). - The browser listener re-reads its certificate pair
(
[proxy].web_cert_file/web_key_file) while it runs, as the client-facing and registration listeners already did, so renewing it needs no restart.SIGHUPre-reads all three immediately, and naming one half of aweb_*pair without the other is refused at start-up. See Renewing a listener certificate.
v0.8.6¶
Added¶
[connector].authority_transportpicks the route a connector's Authority control-plane calls take, independently of the data-plane reverse tunnel:direct(the default) opens a second connection straight to Authority;frontedsends the same calls to the address already used for the Proxy, carrying the role-specific TLS server nameauth.gdsgate.internalso a front can route them apart;tunneledcloses the connector's Authority egress entirely and rides the reverse tunnel, relayed by the Proxy without terminating the TLS, so Authority still authenticates the connector's certificate. Registration and token-less renewal, which happen before the tunnel exists, go to[endpoints].proxy_join. Authority advertises the route it runs to every node it issues an identity to.gdsgate up, the resident client: one long-lived process that gives every catalogued resource a local address, keeps the generatedssh/kubectl/ environment configuration in step with it, obtains access certificates ahead of the connections that need them, and holds the identity all of that runs under.gdsgate statusprints what is published and the command that reaches each resource;gdsgate downstops it.upreturns the terminal and reports the level, the resources published and its journal (~/.gdsgate/gdsgated.log);gdsgate up --foregroundkeeps the client in the starting process, which is what the shipped systemd units use.[client].accessselects how a resource is addressed:basic(a port on127.0.0.1),direct(a loopback address per resource on the protocol's port),transparent(a virtual address on an interface the client creates,CAP_NET_ADMINon Linux), orauto, which probes for the most capable level the host allows and reports where it stopped.[client].resolverdecides independently whether.gdsnames are served through the hosts file or the system resolver. An address stays with its resource across restarts and is held rather than reassigned when the resource leaves the catalog.gdsgate connect <host>asks a gateway for its endpoints, trust anchor, browser origin and addressing defaults and writes~/.gdsgate/config.tomlandtransport-ca.pem. Trust is settled first: the anchor's fingerprint is confirmed interactively or passed with--fingerprint, and the settings are fetched again over a connection verified against it. There is no trust-on-first-use. Seegdsgate connect.packaging/polkit/40-gdsgate-resolver.rulesgrants members of one group the three resolver actions[client].resolver = "system"uses (set-dns-servers,set-domains,revert), so the mode works under a service running as the person rather than as root. See Names under the system unit.gdsgate authority migratebrings the store's schema up to date and exits, with--checkreporting only and exiting non-zero while migrations are pending.store_auto_migratedecides whether a starting service may change the schema itself. See Operations → Store schema.- A
[[discovery.resources]]entry declareshostname,aliasesandport, and a resource created through the catalog API carries the same addressing.gdsgate lsshows the name and port each resource is addressed by.
Changed¶
- A node renews its identity in the background for as long as it runs,
authenticating with its current certificate instead of a token, and a revoked
node or workload is cut off from its live tunnel and running sessions rather
than only refused at its next renewal. The transport certificate's lifetime is
configurable (
[transport].cert_ttl_secs, minimum 120 seconds, default a day). Tunnel eviction is keyed on the node's identity rather than the key it dialled in with, and everyidentity.renewaudit row names the node it renewed for. gdsgate loginstores the refresh token beside the identity, and the resident client renews it silently for as long as that token lives; an interactive login is needed again only after it expires. A gateway refusal reaches the client as a stated reason and remedy ingdsgate status, in the client's log, as a desktop notification ([client].notify), and, for SSH, in the connection itself.step_up_on_denial = trueraises the passkey ceremony a refusal offers.gdsgate completion <bash|zsh|fish>also completes resource names, read from the resident client's local state.- Running the client under
sudofiles its state under the invoking account's home directory rather than root's, and refuses a stored login belonging to another account even when running as root. The systemd unit no longer assumes a/home/%ilayout and runsProtectSystem=full. GDSGATE_KEYSTORE_ROOTis honoured by every credential write, including the access certificategdsgate sshandgdsgate db loginmint.gdsgate doctorfolds identical findings into one line with a count, names the members under--explain, and keys its run-to-run comparison to the subject of a finding instead of the check id.gdsgate doctorreports the store's schema against the binary and applies no migrations. A migration run against PostgreSQL takes a database lock, so two instances starting together do not collide, and a store carrying migrations recorded by a newer build stops a service start,authority migrateanddoctoralike.
v0.8.1¶
A person can hand an autonomous agent a narrowed subset of their own access, as
capabilities: db:read@prod-pg, ssh:connect@bastion, kube:read@staging,
mcp:search@corp. gdsgate delegate mints the agent its own short-lived
certificate bound to the grant: --bind-key <dir> writes the credential bundle
for the agent to present, and --exec <prog> runs a program for the life of the
grant and revokes it when the program exits. A grant is always a subset of the
delegator's own rights, and every action the agent takes is re-checked against
it on top of the normal policy decision. gdsgate delegations lists your
grants; gdsgate revoke <id> cuts one off on its next use. Run any command with
--as <grant-id> to narrow your own session the same way.
gdsgate delegation status [--json] reads the local credential bundle and
reports the grant's id, owner, capabilities and remaining validity. See
Concepts → Delegation,
User guide → Delegating access
and Integrations → Delegated agents.
By default a delegation certificate lives as long as the grant and cannot be
renewed, so expiry alone revokes it. gdsgate delegate --renewable instead
mints a short-lived certificate the connector rolls forward with
gdsgate delegation renew within the grant window, bounded by the grant's own
TTL and still cut off by a revoke. Grants are sealed with a dedicated HMAC key
kept outside the database (gdsgate authority init-delegation-key,
[audit].delegation_key_path), and delegation is
refused fail-closed until it is set.
gdsgate revoke --all revokes every delegation grant you own;
gdsgate revoke --owner <user> (administrative, gated by the disableUser
policy action) revokes every grant a given user has ever handed out, without
enumerating grant ids. Either moves the owner's revocation epoch forward, so
grants minted after the owner is re-onboarded are unaffected, effective on each
delegate's next action, and shown as revoked in gdsgate delegations.
v0.8.0¶
Not wire-compatible with v0.6.x on the certificate-verify path
Access and workload certificates gain an owner field in their signed
body, so a v0.6.x connector or proxy cannot verify a certificate a v0.8.0
Authority issues, and vice versa. Upgrade Authority, the proxies and the
connectors together: a mixed v0.6.x / v0.8.0 fleet cannot open sessions
across the version boundary. The store gains four forward-only migrations
(m0030–m0033); a v0.8.0 store cannot be downgraded to v0.6.x. See
Upgrading from v0.6.x.
Added¶
- A machine (a CI job, a service, an autonomous agent) can get its own
short-lived identity without a human in the loop.
gdsgate machine-idproves what the workload is with a platform token (a GitLab CI id-token) or an admin-minted provision token, and receives a certificate named by a SPIFFE ID (spiffe://<trust-domain>/<issuer>/...); the private key is generated locally and never leaves the host. Off by default, turned on with[workload]. See Integrations → Workload identity. - A workload certificate carries no per-resource scope: each connection it opens
(SSH, PostgreSQL / MySQL, Kubernetes, MCP, or raw TCP) is authorized on its
own against the Cedar
Agentprincipal, which gains an optionalowner(the human or service the workload acts for) andspiffe_id; every action is attributed to that owner in the audit log. On an[[connector.backends]]backend withenforce_tool_policy = true, a workload's MCP calls are further gated per tool, deny-by-default. See Policy → Authorizing a workload. - A workload bootstrapped from a provision token is tracked in a registry and
renews by presenting its current certificate plus a proof of possession, with
no new token needed.
gdsgate authority list-workloadslists the registry;gdsgate authority revoke-workload <spiffe-id>refuses an identity at its next renewal. A workload bootstrapped from a platform token stays stateless, re-running the bootstrap to stay fresh. - A dedicated SPIFFE certificate authority signs workload certificates,
separate from the user-SSH, onward-SSH and transport authorities, carried in
the trust bundle so connectors and proxies verify workload certificates
offline, and rotated the same way the others are
(
gdsgate authority rotate-spiffe-ca).
Limitations¶
- A workload can open a session on every lane, but the derived actions on top of
an SSH session, jump-host onward certificates (model B) and
-L/-Rport forwards, refuse a workload certificate fail-closed; use a human identity for those. - A GitLab CI id-token and an admin provision token are the supported bootstrap
proofs today. Other platform issuers are additive
[[workload.issuers]]entries, documented as they are validated.
Upgrading from v0.6.x¶
v0.8.0 is forward-only. Because the certificate body changed, a v0.6.x connector or proxy cannot verify a v0.8.0-issued certificate, so plan the upgrade as a coordinated roll of the whole fleet:
- Stop every gdsgate service. Clients can stay up; their certificates remain valid until they expire.
- Back up the state store. v0.8.0 applies migrations
m0030–m0033on first start and they do not roll back; a downgrade means restoring this backup. -
To adopt workload identity, enable it. Set the cluster's SPIFFE trust domain and, for a platform issuer, one
[[workload.issuers]]entry:[workload] trust_domain = "gds.example" [[workload.issuers]] slug = "gitlab" issuer = "https://gitlab.example" audience = "https://gate.example.com" path_template = "project/{project_path}/ref/{ref}" groups = ["ci"] owner_claim = "user_login"For per-workload revocation, wire the revocation-receipt key (the same key the per-node registry uses) with
gdsgate authority init-revocation-keyand[audit].revocation_key_path. Skip this step to run without workload identity; the gateway then behaves as v0.6.x did for human access. 4. Start Authority, then the proxies and connectors, on the v0.8.0 binary. Migrations apply automatically on first boot; existing Cedar policies keep validating, since the schema only grew. Addpermitrules for theAgentprincipal only when you adopt workload identity.
v0.6.2¶
Compatible with v0.6.1
A backward-compatible upgrade: no wire, store, or schema change. Both items are client-side, and only behaviour changes.
Changed¶
gdsgate doctorscopes its checks to the role the config declares. A connector/proxy config no longer reports the Authority security matrix (audit anchoring, JIT, revocation, policy, OIDC, WebAuthn, HA) or client login caches; an Authority/all config no longer reports client login caches. Daemon-wide observability (metrics, OpenTelemetry tracing, the periodic health check) is reported for any daemon, including a connector/proxy. The output reads in a clearer hierarchy on a terminal: bold section headings, the check name at full weight with its value/status dimmed beside it, the config key or command in a remedy emphasised, and a warning carrying its own orange!marker, distinct from a neutral#note. The plain== … ==/!/ backtick form is preserved in a pipe or CI.gdsgate doctorflags any state path on an ephemeral filesystem. The reboot-survival check that already covered the nodestate_diralso covers the Authority store (a fileSQLitestore_url) and every configured path (policy, certs, audit keys, admin identity): a file on/tmp,/dev/shm, or/runsurvives a restart but not a reboot, so it warns instead of reading as present and persistent.
Fixed¶
- The connector detects a half-open reverse tunnel. The connector's tunnel to the proxy notices a wedged connection (a half-open TCP drop with no FIN) within 30 to 60 seconds instead of hanging on the OS TCP timeout, through HTTP/2 keepalive on the gRPC tunnel, a read deadline on the inbound stream and TCP keepalive on the WebSocket socket, then drops into the existing backoff reconnect. Client-side only; no wire change.
v0.6.1¶
Compatible with v0.6.0
A backward-compatible upgrade: no store migration, and the wire stays
compatible, so a mixed v0.6.0 / v0.6.1 fleet interoperates and shares one
database. The new ReportHealth / ListNodeHealth RPCs are additive, so an
older peer does not call them. Behaviour changes only; see the upgrade notes
below.
Added¶
gdsgate doctor, a read-only health checklist for the local config and state. It groups findings into configuration (file paths, secret permissions, semantic invariants, connector-backend validity), features (what is enabled and what is silently off), and state (store persistence and CA consistency, node identity freshness, client login caches); every warning or failure carries a fix.--jsonfor machines; exit code 2 on failure, so it works as a preflight or healthcheck. Nothing is mutated.- Cluster health from the periodic
doctorcheck. With[doctor].check_interval_secs(default 300s) each daemon re-runs the checklist on a timer. A proxy or connector pushes the summary to Authority, read the fleet-wide view withgdsgate authority list-nodes --health, and, when[telemetry].metrics_listenis set, publishes it locally asgdsgate_doctor_*gauges. See Operations → Surviving a redeploy.
Changed¶
gdsgate doctorflags drift since the last run. Each run snapshots its results to~/.gdsgate/last-doctor.json; the next run annotates any check whose status changed (↓ regressed/↑ recovered) and counts regressions.renew_endpointdefaults to the Authority control-plane. A split-deploy node with[enroll].renew_endpointunset derives it fromhttps://<endpoints.authority>(logged), so token-less mTLS renewal works without separate setup instead of silently falling back to the one-time join token.- The client recognises a rejected stale access certificate. When the cluster CA has rotated, or the certificate was revoked, a cached access certificate is refused at session open; the client prints a guided message and clears the stale cache (a re-login or retry mints a fresh one) instead of reporting "no connector serves that resource". A planned rotation prints a one-line notice on the next fresh issue. No wire change.
- The default state backend is a persistent file rather than in-memory. With
store_urlunset,gdsgate all/authorityresolves to a file SQLite at~/.gdsgate/state/store.db(created on first start, next to the enrolled node identity), so a restart keeps the same transport/SSH CA and registered nodes stay trusted.sqlite::memory:is an explicit opt-in for ephemeral runs; Authority logs a warning when the store is in-memory. - Authority reconciles an inconsistent CA store on startup. If
ca_rotation_statereferences a generation absent fromca_private_key, losing the active signer is fatal, since the private key is unrecoverable, and Authority exits with a guided recovery message instead of minting a fresh CA that would invalidate every enrolled node. Losing only aretiringorcandidategeneration is auto-healed: the rotation state is reset to active-only Standby with aWARN, since those are held in the trust bundle by public key alone.
Upgrade notes¶
These releases keep wire and store-schema compatibility with v0.6.x. Only behaviour changes:
- Default store, only if
store_urlwas unset. Agdsgate all/authoritythat ran withoutstore_urlused an ephemeral in-memory store on v0.6.0; it now persists to~/.gdsgate/state/store.db. The first start after upgrading is a cold start with a fresh CA, since nothing was persisted before. Deployments that setstore_urlexplicitly are unaffected. - A pre-existing inconsistent store is reconciled instead of silently
regenerated. Where v0.6.0 minted a fresh CA, this release either stops with a
recovery recipe (active generation lost) or auto-heals with a
WARN(only a retiring or candidate generation lost). A healthy, consistent store is unaffected.
v0.6.0¶
Not wire-compatible with v0.5.x on the session-open path
The proxy-to-client and connector-to-proxy session-open messages changed
(ServerMessage.opened is now a SessionOpened carrying the session id and
a recording flag; ServedResource gains recorded). Upgrade the proxy,
connectors and clients together: a mixed v0.5.x / v0.6.0 fleet cannot open
data-plane sessions across the version boundary. No store migration; the
control-plane (auth) RPCs are unchanged.
Changed¶
- Lean client console output. The client subcommands narrate themselves on
stderr in a compact dialect (
→in progress,✓done,#a note,✗a failure) instead of timestampedtracingINFO lines. A terminal gets glyphs and a little colour; a pipe,ProxyCommandor CI gets plain ASCII, andNO_COLORis honoured.loginshows✓ authenticated as <user> (group: …);ssh/db loginshow✓ certificate issued · ttl <n> · <resource>; a data-plane connection shows# connected · session <id> · recording. Client diagnostic logs default towarn;RUST_LOGstill raises them, and-q/--quietsilences the narration. Daemons are unchanged (structured logs at INFO). Command data on stdout is untouched, so scripts andproxy-sshkeep working.
Fixed¶
- No spurious disconnect banner after
ssh … --exec. A nativesshsession through the gateway no longer printsReceived disconnect … : session ended/Disconnected from …when the remote command finishes: the session ends on the client's own close rather than a server-initiated disconnect. Session recordings still finalize on a normal exit, and an abruptly closed client no longer leaves a stray process on the connector host.
v0.5.2¶
Compatible with v0.5.x
A maintenance release covering internal testing and tooling only. No behaviour, API, wire, or store change.
v0.5.1¶
Compatible with v0.5.0
A backward-compatible upgrade: no store migrations, no new RPCs, the wire stays compatible. Trace export is opt-in and off by default.
Added¶
- OpenTelemetry trace export. Set
[telemetry].otel_endpointto an OpenTelemetry collector and a service exports traces over OTLP/HTTP, off by default and outbound: the service dials the collector and pushes spans, so it binds nothing for this. The W3Ctraceparentheader is propagated across the proxy-to-auth, connector-to-auth and connector-to-proxy gRPC seams, so a session open and the authorization decision it triggers stay in one trace. Authorization spans carryaction,resource,decision, andsession_uidwhere they apply. See Operations → Distributed tracing. - Per-request authorization metric.
gdsgate_request_authorize_total{kind,decision}counts the per-request gates (Kubernetes requests, DB connects, and per-statement SQL,kind=kube/db_connect/db_query), distinct from the issuance-timegdsgate_authorize_total. See Operations → Metrics and health.
v0.5.0¶
Compatible with v0.4.x
A backward-compatible upgrade: no store migrations, no new RPCs, and the
wire stays compatible, so a mixed v0.4.x / v0.5.0 fleet interoperates and
the store needs no change. The Cedar schema gains additive AccessRequest
attributes (target_project plus the target's labels as tags); existing
policies are unaffected.
Added¶
- Operational metrics and health endpoints. Set
[telemetry].metrics_listenand a service exposes Prometheus/metricsplus liveness/healthzand readiness/readyzon that address (off by default). The metrics cover authorization decisions, proxy sessions, and connector reconnects. See Operations → Metrics and health. OpenTelemetry trace export is not wired yet. - Approver scoping by project and label. An
approveRequestpolicy can scope approver groups by the target resource's project (resource.target_project) and labels (resource.getTag(...)/hasTag(...)), not only its environment. See Policy → Approving access requests.
Changed¶
- Kubernetes 403s carry a remediation hint. When a per-request policy denies a
kubectlcall that a step-up or approval could clear, the connector's403names thegdsgatecommand to run, in the body and anX-Gdsgate-Remediationheader. A deny that nothing can clear stays a plain403. kubectl cannot remediate inline: run the command, then re-issue the request.
v0.4.0¶
Not compatible with v0.3.x
v0.4.0 expands the Cedar schema (new context attributes for IdP-login and
gateway step-up MFA, and a requested_action on the AccessRequest entity)
and adds control-plane RPCs for MFA, WebAuthn, and step-up that a v0.3.x
client, Connector, or Proxy does not speak. Mixed v0.3.x / v0.4.0 nodes are
not supported. The store gains six forward-only migrations
(m0024–m0029); a v0.4.0 store cannot be downgraded to v0.3.x. See
Upgrading from v0.3.x.
Added¶
- Gateway MFA step-up. A policy can demand a step-up the gateway performs
itself, separate from MFA done at the identity provider. The context carries
two channels:
context.mfa_satisfiedreflects an MFA login at the IdP (read from the token'samr/auth_time), andcontext.step_up_satisfiedreflects a gateway step-up. A sensitive action can require the gateway step-up even when the user already used MFA at the IdP. - TOTP and WebAuthn step-up factors. TOTP enrolls with
gdsgate mfa enroll/confirmand steps up withgdsgate mfa verify; WebAuthn passkeys register withgdsgate mfa register-passkeyand step up withgdsgate mfa step-up. Each factor has its own Cedar channel (context.totp_satisfied,context.webauthn_satisfied, each with an*_age_seconds), so a policy gates on any factor (step_up_satisfied) or a specific one. Configure the factors under[mfa]/[mfa.webauthn]. - WebAuthn passkeys on the gateway origin. The passkey ceremony runs on the
gateway's own browser origin (set
[proxy].public_origin), so a credential is bound to the gateway domain instead of a workstation. The relying party is configured under[mfa.webauthn](rp_id,origins); a localhost loopback remains for development. - One-time step-up grants. For an action that must be re-authorised on every
access, a policy can require a single-use grant bound to
(user, resource, action)that the gateway consumes on the allow. A user mints one withgdsgate step-up <resource>(passkey ceremony) or--code <totp>; the minting factor is available to the policy, so a rule can demand a specific factor. - Inline remediation on a curable denial. When an access attempt is denied for a
reason the user can fix, a missing step-up or a pending JIT approval, the
client surfaces the remedy, performs it (a step-up ceremony, or files and
waits on an access request), and retries the original access in one command.
--no-remediateopts out,--step-up-factorforces a factor, and--wait-timeoutbounds the wait for an approval. - MFA at per-request gates. The IdP-login MFA time is signed into the access
certificate, so the per-tool MCP, per-request Kubernetes, and per-statement
database gates honour
mfa_satisfiedwithout a separate step-up. A curable MCP or database deny returns the step-up or approval that would satisfy the policy, so the client raises the matching ceremony and retries. - Restricted-mode database hardening. Restricted mode refuses PostgreSQL
fast-path function calls and
COPY … TO/FROM PROGRAM, closing two paths around the per-statement category gate. - JSON logs.
GDSGATE_LOG_FORMAT=jsonemits one JSON object per line for a log shipper; the default stays the human-readable format.
Changed¶
- A JIT access request can no longer be self-approved, enforced in Cedar and at the store. Approver groups can be scoped to the request's target environment.
- A repeated access request for the same resource and action resumes the open one instead of creating a duplicate, and the request list shows only the requester's own requests and those a caller may approve.
Upgrading from v0.3.x¶
v0.4.0 is forward-only. Run it as a fresh cluster where you can; an in-place upgrade is:
- Stop every gdsgate service. Clients can stay up; their certificates remain valid until they expire.
- Back up the state store. v0.4.0 applies migrations
m0024–m0029on first start and they do not roll back; a downgrade means restoring this backup. -
For WebAuthn step-up, configure the relying party. Point the ceremony at the gateway's own origin so passkeys bind to the gateway domain:
[proxy] public_origin = "gate.example.com" [mfa.webauthn] rp_id = "gate.example.com" origins = ["https://gate.example.com"]Skip this if you use only the TOTP factor, which needs no configuration, or no gateway step-up at all. 4. Start Authority. The migrations apply automatically on first boot. Existing Cedar policies keep validating, since the schema only grew. Add gates on the MFA and step-up channels only when you adopt them. 5. Roll every Connector and Proxy to a v0.4.0 binary.
v0.3.0¶
Not compatible with v0.2.x
v0.3.0 expands the Cedar schema (new actions dbQuery, mcpCallTool,
viewTools, killSession; new entity types Tool and Session) and adds
control-plane RPCs that a v0.2.x Connector or Proxy does not speak. Mixed
v0.2.x / v0.3.0 nodes are not supported. The store gains three forward-only
migrations (m0021–m0023); a v0.3.0 store cannot be downgraded to v0.2.x.
See Upgrading from v0.2.x.
Added¶
- Live resource discovery. A connector enumerates the databases behind a
PostgreSQL catalog entry and publishes them, so each appears in
gdsgate lsas a connectable<host>.<db>resource, filtered per identity by the Cedarviewaction. Each snapshot is owned by exactly one connector, carries a freshness window so stale entries drop out, and is sealed with an integrity receipt held outside the state store. Set[audit] inventory_key_pathand rungdsgate authority init-inventory-keyto enable the receipt; discovery is opt-in per backend. - Per-database connect. A discovered database is directly connectable as
Database::"<host>.<db>", so a policy can grant a single database on a host or every database under it. - Per-statement database authorization. The Cedar
dbQueryaction gates a session by statement category (read, write, DDL, DCL) enforced at the connector on every statement. A session opens with the categories the policy permits for its role and is held to them for its lifetime. - Session-role impersonation. A client may connect to PostgreSQL as a named role; the policy decides which roles each principal may assume.
- Per-tool MCP authorization. The Cedar
mcpCallToolaction authorizes everytools/callagainst aToolentity (tool names match directly, includinglikewildcards);viewToolsfilterstools/listresponses per identity. An operator-setis_destructivemarker, driven by[mcp] destructive_patterns, lets a policy block destructive tools. The connector's staticallowed_toolslist remains as a second gate. - Live MCP tool discovery. An MCP backend's tools are probed and used to filter
tools/listresponses per identity (viewTools). Tools are runtime operations rather than connect targets, so they are not listed ingdsgate ls; only the MCP server is. - Live session monitoring and control.
gdsgate session listshows the active sessions andgdsgate session watchstreams opens and closes as they happen;gdsgate session killends a session by id or by--principal/--resource. Termination is authorized by the CedarkillSessionaction over aSessionentity and is audited. - Live recording streaming.
gdsgate authority recordings livetails an in-progress SSH orkubectl execrecording in real time, gated by the sameviewRecordingsaction as the finished-recording viewer. - Proxy admission control. The
[proxy]section caps concurrent sessions, per-tunnel streams, public connections, and the per-IP accept rate, and can pace per-session bandwidth. Every limit defaults to0(unlimited). - OIDC signing-algorithm allow-list.
[oidc] id_token_signing_algsrestricts which id-token signatures Authority accepts (default: every asymmetric algorithm; symmetricHS*is always refused).
Changed¶
- Authority and Proxy authenticate the caller before routing a request, and an unroutable attempt is audited instead of dropped silently.
- Decision audit records use the short Cedar action name, so a filter on
action = "dbConnect"matches both the decision and the session events. - The connector restricts backend dial targets to a configured allow-list.
- Dependency crates are quiet in the log by default:
gdsgate*atINFO, everything else atWARN.
Fixed¶
- SSH session recordings upload to the central store in
centralmode; they previously fell back to the on-connector copy every time. - MCP JSON-RPC input is capped per line, closing a memory-exhaustion path on an unterminated message.
- Non-UTF-8 SQL captures are annotated with a stable digest instead of dropping the query-log entry.
Upgrading from v0.2.x¶
v0.3.0 is forward-only. Run it as a fresh cluster where you can; an in-place upgrade is:
- Stop every gdsgate service. Clients can stay up; their certificates remain valid until they expire.
- Back up the state store. v0.3.0 applies migrations
m0021–m0023on first start and they do not roll back; a downgrade means restoring this backup. -
For live discovery, generate the inventory key:
It writes a hex-encoded 32-byte key with mode
0o600and refuses to overwrite an existing file. Wire the path into Authority's config and back the file up alongside the database:Without it, published inventory snapshots are stored receipt-less and read back without integrity verification. Skip this step if you do not use discovery. 4. Start Authority. The migrations apply automatically on first boot. Existing Cedar policies keep validating, since the schema only grew. Add
permitrules fordbQuery,mcpCallTool,viewTools, orkillSessiononly when you adopt those features. 5. Roll every Connector and Proxy to a v0.3.0 binary.
v0.2.0¶
Not compatible with v0.1.x
v0.2.0 changes the wire format of the trust-bundle gRPC message, the Cedar schema (three actions removed, one entity type removed), and introduces eight forward-only schema migrations on the state store. Earlier clients, connectors and proxies will not interoperate with a v0.2.0 Authority, and a v0.2.0 store cannot be downgraded to v0.1.x. See Upgrading from v0.1.x below.
Added¶
- High availability with split-brain protection. Several Authority instances can share one state store. They elect a single audit-chain writer through a database lease that carries a monotonic fencing token; if a formerly leading instance is partitioned away and another takes over, the ex-leader's subsequent writes are refused at the chain-commit transaction itself, even before its own controller has observed the loss. See Admin guide → High availability.
- External audit anchor. Each batch commit to the audit chain is sealed under an
HMAC key that lives on the Authority host's filesystem rather than in the
database. A separate
gdsgate authority verify-auditcommand replays the chain off-band and cross-checks every anchor, surfacing a database-side truncate or rewrite that the in-database hash chain alone could not detect. See Operations → Audit. - Per-node revocation receipts. Revoking a registered node writes an HMAC-signed receipt next to the revocation marker. A database rollback that tries to un-revoke a node by restoring an earlier row is detected on the next renewal and refused. The receipt key, like the audit-anchor key, lives on the Authority host.
- Typed certificate authorities. Trust bundles carry the kind of each CA (User SSH / Onward SSH / HostTls transport) alongside its public key, and verification refuses to trial a key from the wrong family. Every authority also carries an explicit validity window, so a calendar-expired CA is dropped from the bundle even if it is still in the database.
- Source-IP enforcement is signed end-to-end. When the Proxy passes the client's
TCP-peer address to the Connector for
pin_src_ipenforcement, it signs that value with a short-lived key the Connector verifies, so the source IP cannot be spoofed from a compromised Proxy. - Hardened OIDC. The id-token validator hot-swaps its JWKS when the identity
provider rotates keys (an on-demand refetch fires on the first unknown
kid, so a stale key never causes an outage), refuses non-HTTPS discovery and JWKS URLs, rejects mixed-signing-algorithm metadata, and binds the PKCE nonce end-to-end through the browser-login path. - Cedar policy editing with quorum and break-glass recovery. Policy edits can
require approval from a configurable number of reviewers
(
[policy.editor] min_approvers) before they become active, and the schema's hard meta-invariants refuse a push that would lock break-glass out or self-lock the proposer. A configurable break-glass policy file ([policy.editor] break_glass_policy_file) at boot restores access if an active version ever fences the editor out. - HTTP hardening for the cluster's outbound calls. Authority's calls to the identity provider's discovery and JWKS endpoints, and to any external target it dials, run through a hardened HTTP client with TLS-only enforcement, redirect handling, body caps and request timeouts.
- Kubernetes proxy hardening. Port-forward is opt-in per backend
(
allow_port_forward = truein the connector's[[connector.backends]]), WebSocket message and frame sizes are capped 32x below the upstream default, sessions are torn down when the carrying certificate expires mid-stream, request paths are canonicalised before authorisation, and audit-field injection through user-controlled byte values is closed by percent-encoding the four framing characters at the producer. - Constant-time digest compare and zeroized secrets. Recording digest mismatches and similar comparisons are constant-time, and the in-memory SSH seed and PKCS#8 DER for newly minted keys are wiped on drop.
- Per-peer-IP enrollment rate-limit. The plaintext enrollment listener runs a token bucket per source IP before the gRPC handler sees the request, so a busy attacker cannot starve a real joiner.
- Improved registration-token registry. Registration tokens filter by role
inside the conditional UPDATE that consumes them; a role-mismatched request
fails as
Unknownwithout burning the token, so a typo, or an adversary who learned the secret but not its bound role, does not lock the legitimate joiner out. - Tighter admin-action surface. Three Cedar actions (
rotateCA,enrollNode,disableUser) are enforced through admin gRPC instead of any direct-store path. Admin commands require a bootstrap-admin identity loaded from[admin].identity_dir; see Admin guide → First-boot bootstrap.
Changed¶
- The Cedar schema this release ships contains exactly the actions the cluster
authorises today.
dbQuery,dbWriteandmcpCallTool, plus theToolentity type, appeared in earlier drafts and are removed from the schema for this release; the corresponding per-statement database authorisation and per-tool MCP authorisation return in a later one.
MCP per-tool authorisation
For 0.2.0, MCP sessions are authorised at connect-level through the
tcpConnect action over a TcpService resource (the proxy maps
kind = "mcp" to the same action as raw TCP). Until mcpCallTool returns
to the schema, per-tool restrictions live on the connector's static
[[connector.backends]] allowed_tools = […] allow-list on the MCP backend.
Upgrading from v0.1.x¶
v0.2.0 is forward-only. You cannot run a v0.2.0 Authority against an older client, connector or proxy, nor a v0.1.x Authority against a database that v0.2.0 has migrated. Plan an upgrade as a fresh cluster (new store, new registration) when possible; an in-place upgrade requires:
- Stop every gdsgate service in the cluster. Clients can stay up; their certificates remain valid until they expire.
- Back up the state store. v0.2.0 applies forward-only schema migrations on first start; a downgrade requires restoring the backup.
-
Generate the new HMAC keys the v0.2.0 audit anchor and per-node revocation receipt need:
Both write a hex-encoded 32-byte key with mode
0o600and refuse to overwrite an existing file. Back the resulting key files up alongside the database; losing them disables verification of anchored events and revocation receipts. -
Wire the resulting paths into Authority's config:
-
Provision an admin identity if you do not already have one:
Then point your operator workstation's config at it (
[admin].identity_dir,[admin].endpoint). -
Drop
dbQuery/dbWrite/mcpCallToolreferences from your Cedar policies before pushing them: these actions and theToolentity type are not part of the 0.2.0 schema. Connect-level authorisation is unchanged, so a database session goes throughdbConnectand an MCP session goes throughtcpConnectover aTcpService, as raw TCP does. For MCP, express per-tool restrictions through the connector's static[[connector.backends]] allowed_tools = […]allow-list until the per-tool action returns to the Cedar schema. -
Start Authority. The schema migrations apply automatically on the first boot.
-
Roll every Connector and Proxy to a v0.2.0 binary. Their trust bundles are re-fetched with the new typed-key wire format on the next
RenewIdentity.
To run several Authority instances behind one store, enable HA in the config
([ha] enabled = true, one unique owner per instance); see
Admin guide → High availability for the
lease tunables.
v0.1.4¶
- Kubernetes
exec/attachsession recording. The connector's Kubernetes proxy demuxes thev5.channel.k8s.io(andv4) WebSocket subprotocol thatkubectl1.30 and newer negotiates onpods/.../execupgrades and writes the captured stdout and stderr as a regular asciicast v2 cast, with the same disk and central storage as SSH session recordings and the samegdsgate authority recordings list/showviewer. Each session lands askube.session.closein the audit chain with the recording URI plus per-channel byte counters (stdin_bytes=… stdout_bytes=… stderr_bytes=… error_bytes=… resize_events=…) and the negotiated subprotocol. Legacy SPDY/3.1 exec stays as a transparent byte bridge with rawbytes_up=… bytes_down=…, with no demux and no recording. [recording] capture_stdin = falsedefault. The session recorder drops stdin chunks by default for both SSH and kube exec/attach. Echo-off password prompts (sudo,ssh,passwd, anyread -s) hand the typed bytes to the server through stdin without echoing them back, so a stdin-on cast holds those passwords in plain text. Enable per cluster with[recording] capture_stdin = truein the connector's config where the deeper audit trail is worth the PII risk.- Graceful WebSocket close. The Kubernetes exec bridge forwards
Closeframes symmetrically between client and apiserver and waits for both halves to close before dropping the sockets, sokubectlno longer reportswebsocket: close 1006 (abnormal closure)on every exec. - Central-store upload race fixed. The central recording sink retries
Authority.StoreRecordingwith capped exponential backoff (200/400/800/1600/3200 ms) on the well-known"no session.close event found"precondition, and the kube path emits the close-audit event before uploading; previously Authority rejected the upload because the matching session.close was not on the chain yet.gdsgate authority recordings showalso queries Authority bysession_uidregardless of the URI scheme in the audit row, so casts uploaded centrally are reachable from any client that holdsviewRecordings.
v0.1.3¶
- SSH session recording. Every SSH session through gdsgate is recorded as an
asciicast v2 stream. Two storage modes are selectable via
[recording] mode = "disk" | "central":diskkeeps the cast on the connector,centraluploads it to Authority at session close, with a local DR copy. Each session-close audit event carries the recording URI and byte count. - Recording viewer.
gdsgate authority recordings listreturns recent session-close events;gdsgate authority recordings show <session_uid>prints the cast, which pipes intoasciinema playto replay. Gated by theviewRecordingsCedar permission. - SFTP audit. File-transfer activity over SFTP is auditable in both model A and
model B: each
open/close/remove/rename/mkdir/rmdirlands in the audit chain; byte counts are aggregated per handle and flushed on close. gdsgate ssh --exec. One-shot SSH: mints the access certificate and immediately runs the nativesshwith theProxyCommandwiring passed inline, so thegdsgate ssh-config/~/.ssh/configstep is not required for the simple case. Arguments after--are forwarded tossh. See CLI →gdsgate ssh.- Token-less mTLS identity renewal. A node whose transport leaf is within its
renew window refreshes it over mTLS instead of burning a fresh registration
token. Opt in per node with
[enroll].renew_endpoint, Authority's mTLS control plane URL; a near-expiry node without that field falls back to the legacy token-based path, so the upgrade is non-breaking. The peer is identified by the verified leaf's CN (gdsgate-connector/gdsgate-proxy). - Per-node revocation registry. Every enrolled node lives as a row in
Authority's store (role, CN, public-key fingerprint, enrolled-at). Two new
sub-actions of
gdsgate authorityoperate against the sharedstore_url:authority list-nodesprints the rows;authority revoke-node <id>flips the row to revoked, so the node can no longer renew over mTLS. The already-issued leaf stays chain-valid until itsnot_after, so pair revoke with short certificate TTLs for prompt eviction. - HostTls (transport) CA rotation. The transport CA is operator-rotatable on the
same paced double-signing controller as the User SSH and Onward SSH CAs.
gdsgate authority rotate-host-tls-carecords the request; the running Authority promotes the new generation afterpropagation_secsand retires the old afterretire_secs. Nodes pick up the new trust bundle on their next mTLS renew, so schedule the rotation window accordingly. There is no auto-schedule for HostTls; the cadence is operator-timed. - Kubernetes
exec/attach/port-forward.kubectl exec,kubectl attach, andkubectl port-forwardflow through the connector: when a request carries anUpgrade:header, the proxy opens the raw HTTP/1.1 connection to the API and bridges the upgraded client and upstream sockets after both sides reach 101. Impersonation still applies:AuthorizationandImpersonate-*headers are stripped on the upgrade path too. The audit chain recordsk8s.requeston 101 andk8s.exec.closewithbytes_up/bytes_down. Stream-level recording of the exec stdout is not captured, only the audit metadata. - Source-IP pinning, opt-in per resource. A certificate can bind the client TCP
peer it was issued for, and the connector rejects it at session-open if the
actual peer does not match, on top of issue-time CIDR gating. Opt in per
resource by tagging the catalogue entry with
pin_src_ip = "true"; unlabelled resources keep issuing un-pinned certificates. See Configuration → discovery rules.
v0.1.2¶
Remote policy editing: the Cedar-declared editPolicy action becomes
operational, and a built-in TUI editor lands in the binary.
Remote policy editing:
gdsgate authority policy pull/pushgo through Proxy to Authority and are gated by the CedareditPolicyaction over the verified caller.pushstrict-validates locally first, the server strict-validates again before storing, and the new version is hot-reloaded cluster-wide, with no Authority restart. Every version is retained for history and rollback. Allow, deny, and invalid-policy attempts are recorded in the audit chain asAdminAction.- The
[policy].pathfile becomes a seed: it loads the policy into the store on first boot, and from that point on the store is the source of truth. Restart with the file only to recover from a lock-out, where the file replaces the active version on the next start.
Interactive policy editor (gdsgate authority policy edit):
- Full-screen terminal editor over
tui-textareaplus a custom Cedar renderer. Pulls the active policy on entry, validates and pushes cluster-wide onCtrl-S. - Live lint: every keystroke re-runs the strict Cedar validator; the first error message shows in the status bar and the offending line is painted red in the buffer.
- Syntax highlighting for keywords, strings, comments, and entity-type names.
- Context-aware completion (Tab): inside a
Type::"…"literal only ids of that type from the schema and live catalog; afterresource./principal./context.only the attributes Cedar declares for that base (resource also offersgetTag/hasTag); elsewhere entity types, actions, keywords and live resource ids. The popup narrows as you keep typing. - Search (
Ctrl-F), Cedar formatter (Ctrl-L), selection and clipboard (Shift with arrows,Ctrl-A/Ctrl-C/Ctrl-X/Ctrl-V), undo and redo (Ctrl-Z/Ctrl-R), dirty-buffer quit confirmation (Ctrl-Q).
The full CLI surface is on the CLI page; the editor's keybindings live
at authority policy edit. For grants,
lock-out recovery, and operational notes see
Admin guide → Policy lifecycle.
v0.1¶
The first gdsgate release: a single binary that gates identity-aware, audited, least-privilege access to SSH, databases, Kubernetes, MCP, and TCP services through one proxy.
Access and identity:
- Single binary, four roles (Authority / Proxy / Connector / client) by subcommand.
- OIDC login, device flow and Authorization Code with PKCE; identity tokens verified against the provider's JWKS (RS256), claims mapped to a principal and its groups.
- Cedar authorisation over
(principal, action, resource), with the action derived from the resource's kind; deny-all by default. - Short-lived access certificates issued on
Allow; expiry is the revocation mechanism.
Resources:
- SSH model A (connector-terminated, recorded) and model B (jump host with
per-connection downstream certificate), PostgreSQL / MySQL (forwarded with a
query-log tap), Kubernetes (identity impersonation), MCP (
tools/callallow-list), raw TCP, declared per connector in[[connector.backends]]. - Native client experience:
gdsgate login, thenssh,psql/mysql,kubectl,redis-clithrough the gateway. - SSH
-Land-Rwith two-layer gating: a per-backend allow-list and the CedarsshForwardLocal/sshForwardRemoteactions. - Persistent connector-side SSH host key, so a client's
known_hostsentry keeps verifying across sessions.
Transport and trust:
- Node registration with one-time tokens; internal mutual TLS and public TLS off
a persisted transport CA; the connector reverse tunnel (gRPC, WebSocket
fallback,
HTTP CONNECTproxy support) so backends need no inbound access. - Three certificate authorities (transport, User SSH, Onward SSH), all persisted, the latter two paced-double-signing rotatable at runtime.
- Tamper-evident, hash-chained, durable audit (persist-before-grant) with JSON / Splunk HEC / CEF export.
Discovery and JIT:
- Resource catalog seeded from
[discovery], surfacing ingdsgate lsand supplying live attributes (environment, labels) to Cedar. - Just-in-time access via
gdsgate request-access/gdsgate approve, with a per-resource, per-environment and global approvers cascade.
Supply chain:
- Reproducible, byte-for-byte builds; cosign-signed
SHA256SUMS; CycloneDX SBOM. Verify withcosign verify-blobandsha256sum -c; see Admin guide → Installation and Operations.
Validation:
- The full multi-node topology has been exercised end to end against a real Keycloak identity provider, covering group-scoped access over databases, SSH (both models), Kubernetes, raw TCP, and MCP (allow for one group, deny for another); node registration with mutual TLS; public-TLS verification; network-zone isolation; least-privilege containers.
Targets: x86_64-unknown-linux-gnu and a static x86_64-unknown-linux-musl.