Skip to content

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 sync provides root-status, prepare-root, activate-root and cancel-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_key selects a declared wrapping provider for the local distribution root. authority keys sync wrap-root migrates an existing plaintext root after verifying the persisted wrapped value. Provider failure refuses startup without falling back to plaintext.
  • connector.discovery_sources supports 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 change combines 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 up installs and starts the resident service on Linux with systemd. sudo gdsgate up preserves the original user's configuration and identity, starts the system unit with network capabilities, and needs no second sudo. service install client --start supports explicit installation.
  • Linux resident endpoints restrict connections to their owner's OS UID by default. The protection covers basic, direct and transparent access; 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_offset defaults to 10000: 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 web resources 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 --explain shows 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.
  • doctor checks that /var/lib/gdsgate is a directory with mode 0755. Privileged service install creates 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.close without 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

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_count selects 1–16 distinct Proxy connections; its default remains 1 for local-session compatibility. The HA examples set it to 2 to 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 schema writes the configuration JSON Schema. gdsgate config check validates a configuration offline and reports a corrective action. setup prepare --dry-run lists 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_join supports 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. doctor prints 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-missing can 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 strict profile 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 installs gdsgate.exe into %ProgramFiles%\gdsgate, adds it to the system PATH, and installs and starts the gdsgate-client-net service.
  • gdsgate service install|uninstall <role> for authority, proxy and connector on Linux (a systemd unit) and on Windows (a service under NT SERVICE\gdsgate-<role>), and client-net on Windows. gdsgate setup installs and starts roles through it; setup prepare --start runs that step from the command line.
  • On Windows: configuration, state and logs under %ProgramData%\gdsgate; local sockets are named pipes; gdsgate up with Wintun and NRPT through gdsgate-client-net; delegate --sandbox basic|strict in 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=VALUE sets a variable for the program; --show-agent-profile NAME prints an agent profile; --sandbox-listen PORT lets this machine reach a port the caged program listens on; --keep-run-dir keeps the run directory.
  • [[agent_profiles]] config_dir_env names the variable that points the program at its configuration directory under a cage.
  • authority audit list shows people by display name and accepts it in --actor; --json adds actor_name, owner_name and work. The name is taken at sign-in from the claims in [oidc].display_name_claims (default preferred_username, then email).
  • authority forget-user <subject> removes a person's display name. Gated by Cedar disableUser; audit code AUT-0401.
  • setup change --set and --backend read a value from a file (@PATH) or standard input (@-); @@ starts a value with @. The plan does not show the value.
  • gdsgate up --foreground on 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 install and setup prepare refuse a configuration whose role writes outside /var/lib/gdsgate, directly in it, or in two directories under it. install-connector.sh requires --state-dir under /var/lib/gdsgate/connector on a systemd host.
  • setup apply run as root requires --approve-plan with the digest --dry-run printed. A bundle writes only .toml, .cedar, .service and .keys files and reads only files inside the bundle.
  • setup prepare writes the Authority's [security].profile into the Proxy's configuration.
  • A role warns at start about each secret file its configuration names that other accounts can read.
  • delegate --sandbox strict refuses 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 exec secret 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-plan accepts the digest its --dry-run printed.
  • bootstrap-admin does 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_unix in 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-agent and list-agents, gated by the Cedar actions declareAgent, switchAgent and viewAgents. 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_expiry freezes 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-agents shows what each host last reported about its agent (seen) beside what was declared (state).
  • A web face declared identity = "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-resource and set-scope take --from <file>, a TOML document; list-agents, list-resources and list-scopes print the same form with --declaration.
  • Agent declarations carry a version. set-agent --from sends back the version --declaration printed, --if-version does 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), and authority audit list filters on it with --work.
  • gdsgate doctor on an agent host reports whether the host can cage an agent (state.agent.cage).
  • packaging/systemd/gdsgate-agent-host.service (systemd 254 or later) and packaging/apparmor/gdsgate, the AppArmor profile hosts restricting unprivileged user namespaces need.
  • gdsgate join writes [endpoints].proxy_public and [client].transport_ca for a node that opens sessions of its own.
  • A web backend with identity = "headers" sends in X-Remote-Groups the 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_auth is a list of the modes a target in the boundary may name, and a boundary admitting ssh must set it: Authority refuses to start without it, and authority set-scope refuses the scope. Each discovered ssh target states its own mode.
  • A web-lane request carrying a gdsgate-identity header 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

  • ssh targets published into a discovery boundary are reachable: the onward mode is taken from the target and checked against onward_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 under Delegate=yes with DelegateSubgroup=.
  • authority list-resources --json carries scope and lease_until on a row a connector wrote into a boundary.
  • gdsgate doctor checks the agent host cage (state.agent.cage) only where the configuration declares [agent_host]. On v0.9.0-rc.9 and v0.9.0-rc.10 every 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-agent and 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 prepare writes a new installation, setup change edits one that is already running, setup restore puts back what a change replaced, and setup check reports whether a host can run the role it is asked to.
  • Every one of those commands plans before it acts. --dry-run shows what the run would do and changes nothing; --json prints 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 prepare produces 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, and gdsgate authority init states what it will do before it creates any key material.
  • Changes are checkpointed: each run keeps a copy of what it replaced, in .checkpoints beside the file or at --checkpoint-dir, and setup restore puts one back.
  • Installed roles can be started as systemd services by the same run.
  • gdsgate doctor reads 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 -.
  • --json is on every listing, with the wire's own numbers and no truncation. --wide adds the columns the table leaves out; the set of columns does not follow the width of the terminal.
  • authority recordings list prints the table rather than tab-separated fields, and the session id in it is the whole UUID. Anything that parsed the old output reads --json instead, where absence is an absent field.
  • authority audit list and authority recordings list take --since, which reads an interval back from now (90s, 30m, 2h, 7d) or a date (2026-09-21, midnight UTC). --since-ms stays for a caller holding an instant; the two conflict rather than one quietly winning.
  • authority recordings list --limit defaults to 50 rather than to the server's own cap.
  • --as and 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 N returns 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

  • rustls is updated past RUSTSEC-2026-0285 (TLS 1.3 handshake messages accepted across encryption level boundaries) and cryptoki past 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 setup family has a reference entry per command, and --checkpoint-dir is documented with the rest of setup 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 the RdpHost Cedar entity. Resource kind rdp, default port 3389. Authorisation on opening is connect over RdpHost::"<resource>".
  • A backend that names windows_user and credential_file terminates 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 over context.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. --exec starts a local client against it. The name travels on the client's command line and the password does not: a FreeRDP client is pointed at gdsgate through FREERDP_ASKPASS and asks it (FreeRDP 3.8 or newer), and mstsc asks 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 code POL-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 Agent principal, 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: clipboard and clipboard_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_files narrows clipboard, 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_overrun on the close row and an outcome of Success: nothing broke, one side stopped keeping up.
  • graphics = "redraw" | "passthrough". redraw decodes the host's picture and encodes it again for the caller. passthrough asks 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: record and record_max_bytes apply as they do on kind = "ssh", recording_kind is rdp, and the artifact is <session>.rdp. A recording under graphics = "redraw" plays back frame by frame; one under passthrough holds 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 shared allow_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, and channels, what the session ended up carrying.
  • A desktop is a catalogue resource like any other: kind = "rdp" in [[discovery.resources]], on authority set-resource --kind, and in a connector's --serve spec; default port 3389. Cedar manageCatalog applies to RdpHost, 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, graphics and the three pinning keys named nothing.
  • On Windows, a client's own records under ~/.gdsgate are written with an access list of their own (the account that owns them and SYSTEM) 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 under passthrough, where the screen is inside the pipeline in codecs this gateway never reads. Where a recording has to be reviewed, redraw is 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, per exec and per subsystem an SSH session runs, on both SSH models. Context: program_kind, plus git_operation, git_service, repository and subsystem on the resource. Audit code POL-0106.
  • Cedar action sshUseClientKey, asked before a session may authenticate to the host beyond a jump host with the caller's own key. Audit code POL-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, migration m0059); 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_archive and git_lfs_authenticate, which has no default. repository_root is 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, migration m0062) 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 the operation field of the body, read up to [connector.git].lfs_batch_body_max. httpRequest context gains git_operation and repository.
  • 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, migration m0064); a connector's file states a proposal, and a resource the two disagree about is served under neither reading. Set it with authority set-resource --onward-auth.
  • client_key forwards 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_source and [[client.agent_resources]] are the client's per-resource consent; [[connector.backends]] agent_identities_max and agent_auth_attempts_max bound what one authentication may spend.
  • login_users accepts "$subject" under onward_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 (migration m0061) and gdsgate authority activate --epoch <n>. Epoch 1 starts asking sshProgram; epoch 2 reads an SSH resource's onward_auth from the catalogue only. The value rises and never falls. --force proceeds 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.capabilities and capabilities_at (migrations m0063, 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 whose onward_auth the catalogue does not name is answered mode_not_confirmed. Authority issues a one-time receipt on each activation-lease renewal (enrolled_node.capability_nonce, migration m0067), and the node presents it at its next registration.
  • offered_resource.ssh_profile and offered_resource.onward_auth (migration m0065) record what a connector proposes, so authority list-offered shows a disagreement with the catalogue.
  • [[connector.backends]] audit_command writes 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_before and activation_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_hosts entry for a jump host now stays valid across sessions and restarts.
  • A connector reads and writes its state under one effective directory: [enroll].state_dir when set, the role-resolved path under $HOME when 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_dir previously 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 Register as 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 fetch and git push through 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.
  • AuthorizeConnectorRegistration and 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 doctor read 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_user names the one account a resource offers; login_users names 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 (migration m0054): 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_user in 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]] (migration m0055): 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.scope and resources.lease_expires_at (migration m0056) 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 under CAT-0302.
  • [connector.backends.discovery] scope publishes 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" enumerates Service, Pod and Node by label_selector and publishes them into a scope. An object states its kind with gdsgate.io/kind, its port with gdsgate.io/port, and an ssh target's host key with gdsgate.io/ssh-host-key. The connector's service account needs list on the collections named in sources.
  • mode = "list_namespaces" publishes a cluster's namespaces as inventory; PublishBackendInventory now accepts kubernetes resources.
  • ssh_host_key = "annotation" | "tofu" on a scope selects the source of a discovered ssh target's host-key fingerprint. annotation reads gdsgate.io/ssh-host-key and does not serve a target without it; tofu accepts the first key seen for the lease.
  • fallback = "parent" | "deny" on a scope selects how a target with no row is authorized: parent through its parent resource, deny not at all.
  • gdsgate ls gains a LEASE column; gdsgate ls --leased lists only rows written inside a scope.
  • Audit codes CAT-0301 scope.published (one row per snapshot) and CAT-0302 scope.refused. The published row carries two digests: sha256 over the targets the snapshot published, and held_sha256 over 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 to scope.published, scope.refused, the expired-lease sweep and catalog.manage for 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.
  • doctor checks 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).
  • ListResourcesAdmin answers with scope and lease_expires_at per 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-resources prints scope=<prefix> and lease_until=<unix seconds> on a row written inside a scope. A declared row prints neither.

Changed

  • max_live bounds 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 hold max_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 need max_live raised, in [[discovery.auto_scopes]] and with authority set-scope; the catalog shrinks to the ceiling as leases run out.
  • authority list-scopes counts 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-scope has to be written into [[discovery.auto_scopes]] as well to survive a restart.
  • Migration m0058 indexes resources.scope and resources.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 hostname is listed under it even when the row has a parent. Sub-resources synthesised from inventory declare none and are listed as before.

Fixed

  • gdsgate all did 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 gateway refused authority 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 (migration m0053) 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 beside submitter.
  • StoreRecording answers with the digest of the bytes it took. A connector in central mode removes its local copy once that digest matches its own, and keeps the file otherwise.
  • [recording].sweep_interval_secs (default 900; 0 disables) runs a delivery pass over the recordings a session close left behind. Each pass writes .delivery.json beside them.
  • connector recordings push [--dry-run] asks a running connector for that pass on demand, over a 0600 control socket in its state directory. Exit 0 delivered, 1 held back, 3 nothing waiting.
  • authority recordings list gains a tenth column: central:<epoch> once the store holds the bytes, connector while it does not, - for a session with no recording. The sealed file:// URI answers where the connector wrote it, not where it is now.
  • join --recording <disk|central>, and the same flag on install-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; 0 disables). Authority writes SES-0104 session.abandoned per session and one SYS-0101 connector.tunnel_lost for the node.
  • CloseLateRecording writes the close a killed connector owed, from the file's length and digest, so its recording can be delivered. Outcome Failure, reason: late_close.
  • REC-0201 recording.deliver records a late delivery: session, bytes, digest, and how long the file waited.
  • SES-0105 session.claim_refused refuses an upload or a late close from a node other than the one that opened the session. operation says which call was refused; one row per caller, not per attempt.
  • doctor on a connector in central mode adds six checks about recordings waiting, held back, and the store's reachability. All six are silent in disk mode.

Changed

  • The published binary carries every wrapping-key provider. vault, aws and gcp now work from the release artifact as exec already did; they needed a build of your own before.
  • SubmitAuditEvents refuses 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.sh passes --recording central unless 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. The authority unit also reads authority.env; auth.env stays supported.
  • doctor reported 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_file keeps the state backend's password out of store_url. A path or a secret reference, also set by GDSGATE_STORE_PASSWORD_FILE. PostgreSQL only.
  • [security].profile: dev, pilot or production, the deployment profile a cluster is held to at start-up. Unset changes nothing. pilot and production refuse 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; production refuses more, and a refusal names every fault at once.
  • [security.integrity] with control_keyring and audit_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_path parameters.
  • authority keys, nine sub-actions over the two keyrings and the CA signing seeds kept beside them: status, verify, backup-manifest, accept-loss, rotate, checkpoint-keys, migrate-legacy, wrap-ca-seeds and prune-ca-keys. None of them prints key material.
  • keys migrate-legacy takes the keys the five [audit].*_key_path parameters name into the keyrings, as records that verify existing rows and seal nothing new. No key is generated.
  • keys rotate <control|audit> --prepare | --activate | --retire moves a keyring on to a new key in three steps, with a rolling restart between the first two. --activate is refused while a replica has not published that it holds the prepared key; --retire while a row still names the key, and always for the audit keyring. --prepare --kind signing rotates the checkpoint-signing key instead.
  • keys accept-loss --key-id --accepted-by records that a key is gone. authority verify-audit then 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 the audit keyring 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-keys prints 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. Under production it refuses to mint a keyring on this host: name one already in place, or a wrapped one whose key --kms-key declares.
  • authority set-profile --profile --changed-by declares 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 over env: and exec:, 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), aws and gcp need a build carrying kms-vault, kms-aws or kms-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-seeds converts the generations already stored, and a table converted partway runs either way.
  • keys prune-ca-keys removes the private keys of CA generations nothing will use again. Without --delete it 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_path keys until a keyring holds an active key of its own, so those parameters keep being read after keys 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-anchor and the four other init-*-key commands refuse when the parameter they would write names a variable, a command or a wrapped source instead of a file. --path names the file to write.
  • doctor reports 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.
  • doctor also reports the declared profile, whether each [[kms_keys]] entry is one this build can use, and which of the five legacy key files a named keyring has not taken in yet.

Fixed

  • A password left in store_url is withheld wherever the connection string is printed: log lines, doctor output, and the refusals the database driver raises with the string quoted inside them.
  • doctor checks all five audit signing keys and all four listener keys. The delegation seal, the inventory seal and [proxy].webapps_key_file had 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_ranges on a kind = "web-egress" backend: the address ranges this way out may dial although no registry allocates them to the public internet. Default empty.
  • allow_agent_forward on a kind = "ssh" backend, default false, and the sshForwardAgent policy action. ssh -A requires both.
  • ssh:forward@<host> and ssh:agent@<host> in the capability vocabulary. ssh -L and ssh -R take the first, ssh -A the second, and a delegated session carries ssh:connect@<host> alongside either. Minting ssh:forward requires the delegator to hold both sshForwardLocal and sshForwardRemote.
  • 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 names db:read refuses 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 --as carries the delegation grant in its signed bytes. A connector or proxy older than this release refuses it. A certificate issued without --as is unchanged.
  • A delegated session forwards only what its grant names. ssh:connect names no forward, so a grant that opened ssh -L, ssh -R or ssh -A needs 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 -A is refused unless the backend enables it and the policy permits it. A refused session receives no SSH_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=true on a named object is a get, as the API server reads it.
  • The Kubernetes path parser reads the legacy watch and proxy prefixes, treats HEAD as GET, 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 sub is spelled like a delegation subject no longer places the session in that grant.
  • gdsgate delegation status no 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> --listen opens one locally, and a resource of this kind also answers by name under gdsgate up. See User guide → Web applications and The web lane.
  • httpRequest, decided for every request on the web lane over WebApp or WebEgress. 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 = true terminates 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: CONNECT out of the zone and a WebSocket upgrade into an application, neither covered by web: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 in Gdsgate-Identity, audience-bound to the resource, and stops the caller's own Authorization from travelling. Applications verify it against the JWKS at /.well-known/gdsgate/jwks.json, rotated by gdsgate authority rotate-jwt-ca. See Telling the application who came.
  • Clientless browser entry: with webapps_zone, webapps_addr and a wildcard certificate for that zone, a browser reaches an application at https://<application>.<zone> after signing in at the identity provider. The session appears in gdsgate 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_from and [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_name is answered for by the resident client where the local [client].intercept_names also names it, with TLS terminated by that machine's own root (gdsgate web trust). See Resident client → An application's own name.
  • context.presented is "key" or "cookie" on connect and httpRequest, 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" and enforce_query_categories included. gdsgate doctor's session-role grant probe does not cover it.
  • read_only = true on a session-role profile, checked by gdsgate doctor against every database the session can reach, and settable_parameters, which widens what a restricted client may SET. 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, viewRecordings and issueJoinToken. See gdsgate step-up.
  • authority policy push warns about a connect rule that names no kind of resource, and authority policy validate --output <FILE> writes the migrated policy with every change listed. See gdsgate authority policy validate.
  • Raw-TCP sessions are recorded as session.open / session.close with kind tcp, and the close record carries bytes_up, bytes_down and reason.
  • Audit rows carry an event code, shown as a column by authority audit list, filtered with --code <CODE> and reported as code under --json. See gdsgate authority audit list.
  • max_request_bytes bounds the request body a web or web-egress backend carries. Default 0, 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. k8sAccess and k8sReadOnly are kubeRequest, with the read-only distinction in context.read_only; Server is SshHost; a catalogue-wide decision is taken over Catalog::"all"; an MCP backend opens over McpServer. Existing policies are migrated on push and on read, and the rewriter reports what it will not touch. See Policy → One connect, seven kinds of resource.
  • A policy decision is recorded as policy.decision, with the schema action in the detail under cedar_action, so --action kubeRequest becomes --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 as key=value pairs on a branch line. See Operations → What lands in audit.
  • Session records use one pair of names, session.open and session.close, with the protocol in kind; k8s.request is kube.request; Kubernetes requests and sftp.* records carry session_uid.
  • Control-plane event names carry a dot: editPolicy is policy.edit, manageCatalog is catalog.manage, rotateCA is ca.rotate. Event codes did not move, so a filter on --code spans both spellings.
  • The Authorization class holds policy decisions only. session.no_route moved to SessionActivity, and the seven MCP descriptor records to a new class, ResourceIntegrity (--class resource-integrity). A filter written on --class authorization to 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 (Success 1, Allow 3, Failure 5, Deny 7). A SIEM rule that read the outcome from the seventh header field must read the outcome extension key. See Administration → Audit export.
  • Policy denies on the Kubernetes, MCP and model lanes, and a refused database statement, are sealed as deny rather than failure. A backend refusing its own caller stays a failure, 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 413 is gone, replaced by max_request_bytes. A recorded request with a body is written as a head, its chunks and an end, making the gdsgate.http.exchange format version 2; recordings show renders version 1 unchanged.
  • A client whose request body ends before its declared length is answered 400 rather than 502.
  • The settings advertised to a joining node no longer carry a client-facing TLS server name.
  • context.db_role is present only when the client named a session role; a rule reading it needs context 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 DO body too. Categories follow what a statement does: SELECT … INTO is ddl, a locking clause is write, COPY … TO STDOUT is read. See Policy → Per-statement database authorization.
  • authority bootstrap-admin renews 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 kubeRequest on every apiserver call; previously the session-open decision stood for the whole session. This is stricter: calls are denied wherever no kubeRequest rule permits them, so review the cluster's rules before upgrading such a connector. See Policy → kubeRequest and context.read_only.
  • A local forward is judged by policy on every connector. This is stricter: ssh -L on a connector that held no other line to the control plane now needs a sshForwardLocal rule, which the baseline policy grants to nobody. -R is 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. actor and target are 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 error line when the retry budget runs out.
  • SSH sessions, remote commands and sftp through a jump host answer clients that block until a request is confirmed (Go's x/crypto/ssh, libssh, paramiko). env and x11-req are 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 exec output.
  • A Kubernetes request stopped by an unreachable gate is recorded with the status the client received, and a Kubernetes exec that 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 kill reports only sessions it terminated, a health report Authority turned down is not logged as reported, and gdsgate login notifies a resident client, so gdsgate status reports the new identity at once.
  • authority policy edit reads a document in the retired vocabulary the way authority policy validate does, listing what the migration will change rather than marking every such line an error. Page Up, Page Down, Ctrl-Home and Ctrl-End move in it.
  • gdsgate login --browser reports 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_access is 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, serving among them.
  • authority list-serving asks 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 getent when 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/gdsgate inside the image matches the line in the signed SHA256SUMS.

v0.8.21

Added

  • Authority.ListAuditEvents serves 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 is viewAudit, taken over AuditLog::"chain", and it is granted on its own rather than as part of viewNodes or viewRecordings. 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 list takes the same --route as every other administrative command, with the database available as --route store where the caller has it.
  • gdsgate authority audit list --json: one JSON object per row, the same object on both --route values. The column layout stays the default and is unchanged. A class or outcome code this build does not know is kept as class(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-audit is what proves the chain.
  • gdsgate doctor reports whether its sandbox readings hold for other runs. The board's properties are measured inside the doctor process, and on a host that restricts unprivileged user namespaces what a caged program gets depends on the confinement profile the run carries. The sandbox.userns_confinement row 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 in allowed_request_headers and allowed_query_params rather 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 and credential_header itself. 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 join reads the piped token to the end. A run refused from its arguments and a --print pass 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 status reports whether anything is serving a resource, in a new SERVING column beside ACCESS. ACCESS ok means 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 for no. Only a gateway can answer it, since the catalog holds no routing table, so it fills ResourceEntry.serving in as the listing passes through, and Authority answers SERVING_UNKNOWN on its own lane. An unexpected no usually 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. --exec may be given without a value, and the program then comes from the row; without --exec the same row mints the grant and prints its id. The row also carries environment, which is what makes a caged agent usable: --sandbox basic scrubs 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 basic cage carries a /proc of its own. It denied /proc wholesale, because that is where /proc/<pid>/environ lives, and a runtime that reads /proc/self/maps for 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|no is 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 delivers basic in full.

Changed

  • The gdsgate status header is four lines. identity carries the identity, the level and when the catalog was last read; names and access are the two settings that can be half working; state is dimmed and collects the state file, and at the transparent level the interface and what it has refused. The level, catalog, interface and refused lines are gone as lines of their own. An error line 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 names reason 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 doctor names 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 the docker group it named three where there is one: containerd's is root:root 0660, and /var/run/docker.sock is the same inode as /run/docker.sock, because /var/run is a symlink to /run.

v0.8.18

Added

  • gdsgate authority list-offered reports 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, and declared=yes|no. declared=no means the connector serves it and this cluster routes nothing to it, until you declare it with authority 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 Cedar viewNodes, the same action as authority list-nodes, and it takes --route gateway like 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 behind gdsgate 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, so verify-audit and existing exports are unaffected.
  • Enumerations in help text and refusal messages name the model lane. The set-resource refusal for an unknown kind, the step-up refusal listing the actions a one-time grant can be minted for, and the help for --action and --port all name llm. --action also 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 llm subcommand, because the caller is a program that already speaks a provider's API and is handed an address. gdsgate ls lists 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.toml documents 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 command MCP server and its decider_command are 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 /proc are 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 = true puts every open the 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 per open, 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. PDEATHSIG is 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.close carries a cage= field beside transport=, 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 sets allow_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 Cedar llmCall action and the LlmService / LlmModel entity types are in Policy, and docs/config.example.toml carries a commented model backend.
  • A registry of agent profiles: gdsgate delegate --exec translates 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-profiles prints 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 carries source and checked, 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 for claude, codex, opencode, openclaw and pi, 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 delegated llm: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 gateway over a grant that names no model service is refused, as are direct and off over a grant that names one. --model direct requires --sandbox none, because a cage has no route to a provider, and --allow-degraded does not waive it.
  • --sandbox none reports what it does not enforce before the program starts, naming what stays reachable: the delegator's own credential by its path, their ~/.ssh, ~/.aws and ~/.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 --exec no longer writes a credential to disk. The key and the certificate live in the gdsgate process that serves the endpoints, and the bundle directory under the system temporary directory is gone. GDSGATE_KEYSTORE_ROOT is no longer set for the program and is removed from its environment. --bind-key still writes a bundle.
  • --renewable --exec is rolled forward by gdsgate itself, since the program has no bundle to roll forward from, so it needs a registration listener: such a run with no [enroll].endpoint is refused before the grant is minted. --renewable --bind-key is unchanged.
  • A caged --exec run's scratch directory (the program's $HOME and $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 beside postgres, mcp and the rest: delegated as llm:call@<service>, decided by Cedar, audited, revoked, discovered by gdsgate ls and recorded. addr on this kind is a base URL rather than a host: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 llmCall action before anything leaves for the provider. The model is an entity (LlmModel::"<service>.<model>") under its LlmService, so a rule can cover a whole service or one model. allowed_models is a per-resource ceiling under that decision, the counterpart of allowed_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 llm backend refuses to serve a session it cannot decide for.
  • The lane presents the resource's own credential in place of the caller's. With credential_file set it substitutes: the organisation's key goes to the provider, and the caller's headers travel by allow-list, allowed_request_headers on top of the content-negotiation defaults (accept, accept-encoding, content-type), and allowed_query_params for the query string, empty by default, so a call carrying any query string is refused, because some services take a key there. credential_header and credential_format shape what is presented, and a header the lane writes itself is refused at startup. With credential_file unset 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 same viewRecordings action as an SSH cast. The two directions are configured independently, record_request and record_response, each off, digest (the default), redacted through the existing secret redactor, or full, and any composition is valid: off / full keeps what a model sent without keeping what it was sent, redacted / off keeps what left the perimeter with secrets stripped. digest is 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, and gdsgate doctor reports 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. credential is 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 --exec can confine the program it runs: --sandbox none | basic | strict bounds 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. none is the default, and a run that predates the flag behaves as it did.
    • basic promises 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 if gdsgate dies (PR_SET_PDEATHSIG), and the program's whole process tree dying with the run through a private PID namespace whose first process is gdsgate's own. setsid does not escape it.
    • strict promises 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 under basic), 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-degraded runs anyway, loudly. gdsgate doctor reports 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 $HOME and $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: --exec serves 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 --exec run ends when its grant does.
  • Every --exec run 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-degraded let the run continue. --sandbox none reports too. It is a ClientAssertion (claimSandbox): the gateway verifies that the caller owns the grant being described and nothing more, since it cannot inspect a kernel on another machine, and outcome is Success where the boundary matched the profile requested and Failure where it fell short. A run whose row cannot be sealed is refused: the program is killed and the grant revoked. See CLI → --sandbox basic and --sandbox strict.

v0.8.12

Added

  • resources/read and prompts/get are decided and recorded: mcpReadResource against McpResource::"<server>.<uri>" and mcpGetPrompt against McpPrompt::"<server>.<name>", both sub-resources of their McpServer, both under the enforce_tool_policy opt-in, fail-closed on a policy deny or an unreachable Authority. A resource entity carries scheme beside uri. Both produce mcp.resource_read / mcp.prompt_get with the same attribution, session id and payload handling as a tool call. New RPCs: AuthorizeMcpResourceRead, AuthorizeMcpPromptGet.
  • A resources/read whose 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> and mcp:prompt:<name>@<server> beside the tool form, which may also be written mcp:tool:<name>@<server>. Existing grants are unaffected, since a token without a kind means a tool. A resource is named as the server's own resources/list names 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 --can is checked against the delegator's own rights per kind.
  • Server-to-client asks are gated: sampling/createMessage, elicitation/create and roots/list, in both the pre-2026-07-28 dialect and the inputRequests field the 2026-07-28 revision moved them to. An ask is carried only where the backend's allowed_input_requests names it, and that list is empty by default; allow_elicitation_url separately 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 to prompts/get, resources/read or tools/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 of addr, with optional command_env. Setting both, or neither, is refused at startup. Every gate applies unchanged, and mcp.session.close records the transport (stdio or socket). The program runs on the connector's node as the connector's own user; its environment is cleared and rebuilt from PATH, HOME, LANG, LC_ALL, LC_CTYPE, TZ, USER plus command_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: url speaks Streamable HTTP, one HTTP POST per message, mutually exclusive with addr and command. Every gate is the same implementation. Mcp-Method, Mcp-Name and MCP-Protocol-Version are mirrored out of the body and never invented, and Mcp-Param-* headers are rebuilt from the x-mcp-header annotations in a tool's inputSchema, so a tool must have been listed before it can be called. Redirects are not followed, https is verified against the platform roots plus ca_path, and a credential comes from credential_file. allowed_backend_hosts constrains a url as it constrains an addr. See A remote MCP server over HTTP.
  • The MCP endpoint speaks Streamable HTTP, protocol revision 2026-07-28: point a client at http://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 an Origin header, and any whose Host is not its own loopback address, which closes the DNS-rebinding case. Per the revision, a header that disagrees with the body is refused 400 with JSON-RPC error -32020, GET and DELETE are answered 405, and an Mcp-Session-Id is ignored. The previous newline-delimited framing still answers on the same port.
  • MCP tool descriptors can be pinned. pin_tool_descriptors = true on 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 the tools/call, not the listing, until the change is re-approved through gdsgate 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's instructions are pinned the same way and refuse nothing. See Pinning tool descriptors.
  • refuse_unfiltered_tool_listing = true answers a caller with a JSON-RPC error when the per-caller viewTools decision cannot be obtained, instead of forwarding the listing unfiltered. The default remains to forward and record mcp.tool_listing_undecided with the cause, the posture and the number of tool names at stake; the call gate is fail-closed either way. Requires enforce_tool_policy. See When Authority cannot answer.
  • An external decider can be consulted before an MCP call leaves: decider_url (one POST per call) or decider_command (a program on this node, fed the request on stdin), synchronously, on all three data paths. The contract is allow|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 (default true) covers every shape of no answer, and every consultation writes mcp.decider_hook. decider_command costs about 1.9 ms per call against 0.35 ms for a loopback POST. decider_ca_path verifies an https decider against an operator's own CA. See The external decider.
  • MCP sessions are bracketed by mcp.session.open / mcp.session.close with a session_uid that every mcp.tool_call and mcp.input_request row carries. A backend that cannot be reached is recorded as a session.open with 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.arguments rides 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 from params._meta only. In the 2026-07-28 revision both io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities are 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/discover and falls back to initialize only when that is refused. A server's handshake instructions are relayed unchanged, and their presence and size are recorded as mcp.instructions.

Changed

  • Wire change: a JSON-RPC error from the MCP gateway carries code -31001 instead 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.AuthorizeMcpViewTools writes a chain row on a first sighting or a change, so a follower replica answers Unavailable and 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 authority administrative 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].endpoint over the control-plane certificate; or emergency, the socket on the Authority host. auto, the default, takes certificate where the configuration names an [admin] section and gateway otherwise, so an existing workstation keeps its route with no flag. policy pull and policy push always resolve through the gateway. See Administering from outside the control zone.
  • Two Cedar actions, issueJoinToken and viewNodes, 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. See manageCatalog.
  • 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 list prints 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 down 0700. A call arriving there carries no identity of its own, and a token or asserted operator name is refused. It is decided by Cedar over User::"gdsgate-break-glass", the principal every accepted policy must keep able to edit the policy, and carries policy pull, policy push, the catalog calls and the registry listings, refusing every other method by name. gdsgate doctor reports 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_grant carries { 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" is Group::"gdsgate-admin" and Group::"provisioner" is Group::"gdsgate-provisioner". Both live inside a gdsgate- namespace an identity token may never carry, since a sub or groups claim starting with gdsgate- 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 the gdsgate- prefix.
  • The starting policy grants its group of people manageCatalog, viewNodes, disableUser and issueJoinToken for EnrolledNode::"gdsgate-connector", EnrolledNode::"gdsgate-proxy" and EnrolledNode::"gdsgate-workload", naming the roles the join-token rule admits. rotateCA and enrollNode stay with the certificate identities and ship as commented examples. See The administrative actions people get.
  • Every manageCatalog and viewNodes audit 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 as src_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), and gdsgate step-up on one is refused saying so.
  • A policy that gates editPolicy on 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, the view catalog listing, viewTools, mintOnwardSshCert, sshForwardLocal / sshForwardRemote, revoke --owner, the dbConnect gate) or an administrative action (enrollNode, disableUser, manageCatalog, issueJoinToken, viewNodes, rotateCA) now sees the caller's real mfa_* / step_up_* / totp_* / webauthn_* status; all of them previously built their Cedar context with every step-up channel pinned to false, 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, DeleteResource and ListResourcesAdmin, 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 are authority set-resource, authority remove-resource and authority list-resources, which take --hostname, a repeatable --alias and --port under 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 carries min_approvers (store migration m0038), the narrowest level of the JIT approval cascade. See Policy → Administrative actions.
  • gdsgate authority policy template prints the canonical starting policy on stdout, offline, with no store and no running Authority. It is how a first policy reaches a cluster, since policy push is 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 publishes gdsgate:<tag>, and gdsgate:latest for a final release, signed by digest with the key that signs SHA256SUMS. packaging/systemd/gdsgate-{auth,proxy,connector}.service run each role under DynamicUser=yes with a read-only filesystem apart from its state directory, and packaging/config/*.toml.example carry 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 fingerprint computes the transport CA's SHA-256 fingerprint, matching what a connecting client compares against, without shelling out to openssl.
  • A second control-plane role, CN=gdsgate-provisioner speaking as Group::"provisioner", which declares catalog resources, mints join tokens and reads the node registry, and holds no enrollNode, disableUser, rotateCA or viewRecordings and no join token for a control-plane role. gdsgate authority bootstrap-provisioner issues and renews that identity without opening the cluster's database. See Policy → The starting policy.
  • --if-missing on gdsgate authority bootstrap-admin and the five authority init-* key commands: what exists is left alone and reported under exit code 3. For bootstrap-admin, "already there" means an identity this cluster would still accept. See CLI → Exit codes.
  • profile labels every Prometheus series, structured log line and exported trace of the process. Unset by default; must be non-empty, at most 64 bytes, drawn from A-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, amr or 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_secs and join_idle_timeout_secs. Unlike other limits in that section, an omitted key keeps the built-in figure rather than meaning unlimited. gdsgate doctor reports 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.toml and 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-missing reconciles instead of installing over an existing node and spends no token when the identity is current. packaging/install-connector.sh and 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-node removes 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_join carries 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 migrate brings the store's schema up to date and exits, opening the store directly with no running Authority, and applies nothing on a second run; --check reports only and exits non-zero while migrations are pending. store_auto_migrate (top-level, true by 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 migration m0039). 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 the editPolicy gate. A restart against [policy].path does 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 doctor reports 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 migrate and doctor alike.
  • 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 --config path that does not exist is an error for every command; only the discovered ~/.gdsgate/config.toml may be absent. A listing command that fails exits non-zero.
  • gdsgate doctor names 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_endpoint or [admin].endpoint is refused at start-up: both are mutual-TLS calls whose scheme decides whether TLS runs at all. gdsgate doctor carries 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. SIGHUP re-reads all three immediately, and naming one half of a web_* pair without the other is refused at start-up. See Renewing a listener certificate.

v0.8.6

Added

  • [connector].authority_transport picks 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; fronted sends the same calls to the address already used for the Proxy, carrying the role-specific TLS server name auth.gdsgate.internal so a front can route them apart; tunneled closes 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 generated ssh / 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 status prints what is published and the command that reaches each resource; gdsgate down stops it. up returns the terminal and reports the level, the resources published and its journal (~/.gdsgate/gdsgated.log); gdsgate up --foreground keeps the client in the starting process, which is what the shipped systemd units use.
  • [client].access selects how a resource is addressed: basic (a port on 127.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_ADMIN on Linux), or auto, which probes for the most capable level the host allows and reports where it stopped. [client].resolver decides independently whether .gds names 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.toml and transport-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. See gdsgate connect.
  • packaging/polkit/40-gdsgate-resolver.rules grants 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 migrate brings the store's schema up to date and exits, with --check reporting only and exiting non-zero while migrations are pending. store_auto_migrate decides whether a starting service may change the schema itself. See Operations → Store schema.
  • A [[discovery.resources]] entry declares hostname, aliases and port, and a resource created through the catalog API carries the same addressing. gdsgate ls shows 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 every identity.renew audit row names the node it renewed for.
  • gdsgate login stores 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 in gdsgate status, in the client's log, as a desktop notification ([client].notify), and, for SSH, in the connection itself. step_up_on_denial = true raises 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 sudo files 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/%i layout and runs ProtectSystem=full.
  • GDSGATE_KEYSTORE_ROOT is honoured by every credential write, including the access certificate gdsgate ssh and gdsgate db login mint. gdsgate doctor folds 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 doctor reports 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 migrate and doctor alike.

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-id proves 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 Agent principal, which gains an optional owner (the human or service the workload acts for) and spiffe_id; every action is attributed to that owner in the audit log. On an [[connector.backends]] backend with enforce_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-workloads lists 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 / -R port 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:

  1. Stop every gdsgate service. Clients can stay up; their certificates remain valid until they expire.
  2. Back up the state store. v0.8.0 applies migrations m0030–m0033 on first start and they do not roll back; a downgrade means restoring this backup.
  3. 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-key and [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. Add permit rules for the Agent principal 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 doctor scopes 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 doctor flags any state path on an ephemeral filesystem. The reboot-survival check that already covered the node state_dir also covers the Authority store (a file SQLite store_url) and every configured path (policy, certs, audit keys, admin identity): a file on /tmp, /dev/shm, or /run survives 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. --json for machines; exit code 2 on failure, so it works as a preflight or healthcheck. Nothing is mutated.
  • Cluster health from the periodic doctor check. 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 with gdsgate authority list-nodes --health, and, when [telemetry].metrics_listen is set, publishes it locally as gdsgate_doctor_* gauges. See Operations → Surviving a redeploy.

Changed

  • gdsgate doctor flags 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_endpoint defaults to the Authority control-plane. A split-deploy node with [enroll].renew_endpoint unset derives it from https://<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_url unset, gdsgate all / authority resolves 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_state references a generation absent from ca_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 a retiring or candidate generation is auto-healed: the rotation state is reset to active-only Standby with a WARN, 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_url was unset. A gdsgate all / authority that ran without store_url used 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 set store_url explicitly 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 timestamped tracing INFO lines. A terminal gets glyphs and a little colour; a pipe, ProxyCommand or CI gets plain ASCII, and NO_COLOR is honoured. login shows ✓ authenticated as <user> (group: …); ssh / db login show ✓ certificate issued · ttl <n> · <resource>; a data-plane connection shows # connected · session <id> · recording. Client diagnostic logs default to warn; RUST_LOG still raises them, and -q/--quiet silences the narration. Daemons are unchanged (structured logs at INFO). Command data on stdout is untouched, so scripts and proxy-ssh keep working.

Fixed

  • No spurious disconnect banner after ssh … --exec. A native ssh session through the gateway no longer prints Received 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_endpoint to 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 W3C traceparent header 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 carry action, resource, decision, and session_uid where 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-time gdsgate_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_listen and a service exposes Prometheus /metrics plus liveness /healthz and readiness /readyz on 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 approveRequest policy 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 kubectl call that a step-up or approval could clear, the connector's 403 names the gdsgate command to run, in the body and an X-Gdsgate-Remediation header. A deny that nothing can clear stays a plain 403. 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_satisfied reflects an MFA login at the IdP (read from the token's amr / auth_time), and context.step_up_satisfied reflects 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 / confirm and steps up with gdsgate mfa verify; WebAuthn passkeys register with gdsgate mfa register-passkey and step up with gdsgate 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 with gdsgate 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-remediate opts out, --step-up-factor forces a factor, and --wait-timeout bounds 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_satisfied without 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=json emits 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:

  1. Stop every gdsgate service. Clients can stay up; their certificates remain valid until they expire.
  2. Back up the state store. v0.4.0 applies migrations m0024–m0029 on first start and they do not roll back; a downgrade means restoring this backup.
  3. 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 ls as a connectable <host>.<db> resource, filtered per identity by the Cedar view action. 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_path and run gdsgate authority init-inventory-key to 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 dbQuery action 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 mcpCallTool action authorizes every tools/call against a Tool entity (tool names match directly, including like wildcards); viewTools filters tools/list responses per identity. An operator-set is_destructive marker, driven by [mcp] destructive_patterns, lets a policy block destructive tools. The connector's static allowed_tools list remains as a second gate.
  • Live MCP tool discovery. An MCP backend's tools are probed and used to filter tools/list responses per identity (viewTools). Tools are runtime operations rather than connect targets, so they are not listed in gdsgate ls; only the MCP server is.
  • Live session monitoring and control. gdsgate session list shows the active sessions and gdsgate session watch streams opens and closes as they happen; gdsgate session kill ends a session by id or by --principal / --resource. Termination is authorized by the Cedar killSession action over a Session entity and is audited.
  • Live recording streaming. gdsgate authority recordings live tails an in-progress SSH or kubectl exec recording in real time, gated by the same viewRecordings action 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 to 0 (unlimited).
  • OIDC signing-algorithm allow-list. [oidc] id_token_signing_algs restricts which id-token signatures Authority accepts (default: every asymmetric algorithm; symmetric HS* 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* at INFO, everything else at WARN.

Fixed

  • SSH session recordings upload to the central store in central mode; 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:

  1. Stop every gdsgate service. Clients can stay up; their certificates remain valid until they expire.
  2. Back up the state store. v0.3.0 applies migrations m0021–m0023 on first start and they do not roll back; a downgrade means restoring this backup.
  3. For live discovery, generate the inventory key:

    gdsgate --config auth.toml auth init-inventory-key
    

    It writes a hex-encoded 32-byte key with mode 0o600 and refuses to overwrite an existing file. Wire the path into Authority's config and back the file up alongside the database:

    [audit]
    inventory_key_path = "/var/lib/gdsgate/inventory.key"
    

    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 permit rules for dbQuery, mcpCallTool, viewTools, or killSession only 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-audit command 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_ip enforcement, 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 = true in 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 Unknown without 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, dbWrite and mcpCallTool, plus the Tool entity 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:

  1. Stop every gdsgate service in the cluster. Clients can stay up; their certificates remain valid until they expire.
  2. Back up the state store. v0.2.0 applies forward-only schema migrations on first start; a downgrade requires restoring the backup.
  3. Generate the new HMAC keys the v0.2.0 audit anchor and per-node revocation receipt need:

    gdsgate --config auth.toml auth init-anchor
    gdsgate --config auth.toml auth init-revocation-key
    

    Both write a hex-encoded 32-byte key with mode 0o600 and 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.

  4. Wire the resulting paths into Authority's config:

    [audit]
    anchor_key_path = "/var/lib/gdsgate/anchor.key"
    revocation_key_path = "/var/lib/gdsgate/revocation.key"
    
  5. Provision an admin identity if you do not already have one:

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

    Then point your operator workstation's config at it ([admin].identity_dir, [admin].endpoint).

  6. Drop dbQuery / dbWrite / mcpCallTool references from your Cedar policies before pushing them: these actions and the Tool entity type are not part of the 0.2.0 schema. Connect-level authorisation is unchanged, so a database session goes through dbConnect and an MCP session goes through tcpConnect over a TcpService, 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.

  7. Start Authority. The schema migrations apply automatically on the first boot.

  8. 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 / attach session recording. The connector's Kubernetes proxy demuxes the v5.channel.k8s.io (and v4) WebSocket subprotocol that kubectl 1.30 and newer negotiates on pods/.../exec upgrades 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 same gdsgate authority recordings list/show viewer. Each session lands as kube.session.close in 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 raw bytes_up=… bytes_down=…, with no demux and no recording.
  • [recording] capture_stdin = false default. The session recorder drops stdin chunks by default for both SSH and kube exec/attach. Echo-off password prompts (sudo, ssh, passwd, any read -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 = true in the connector's config where the deeper audit trail is worth the PII risk.
  • Graceful WebSocket close. The Kubernetes exec bridge forwards Close frames symmetrically between client and apiserver and waits for both halves to close before dropping the sockets, so kubectl no longer reports websocket: close 1006 (abnormal closure) on every exec.
  • Central-store upload race fixed. The central recording sink retries Authority.StoreRecording with 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 show also queries Authority by session_uid regardless of the URI scheme in the audit row, so casts uploaded centrally are reachable from any client that holds viewRecordings.

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": disk keeps the cast on the connector, central uploads 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 list returns recent session-close events; gdsgate authority recordings show <session_uid> prints the cast, which pipes into asciinema play to replay. Gated by the viewRecordings Cedar permission.
  • SFTP audit. File-transfer activity over SFTP is auditable in both model A and model B: each open/close/remove/rename/mkdir/rmdir lands 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 native ssh with the ProxyCommand wiring passed inline, so the gdsgate ssh-config / ~/.ssh/config step is not required for the simple case. Arguments after -- are forwarded to ssh. 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 authority operate against the shared store_url: authority list-nodes prints 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 its not_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-ca records the request; the running Authority promotes the new generation after propagation_secs and retires the old after retire_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, and kubectl port-forward flow through the connector: when a request carries an Upgrade: 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: Authorization and Impersonate-* headers are stripped on the upgrade path too. The audit chain records k8s.request on 101 and k8s.exec.close with bytes_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 / push go through Proxy to Authority and are gated by the Cedar editPolicy action over the verified caller. push strict-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 as AdminAction.
  • The [policy].path file 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-textarea plus a custom Cedar renderer. Pulls the active policy on entry, validates and pushes cluster-wide on Ctrl-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; after resource. / principal. / context. only the attributes Cedar declares for that base (resource also offers getTag / 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/call allow-list), raw TCP, declared per connector in [[connector.backends]].
  • Native client experience: gdsgate login, then ssh, psql / mysql, kubectl, redis-cli through the gateway.
  • SSH -L and -R with two-layer gating: a per-backend allow-list and the Cedar sshForwardLocal / sshForwardRemote actions.
  • Persistent connector-side SSH host key, so a client's known_hosts entry 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 CONNECT proxy 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 in gdsgate ls and 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:

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.