Skip to content

Client commands

Common global behavior is documented in the CLI hub. The complete client command family follows.

Client commands

Commands a user runs from their own machine. They read the client configuration ([endpoints], [client], [oidc]) plus the identity-token environment variables.

Issued access certificates are cached under ~/.gdsgate/<cluster>/<user>/ and reused while in scope and valid. If the cluster CA has rotated, or the certificate was revoked, the cached certificate is rejected at session open. The client recognises this, prints "cached access certificate was rejected … re-run gdsgate login" and clears the stale cache, so a re-login, or a plain retry, mints a fresh one.

gdsgate connect <host[:port]>

Fetches a cluster's settings and writes the client configuration from them. The first command run against a new cluster, and the only one that needs no configuration of its own.

gdsgate connect gdsgate.example.com:50061
PROXY_FINGERPRINT="<approved-proxy-fingerprint>"
gdsgate connect gdsgate.example.com --fingerprint "$PROXY_FINGERPRINT"  # set from the approved bundle

The port defaults to 50061. What it writes:

Path Contents
~/.gdsgate/config.toml [endpoints].proxy_public, [client] (anchor path, server name), the addressing defaults the cluster publishes, and [proxy].public_origin if it has one
~/.gdsgate/transport-ca.pem the cluster's transport CA, which [client].transport_ca points at

Both are owner-only (0600) and written atomically. Every later command reads ~/.gdsgate/config.toml when --config names nothing else.

Trust is settled before anything is written. The settings arrive over a connection that cannot yet be verified, since the anchor that would verify it is inside them, so a cluster's own claim is never the reason to believe it. connect shows the fingerprint of the anchor on offer and waits for an answer, or compares it with --fingerprint. With neither a fingerprint nor a terminal it stops. There is no trust-on-first-use.

Once the anchor is confirmed, connect fetches the settings again over a connection verified against it, and writes that second answer. A gateway that replays the real cluster's anchor, which is public, cannot complete that handshake, because it has no certificate the real authority signed.

The operator publishes the fingerprint with gdsgate ca fingerprint:

gdsgate --config /etc/gdsgate/proxy.toml ca fingerprint

Flags:

Flag Effect
--fingerprint <SHA256> The expected anchor fingerprint. Separators and case ignored. A mismatch is an error. Required off a terminal.
--output <PATH>, -o Write elsewhere. The anchor goes in the same directory.
--force Replace files that already exist. Without it, an existing file is an error and nothing is written.
--sni <NAME> Server name to send and verify, overriding what the cluster advertises. For a front that routes by name.
--insecure Bootstrap from a gateway that serves no TLS. Nothing is encrypted and nothing is verified, so development gateways on this host only. Conflicts with --fingerprint.
--timeout <SECS> How long to wait for the gateway. Default 15.

What it deliberately does not do:

  • It carries no identity. The settings say where the cluster is and whom to trust; nothing in them authenticates anybody, and connect leaves your login and certificate cache untouched. Sign in afterwards with gdsgate login, or gdsgate machine-id for a workload.
  • It does not overwrite. A configuration you may have edited is never replaced without --force.
  • It writes nothing on failure. A mismatched fingerprint, a declined confirmation, an unreachable gateway or a value the configuration would refuse all leave the filesystem as it was.

Values the cluster publishes are held to the same rules as hand-written ones: a name zone must be a single label, address pools must parse, and a browser origin must be a bare scheme://host[:port], HTTPS unless it is on this host. Anything else is refused by name rather than written.

gdsgate login [--browser]

Authenticate to the cluster.

  • With an [oidc] provider configured, login runs the device flow by default: prints a URL and a short code, waits for approval, then caches the obtained identity token under ~/.gdsgate/<cluster>/id_token, mode 0600. Later commands reuse it.
  • --browser runs Authorization Code with PKCE over a loopback redirect, so the browser opens on the same machine.
  • Without [oidc], login confirms reachability and a local-dev principal is used (GDSGATE_USER, default dev).

Headless and CI: skip login and supply the token through GDSGATE_ID_TOKEN.

gdsgate up performs the same sign-in itself when it finds no identity, so login is a separate step only when you want it to be.

gdsgate up [--browser] [--foreground]

Starts the resident client: publishes a local address for every resource in your catalog, keeps the generated ssh / kubectl / environment configuration in step with it, and holds the identity both need.

Signs in first when there is none, the same flow login runs, with --browser for the browser instead of the device code. Without a terminal to sign in on, a service or a pipe, it stops and names the command to run.

It returns the terminal. The client is started as a process of its own and up returns once it is serving, so the next command in the same terminal is gdsgate status. Exit code 0 means there is a client running; a client that died or never answered is a non-zero exit with the end of its journal (~/.gdsgate/gdsgated.log) on the screen. Starting a second one beside a running client is refused, and the running one is left alone. gdsgate down stops it.

--foreground runs the client in this process instead and holds the terminal; an interrupt then stops it, performing the same teardown down does. That is what the shipped systemd units use, since a service manager supervises the process it started, and what to watch a start with when one is going wrong.

gdsgate down

Asks a running resident client to stop, over its control socket. Finding none is a success, so a teardown script does not have to guard a call with nothing left to do.

gdsgate status [--json] [--explain]

What the resident client is doing: the identity it holds, the level it settled on, when the catalog was last read, how names are served, how access is established and how a refusal reaches you, the last failure, and one row per resource with the command that reaches it.

identity  identity live for 42m · direct · catalog last read 14s ago
names     hosts · zone gds in /etc/hosts · 3 answering
access    certificates issued ahead of the connections that need them · notify: /usr/bin/notify-send
state     /home/me/.gdsgate/endpoints.json

RESOURCE     NAME             KIND        ADDRESS          ACCESS   SERVING  CONNECT
pg-prod      pg-prod.gds      postgres    127.70.0.2:5432  ok       yes      psql 'postgres://pg-prod.gds'
dev1         dev1             ssh         -                step-up  yes      -
prodcluster  prodcluster.gds  kubernetes  127.70.0.4:6443  ok       yes      kubectl --context prodcluster
pg-demo      pg-demo.gds      postgres    127.70.0.5:5432  ok       no       -

dev1     port 22 is privileged, and an unprivileged process cannot bind it
dev1     run `gdsgate step-up dev1 --action connect`, then connect
pg-demo  declared, and no connector is serving it
# `gdsgate status --explain` prints each of these in full

The header is four lines. The first carries what decides whether anything works at all; names and access are the two settings that can be half working; state collects what you need only when you are asked for a path, the state file and, at the transparent level, the interface and what it has refused. An error line appears when the last poll or the last file write failed, and is never folded into another line.

ACCESS is what stands between you and the resource: ok; step-up, approval or mfa, meaning refused with one thing you can do to cure it, which the line under the table names; denied, refused with only an administrator able to change that; ?, the gateway could not be asked; -, it has not been.

SERVING is the other half of the same question, and the half a certificate cannot answer: whether a connector was serving the resource when the catalog was last read, as the gateway's routing table had it. A resource stays declared, named and addressed after whatever served it goes away, so ACCESS ok on its own never meant reachable. yes; no, declared with nothing behind it, where a connection would be refused rather than relayed; -, nobody answered the question, because the listing did not come through a gateway, which is not a synonym for no.

A resource with no listener, a resource the gateway refuses, and a resource nothing serves keep their rows and are repeated below the table with the reason and the command that cures it, one line each. None of the three is offered a command to copy: one printed there is one you run and watch fail, or, for the unserved, watch hang. --explain prints the whole of every reason instead, and adds which local process reached which resource; where there are more than a handful, the folded form counts the rest instead of printing them.

--json prints the same for a script, with a connect string per resource and a running flag; both a running and a stopped client exit zero. It is unaffected by --explain, since the machine-readable form carries the whole of every reason either way.

Flag Purpose
--json Machine-readable output, always complete.
--explain Print every reason under the table in full instead of folding it to a line.

See Resident client for the whole picture.

gdsgate completion <bash|zsh|fish>

Emits a completion script for the shell.

gdsgate completion bash > ~/.local/share/bash-completion/completions/gdsgate
gdsgate completion zsh  > ~/.local/share/zsh/site-functions/_gdsgate
gdsgate completion fish > ~/.config/fish/completions/gdsgate.fish

Besides commands and flags it completes resource names where one is expected (gdsgate ssh <Tab>, gdsgate db proxy <Tab>). The names come from the resident client's ~/.gdsgate/endpoints.json, so completing never calls the cluster and never waits on a process: it is instant with the client stopped, and a machine that has never run one offers no names.

gdsgate completion --resources prints those names, one per line. It is what the generated script runs, and is rarely useful by hand.

gdsgate machine-id [--token <jwt> | --provision-token <t> | --renew]

Bootstrap or renew a workload identity, the machine counterpart of login. Generates a key pair locally, with the private half never leaving the host, exchanges the proof for a short-lived SPIFFE-named certificate, and caches it where the resource commands look for credentials. Prints the issued SPIFFE ID, its owner and groups if any, and the validity.

Flag Purpose
--token <jwt> A platform id-token addressed to the gateway, for example --token "$GITLAB_OIDC" from a GitLab id_tokens: job.
--provision-token <t> An admin-minted provision token (gdsgate authority create-token --role workload), the fallback for a host with no platform issuer.
--renew Token-less renew for a provision-token workload: presents the stored certificate plus a proof of possession and rolls to a fresh key. A platform-token workload instead re-runs the bootstrap with a fresh token.
--endpoint <url> Bootstrap listener URL. Defaults to [enroll].endpoint.
# In a GitLab CI job (id_tokens: configured for the gateway audience)
gdsgate machine-id --token "$GDSGATE_OIDC"
gdsgate db proxy prod-db --listen 127.0.0.1:5433 &
psql -h 127.0.0.1 -p 5433