Skip to content

One Linux host (VPS)

Authority and Proxy on one Linux host with a public address, PostgreSQL on the same host, connectors on other hosts. Profile: pilot.

Placeholder Meaning
gate.example.com The name clients and connectors dial. Resolves to 203.0.113.10.
203.0.113.10 The host's public address.
https://idp.example.com/realms/gdsgate The OIDC issuer.

Before you start

Item How to check Reference
Linux with systemd, a root shell systemctl --version service install
gdsgate at /usr/local/bin/gdsgate gdsgate --version Installation
Inbound ports open before you install, per the connector route. The administrator opens them; without them join and connect from other hosts fail from outside, after start: nc -vz 203.0.113.10 50061 Open the ports
The name resolves to the host (DNS modes) getent hosts gate.example.com prints 203.0.113.10 Certificates
Certificate files for the chosen mode openssl x509 -in /etc/gdsgate/tls/fullchain.pem -noout -subject -ext subjectAltName Certificates
OIDC client gdsgate: public client, device authorization grant on; loopback redirect URI http://127.0.0.1:<port>/callback for gdsgate login --browser curl -s https://idp.example.com/realms/gdsgate/.well-known/openid-configuration prints JSON [oidc], gdsgate login
PostgreSQL on the host, database and role gdsgate psql postgres://gdsgate@127.0.0.1:5432/gdsgate -c 'select 1' State store
Group gdsgate for the service accounts getent group gdsgate service install
production only: wrapped keyrings setup prepare --security-profile production --dry-run names what is missing [security.integrity]

Ports

Purpose, dialer, and exposure per port: Listener ports. On this host, 50050/50051 stay on the loopback unless the route is direct.

What each port carries: Endpoints.

Open the ports

Open the ports before you install. The host firewall is the administrator's responsibility: setup prepare, service install and --start do not change it. While a port is closed, the steps that cross it fail: gdsgate join on a connector host, gdsgate connect on a workstation.

Route Open to every address Open to connector addresses only
tunneled 50061, 50062, 50063
tunneled, IP address only 50061, 50062, 50063, 50064
direct 50061, 50062, 50063 50050, 50051
fronted 50061, 50062, 50063

The tunneled route:

Firewall Command
ufw ufw allow 50061:50063/tcp
firewalld firewall-cmd --permanent --add-port=50061-50063/tcp && firewall-cmd --reload
nftables, the inet filter table of /etc/nftables.conf nft insert rule inet filter input tcp dport 50061-50063 accept, and the same rule in /etc/nftables.conf
Windows, elevated PowerShell New-NetFirewallRule -DisplayName gdsgate-proxy -Direction Inbound -Protocol TCP -LocalPort 50061,50062,50063 -Action Allow

For 50064 add it to the same command. For 50050 and 50051 on the direct route, allow the connector networks only: the Authority host's rule. A network firewall in front of the host, such as a cloud security group, needs the same ports.

Connector routes

A connector reaches Authority by one of three routes. The route decides which ports face the connectors' network.

Route Ports open on the host What to set When
tunneled (recommended) 50061, 50062, 50063 setup prepare, no flag: [proxy].public_join = true on both files, [connector].authority_transport = "tunneled" in Authority's. Connector: gdsgate join --enroll-endpoint https://gate.example.com:50061 writes the rest. A public host with a DNS name. Authority listens on the loopback.
tunneled, IP address only 50061, 50062, 50063, 50064 setup prepare --join-relay: [endpoints].proxy_join on both files, authority_transport = "tunneled" in Authority's, the registration certificate from the cluster CA. Connector: --enroll-endpoint https://203.0.113.10:50064 --enroll-fingerprint "$CA_FINGERPRINT" --enroll-sni enroll.gdsgate.internal (registration at an IP address). A public host without a DNS name.
direct 50050, 50051, 50061, 50062, 50063 setup prepare --authority-bind 203.0.113.10 --no-public-join. Connector: --enroll-endpoint https://203.0.113.10:50050. Connectors in a network you trust. Allow 50050, 50051 from connector addresses only.
fronted 50061, 50062, 50063 setup prepare, no flag. Connector: gdsgate join --enroll-endpoint https://gate.example.com:50061 --route fronted writes the route, [endpoints].authority and [enroll].renew_endpoint for that address. A front that routes by TLS server name. At most public_admin_max_connections (default 8) connectors reach Authority at once.

On 50061 a connector checks the certificate of --tls-cert for the name or address it dials. Renewal there is relayed to Authority unterminated, at most public_renewal_max_connections (default 64) at once; registration is limited per source address by public_join_rate_per_ip (default 5 per second, burst 10). Clients on the same port are served as before.

Full reference: split deployment, routes.

Configure

  1. PostgreSQL on the host:

    apt-get install -y postgresql
    sudo -u postgres psql -c 'CREATE ROLE gdsgate LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE'
    sudo -u postgres psql -c '\password gdsgate'
    sudo -u postgres psql -c 'CREATE DATABASE gdsgate OWNER gdsgate'
    
  2. The configuration directory and the store password. Supply the same password in a protected file named store.password:

    groupadd --system --force gdsgate
    install -d -m 0750 -g gdsgate /etc/gdsgate
    install -o root -g gdsgate -m 0640 store.password /etc/gdsgate/store.password
    
  3. Certificate files, per Certificates:

    Mode Files
    Let's Encrypt /etc/gdsgate/tls/fullchain.pem, /etc/gdsgate/tls/privkey.pem, and the deploy hook
    Your own CA the same two files; the root in the host's system store
    IP address only none: setup prepare has the cluster CA issue the registration certificate
  4. Write the configurations. Run with --dry-run first: the to prepare section lists what the files will not satisfy; an x: line is refused at start.

    gdsgate setup prepare --domain gate.example.com --security-profile pilot \
      --store-url postgres://gdsgate@127.0.0.1:5432/gdsgate \
      --store-password-file /etc/gdsgate/store.password \
      --enroll-cert /etc/gdsgate/tls/fullchain.pem --enroll-key /etc/gdsgate/tls/privkey.pem \
      --tls-cert /etc/gdsgate/tls/fullchain.pem --tls-key /etc/gdsgate/tls/privkey.pem \
      --oidc-issuer https://idp.example.com/realms/gdsgate --oidc-client-id gdsgate \
      --dry-run
    

    Then the same command without --dry-run. For the direct route add --authority-bind 203.0.113.10 --no-public-join.

    gdsgate setup prepare --domain 203.0.113.10 --security-profile pilot \
      --store-url postgres://gdsgate@127.0.0.1:5432/gdsgate \
      --store-password-file /etc/gdsgate/store.password \
      --oidc-issuer https://idp.example.com/realms/gdsgate --oidc-client-id gdsgate \
      --join-relay --dry-run
    

    Then the same command without --dry-run. For the direct route replace --join-relay with --authority-bind 203.0.113.10 --no-public-join: Authority binds that address and the Proxy dials it there.

    The run also has the cluster CA issue the registration certificate to /var/lib/gdsgate/authority/enroll.pem and writes the cluster CA to /etc/gdsgate/transport-ca.pem for the Proxy. Authority renews the certificate. See Certificates, IP address only. Windows: the Windows tab, IP address only.

    Each run writes the configurations, the seed policy, the two keyrings and the systemd units, and prints transport_ca.fingerprint. Flags: setup prepare.

  5. The audit export, and the services:

    gdsgate setup change --authority-config /etc/gdsgate/authority.toml \
      --set authority:audit.export_path=/var/lib/gdsgate/authority/audit.jsonl
    gdsgate --config /etc/gdsgate/authority.toml service install authority
    gdsgate --config /etc/gdsgate/proxy.toml service install proxy
    

    service install writes and enables each unit and gives the gdsgate group /etc/gdsgate and every file the configuration names in it, keys included.

  6. The files as written, comments shortened:

    /etc/gdsgate/authority.toml:

    store_url           = "postgres://gdsgate@127.0.0.1:5432/gdsgate"  # cluster state
    store_password_file = "/etc/gdsgate/store.password"               # password, read at connect
    
    [security]
    profile = "pilot"                               # checks applied at start
    
    [security.integrity]
    control_keyring = "/etc/gdsgate/control.keys"   # seals control rows
    audit_keyring   = "/etc/gdsgate/audit.keys"     # seals the audit chain
    
    [endpoints]
    authority        = "127.0.0.1:50051"            # control plane, bound
    authority_enroll = "127.0.0.1:50050"            # registration, bound
    proxy_public     = "gate.example.com:50061"     # told to joining nodes
    proxy_internal   = "gate.example.com:50062"     # told to joining nodes
    proxy_ws         = "gate.example.com:50063"     # told to joining nodes
    
    [telemetry]
    metrics_listen = "127.0.0.1:9090"               # /readyz, /healthz, /metrics
    
    [proxy]
    public_join = true                              # nodes renew at proxy_public
    
    [connector]
    authority_transport = "tunneled"                # route told to connectors
    
    [authority]
    enroll_cert_file = "/etc/gdsgate/tls/fullchain.pem"  # registration listener TLS
    enroll_key_file  = "/etc/gdsgate/tls/privkey.pem"
    
    [policy]
    path = "/etc/gdsgate/policy.cedar"              # seed, read while the store has no policy
    
    [oidc]
    issuer    = "https://idp.example.com/realms/gdsgate"
    client_id = "gdsgate"
    
    [audit]
    export_path = "/var/lib/gdsgate/authority/audit.jsonl"  # off-cluster copy for a log shipper
    

    /etc/gdsgate/proxy.toml. The three public listeners bind every interface; the file holds them in [endpoints] beside the two Authority addresses:

    Key Value Listener
    proxy_public 0.0.0.0:50061 clients; connector registration and renewal
    proxy_internal 0.0.0.0:50062 connector tunnels
    proxy_ws 0.0.0.0:50063 connector tunnels, WebSocket
    [endpoints]
    # proxy_public, proxy_internal, proxy_ws: the table above
    authority        = "127.0.0.1:50051"            # dialled
    authority_enroll = "127.0.0.1:50050"            # where registration is forwarded
    
    [proxy]
    public_cert_file = "/etc/gdsgate/tls/fullchain.pem"  # what clients and connectors see
    public_key_file  = "/etc/gdsgate/tls/privkey.pem"
    public_join      = true                         # connectors register and renew on 50061
    
    [enroll]
    endpoint      = "https://127.0.0.1:50050"       # the Proxy's own registration
    transport_sni = "gate.example.com"              # name verified on that certificate
    state_dir     = "/var/lib/gdsgate/proxy-state"  # enrolled identity
    
    [oidc]
    issuer    = "https://idp.example.com/realms/gdsgate"  # advertised to gdsgate connect
    client_id = "gdsgate"
    
    [security]
    profile = "pilot"
    
    [telemetry]
    metrics_listen = "127.0.0.1:9091"
    

    /etc/gdsgate/authority.toml differs in the addresses told to joining nodes, in [authority], and has the join relay in place of [proxy]:

    [endpoints]
    authority        = "127.0.0.1:50051"
    authority_enroll = "127.0.0.1:50050"
    proxy_public     = "203.0.113.10:50061"
    proxy_internal   = "203.0.113.10:50062"
    proxy_ws         = "203.0.113.10:50063"
    proxy_join       = "203.0.113.10:50064"     # where nodes register and renew
    
    [authority]
    enroll_cert_from_cluster_ca = true             # issued from the cluster CA, renewed by Authority
    enroll_cert_file = "/var/lib/gdsgate/authority/enroll.pem"
    enroll_key_file  = "/var/lib/gdsgate/authority/enroll.key"
    

    /etc/gdsgate/proxy.toml; the listeners as in the DNS tab plus proxy_join on port 50064, the join relay; no [proxy] table:

    [endpoints]
    # proxy_public, proxy_internal, proxy_ws, proxy_join: 0.0.0.0, every interface
    authority        = "127.0.0.1:50051"
    authority_enroll = "127.0.0.1:50050"
    
    [enroll]
    endpoint     = "https://127.0.0.1:50050"
    transport_ca = "/etc/gdsgate/transport-ca.pem"  # the cluster CA, written by setup prepare
    state_dir    = "/var/lib/gdsgate/proxy-state"
    
    [oidc]
    issuer    = "https://idp.example.com/realms/gdsgate"
    client_id = "gdsgate"
    
    [security]
    profile = "pilot"
    
    [telemetry]
    metrics_listen = "127.0.0.1:9091"
    

    Key reference: top level, [endpoints], [enroll], [proxy], [authority], [audit].

  7. Check Authority's file:

    gdsgate config check /etc/gdsgate/authority.toml --role authority
    

    Expected: 0 error(s), 0 warning(s). The Proxy's file is checked once its token file is installed (Start). See config check.

Start

systemctl start gdsgate-authority
gdsgate --config /etc/gdsgate/authority.toml authority bootstrap-admin \
  --output-dir /etc/gdsgate/admin
gdsgate setup change --authority-config /etc/gdsgate/authority.toml \
  --set authority:admin.identity_dir=/etc/gdsgate/admin \
  --set authority:admin.endpoint=https://127.0.0.1:50051
(umask 077
 gdsgate --config /etc/gdsgate/authority.toml authority create-token \
   --role proxy > /etc/gdsgate/proxy.token)
chgrp gdsgate /etc/gdsgate/proxy.token
chmod 0640 /etc/gdsgate/proxy.token
gdsgate setup change --proxy-config /etc/gdsgate/proxy.toml \
  --set 'proxy:enroll.token=${file:/etc/gdsgate/proxy.token}'
gdsgate --config /etc/gdsgate/proxy.toml service install proxy
gdsgate config check /etc/gdsgate/proxy.toml --role proxy
systemctl start gdsgate-proxy

With --authority-bind 203.0.113.10, [admin].endpoint is https://203.0.113.10:50051. The administrative identity lasts one day; the same bootstrap-admin command renews it. The Proxy spends the token on its first start and keeps its identity in /var/lib/gdsgate/proxy-state. After readiness, remove the spent token reference and file:

gdsgate setup change --proxy-config /etc/gdsgate/proxy.toml \
  --set proxy:enroll.token=
rm /etc/gdsgate/proxy.token
systemctl restart gdsgate-proxy

Readiness must recover from the persisted identity without another token. Every service install checks the shared state parent at 0755; role state remains private. For a working-directory refusal see service permissions.

Step 4 with --start in place of --dry-run installs and starts both services, bootstraps the admin identity and enrolls Proxy. Configure audit export separately and remove the spent enrollment input after verification. It reports the gateway started once the Proxy's /readyz answers, and otherwise fails with the last lines the Proxy logged.

From an elevated PowerShell prompt, with the files under %ProgramData%\gdsgate. Step 4 with --start:

gdsgate setup prepare --domain gate.example.com --security-profile pilot `
  --store-url postgres://gdsgate@127.0.0.1:5432/gdsgate `
  --store-password-file C:\ProgramData\gdsgate\store.password `
  --enroll-cert C:\ProgramData\gdsgate\tls\fullchain.pem --enroll-key C:\ProgramData\gdsgate\tls\privkey.pem `
  --tls-cert C:\ProgramData\gdsgate\tls\fullchain.pem --tls-key C:\ProgramData\gdsgate\tls\privkey.pem `
  --oidc-issuer https://idp.example.com/realms/gdsgate --oidc-client-id gdsgate `
  --start

The firewall rule with the join relay:

New-NetFirewallRule -DisplayName gdsgate-proxy -Direction Inbound -Protocol TCP `
  -LocalPort 50061,50062,50063,50064 -Action Allow
gdsgate setup prepare --domain 203.0.113.10 --security-profile pilot `
  --store-url postgres://gdsgate@127.0.0.1:5432/gdsgate `
  --store-password-file C:\ProgramData\gdsgate\store.password `
  --oidc-issuer https://idp.example.com/realms/gdsgate --oidc-client-id gdsgate `
  --join-relay --start

The registration certificate is in C:\ProgramData\gdsgate\state\authority\enroll.pem, the cluster CA for the Proxy in C:\ProgramData\gdsgate\transport-ca.pem. The run prints transport_ca.fingerprint for the connector's --enroll-fingerprint.

--start registers and starts gdsgate-authority, takes the administrative identity, writes the Proxy token to proxy.token, and starts gdsgate-proxy. It does not write the audit export of step 5. Add it, restart Authority and check the file:

Add-Content C:\ProgramData\gdsgate\authority.toml "`n[audit]`nexport_path = 'C:\ProgramData\gdsgate\state\authority\audit.jsonl'"
Restart-Service gdsgate-authority
gdsgate config check C:\ProgramData\gdsgate\authority.toml --role authority

Expected: 0 error(s), 0 warning(s). Certificate reload: sc.exe control gdsgate-proxy paramchange.

A connector on another host

On this host, the catalogue row and a token:

gdsgate --config /etc/gdsgate/authority.toml authority set-resource demo-ssh \
  --kind ssh --onward-auth local
(umask 077
 gdsgate --config /etc/gdsgate/authority.toml authority create-token --role connector \
   > connector.token)

On the connector host, as root, with connector.token copied there. For the IP-only modes, set the approved fingerprint from Authority:

CA_FINGERPRINT='<approved SHA-256 fingerprint>'

The explicit configuration and state paths below are for a system service. For a home-directory foreground installation and two Proxy connections, use the HA Connector procedure.

gdsgate join gate.example.com --name conn-1 \
  --enroll-endpoint https://gate.example.com:50061 --route tunneled \
  --serve demo-ssh=ssh --recording central \
  --output /etc/gdsgate/connector.toml --state-dir /var/lib/gdsgate/connector < connector.token
gdsgate --config /etc/gdsgate/connector.toml service install connector
systemctl start gdsgate-connector
rm connector.token

With your own CA and the root not in the connector host's store, add --enroll-ca /etc/gdsgate/root.pem. At the host's address, --enroll-endpoint https://203.0.113.10:50061 when the certificate carries IP:203.0.113.10, or with --enroll-sni gate.example.com when it does not.

Prepared with --join-relay. The fingerprint is transport_ca.fingerprint from step 4:

gdsgate join 203.0.113.10 --name conn-1 \
  --enroll-endpoint https://203.0.113.10:50064 --route tunneled \
  --enroll-fingerprint "$CA_FINGERPRINT" --enroll-sni enroll.gdsgate.internal \
  --serve demo-ssh=ssh --recording central \
  --output /etc/gdsgate/connector.toml --state-dir /var/lib/gdsgate/connector < connector.token
gdsgate --config /etc/gdsgate/connector.toml service install connector
systemctl start gdsgate-connector
rm connector.token

The certificate is checked against the address: registration at an IP address.

Authority prepared with --authority-bind 203.0.113.10 --no-public-join and no --enroll-cert. The fingerprint is transport_ca.fingerprint from step 4:

gdsgate join 203.0.113.10 --name conn-1 \
  --enroll-endpoint https://203.0.113.10:50050 --route direct \
  --enroll-fingerprint "$CA_FINGERPRINT" --enroll-sni enroll.gdsgate.internal \
  --serve demo-ssh=ssh --recording central \
  --output /etc/gdsgate/connector.toml --state-dir /var/lib/gdsgate/connector < connector.token
gdsgate --config /etc/gdsgate/connector.toml service install connector
systemctl start gdsgate-connector
rm connector.token

From an elevated PowerShell prompt:

Get-Content -Raw connector.token | gdsgate join gate.example.com --name conn-1 `
  --enroll-endpoint https://gate.example.com:50061 --serve demo-ssh=ssh
gdsgate service install connector
sc.exe start gdsgate-connector
Remove-Item connector.token

IP address only: gdsgate join 203.0.113.10 with the --enroll-endpoint and --enroll-fingerprint of the tab "tunneled, IP address only".

join writes /etc/gdsgate/connector.toml and the identity in /var/lib/gdsgate/connector; on Windows C:\ProgramData\gdsgate\connector.toml and C:\ProgramData\gdsgate\state\connector. Reference: gdsgate join, [connector].

Verify

Command Expected If not: cause → fix
systemctl is-active gdsgate-authority gdsgate-proxy active twice journalctl -u gdsgate-authority -n 30; see the rows below.
curl -s 127.0.0.1:9090/readyz; curl -s 127.0.0.1:9091/readyz ready twice cannot bind `[telemetry].metrics_listen` = 127.0.0.1:9091: Address in use in the log → another process has that port; stop it, or change metrics_listen.
journalctl -u gdsgate-authority -n 30 auth service serving gRPC ... enroll_mode="TLS from the configured certificate" the private key of ...: Permission denied (os error 13) → the key is outside /etc/gdsgate; copy it there and run service install authority again.
journalctl -u gdsgate-proxy -n 30 enrolled and persisted a new transport identity, then proxy serving gRPC enrollment endpoint https://127.0.0.1:50050: certificate refused (...); checked for the name `...` against ... → the name or the trust named there; see Certificates.
gdsgate --config /etc/gdsgate/authority.toml doctor ok registration listener TLS · ... The failing line names the key.
gdsgate config check /etc/gdsgate/authority.toml --role authority 0 error(s), 0 warning(s) [audit-export] warning → step 5; on Windows, the Add-Content of the Windows tab. audit export file: the service cannot write it → export_path outside /var/lib/gdsgate/authority (Windows: %ProgramData%\gdsgate\state\authority).
journalctl -u gdsgate-authority \| grep 'audit export' audit export file open (JSON-lines) path=/var/lib/gdsgate/authority/audit.jsonl Read-only file system → as the row above.
journalctl -u gdsgate-proxy \| grep 'node registration on the public listener' forward="TLS" forward="cleartext" → the Proxy's [enroll].endpoint is http://. No line → prepared with --join-relay or --no-public-join.
gdsgate join ... on the connector host ok: joined gate.example.com as conn-1 · role connector certificate refused → the connector host does not trust the certificate on 50061 (--enroll-ca), or it does not name the name or address dialled. UNAVAILABLE → the Proxy cannot reach or verify the registration listener; the Proxy log names why.
tunneled: the same join output cluster settings: ... auth tunneled at none, renew https://gate.example.com:50061 renew https://gate.example.com:50064 → prepared with --join-relay.
fronted: the same join output --route fronted: Authority gate.example.com:50061, renewal https://gate.example.com:50061 Line missing → --route fronted was not passed, and renewal goes to Authority's own address; run join again with --route fronted --if-missing.
gdsgate --config /etc/gdsgate/authority.toml authority list-nodes rows admin, proxy, connector, state active No connector row → the join output names the failure.
gdsgate --config /etc/gdsgate/authority.toml authority list-offered demo-ssh ssh ... yes No row → journalctl -u gdsgate-connector; Read the reason: not_in_catalog needs a catalogue row; mode_not_confirmed needs its SSH mode; gate_not_offered needs a capability receipt. See startup diagnostics.
On a workstation: gdsgate connect gate.example.com (DNS name) or gdsgate connect 203.0.113.10:50061 --fingerprint "$CA_FINGERPRINT" ok: wrote ~/.gdsgate/config.toml · gateway <version> See Certificates.
On the workstation: gdsgate setup check --role client ok: trust anchor ...: the file --fingerprint wrote, or this host's trust store for a public certificate ? no trust anchor is configured → run gdsgate connect first.
gdsgate login -> to sign in, open https://idp.example.com/realms/gdsgate/device and enter code ... Error from the provider → the OIDC client in Before you start.

If login uses the development issuer, check [oidc] on Proxy, restart it, then refresh the client configuration with gdsgate connect --force and sign in again. If login succeeds but later calls return invalid identity token, check matching issuer, client audience and validation on every Authority. Use the same client configuration for login and resource commands.

After these checks pass, complete the access checks from a client workstation.

Profile

pilot. These pages satisfy the profile matrix rows profile-declared, store-postgres, registration-tls, enroll-endpoint-tls, proxy-public-tls, proxy-internal-mtls, connector-enrolled, audit-export, keyrings-distinct and open-secret-file. For production add oidc-provider (already set here), keyrings-named with wrapped keyrings, and key-file-access; run setup prepare --security-profile production --dry-run for the list.