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 workspace version is 0.10.0-rc.13. Channel status, artifact verification, rollout classes, and the recovery boundary are in the upgrade guide.
v0.10.0-rc.13¶
The store schema is unchanged against v0.10.0-rc.10. This version adds
independent Authority receiver credentials and signed distribution-root
transitions. Upgrade every Authority before rotating the root. Preserve a
paired store and local credential backup; see the
upgrade procedure for verification and rollback.
Added¶
authority keys syncprovidesroot-status,prepare-root,activate-rootandcancel-root. Each retained Authority acknowledges the new root after protected persistence. Activation commits the root and membership together; running retained Authorities install it automatically. An excluded Authority stops and refuses restart with its old state. See root rotation.ha.keyring_sync.root_kms_keyselects a declared wrapping provider for the local distribution root.authority keys sync wrap-rootmigrates an existing plaintext root after verifying the persisted wrapped value. Provider failure refuses startup without falling back to plaintext.connector.discovery_sourcessupports Kubernetes discovery without exposing the Kubernetes API as a resource. The source publishes permitted targets inside its assigned scope and uses read-only discovery credentials.
Changed¶
- Authority readiness checks the store schema, live leader lease and verified membership. Routed followers also check their TLS route to the leader. Proxy readiness accepts a healthy Authority failover endpoint. Dependency failure returns not-ready while liveness remains available.
- Kubernetes API requests, upgrades and discovery passes read the current referenced token. Projected-token changes take effect without restarting the Connector. Missing or expired credentials refuse the operation; 401/403 responses do not cause a retry with the previous token. Existing streams retain the Kubernetes API server's authentication behavior.
Fixed¶
setup changecombines backend additions and role settings into one write when they affect the same configuration path. Apply preserves both changes; checkpoint restore recovers the previous contents.- Discovery-only Connectors retain a control relay when their target set is empty or withheld. After permission recovery, they register the same targets again without restarting the Pod.
- Proxy peer forwarding supports Connectors with one configured tunnel. Session cleanup retries temporary Authority failures within a bounded deadline so a leader handoff can remove closed sessions from the active list.
- Windows Connector services accept administrative control commands and support custom transport-identity directories. Doctor reports verified Proxy connections.
- Session commands terminate when stopped and report their signal exit status.
Documentation¶
- Authority bootstrap documents signed root preparation, cancellation after initiator loss, withdrawal, catch-up and paired recovery. Withdrawal does not revoke keys or credentials the former Authority already held; those have separate revocation procedures.
- Kubernetes configuration and discovery references separate API backends from discovery sources and describe token refresh. HA guides include dependency readiness and failure checks.
v0.10.0-rc.10¶
The store schema is unchanged against v0.10.0-rc.9; the wire
contract adds the Authority bootstrap service. Follow the
upgrade procedure before enabling keyring sync.
Added¶
- Additional Authorities can bootstrap through an expiring grant bound to their lease owner and receive encrypted control and audit keyrings. Bootstrap verifies the registration CA fingerprint. Keyring revisions synchronize during operation; activation requires a fresh acknowledgement from every configured member. See Authority bootstrap.
- Proxies support failover between Authority enrollment listeners through
endpoints.authority_enroll_failover. This complements the existing control API failover and supports HA deployment without an Authority LB. gdsgate upinstalls and starts the resident service on Linux with systemd.sudo gdsgate uppreserves the original user's configuration and identity, starts the system unit with network capabilities, and needs no second sudo.service install client --startsupports explicit installation.- Linux resident endpoints restrict connections to their owner's OS UID by
default. The protection covers
basic,directandtransparentaccess; failure to install it prevents startup. Root and processes under the same UID remain outside this boundary. Windows UID isolation is not implemented. [client.direct].privileged_port_offsetdefaults to10000: local ports below 1024 move to higher ports. SSH port 22 becomes 10022; upstream ports and the catalog remain unchanged.-
Kubernetes discovery can publish HTTP Services as
webresources with per-request access checks. For HTTPS or a custom CA, configure a static web backend. See discovery constraints. -
Evaluation licences accept an exact expiry in Unix seconds with
license issue --expires-unix.
Changed¶
status --explainshows short causes and corrective actions, uses the same availability colors as the table, and groups diagnostics by resource. The table, explain output and JSON sort by availability, then resource ID.- Withdrawn resources retain their local addresses for at most 24 hours. The resident then removes them from local state and status, including when catalog polling fails. Duplicate Connector availability findings are merged.
doctorchecks that/var/lib/gdsgateis a directory with mode0755. Privilegedservice installcreates or repairs it before installing a unit; unknown symlinks and regular files are refused. Private subdirectories keep their own permissions.
Fixed¶
- Transparent TCP closes the relay after FIN or RST and preserves queued final
bytes. SSH through a resident IP now finalizes recording and
session.closewithout waiting for the idle timeout. - Changing Unix control-socket ownership during privileged startup no longer
opens the socket as a regular file, avoiding
No such device or address. - Connector startup retries Proxy identity lookup through a replacement relay when the bootstrap tunnel closes. Receipt acquisition is logged at INFO.
Documentation¶
- HA deployment separates topology from packaging, with Linux and Compose tabs for Authority behind an LB and Authority with direct failover.
- Shared component guides cover Authority bootstrap, Proxy and Connector HA, IP-only TLS, OIDC configuration and failure checks.
- The resident guide documents service installation, port offsets, UID isolation and recording through resource names or IPs.
v0.10.0-rc.9¶
Release identity and rollback boundary
v0.10.0-rc.9 adds store migrations m0078 and m0079 against
v0.10.0-rc.7. Migrations are forward-only. Take a paired store and key
backup; upgrade Authorities before Proxies, and withdraw older Proxies
before enabling the HA pool: upgrade procedure.
Added¶
- Authorities share one TCP load-balancer address and forward requests to the current write leader over their private connections.
- Proxies relay resource connections to another Proxy over authenticated private peer connections. Clients and Connectors use the public TCP LB.
connector.proxy_connection_countselects 1–16 distinct Proxy connections; its default remains1for local-session compatibility. The HA examples set it to2to enable verified tunnels and peer opens.- Session listing, watching, termination and UID admission limits work across Proxies. Browser login callbacks and logout can reach different Proxies.
- Authority and Proxy readiness supports traffic withdrawal and graceful shutdown. Proxies replace a failed Authority connection for later requests.
Changed¶
- Connector routes expire when their owner cannot renew them. Route discovery excludes expired routes and withdrawn node identities.
- A failed backend permits new requests through another ready instance. Established streams end when their owning Proxy or Connector stops.
Documentation¶
- HA deployment includes Compose files, DNS and IP-only configurations, enrollment, client access and failure checks.
- Helm and Kustomize assets include Authority peer addresses, Proxy peer listeners, readiness probes and termination settings.
v0.10.0-rc.7¶
Release identity and rollback boundary
v0.10.0-rc.7 is the exact manifest/tag identity. Against
v0.10.0-rc.6 it adds store migrations m0075 through m0077; store
migrations are forward-only, and an older binary refuses a store they
have been applied to. Take a paired store and key backup. Empty the audit
queue and upgrade every Authority instance in one window with CA rotation
paused: to v0.10.0-rc.7.
Added¶
gdsgate config schemawrites the configuration JSON Schema.gdsgate config checkvalidates a configuration offline and reports a corrective action.setup prepare --dry-runlists the remaining work for the selected security profile.- The security profile matrix is available in the CLI and documentation. It lists each check, the profiles that require it, and the delivered value.
- Policy administration has version history, statement-level diff and revert, and an interactive review that accepts or rejects selected changes. The editor has a statement tree and a full-text view of every statement Cedar reads.
- Deployment assets and procedures cover one host, split hosts, Docker Compose, Docker Swarm, Kubernetes with Helm, and a Kustomize base.
- Connector enrollment through
public_joinsupports a selected route and enrollment SNI. An IP-addressed gateway can obtain its registration certificate from the cluster CA. - CLI help groups commands by task.
doctorprints a status table followed by the actions needed to clear its findings.
Changed¶
- Every Authority replica serves registration, renewal, administrative writes, and workload certificates. A proxy finds the current write leader after a leader refusal. Authority instances starting together on an empty store use one shared set of certificate authorities.
- Concurrent policy approvals and rejections preserve one active version. Policy and CA changes converge across Authority replicas after a missed notification or restart.
- Audit events coupled to control-plane changes survive a writer failure. A
deleted, changed, duplicated or withheld queued event is reported by audit
verification,
doctor, logs and metrics. - CA rotation requests remain pending until a rotation starts. Propagation and retirement waits survive restart and leader failover, and a former leader cannot advance the rotation.
join --if-missingcan renew a connector at a new registration address. Route validation and local write checks run before a one-time token is redeemed.
Fixed¶
- A damaged private key beside an automatically issued registration certificate causes the pair to be issued again.
- Windows private writes refuse a file owned by another account. Rejoining a service-owned installation checks ownership and write access before spending the token.
- Setup validates listener addresses, trust, service state directories and audit-export paths before starting roles. A failed check leaves the join token unused.
- The Windows resident network service releases data received from an expired lease holder. Policy review renders long lines and terminal control bytes as text.
- Sandbox diagnostics name the
strictprofile as the mode that closes Unix socket access even when no container runtime socket is present.
Documentation¶
- Quick start begins with a deployment-scenario chooser. Each deployment page states its ports, certificates, role placement, start order and checks.
- Configuration pages follow the operator's sequence: start, roles, network, access, operations and agents. The change guide maps each setting to the roles, reloads and sessions it affects.
- CLI reference pages are grouped by audience, with setup on a page of its own.
v0.10.0-rc.6¶
Release identity and rollback boundary
v0.10.0-rc.6 is the exact manifest/tag identity. It absorbs
v0.10.0-rc.1 to v0.10.0-rc.5. Against v0.9.0 it adds store migration
m0074; store migrations are forward-only, and an older binary refuses a
store it has been applied to. Take a store backup before the upgrade. Linux
hosts running the shipped systemd units reinstall each role:
to v0.10.0-rc.6.
Added¶
- Windows x86_64 (Windows 10 1809 or Windows Server 2019 and later).
Authority, Proxy, connector and client run on Windows from the same
gdsgate.exe. The agent host role runs agents on Linux only. - The Windows workstation installer
gdsgate-<version>-x86_64-pc-windows-msvc.msi, available on request. It installsgdsgate.exeinto%ProgramFiles%\gdsgate, adds it to the systemPATH, and installs and starts thegdsgate-client-netservice. gdsgate service install|uninstall <role>forauthority,proxyandconnectoron Linux (a systemd unit) and on Windows (a service underNT SERVICE\gdsgate-<role>), andclient-neton Windows.gdsgate setupinstalls and starts roles through it;setup prepare --startruns that step from the command line.- On Windows: configuration, state and logs under
%ProgramData%\gdsgate; local sockets are named pipes;gdsgate upwith Wintun and NRPT throughgdsgate-client-net;delegate --sandbox basic|strictin an AppContainer and a job object; connector SSH sessions on a pseudoconsole, with SFTP and agent forwarding. - PowerShell completion. Completion of a resource name offers only the resources the command takes.
gdsgate delegate:--save-profile NAME(with--replace-profile) saves the run as a[[delegation_profiles]]row;--env NAME=VALUEsets a variable for the program;--show-agent-profile NAMEprints an agent profile;--sandbox-listen PORTlets this machine reach a port the caged program listens on;--keep-run-dirkeeps the run directory.[[agent_profiles]] config_dir_envnames the variable that points the program at its configuration directory under a cage.authority audit listshows people by display name and accepts it in--actor;--jsonaddsactor_name,owner_nameandwork. The name is taken at sign-in from the claims in[oidc].display_name_claims(defaultpreferred_username, thenemail).authority forget-user <subject>removes a person's display name. Gated by CedardisableUser; audit codeAUT-0401.setup change --setand--backendread a value from a file (@PATH) or standard input (@-);@@starts a value with@. The plan does not show the value.gdsgate up --foregroundon a terminal also writes its output to~/.gdsgate/gdsgated.log.
Changed¶
- Each shipped systemd unit has a state directory of its own,
StateDirectory=gdsgate/<dir>.service installandsetup preparerefuse a configuration whose role writes outside/var/lib/gdsgate, directly in it, or in two directories under it.install-connector.shrequires--state-dirunder/var/lib/gdsgate/connectoron a systemd host. setup applyrun as root requires--approve-planwith the digest--dry-runprinted. A bundle writes only.toml,.cedar,.serviceand.keysfiles and reads only files inside the bundle.setup preparewrites the Authority's[security].profileinto the Proxy's configuration.- A role warns at start about each secret file its configuration names that other accounts can read.
delegate --sandbox strictrefuses io_uring.credential_header = "accept-encoding"on a model service is refused at startup.- An MCP resource URI with a backslash in its path is refused.
- The web lane removes the gateway identity headers from the application's answer.
- An SFTP read returns at most 256 KiB per request.
- Output of an
execsecret reference is limited to 1 MiB. - Log lines are coloured only on a terminal.
Fixed¶
- A connector with
authority_transport = "tunneled"whose activation lease ran out while Authority was unreachable reconnects when Authority is back. Previously it needed a restart. - With Authority and Proxy on one Linux host, restarting one no longer takes the other's state directory.
setup restore --approve-planaccepts the digest its--dry-runprinted.bootstrap-admindoes not ask for an[admin]section the configuration already has.delegate: a run that fails before its program starts revokes its grant; an interrupt goes to the program and does not end the grant; a killed run's directory is removed by the next run.- SSH: a closed channel releases its terminal; agent forwarding uses one socket per session.
- Improved handling of files changed by root, of the jump-host relay backlog, and of the resident client's stop.
Documentation¶
- Every procedure that differs between Linux and Windows gives both: installation, services, paths, certificate reload, the resident client and the delegated sandbox.
- The reference pages are shorter; behaviour is given in tables of condition and result, or symptom, cause and what to do.
gdsgate_license_expires_unixin the metrics reference.
v0.9.0¶
Release identity and rollback boundary
v0.9.0 is the exact manifest/tag identity, and it carries the content of
v0.9.0-rc.11 unchanged. Against v0.8.23 it adds store migrations
m0068–m0073; store migrations are forward-only, and an older binary
refuses a store they have been applied to. Take a store backup before the
upgrade. Upgrade order and the onward_auth step:
to v0.9.0-rc.11.
Added¶
- The agent host role runs long-lived agents, each in a cage of its own and
under an identity of its own. What runs where is declared centrally with
authority set-agent,switch-agent,remove-agentandlist-agents, gated by the Cedar actionsdeclareAgent,switchAgentandviewAgents. A running agent is given a resource without a restart; taking a capability away restarts it. Access is held on a lease the host renews; when renewal stops,on_expiryfreezes or stops the agent. - An agent is published as catalog rows, one per face: its console (
ssh), a page (web), a model API (llm), tools (mcp) or a byte stream (tcp), in the boundary named by[agent_host].scope. People reach a face the way they reach any other resource of its kind. A console can be watched without being typed into. authority list-agentsshows what each host last reported about its agent (seen) beside what was declared (state).- A
webface declaredidentity = "signed"passes the agent a signed statement of the visitor. With it the agent can call a model, an MCP server, a web application or a Kubernetes cluster for that person: the call is allowed only where both the agent and the person may make it, and its audit row names the person (owner,on_behalf_of,arrived_at). set-agent,set-resourceandset-scopetake--from <file>, a TOML document;list-agents,list-resourcesandlist-scopesprint the same form with--declaration.- Agent declarations carry a version.
set-agent --fromsends back the version--declarationprinted,--if-versiondoes the same for flags, and an edit of a declaration somebody changed since is refused.[authority].agent_declaration_version = "required"refuses a rewrite that names no version. - Audit rows carry the agent they concern (
work), andauthority audit listfilters on it with--work. gdsgate doctoron an agent host reports whether the host can cage an agent (state.agent.cage).packaging/systemd/gdsgate-agent-host.service(systemd 254 or later) andpackaging/apparmor/gdsgate, the AppArmor profile hosts restricting unprivileged user namespaces need.gdsgate joinwrites[endpoints].proxy_publicand[client].transport_cafor a node that opens sessions of its own.- A
webbackend withidentity = "headers"sends inX-Remote-Groupsthe groups Authority returned for the request. - The resident client reads the catalog as soon as its name server is asked
for a name in the zone it does not have, and on
gdsgate status --refresh. Two reads are at least 5 seconds apart.
Changed¶
[[discovery.auto_scopes]] onward_authis a list of the modes a target in the boundary may name, and a boundary admittingsshmust set it: Authority refuses to start without it, andauthority set-scoperefuses the scope. Each discoveredsshtarget states its own mode.- A web-lane request carrying a
gdsgate-identityheader is decided for both the caller and the person named in the header, and refused unless both are allowed. Previously the header was removed. - An agent or other workload is impersonated in Kubernetes under its SPIFFE ID
(
spiffe://gdsgate/agent/<project>/<id>for an agent).
Fixed¶
sshtargets published into a discovery boundary are reachable: the onward mode is taken from the target and checked againstonward_auth.- A node that declares no backend is admitted.
- Kubernetes sessions opened with a workload certificate are served.
- Model A session ceilings (
memory_max,tasks_max,cpu_quota) work underDelegate=yeswithDelegateSubgroup=. authority list-resources --jsoncarriesscopeandlease_untilon a row a connector wrote into a boundary.gdsgate doctorchecks the agent host cage (state.agent.cage) only where the configuration declares[agent_host]. Onv0.9.0-rc.9andv0.9.0-rc.10every enrolled proxy and connector reported it failed where a full cage cannot be built, containers among them.
Documentation¶
- Agent host: installing an agent host, and seven use cases: an HTTP API over a model, a temporary web UI for it, a page that acts for the visitor, MCP tools for editors, an agent with a console, narrow database and cluster access, and an agent started on demand.
- Agent host configuration and the
administration reference for
set-agent,list-agents,switch-agent,remove-agentand declaration documents.
v0.9.0-rc.7¶
Release identity and rollback boundary
v0.9.0-rc.7 is the exact manifest/tag identity. The release adds no store
migrations and no wire fields, so a downgrade to v0.9.0-rc.3 is possible.
Added¶
gdsgate setup: installing and configuring a deployment from the host it runs on, instead of writing each role's file by hand.setup preparewrites a new installation,setup changeedits one that is already running,setup restoreputs back what a change replaced, andsetup checkreports whether a host can run the role it is asked to.- Every one of those commands plans before it acts.
--dry-runshows what the run would do and changes nothing;--jsonprints the same plan for a script;--approve-plan <digest>refuses unless the plan is the one that digest names, so a plan reviewed by one person is the plan another applies. gdsgate setup --tui: the same three tracks on a screen, for an operator who would rather answer questions than assemble a file. It reads an installation that is already there, including the file a running role was started with.- Authority and Proxy can be installed on two machines from one plan.
setup prepareproduces a bundle,setup apply --bundle <dir>carries it out on the second machine, and no key material travels in it. gdsgate proxy init --domain <name>writes a Proxy's file in one command, andgdsgate authority initstates what it will do before it creates any key material.- Changes are checkpointed: each run keeps a copy of what it replaced, in
.checkpointsbeside the file or at--checkpoint-dir, andsetup restoreputs one back. - Installed roles can be started as systemd services by the same run.
gdsgate doctorreads what a node declares against what the cluster was told, and reports where the two disagree.
Changed¶
- Every listing prints a table: a header, columns as wide as their contents, and
(none)under the header where nothing matched. Times are UTC (YYYY-MM-DD HH:MM:SS), sizes and intervals are one unit (48.2K,14m), and a field the row does not carry is-. --jsonis on every listing, with the wire's own numbers and no truncation.--wideadds the columns the table leaves out; the set of columns does not follow the width of the terminal.authority recordings listprints the table rather than tab-separated fields, and the session id in it is the whole UUID. Anything that parsed the old output reads--jsoninstead, where absence is an absent field.authority audit listandauthority recordings listtake--since, which reads an interval back from now (90s,30m,2h,7d) or a date (2026-09-21, midnight UTC).--since-msstays for a caller holding an instant; the two conflict rather than one quietly winning.authority recordings list --limitdefaults to 50 rather than to the server's own cap.--asand the inline-remediation flags (--wait,--wait-timeout,--no-remediate,--step-up-factor) appear only on the commands that read them, instead of on every command's help.- A store with no cluster in it starts at the activation epoch of the build that opens it, rather than with every gate switched off and nothing said.
- The baseline policy a new deployment is seeded with grants a session that can run something, so a fresh installation is usable before a policy is written.
Fixed¶
gdsgate join <domain>derives the registration address from the domain at the port the join relay binds, so a node joining a deployment the wizard installed needs no--enroll-endpoint.authority recordings list --limit Nreturns N recordings wherever they sit in the chain. It read a window sized from the limit, and a deployment that opens far more sessions than it records has no recording in that window: the listing answered(none)on a chain holding hundreds. Authority has to be on this version for it. Against an older one, read the whole chain with--limit 0.- Enrollment, policy editing, audit keyrings, and the doctor's path checks: fixed.
Security¶
rustlsis updated past RUSTSEC-2026-0285 (TLS 1.3 handshake messages accepted across encryption level boundaries) andcryptokipast RUSTSEC-2026-0286 (out-of-bounds read). Both are dependency updates; nothing in the deployment changes.
Documentation¶
- Running the image: how the image is given a role and its settings, what to mount, and two deployment recipes: Authority and Proxy in Kubernetes, and everything through the Proxy with a certificate from an external ACME client.
- The
gdsgate setupfamily has a reference entry per command, and--checkpoint-diris documented with the rest ofsetup change.
v0.9.0-rc.3¶
Release identity and rollback boundary
v0.9.0-rc.3 is the exact manifest/tag identity.
The release adds no store migrations. The access certificate gains one
additive field (channels); a certificate carrying it is refused by a build
that predates it, so upgrade Authority and the connectors together. The
per-connect authorisation gains a request and a response field, so a
connector newer than its Authority opens no desktop for a workload until
both are upgraded.
A client whose local root was created by an earlier release is replaced at
the next start of gdsgate up. See Changed.
Added¶
kind = "rdp"connector backend and theRdpHostCedar entity. Resource kindrdp, default port 3389. Authorisation on opening isconnectoverRdpHost::"<resource>".- A backend that names
windows_userandcredential_fileterminates the session: the connector opens it on the Windows host, logs in with the stored account, and serves the desktop to the caller over its own listener. A backend that names neither forwards the stream. [[connector.backends.windows_users]]offers several accounts on one host. The caller names one (gdsgate rdp proxy <resource> --windows-user), policy decides overcontext.windows_user, and the access certificate carries the answer.gdsgate rdp proxy <resource>opens a loopback listener and prints a username and password for it. The pair is that listener's until the command exits, and the listener carries one session at a time.--execstarts a local client against it. The name travels on the client's command line and the password does not: aFreeRDPclient is pointed atgdsgatethroughFREERDP_ASKPASSand asks it (FreeRDP3.8 or newer), andmstscasks the person. Such a client is also told which certificate to accept, by the digest of the leaf that session minted, so it needs neither the local root installed nor a person to answer for it.- Cedar action
desktopChannel, asked once per channel and direction while the access certificate is issued. Context:channel,direction,presented. Audit codePOL-0208. Deny-by-default: a cluster whose policy does not mention the action carries no channels. A session whose certificate carries no decision at all is refused at the serving node; the backend's configuration is not a fallback. - A workload or a delegated program holds no access certificate, so its account
and its channels are settled on the connect call that authorises it, over the
same
Agentprincipal, and travel back with the permit. A policy that mentions only people gives an agent a desktop with no channels; a backend offering several accounts refuses the session where the policy leaves more than one open to it. - Backend keys for what a desktop carries:
clipboardandclipboard_files(off,in,out,both),audio,drives,printers,ports,smart_cards. A channel nothing permits is not opened, and a client asking for it is told there is no such channel.clipboard_filesnarrowsclipboard, and each of its directions is decided on its own. - A desktop session holds at most 16 MiB in the queues between its two halves.
Past that it ends, with
reason=queue_overrunon the close row and an outcome ofSuccess: nothing broke, one side stopped keeping up. graphics = "redraw" | "passthrough".redrawdecodes the host's picture and encodes it again for the caller.passthroughasks Windows for its graphics pipeline (MS-RDPEGFX) and relays that channel to the caller unread; it needs a client that speaks the pipeline.- Session recording for the lane:
recordandrecord_max_bytesapply as they do onkind = "ssh",recording_kindisrdp, and the artifact is<session>.rdp. A recording undergraphics = "redraw"plays back frame by frame; one underpassthroughholds the host's stream with its screen inside the pipeline, in codecs the caller and the host agreed between themselves, and the shipped player draws none of them. - Host certificate pinning for a terminating backend:
host_cert_fingerprints,host_cert_fingerprints_file, and the sharedallow_unpinned_upstream. An unpinned host is refused before the stored password is presented.gdsgate ca learn-host-cert <host>prints the line to write. - The session close row carries
windows_session, the host's own session number, andchannels, what the session ended up carrying. - A desktop is a catalogue resource like any other:
kind = "rdp"in[[discovery.resources]], onauthority set-resource --kind, and in a connector's--servespec; default port 3389. CedarmanageCatalogapplies toRdpHost, so a rule may say who declares one.
Changed¶
- The client's local root certificate is ECDSA P-256/SHA-256. TLS channel binding names a hash through the certificate's signature algorithm, and Ed25519 names none, so a desktop client refused the connection before authentication. A root created by an earlier release is replaced at startup, with a line in the log; anything that trusted the old root trusts the new one after the trust step is repeated.
- Keys that describe a terminated desktop session are refused at startup on a
backend that opens none. On
kind = "rdp"without an account,record,record_max_bytes, the channel keys,graphicsand the three pinning keys named nothing. - On Windows, a client's own records under
~/.gdsgateare written with an access list of their own (the account that owns them andSYSTEM) instead of inheriting whatever the directory they sit in gives, and the check that refuses a record readable by anybody else reads that list rather than assuming it. A record whose reach was widened is refused where an earlier client accepted it, and one whose list holds an entry this build cannot read is refused rather than called owner-only. - A desktop recording is a picture under
graphics = "redraw"and a stored stream underpassthrough, where the screen is inside the pipeline in codecs this gateway never reads. Where a recording has to be reviewed,redrawis the mode that gives one.
v0.8.23¶
Release identity and rollback boundary
v0.8.23 is the exact manifest/tag identity, and it carries the content of
v0.8.23-rc.11 unchanged. The release adds nine
store migrations, m0059 to m0067; store migrations are forward-only,
and an older binary refuses a store they have been applied to. Take a store
backup as usual.
Nothing in this release changes behaviour until
gdsgate authority activate raises the activation epoch. On a cluster that
has raised it, upgrade the connectors before Authority: see
Connectors before the authority.
Added¶
- Cedar action
sshProgram, asked once per shell, perexecand per subsystem an SSH session runs, on both SSH models. Context:program_kind, plusgit_operation,git_service,repositoryandsubsystemon the resource. Audit codePOL-0106. - Cedar action
sshUseClientKey, asked before a session may authenticate to the host beyond a jump host with the caller's own key. Audit codePOL-0107. ssh_profile = "git"serves the four recognised git commands on an SSH resource and refuses everything else, including a shell. The catalogue owns the value (resources.ssh_profile, migrationm0059); the same key on[[connector.backends]]is the node's proposal, and selects the mechanism there: a recognised command runs directly, with no login shell. The programs come from[connector.git]:git_upload_pack,git_receive_pack,git_upload_archiveandgit_lfs_authenticate, which has no default.repository_rootis the directory a repository argument resolves beneath, and a path that leaves it is refused.- Each git program is opened at startup and run through that descriptor. A
#!wrapper runs through its interpreter, pinned the same way. An operation whose program cannot be pinned is refused at startup, with the reason in the log. web_profile = "git"(resources.web_profile, migrationm0062) refuses a request on that resource that is not one of the git operations. The classification runs on every web request whatever the profile: the operation comes from the method and the path, and for the LFS Batch endpoint from theoperationfield of the body, read up to[connector.git].lfs_batch_body_max.httpRequestcontext gainsgit_operationandrepository.onward_auth = "local" | "certificate" | "client_key"states how a session authenticates to the host beyond a jump host. The catalogue owns the value (resources.onward_auth, migrationm0064); a connector's file states a proposal, and a resource the two disagree about is served under neither reading. Set it withauthority set-resource --onward-auth.client_keyforwards the caller's agent over the session's own channel: no key material reaches the gateway, and the connector mints no certificate.[client] agent_key_sourceand[[client.agent_resources]]are the client's per-resource consent;[[connector.backends]] agent_identities_maxandagent_auth_attempts_maxbound what one authentication may spend.login_usersaccepts"$subject"underonward_auth = "client_key": the account on the host beyond is the caller's own identity, taken from the certificate that admitted the session.activation_epoch(migrationm0061) andgdsgate authority activate --epoch <n>. Epoch 1 starts askingsshProgram; epoch 2 reads an SSH resource'sonward_authfrom the catalogue only. The value rises and never falls.--forceproceeds over the named shortfalls and writes them to the chain.- The activation check asks the fleet before raising: every node serving an SSH
resource that has been heard from recently must state it can serve past the
gate.
enrolled_node.capabilitiesandcapabilities_at(migrationsm0063,m0066) hold that statement and when it was last made. - Registration enforces the same question afterwards. A node that has not shown
it can serve past a switched-on gate is served no SSH resource, answered
gate_not_offered; a resource whoseonward_auththe catalogue does not name is answeredmode_not_confirmed. Authority issues a one-time receipt on each activation-lease renewal (enrolled_node.capability_nonce, migrationm0067), and the node presents it at its next registration. offered_resource.ssh_profileandoffered_resource.onward_auth(migrationm0065) record what a connector proposes, soauthority list-offeredshows a disagreement with the catalogue.[[connector.backends]] audit_commandwrites the request string a session asked to run into the audit row. Off by default, and never part of a decision.- Audit detail keys
program_kind,git_operation,git_service,repository,subsystem,command,denied_by,activation_epoch,activation_epoch_beforeandactivation_overridden.
Changed¶
- A jump host presents the node's persistent SSH host key, the same one a model
A backend presents, instead of a key made per session. A caller's
known_hostsentry for a jump host now stays valid across sessions and restarts. - A connector reads and writes its state under one effective directory:
[enroll].state_dirwhen set, the role-resolved path under$HOMEwhen not. This covers the transport identity, the SSH host key, session recordings, the control socket, the discovery generation and the activation mark. A node that never set[enroll].state_dirpreviously kept no recordings and no discovery generation. An SSH host key already at the working-directory path is still used. - Raising the activation epoch ends the SSH sessions a connector is carrying, with the reason on the wire, and re-opens its tunnel so what it is served is decided again.
- A connector whose activation lease runs out ends the SSH sessions it is carrying as well as refusing new ones.
- A connector serving SSH refuses to start when it cannot read and write
<state_dir>/activation.json. The file is written whole or not at all. - A change in what a discovery probe finds re-opens the connector's tunnel; a
proxy reads
Registeras a tunnel's first frame only. Probes keep their findings per source, so two probes no longer replace each other's targets. - A discovered SSH target is declared as recorded only where the node has a recording sink, matching a backend read from the configuration file.
Fixed¶
git clone,git fetchandgit pushthrough a jump host failed with the remote end hanging up: the relay stopped at EOF and dropped the exit status that follows it. Any program run over a model B session was affected.- An RSA key forwarded to the host beyond a jump host was offered as
ssh-rsa(SHA-1) and refused by servers that require SHA-2. - Losing a connector's tunnel left the caller with a connection that produced nothing, the program the session started running on the connector host, and the session open in the audit chain. The session now ends, its program is reaped, and the proxy closes the client streams the tunnel carried.
AuthorizeConnectorRegistrationand the other control-plane calls a connector makes had no answer ceiling; a control plane that accepted a call and never answered held the session that made it.- A model B channel that closed left its entry in the connector's channel maps.
- The client's working-signature budget counted over the life of the process rather than over the ceiling's window, so a long-lived agent forward eventually refused a signature it should have spent.
gdsgate doctorread an unreachable store as "nothing is switched on" and printed a lenient report of a cluster that may be strict.
v0.8.23-rc.10¶
Release identity and rollback boundary
v0.8.23-rc.10 is the exact manifest/tag identity. The release adds five
store migrations, m0054 to m0058; store migrations are forward-only,
and an older binary refuses a store they have been applied to. A cluster
that declares no [[discovery.auto_scopes]] and runs no model A SSH
resource under a host account is unaffected, apart from the audit queue
described below, which every cluster gets. Take a store backup as usual.
Added¶
login_mode = "host_user"on an SSH model A backend runs a session as an account of the connector's host, started through the host's own login program.login_usernames the one account a resource offers;login_usersnames several, with"$subject"standing for the caller's own identity and"*"for any account the host holds. See Model A under a host account.offered_resource.login_user(migrationm0054): the account a node states it will run a session as. The account is signed into the certificate the session is decided under, and a node that would run another is refused.- Cedar rules read
login_userin the context of an SSH action, so a rule can admit one account and refuse another. - A model A session's processes run in a cgroup of their own, with the ceilings
[connector.backends.session]states. A host that delegates no cgroup is reported at startup and serves sessions without ceilings. [[discovery.auto_scopes]](migrationm0055): a prefix, a node, the kinds, the project and environment, the accepted label keys, a row ceiling and a lease. The named connector writes catalog rows inside the scope.resources.scopeandresources.lease_expires_at(migrationm0056) mark a row written inside a scope and when its lease ends. Authority deletes expired rows every[discovery].lease_sweep_secs(default 60).authority list-scopes,authority set-scope,authority remove-scope, over the control-plane certificate or a person's token through the gateway. Removing or disabling a scope deletes the rows it holds.PublishScopedResources: a connector publishes a snapshot of targets into a scope. The snapshot is refused in full when the publishing node is not the scope's, an id falls outside the prefix, an id appears twice, a kind is one the scope does not admit, the scope would hold more rows than its ceiling admits, the scope was rewritten while the snapshot was being checked, or the generation is not newer than the stored one. Every refusal is audited underCAT-0302.[connector.backends.discovery] scopepublishes a probe's findings as catalog rows. A database probe's targets are<host>.<db>, so its scope prefix ends in a dot.mode = "list_kube_targets"enumeratesService,PodandNodebylabel_selectorand publishes them into a scope. An object states its kind withgdsgate.io/kind, its port withgdsgate.io/port, and ansshtarget's host key withgdsgate.io/ssh-host-key. The connector's service account needsliston the collections named insources.mode = "list_namespaces"publishes a cluster's namespaces as inventory;PublishBackendInventorynow acceptskubernetesresources.ssh_host_key = "annotation" | "tofu"on a scope selects the source of a discoveredsshtarget's host-key fingerprint.annotationreadsgdsgate.io/ssh-host-keyand does not serve a target without it;tofuaccepts the first key seen for the lease.fallback = "parent" | "deny"on a scope selects how a target with no row is authorized:parentthrough its parent resource,denynot at all.gdsgate lsgains aLEASEcolumn;gdsgate ls --leasedlists only rows written inside a scope.- Audit codes
CAT-0301 scope.published(one row per snapshot) andCAT-0302 scope.refused. The published row carries two digests:sha256over the targets the snapshot published, andheld_sha256over the ids the scope holds afterwards. - An audit queue (migration
m0057): a catalog change and the audit row it produces are stored together, and the row reaches the chain afterwards. A chain that is briefly unreachable delays the row; the row is not lost. Applies toscope.published,scope.refused, the expired-lease sweep andcatalog.managefor scopes. - Metrics
gdsgate_scope_publishes_total,gdsgate_scope_live_rows,gdsgate_scope_leases_expired_total,gdsgate_audit_outbox_depth,gdsgate_audit_outbox_oldest_age_secs. doctorchecks declared scopes and reports a probe publishing into a scope the configuration does not declare, a scope holding 90% of its ceiling or more (state.auto_scope.occupancy), and audit rows queued longer than five minutes (state.audit.outbox).ListResourcesAdminanswers withscopeandlease_expires_atper row: the scope that vouches for a row written inside one, and when its lease runs out. Both are empty on a row a person declared. Added to the message, so a client built against an earlier tag reads the listing unchanged.authority list-resourcesprintsscope=<prefix>andlease_until=<unix seconds>on a row written inside a scope. A declared row prints neither.
Changed¶
max_livebounds the rows a scope holds at once, counting rows from earlier snapshots whose leases have not run out. It bounded the size of one snapshot, so a scope whose targets are replaced every pass could holdmax_live × (lease_secs / interval_secs)rows. A snapshot that would leave the scope over the ceiling is refused in full, and the refusal names the live count, the resulting count and the ceiling. Clusters publishing past the ceiling needmax_liveraised, in[[discovery.auto_scopes]]and withauthority set-scope; the catalog shrinks to the ceiling as leases run out.authority list-scopescounts rows whose leases have not run out, which is the count the ceiling is measured against. Expired rows waiting for the sweep are no longer included.- A scope declared in the configuration file is rewritten from the file at every
Authority start. A value changed with
authority set-scopehas to be written into[[discovery.auto_scopes]]as well to survive a restart. - Migration
m0058indexesresources.scopeandresources.hostname. - A discovery snapshot's generation is taken from the wall clock. It was a counter: a connector that lost its state directory had every publish refused as stale against the generation the cluster held.
- A catalog row that declares a
hostnameis listed under it even when the row has a parent. Sub-resources synthesised from inventory declare none and are listed as before.
Fixed¶
gdsgate alldid not run the lease sweeper and did not serve the targets a cluster probe published. Published rows had no route, and expired rows were not deleted.--route gatewayrefusedauthority list-scopes/set-scope/remove-scope.- A connector running in the cgroup root, which is what a container in its own cgroup namespace reports, was told a frame could not be made because of writability. It is told there is no delegated subtree, which is the actual condition.
- A snapshot reached the catalog row by row, with the scope's generation and the audit row following it. A failure part-way left rows from two snapshots and no generation describing them. A snapshot is applied whole or not at all.
- Two publishes into one scope were not serialized, so two Authority instances could interleave their writes. Publishes into one scope are serialized.
- A scope disabled, moved to another node or narrowed while a snapshot was being checked still accepted that snapshot. It is refused.
- Removing a scope deleted its rows and the scope itself separately; a publish arriving in between left rows behind with no scope. Both go together.
- Declaring a scope checked the overlapping prefixes and the declared resources before writing, so two scopes declared at once could both be written with overlapping prefixes.
- Five refusals answered the connector without writing
CAT-0302: an id outside the scope, an unusable id, a kind the scope does not admit, a repeated id, and an id a person declared.
v0.8.23-rc.5¶
Release identity and rollback boundary
v0.8.23-rc.5 is the exact manifest/tag identity. The release adds one
store migration, m0053; store migrations are forward-only, and an older
binary refuses a store it has been applied to. SubmitAuditEvents now
refuses a batch from a connector with no row in the node registry;
register it with authority import-node first. install-connector.sh
now defaults to --recording central, so reinstalling a standing node
moves it off disk unless --recording disk is passed. Take a store
backup as usual.
Added¶
audit_log.submitter_node(migrationm0053) names the connector that shipped a row, read off its mutual TLS leaf and sealed into the chain digest.authority audit list --node <id>filters by it; every export form carries it besidesubmitter.StoreRecordinganswers with the digest of the bytes it took. A connector incentralmode removes its local copy once that digest matches its own, and keeps the file otherwise.[recording].sweep_interval_secs(default 900;0disables) runs a delivery pass over the recordings a session close left behind. Each pass writes.delivery.jsonbeside them.connector recordings push [--dry-run]asks a running connector for that pass on demand, over a0600control socket in its state directory. Exit0delivered,1held back,3nothing waiting.authority recordings listgains a tenth column:central:<epoch>once the store holds the bytes,connectorwhile it does not,-for a session with no recording. The sealedfile://URI answers where the connector wrote it, not where it is now.join --recording <disk|central>, and the same flag oninstall-connector.sh.- A connector finishes recordings an earlier run left half-written before it
registers its tunnel, and writes what it did to
.startup.json. - Proxy reports the sessions a departed connector left open, after
[proxy].abandoned_session_grace_secs(default 300;0disables). Authority writesSES-0104 session.abandonedper session and oneSYS-0101 connector.tunnel_lostfor the node. CloseLateRecordingwrites the close a killed connector owed, from the file's length and digest, so its recording can be delivered. OutcomeFailure,reason: late_close.REC-0201 recording.deliverrecords a late delivery: session, bytes, digest, and how long the file waited.SES-0105 session.claim_refusedrefuses an upload or a late close from a node other than the one that opened the session.operationsays which call was refused; one row per caller, not per attempt.doctoron a connector incentralmode adds six checks about recordings waiting, held back, and the store's reachability. All six are silent indiskmode.
Changed¶
- The published binary carries every wrapping-key provider.
vault,awsandgcpnow work from the release artifact asexecalready did; they needed a build of your own before. SubmitAuditEventsrefuses a whole batch from a connector with no row in the node registry, naming the leaf's fingerprint. None of its rows reach the chain.install-connector.shpasses--recording centralunless told otherwise. It is the only installer default that moves a standing node on reinstall.
Fixed¶
- Both recording journals were truncated before they were written, so a connector that died between the two left neither account. They are written whole or not at all.
- The delivery pass and its socket did not start under
gdsgate all. - Comments, config templates and the systemd units still called the role
Auth. Theauthorityunit also readsauthority.env;auth.envstays supported. doctorreported a connector that has recorded nothing as one whose start-up pass never reported, and called a pass that finished this second "expired".
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, and an older binary refuses a store they have been applied
to. [security].profile left unset changes nothing. Declared as
production it adds start-up refusals, and an Authority does not start
until both integrity keyrings are named, owner-only and distinct, every
replica holds the same keys of each, every table's newest sealed row names
a key a keyring holds or one declared lost, and [oidc].issuer and
[oidc].client_id are both set. The five [audit].*_key_path parameters
are still read; authority keys migrate-legacy brings the keys they name
into the keyrings. Export the audit anchors and take a store backup before
upgrading. Roll back by restoring the pre-upgrade store and its external
keys, not by 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; a bare value is unchanged.exec:runs without a shell and a non-zero exit refuses the start; a listener's certificate key is resolved again at each rotation.store_password_filekeeps the state backend's password out ofstore_url. A path or a secret reference, also set byGDSGATE_STORE_PASSWORD_FILE. PostgreSQL only.[security].profile:dev,pilotorproduction, the deployment profile a cluster is held to at start-up. Unset changes nothing.pilotandproductionrefuse a start that keeps cluster state in a file, serves a listener in the clear, or runs all three roles off a certificate authority held in one process's memory;productionrefuses more, and a refusal names every fault at once.[security.integrity]withcontrol_keyringandaudit_keyring, the two keyrings a cluster's sealed rows go under. Both accept the secret-reference notation. Unset, Authority goes on reading the five[audit].*_key_pathparameters.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-seedsandprune-ca-keys. None of them prints key material.keys migrate-legacytakes the keys the five[audit].*_key_pathparameters name into the keyrings, as records that verify existing rows and seal nothing new. 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.--activateis refused while a replica has not published that it holds the prepared key;--retirewhile a row still names the key, and always for theauditkeyring.--prepare --kind signingrotates the checkpoint-signing key instead.keys accept-loss --key-id --accepted-byrecords that a key is gone.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) while theauditkeyring holds a signing key.verify-audit --checkpoint-keys <FILE>checks each checkpoint by signature, so a reviewer holding no key material can verify the journal;keys checkpoint-keysprints the public halves. 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. Underproductionit refuses to mint a keyring on this host: name one already in place, or a wrapped one whose key--kms-keydeclares.authority set-profile --profile --changed-bydeclares a different profile afterwards, writing the change to the audit chain before rewriting the line in the file.[[kms_keys]]names 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. Credentials come from the process environment, and no material crosses in an argument.provider = "exec"is in every build.vault(the transit engine of Vault and OpenBao),awsandgcpneed a build carryingkms-vault,kms-awsorkms-gcp; a row naming a kind the binary lacks is refused by name.[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 partway runs either way.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.
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.
- New rows go on being sealed under the five
[audit].*_key_pathkeys until a keyring holds an active key of its own, so those parameters keep being read afterkeys migrate-legacy. - Authority leaves closed the signing seed of a CA generation nothing can reach again: neither of its authority's two newest, named by no rotation role, and out of its validity window for more than thirty days.
authority init-anchorand the four otherinit-*-keycommands refuse when the parameter they would write names a variable, a command or a wrapped source instead of a file.--pathnames the file to write.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.doctoralso 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 wherever the connection string is printed: log lines,doctoroutput, and the refusals the database driver raises with the string quoted inside them. 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. A certificate issued
under --as is refused by a connector or proxy older than this release, so
upgrade proxies and connectors before the Authority that mints them. Store
migrations are forward-only; an older binary refuses a store this release
migrated. Export the audit anchors and take a store backup before upgrading.
Roll back by restoring the pre-upgrade store and its external keys, not by
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. 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. 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. One minted for another grant, or 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.