Skip to content

Connector join

Global flags are in the CLI hub. Kinds gdsgate join --serve accepts:

ssh postgres mysql tcp mcp rdp

gdsgate join <domain>

Registers a node and writes its configuration file and transport identity. Run it before gdsgate connector.

TOKEN_FILE="<provision-token-file>"
sudo cat -- "$TOKEN_FILE" | sudo gdsgate join acme.example.com \
    --serve prod-db=postgres@10.0.0.5:5432 \
    --serve jump-host=ssh

On Windows, from an elevated PowerShell prompt:

Get-Content -Raw $TOKEN_FILE | gdsgate join acme.example.com `
    --serve prod-db=postgres@10.0.0.5:5432 `
    --serve jump-host=ssh

Input: the deployment's domain and a one-time join token (gdsgate authority create-token --role connector). Output:

Path (Linux) Path (Windows) Contents
/etc/gdsgate/connector.toml %ProgramData%\gdsgate\connector.toml [endpoints], [enroll], [connector], one [[connector.backends]] per --serve; the deployment label and health address when the flags set them
/var/lib/gdsgate/connector/ %ProgramData%\gdsgate\state\connector\ transport.key (mode 0600 on Linux; owner-and-SYSTEM-only ACL on Windows), transport.crt, transport-ca.pem

These are the paths the gdsgate-connector.service unit reads on Linux and the gdsgate-connector service reads on Windows.

With --role agent-host:

Path (Linux) Path (Windows)
/etc/gdsgate/agent-host.toml %ProgramData%\gdsgate\agent-host.toml
/var/lib/gdsgate/agent-host/ %ProgramData%\gdsgate\state\agent-host\

The file has [agent_host] and no backend list. It sets [endpoints].proxy_public (the Proxy listener agents open sessions through) and [client].transport_ca (the CA in the state directory) from values the cluster sends. When the Proxy's [endpoints].proxy_public is unset or a wildcard address, the cluster sends neither; set both by hand. See agent host. On Windows the agent host registers and starts but runs no agents; running agents is Linux-only.

The token is read from standard input; as an argument it is refused. It is spent by join and written to no file. The first line is the token, in UTF-8 with or without a byte-order mark or in UTF-16 with one: what create-token ... > connector.token writes in PowerShell 7 and Windows PowerShell 5.1. Standard input is read to the end even when no token is needed, so a loop that pipes one on every pass gets no broken pipe.

Before the token is read, join checks that it can write the state directory (each identity file it would replace, or the nearest existing directory above a new one) and the directory of the configuration file. A refusal names the path and the error, for example cannot write the transport identity at /var/lib/gdsgate/connector: Permission denied (os error 13); nothing is sent and the token stays valid.

The domain gives the registration address: the gateway, proxy. under the domain. The registration answer supplies the rest: the gateway's connector addresses, the route to Authority, and the renewal address; --route replaces the last two. See What the cluster tells a registering node. When the answer lacks a value, join names what is missing and derives no address.

Resources come only from the command line. Each --serve takes <resource>=<kind>[@<addr>]; ssh may omit the address (the connector terminates it). Each id needs a matching catalogue entry, or the gateway closes the connector's tunnel; see Resource catalog lifecycle.

Flags:

Flag Effect
--role {connector\|agent-host} Role the token was issued for. Default connector. Other roles enroll with other commands: a workload with machine-id, a provisioning control plane with authority bootstrap-provisioner, the Proxy with gdsgate setup prepare --start (token file beside its configuration: proxy.env on Linux, proxy.token on Windows; join does not read it).
--name <NAME> Node name in the registry ([enroll].node_name) and at the gateway ([connector].id); one string for both. Default: the host's fully qualified name, printed by the run.
--serve <SPEC> A resource this node serves. Repeatable. Kinds: ssh, postgres, mysql, tcp, mcp, rdp. cockroach is rejected; declare a CockroachDB backend in --backends-file.
--backends-file <PATH> TOML fragment with [[connector.backends]] entries only, for backends --serve cannot express: a Kubernetes API with its bundle and service-account token, CockroachDB, a model service addressed by base URL, a jump host with pinned host keys.
--enroll-ca <PATH> PEM anchor for the registration listener's certificate, for a deployment with its own CA. Default: the host's trusted CAs.
--enroll-fingerprint <SHA256> SHA-256 fingerprint of the cluster CA that issued the registration certificate: transport_ca.fingerprint from setup prepare, the value clients pass to gdsgate connect --fingerprint. For a gateway reached by IP address (enroll_cert_from_cluster_ca). The listener's chain is read before the token; a chain without that CA is refused with the token unspent. Writes [enroll].transport_ca = <state-dir>/transport-ca.pem, the cluster CA bundle renewal keeps current. With --enroll-ca, --insecure or an http:// endpoint: argument error.
--enroll-sni <NAME> Name the registration certificate is checked against, and the TLS server name sent. Default: the host in --enroll-endpoint. Written to [enroll].transport_sni. For a certificate issued for an internal name, e.g. authority.internal, reached through the join relay, or for the gateway's name on its client port dialled by address. An IP address or a gdsgate.internal name is an argument error; --insecure with it is an argument error.
--route {direct\|tunneled\|fronted} Route to Authority written to the connector's file. Default: the route the cluster sends. See the table below. --role agent-host with it is an argument error.
--insecure Register over cleartext. No encryption, no verification; the token crosses the network in the clear.
--enroll-endpoint <URL> Registration URL. Overrides the one derived from the domain. An https:// IP address sends the server name enroll.gdsgate.internal and checks the certificate against the address; see Registration at an IP address.
--profile <LABEL> Deployment label on every metric series, log line and exported trace of this node (profile). Default: no label.
--metrics-listen <ADDR> Address for /healthz, /readyz and Prometheus /metrics ([telemetry].metrics_listen). Plain HTTP; keep it on loopback unless read from another host. Default: off.
--recording <disk\|central> Where session casts go ([recording].mode): disk under [enroll].state_dir, central uploaded to Authority. Default: no [recording] section, connector default disk. An explicit disk writes the section, so --if-missing re-runs treat it as set. Any other value is an argument error.
--output <PATH>, -o Write the configuration to this path.
--state-dir <DIR> Keep the identity in this directory. It must survive restarts.
--print Render the configuration and exit. Reads no token, writes nothing, connects nowhere.
--if-missing Reconcile with the existing installation. Without it, join installs over it. Exit 3 when nothing needed to change. A tunneled node given an --enroll-endpoint at a new address renews there, no token; see moving the registration address.
--force Replace an existing configuration, spending a fresh token. Without it, an existing file is an error and nothing is written.

Exit codes: 0 done, 3 nothing to change (--if-missing), 1 failed, 2 invalid arguments.

--route writes these keys. The registration address is the host:port of --enroll-endpoint: the Proxy's join relay, or its client port with public_join. Both pass renewal and control-plane calls to Authority.

--route [connector].authority_transport [endpoints].authority [enroll].renew_endpoint
unset from the cluster from the cluster from the cluster
fronted fronted the registration address https:// + the registration address
direct direct from the cluster from the cluster
tunneled tunneled none from the cluster when it sends tunneled, otherwise https:// + the registration address

--route direct against a cluster that sends no Authority address, as with tunneled, is refused before the token is redeemed. Nothing is written and the token stays valid. An Authority older than this check refuses it after registration, with the token spent.

--if-missing with --route renews at the address the route writes. A node joined without --route fronted is fixed by the same command with --route fronted --if-missing; no token is needed, and the run names the restart it needs.

A connector of a gateway setup prepare wrote, registering and renewing on its client port; the cluster sends tunneled:

sudo cat -- "$TOKEN_FILE" | sudo gdsgate join gate.example.com \
    --enroll-endpoint https://gate.example.com:50061 \
    --serve demo-ssh=ssh

The same gateway at its address, with a certificate that carries IP:203.0.113.10 or, without that entry, with --enroll-sni gate.example.com:

sudo cat -- "$TOKEN_FILE" | sudo gdsgate join gate.example.com \
    --enroll-endpoint https://203.0.113.10:50061 --enroll-ca /etc/gdsgate/root.pem \
    --serve demo-ssh=ssh

A gateway reached by IP address alone, prepared with --join-relay and no --enroll-cert; the fingerprint is transport_ca.fingerprint from setup prepare:

CA_FINGERPRINT='<approved SHA-256 fingerprint>'
sudo cat -- "$TOKEN_FILE" | sudo gdsgate join 203.0.113.10 \
    --enroll-endpoint https://203.0.113.10:50064 \
    --enroll-fingerprint "$CA_FINGERPRINT" --enroll-sni enroll.gdsgate.internal \
    --serve demo-ssh=ssh

The same gateway with a registration certificate from your own CA (setup prepare --enroll-cert): --enroll-ca /etc/gdsgate/enroll-ca.pem in place of --enroll-fingerprint.

A connector behind a fronted gateway with public_join:

sudo cat -- "$TOKEN_FILE" | sudo gdsgate join gate.example.com \
    --enroll-endpoint https://gate.example.com:50061 --route fronted \
    --serve demo-ssh=ssh

A registration certificate for an internal name, through the join relay:

sudo cat -- "$TOKEN_FILE" | sudo gdsgate join gate.example.com \
    --enroll-endpoint https://gate.example.com:50064 \
    --enroll-sni authority.internal --enroll-ca /etc/gdsgate/enroll-ca.pem \
    --serve demo-ssh=ssh

Renewal does not read --enroll-sni: it is a mutual-TLS call to the control plane, checked against the cluster's CA.

--if-missing is for a loop on a timer:

State found Action
Identity fresh Kept; no token spent.
Identity near its expiry Renewed over mutual TLS; no token spent.
Identity expired Refused: an expired certificate cannot renew. --force with a fresh token.
Configuration differs from what the arguments render Rewritten; the run names the service restart it needs. join does not restart anything.
Nothing differs Nothing written; exit 3.

A token piped in and not needed is read and discarded. --if-missing together with --force is refused.

--print reads no token. It renders what the arguments determine, marks the values only the cluster supplies, and parses as a configuration.

join does not:

  • Start anything. No unit is written, no service enabled or restarted. The run ends with the commands that do: gdsgate --config <file> service install connector, then systemctl start gdsgate-connector on Linux or sc.exe start gdsgate-connector on Windows (service install). An agent host is run as the role: gdsgate --config /etc/gdsgate/agent-host.toml agent-host.
  • Write catalogue rows. It prints them; on a host whose authority.toml names an administrative identity ([admin].identity_dir), with --config of that file.
  • Set group or ownership. The configuration is owner-only; the installing script grants the service account read access.
  • Trust on first use. The registration certificate is verified against --enroll-ca, the host's trusted CAs, or the cluster CA that --enroll-fingerprint names.
  • Write anything on failure: an unreachable listener, an unverified certificate, a refused token, or a cluster with nothing to advertise leaves the filesystem unchanged.

For deployment behind a Proxy LB, use the HA Connector steps: explicit configuration/state paths, matching catalogue mode, two verified Proxy identities and startup receipt checks.

A node that loses its state_dir needs a fresh token and registers as a second registry row. Persist that directory.

Re-join a connector

A connector joined again on the same host, with a fresh token and --force, writes into the state directory its service owns, and registers as a new registry row. The resources stay with the old row: the Proxy drops the new node's tunnel (connector declared no catalog-allowed resources) until the old row is revoked, or for ten minutes after the old node's last tunnel. Revoke it on the Authority host, before or after the join:

gdsgate --config /etc/gdsgate/authority.toml authority list-nodes    # the old row's id
gdsgate --config /etc/gdsgate/authority.toml authority revoke-node '<old-node-id>'
Host Owner of the state directory Steps
Linux the unit's transient account; root writes it As root: the join with --force, then systemctl restart gdsgate-connector. The unit hands /var/lib/gdsgate/connector back to its account at the start. Then revoke-node on the old row.
Windows NT SERVICE\gdsgate-connector; join does not write over its files, and an elevated prompt cannot read the lists of the files the service rewrote or remove them The steps below. gdsgate service uninstall connector leaves the directory in place.

Windows, elevated PowerShell:

sc.exe stop gdsgate-connector
takeown /F C:\ProgramData\gdsgate\state\connector /R /D Y
icacls C:\ProgramData\gdsgate\state\connector /reset /T
Remove-Item -Recurse -Force C:\ProgramData\gdsgate\state\connector
# the join, with --force
gdsgate service install connector
sc.exe start gdsgate-connector
# on the Authority host: authority revoke-node '<old-node-id>'

takeown gives the tree to Administrators and icacls /reset replaces the service's lists with inherited ones. Without them Remove-Item stops at transport.key: Access to the path is denied. /D Y is the English answer; a localized Windows takes its own letter (/? shows it).

On Windows without these steps, join refuses before the token is read, and the same token serves the join in the steps. Over a key the service has not rewritten since service install:

cannot write the transport identity at C:\ProgramData\gdsgate\state\connector\transport.key: it belongs to NT SERVICE\gdsgate-connector, and an owner-only rewrite from this account would shut that account out of it. Nothing was sent, and the token on the standard input is unspent. C:\ProgramData\gdsgate\state\connector belongs to NT SERVICE\gdsgate-connector, the account gdsgate-connector runs as. To join this host again, stop the service, take the directory over and remove it, run this join again, and give the directory back to the service:

Over a key the service has rewritten, the reason is Access is denied. (os error 5). The steps above follow either way, the authority revoke-node of the old row included.

Moving the registration address

A tunneled connector moves to another registration listener on the Proxy, for example from the join relay (50064) to the client port with public_join. No token:

sudo gdsgate join gate.example.com --if-missing \
    --enroll-endpoint https://gate.example.com:50061 \
    --serve demo-ssh=ssh

The other arguments are the ones the connector joined with. The move applies when the new address differs from the file's [enroll].endpoint and from its [enroll].renew_endpoint, compared as host:port. direct and fronted nodes renew where they did.

Result Output Files
Moved renewed the transport identity in <dir> at https://gate.example.com:50061, renewal moved: <old> -> <new>, exit 0 identity renewed, current or not; [enroll].renew_endpoint from the cluster's answer
Cluster still advertises the old address Authority's file still sets [endpoints].proxy_join; remove it, restart Authority, run again configuration unchanged; identity renewed
Renewal at the new address fails renewing at <address> and the error unchanged
Identity expired refused before any connection unchanged; --force and a fresh token
Run again nothing to do, exit 3 unchanged

A renewal replaces the key the registry knows the node by. Restart the connector after a run that renewed: the running process holds the previous key, and Authority refuses its renewals with unknown identity.

Kinds --serve accepts

--serve accepts only the kinds listed at the top of this page. The configuration file has no such limit. gdsgate setup change accepts every kind on its --serve, with --backend <id>:<key>=<value> for the remaining settings.