Skip to content

Proxy configuration

Apply supported changes with gdsgate reload --role proxy. See the reload matrix for settings that require restart and the treatment of existing sessions.

Paths in the tables are relative to [proxy]. Check parsing and cross-field rules with gdsgate doctor --config <PATH> --json. Network layout: network zoning guide.

[proxy]

Used by: Proxy.

Listener mode, admission control, and the certificates the Proxy presents. Admission limits default to 0 (unlimited). The join_*, public_join_*, public_renewal_max_connections and public_admin_max_connections keys have built-in defaults; an explicit 0 makes them unlimited. abandoned_session_grace_secs is a delay; 0 turns the watch off.

Key Type Default Purpose
peer_listen string unset Internal mutual-TLS listener for one-hop relaying between enrolled Proxy roles. Requires peer_addr; unavailable in all mode.
peer_addr string unset Individual HTTPS address published with durable connector routes. With peer_listen, it names that listener on this Proxy. Other proxies must reach it directly.
single_port string unset One listen address for the client gRPC API and the connector WebSocket fallback, told apart by TLS ClientHello or HTTP upgrade. The mutual-TLS reverse tunnel stays on proxy_internal; the join relay keeps proxy_join. Use it when egress allows only HTTPS on :443.
max_peer_connections usize 0 (128 connections) Dedicated peer-listener connection cap.
max_peer_requests usize 0 (32 requests) Concurrent peer discovery and admission calls; excess calls are refused before backend admission.
max_peer_sessions usize 0 (128 streams) Concurrent peer streams, including pending opens, separately on ingress and egress. Capacity is released on cancellation.
max_sessions usize 0 Maximum concurrent relayed client sessions. A session over the limit is refused at open.
max_streams_per_tunnel usize 0 Maximum concurrent backend streams per connector tunnel. Control-plane streams of a tunneled connector have their own small limit and are not counted here.
max_public_connections usize 0 Maximum concurrent connections to the public client-facing listener. Connections over the limit are closed at accept. Connector-facing listeners are not affected.
internal_accept_rate_per_ip u32 0 Per-source-IP accept rate (per second) on the connector-facing internal and WebSocket listeners. Over-budget connections are dropped at accept.
internal_accept_burst u32 0 Burst for internal_accept_rate_per_ip. 0 in either key disables the limiter.
public_anonymous_rate_per_ip u32 0 Per-source-IP call rate (per second) for the two unauthenticated RPCs on the public listener: the settings bundle gdsgate connect fetches, and the liveness-and-version probe. Over-budget calls get RESOURCE_EXHAUSTED. Authenticated RPCs are not counted.
public_anonymous_burst u32 0 Burst for public_anonymous_rate_per_ip. 0 in either key disables the limiter. One gdsgate connect makes two bundle calls; a burst of 1 refuses it.
max_bytes_per_second_per_session u64 0 Sustained bytes per second per session, both directions combined. Excess traffic is paced. Nothing is dropped.
session_rate_burst_bytes u64 0 Burst in bytes for max_bytes_per_second_per_session; set it to about one second of expected traffic. 0 in either key disables the throttle.
public_origin string unset Public browser origin of the gateway (for example https://gate.example.com), the base of the WebAuthn step-up URL. Must match an entry in [mfa.webauthn].origins.
web_addr string unset Listen address for the browser WebAuthn origin (HTTPS). Requires web_cert_file and web_key_file. Unset: off.
web_cert_file string unset PEM certificate chain for web_addr, from a browser-trusted CA. Re-read on change.
web_key_file string unset PEM private key for web_cert_file. Setting only one of the pair is refused at start-up.
webapps_zone string unset Zone for the clientless browser entry: applications are served at <application>.<webapps_zone>; the label gdsgate is reserved for sign-in. Unset: the entry is off. This key and the three below are set together; a partial set is refused at start-up. See The clientless browser entry.
webapps_addr string unset Listen address for the clientless browser entry (HTTPS).
webapps_cert_file string unset PEM wildcard certificate chain for *.<webapps_zone>, from a browser-trusted CA. Re-read on change.
webapps_key_file string unset PEM private key for webapps_cert_file.
webapps_session_ttl_secs u64 28800 (8h) Browser session lifetime and the validity of its ticket. Authority caps it at 12h. Each request is still authorized at the connector.
webapps_proxy_protocol_from array [] Addresses or blocks (["10.4.0.0/24", "2001:db8::7"]) the clientless browser entry accepts a PROXY protocol header from. Empty: the header is not read. Set: only connections from a listed address with a header are served. See the clientless browser entry.
public_proxy_protocol_from array [] The same for the public client-facing listener, including single_port. Empty: the socket address is the client address. Behind a front it becomes context.src_ip, the audit src_ip, and the pin_src_ip pin. See Behind a front.
public_cert_file string unset PEM certificate chain for the client-facing listener. System-trusted issuers need no saved anchor. A private issuer can be accepted by fingerprint when the chain includes a CA. Bootstrap publishes the last CA from the accepted pair and follows successful reloads. Unset: the listener presents the enrolled identity. Requires public_key_file.
public_key_file string unset PEM private key for public_cert_file.
public_addr string unset Common client-facing host:port, independent of local listener binds. Role join uses the verified cluster entry. Advertised client and node renewal addresses use it; the process still binds the configured endpoints. Unset retains the previous listener advertisement.
authority_topology bool false Follow individual Authority peers from authorized Proxy metadata and refresh every 30 seconds. New Proxy role join enables this. Existing deployments keep their configured routes. Explicit Authority addresses remain first; accepted membership is saved privately before use.
authority_registration_ca string unset CA file for discovered registration listeners with an external issuer. Unset: renewed cluster transport CA.
authority_registration_sni string unset Certificate name for discovered registration listeners. Unset: enroll.gdsgate.internal.
public_join bool false Serve node join and renewal on the public client-facing listener. See public_join.
public_registration_endpoint unset Public HTTPS registration URL for NAT or a different LB port. With a separate endpoints.proxy_join relay, the default uses its port on the client's actual entry host.
public_join_rate_per_ip u32 built-in 5 Per-source-IP call rate (per second) for registrations under public_join. Separate from the bundle and probe budget.
public_join_burst u32 built-in 10 Burst for public_join_rate_per_ip. 0 in either key disables the limiter.
public_renewal_max_connections usize built-in 64 Maximum concurrent node renewals relayed from the public listener. Raise it when many nodes renew at once.
public_admin_max_connections usize built-in 8 Maximum concurrent administrative connections relayed from the public listener. See relayed lanes.
join_max_connections usize built-in 64 Maximum concurrent joins on the join relay. Raise it when more connectors join at once.
join_accept_rate_per_ip u32 built-in 5 Per-source-IP accept rate (tokens/second) on the join relay. Paired with join_accept_burst.
join_accept_burst u32 built-in 10 Token-bucket burst for join_accept_rate_per_ip. Either being 0 disables the limiter.
join_peek_timeout_secs u64 built-in 5 How long a connection to the join relay may stay silent before it is classified.
join_idle_timeout_secs u64 built-in 30 Idle ceiling on an established relay.
abandoned_session_grace_secs u64 built-in 300 Delay before the Proxy reports sessions left open by a dropped connector tunnel to Authority. 0 turns the watch off. See Sessions a departing connector left open.

web_key_file, webapps_key_file, and public_key_file accept a secret reference. It is resolved again on each certificate change.

public_anonymous_rate_per_ip counts calls

One HTTP/2 connection carries many calls, so this limit counts calls. The bundle and the probe have separate buckets under the same keys.

Size it for the clients behind one source address. The probe runs at every gdsgate login, gdsgate ssh connect and gdsgate db login. An office behind one NAT address, or a front that does not preserve client addresses, is one source address. Scripts that poll the bundle count too. Refusals are counted in gdsgate_public_anonymous_refused_total and are not logged per call.

Internal Proxy peering

Public clients and Connectors use common TCP load-balancer addresses. Each Proxy exposes a separate listener on its private interface for its peers:

[proxy]
peer_listen = "192.0.2.11:50065"
peer_addr = "https://proxy-a.example.com:50065"

Replace the example addresses with this node's private IP and DNS name. Restrict the listener to the other Proxies. Set peer_addr separately on every Proxy; it must reach that instance directly. Peers verify its enrolled transport certificate and its current Proxy identity.

A resource connection uses a local live tunnel first, then at most one peer hop. Set connector.proxy_connection_count = 2 or greater to enable verified Proxy tunnels and peer opens. A Connector configured for two connections can receive a peer session while only one tunnel is available. With the default 1, it uses the compatibility path and receives local sessions only.

Both Proxies reserve capacity before opening the backend. One session UID occupies one cluster admission slot, bounded by ha.max_cluster_sessions in the Proxy configuration. The original client IP, access certificate, proof of possession and session ID reach the Connector unchanged. Session list, watch and kill reach sessions owned by other Proxies.

Browser login callbacks and logout use shared state held by Authority and can reach different Proxies. Configure the same browser frontend addresses on the pool. New peer sessions fail closed while Authority is unreachable; existing owners expire if they cannot renew. Dropping either relay closes its streams. Deployment and failure checks: Proxy HA.

public_join: the join on the client-facing port

With public_join = true the client-facing listener also serves node registration and renewal. A node uses the public entry address and a token.

gdsgate setup prepare writes public_join = true in the Proxy's file and in Authority's, with [connector].authority_transport = "tunneled" in Authority's. --join-relay writes the join relay in its place; --no-public-join writes neither.

A node joining through verified discovery uses that connection's accepted CA and TLS name. With an enrolled Proxy identity, this is proxy.gdsgate.internal; the listener needs no external public certificate. With public_cert_file, it verifies the entry address or the explicit name against that certificate's issuer.

An explicit --enroll-endpoint bypasses discovery. It needs registration trust and a name the certificate carries; an enrolled identity does not carry the public IP or domain. Use --enroll-sni proxy.gdsgate.internal and the approved registration CA when registering directly on this listener. The separate join relay uses the Authority registration certificate and its own name.

doctor and config check currently warn that registration with an enrolled public identity is refused. That warning describes the address-verifying explicit URL path; verified discovery with the Proxy role name can succeed. Check the actual registration route before changing TLS files.

Step On the public port Destination
registration terminated here and forwarded as a call [endpoints].authority_enroll
token-less renewal not terminated; relayed byte for byte [endpoints].authority; with authority_failover, the instance the Proxy last found leading first, then the others

The step is chosen by the TLS server name. Renewal uses the internal name gdsgate.internal; administrative connections use auth.gdsgate.internal. Clients and registration use the gateway's public name or proxy.gdsgate.internal with enrolled transport TLS. Bootstrap by IP uses the Proxy role name, so it stays on the public listener. This name must appear in the certificate issued by the trusted CA. enroll.gdsgate.internal also stays on the public listener for explicit registration. A connection with no server name is served as a client. Both destinations come from the Proxy configuration.

These rules apply to the public listener. On the separate join relay, internal transport names still reach Authority; it has no Proxy client surface.

The forwarded registration follows the Proxy's own [enroll]:

Proxy configuration Forward to authority_enroll Verified against Name checked
[enroll].endpoint = "https://..." TLS [enroll].transport_ca, or the host's trust store [enroll].transport_sni, or the host in endpoint
[authority].enroll_cert_file in the same file (gdsgate all) TLS as above; [enroll].endpoint is required as above
otherwise cleartext h2c - -

The Proxy does not start when the forward is TLS and cannot be verified: no [enroll].endpoint to take the name from, an unreadable transport_ca, an empty trust store. gdsgate config check --role proxy reports it under proxy.public_join. A certificate that does not match fails each registration with UNAVAILABLE; the Proxy logs the handshake error.

# Proxy beside Authority; the registration certificate is issued for the gateway's name.
[endpoints]
authority_enroll = "127.0.0.1:50050"

[proxy]
public_join = true

[enroll]
endpoint      = "https://127.0.0.1:50050"
transport_sni = "gate.example.com"
# transport_ca = "/etc/gdsgate/registration-ca.pem"   # private CA only

Registration is rate-limited per call by public_join_rate_per_ip. A relayed renewal counts against max_public_connections while it is open.

With public_join = true, Bootstrap.GetBundle publishes a node-enrollment registration capability for connector and agent-host, authenticated by a join token. Its empty registration endpoint means the address used to read the bundle, with that connection's verified TLS trust. The registration server validates the token and requested role.

A separate registration URL uses its own CA and TLS name. An empty CA means the host's trust store; an empty TLS name means the registration URL's host. Consumers verify metadata and registration TLS before sending a token. A server without the capability requires explicit registration parameters.

proxy_join serves the same two steps on a separate address. Both can be enabled. On the tunnelled route Authority advertises this address for renewal, so set it in Authority's configuration too; see what Authority tells a joining node.

Limit on this listener Key Default
registrations per source address public_join_rate_per_ip, public_join_burst 5 per second, burst 10
concurrent relayed renewals public_renewal_max_connections 64
concurrent relayed administrative connections ([admin].transport = "fronted") public_admin_max_connections 8

A tunneled connector uses the renewal lane only: its control-plane calls ride the reverse tunnel.

Relayed lanes and their ceilings

The relayed branch serves two server names with separate limits:

Lane Server name Limit
node renewal gdsgate.internal (and other internal names) public_renewal_max_connections
administration with a control-plane certificate auth.gdsgate.internal public_admin_max_connections

Both count against max_public_connections; set it above the sum. Authority terminates TLS on this branch and checks the certificate and its role. A connector certificate is refused by the administrative service.

This path is used by [admin].transport = "fronted". The address in an administrative audit row is the gateway's address, so policy rules on the operator's source network do not work on this path. A direct connection to Authority records the operator's address.

[proxy]
# Share :443 with the connector WebSocket fallback (corporate egress).
single_port = "0.0.0.0:443"

# Browser WebAuthn origin with a browser-trusted certificate.
public_origin  = "https://gate.example.com"
web_addr       = "0.0.0.0:8443"
web_cert_file  = "/etc/gdsgate/web-cert.pem"
web_key_file   = "/etc/gdsgate/web-key.pem"

# Admission control (all default to 0 = unlimited).
max_sessions                 = 2000
max_streams_per_tunnel       = 256
max_public_connections       = 5000
internal_accept_rate_per_ip  = 20
internal_accept_burst        = 40

# Unauthenticated RPCs per source address.
public_anonymous_rate_per_ip = 10
public_anonymous_burst       = 30

# Publicly trusted certificate for the client-facing listener.
public_cert_file = "/etc/gdsgate/public-fullchain.pem"
public_key_file  = "/etc/gdsgate/public-key.pem"

# Join relay: a rollout larger than the built-in 64 concurrent joins.
join_max_connections    = 512
join_accept_rate_per_ip = 50
join_accept_burst       = 100

# Node join on the client-facing port. The limits shown are the defaults.
public_join             = true
public_join_rate_per_ip = 5
public_join_burst       = 10

# Relayed lane limits (defaults).
public_renewal_max_connections = 64
public_admin_max_connections   = 8

# Delay before sessions of a dropped connector are reported (default).
abandoned_session_grace_secs = 300

gdsgate does not obtain public_cert_file, web_cert_file or webapps_cert_file; an ACME client or a corporate CA writes them. The listeners re-read the files within a second of a change, with no restart. New connections get the new certificate; established ones continue. To apply at once: SIGHUP on Linux, sc control gdsgate-proxy paramchange on Windows. See Renewing a listener certificate.

web_addr also serves the key set for web lane identity assertions at GET /.well-known/gdsgate/jwks.json: the JwtSigning keys of Authority's trust bundle, with Cache-Control: public, max-age=300. See Telling the application who came.

Sessions a departing connector left open

A connector that is killed, crashes, or loses its machine leaves its open sessions without a close event in the audit chain and with a partial recording on its disk.

When a connector tunnel ends, the Proxy records the sessions it held. After abandoned_session_grace_secs it sends the list to Authority, which checks the audit chain for each session:

Audit chain for the session Written
a close nothing
an open and no close SES-0104 session.abandoned, one row per session
no open nothing

A report with at least one SES-0104 row is preceded by SYS-0101 connector.tunnel_lost with the node, how the tunnel ended, and the session count. See Sessions a connector never accounted for.

A connector that restarts within the grace period closes its partial recordings and writes its own close events. Authority applies the same grace period by its own clock. 0 turns the watch off.

Pending reports are kept in memory, one per tunnel. When the queue is full the oldest is dropped; a Proxy restart loses them. Metrics:

Metric Counts
gdsgate_abandoned_session_notices_total tunnel losses that left sessions
gdsgate_abandoned_sessions_total session ids, by Authority's result
gdsgate_abandoned_session_notices_dropped_total reports dropped from a full queue; these sessions are never reported

See Metrics and health. Live sessions are not affected.

The clientless browser entry

A user on a device without gdsgate opens https://<application>.<webapps_zone>, signs in at the identity provider, and reaches the application. The other ways into the web lane (a local listener, an address inside a delegated program's cage, a name the resident client publishes) need gdsgate on the device. Set webapps_zone, webapps_addr, webapps_cert_file and webapps_key_file together.

Each application is served on one subdomain label of the zone (grafana.<webapps_zone>). grafana.team.<webapps_zone> is not served. The label gdsgate is reserved for sign-in: register https://gdsgate.<webapps_zone>/callback as the redirect URI at the identity provider. An application catalogued under the name gdsgate is unreachable here; the log says so once.

After sign-in, Authority issues a ticket for the WebApp through the usual connect decision. The ticket is a certificate that carries a SHA-256 digest of the session cookie in place of a public key, is pinned to the browser's address (pinned_ip), and is valid for webapps_session_ttl_secs (at most 12 hours). The connector checks it like any other certificate. Authority accepts ticket requests only from the Proxy's mTLS identity. The id_token is used for one ticket and discarded, so each further application in the same browser session goes through the identity provider again.

Behind an SNI router, a load balancer or nginx, the observed address is the front's. It binds the browser session and the ticket pin and becomes context.src_ip. List the front's addresses in webapps_proxy_protocol_from and configure the front to send a PROXY protocol header, version 1 or 2 (proxy_protocol on in an nginx stream block). With the list set, connections from other addresses or without a header are closed without an answer.

The session is held in Proxy memory. The cookie (Secure, HttpOnly, SameSite=Lax, scoped to webapps_zone) is an opaque handle. Sign-out removes the session and closes its relays. At the ticket's not_after the session ends and open relays are closed. Each request is still authorized at the connector.

Limitation Detail
Not in ListSessions / KillSession Browser sessions are not listed or killed through these calls.
Per-instance sessions A Proxy restart signs every browser out. With several Proxy instances, a session works only on the instance that opened it.
No single_port single_port together with this entry is refused at start-up.

Peer admission budgets

The internal mTLS peer listener has separate ceilings from the public listener. max_peer_connections bounds sockets (128), max_peer_requests bounds concurrent discovery and admission RPCs (32), and max_peer_sessions bounds pending and active peer streams (128). A value of 0 selects these built-in ceilings; it does not disable the peer budget. Each process enforces its ingress and egress stream budgets independently. Discovery examines at most 16 candidates with a three-second deadline. Excess admission is refused before backend connect; cancellation, timeout and disconnect return the capacity. Established streams use bounded queues and the configured per-session byte throttle.