Proxy configuration¶
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 the client-facing listener presents, for clients that reach the gateway by domain name. With it, clients need no cluster trust anchor, and gdsgate connect writes a configuration without one. Re-read on change. Unset: the listener presents the node's transport identity. Requires public_key_file. |
public_key_file |
string | unset | PEM private key for public_cert_file. |
public_join |
bool | false |
Serve node join and renewal on the public client-facing listener. See public_join. |
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 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:
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 needs only https://proxy.<domain> 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 registering node checks the certificate this listener presents
(public_cert_file) for the name or address it dials. Without
public_cert_file the listener presents the enrolled identity, which carries
neither, and registration is refused; gdsgate doctor and
gdsgate config check --role proxy warn under proxy.public_join.
| 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. Registration and clients use the gateway's public name, or
enroll.gdsgate.internal when a node registers at an IP address. A connection
with no server name is served as a client. Both destinations come from the
Proxy configuration.
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.
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.