Skip to content

ProxyHost

Terminates TLS for one or more domains and reverse-proxies to an upstream.

Field Type Required Notes
domains []string yes One or more hostnames served by this host.
upstream Upstream one of Default backend (single). Mutually exclusive with upstreamGroupRef.
upstreamGroupRef string one of Name of an UpstreamGroup for health-checked failover across several backends. Mutually exclusive with upstream.
robotsNoIndex bool no Emit X-Robots-Tag: noindex, nofollow (HTTP and HTTPS) to discourage search-engine indexing. A headers middleware that sets X-Robots-Tag explicitly still wins.
timeouts HostTimeouts no Per-host upstream timeout overrides (below).
tls TLSSettings no Certificate + TLS behaviour.
dns DNSSyncPolicy no Opt this host's domains into DNS record management (below).
middlewares []string no Host-wide middleware names, applied top-down.
accessLists []string no Host-wide access-list names.
auth AuthMiddleware no Gate this host through an identity provider without a Middleware object. Same shape and same gate as a type: auth middleware. See Inline auth and rate limit.
rateLimit RateLimitMiddleware no Throttle this host without a Middleware object. Same shape and same limiter as a type: rate-limit middleware. See Inline auth and rate limit.
locations []Location no Path-scoped overrides (below).
maintenance bool no Take this host out of service without removing it: gpm answers every request with a 503 maintenance page and never dials the upstream. The host keeps its domains, certificate and DNS records. Operator-owned: Ingress discovery preserves it. See Maintenance mode.
compression Compression no Gzip response compression (below). Zero value (enabled: false) is today's behaviour: no compression.
errorPages ErrorPagesConfig no Overrides settings.errorPages for this host's own gpm-generated errors. Unset uses the settings-level pages, if any. Same shape and same validation as the settings block: dir, inline, interceptUpstream.
errorPages.dir string no Directory of <status>.html templates plus an optional default.html, relative to the cert store. Same rules as settings.errorPages.dir.
errorPages.inline map[string]string no Status code (or "default") mapped to html/template source, for a page kept in config rather than a mounted directory.
errorPages.interceptUpstream []int no Status codes for which this host also replaces the upstream's own error body.
securityHeaders map[string]string | map[string]{value,scope} no Merges over settings.securityHeaders per key for this host (replacing the settings value and its scope for a header it names). Unset uses the settings-level default unchanged.
stripResponseHeaders []string no Response headers removed from what this host's upstream sends, in addition to settings.stripResponseHeaders (union, not replacement: a host cannot opt out of a fleet-level strip).
trustedProxies []string (nullable) no The L7 proxies whose X-Forwarded-For is believed when deriving this host's client IP. Three states: omitted or null inherits settings.trustedProxies; [] trusts nobody for this host whatever the fleet default is; a non-empty list replaces the fleet list (never extends it). See Per-host override and Client IP and the three trust tiers.

Upstream: a single backend target. Used by proxyHost.upstream, location.upstream and each member of an UpstreamGroup.

Key Type Default Required Description
scheme string none yes http or https.
host string none yes Backend hostname or IP.
port int none yes 1-65535.
path string "" no Base path prefixed to every forwarded request: /api turns a client /v1/x into /api/v1/x. Must be absolute, must not contain ./.. segments, \, ;, a query string or a fragment. Applied last, after any location prefix strip and any rewrite middleware.
hostHeader string "" no Host header sent upstream. "" keeps the client's Host (today's behaviour); upstream sends the upstream's own host:port; anything else is sent literally and must be a hostname, optionally host:port.
upstream:
  scheme: http
  host: 10.0.0.40
  port: 8080
  path: /api              # backend serves the app under /api
  hostHeader: upstream    # backend keys its vhost off its own address

On an UpstreamGroup member, path and hostHeader are per member, applied by the failover transport on the attempt that actually reaches that member. healthCheck.path is used verbatim and is not prefixed with path, so write the full probe path there.

TLSSettings: certificateRef (a Certificate name; see Which certificate a host serves; it does not select the certificate for an L7 host), forceSSL (redirect HTTP->HTTPS), hsts (enabled, maxAge in seconds, default one year when unset, includeSubdomains, preload), minTLSVersion ("1.2" default | "1.3"), clientAuth (mTLS, below). When hsts.enabled is set, the proxy listeners emit Strict-Transport-Security on HTTPS responses for the host (never over plain HTTP).

ClientAuth (tls.clientAuth) opts the host into mTLS: client certificates are verified at the TLS handshake against a ClientCA. It is editable from the host editor's TLS section ("Client certificates (mTLS)"): a toggle, a Client CA picker and the mode. Turning it on is greyed out with the reason until its preconditions hold (forceSSL on, and at least one enabled ClientCA to point at), while turning it off is always possible, so a host whose stored combination is already invalid is never trapped. Turning forceSSL off under a live mTLS host is refused rather than silently dropping the certificate requirement. A caRef the CA list does not contain is shown flagged and kept as-is on save rather than the picker quietly retargeting the trust anchor, and if the CA list cannot be loaded at all the page says so and leaves caRef/mode untouched instead of guessing.

Field Type Required Notes
caRef string yes ClientCA name. Must exist and be enabled; the host also requires forceSSL: true.
mode string no require (default) rejects the handshake without a valid certificate; optional verifies a presented certificate but lets certless requests through (mTLS as a fallback beside SSO).
identityHeaders ClientCertHeaders no Forward the verified certificate's identity upstream. Unset = forward nothing.

ClientCertHeaders (tls.clientAuth.identityHeaders):

Field Type Default Notes
subjectHeader string X-Client-Cert-Subject Header carrying the certificate subject in RFC 2253 form (CN=ops,O=Corp). Must be a valid header name and may not be an X-Forwarded-* header.
san bool false Send X-Client-Cert-SAN: the subject alternative names (DNS, email, IP, URI), comma-separated.
serial bool false Send X-Client-Cert-Serial: the serial number in lower-case hex.
fingerprint bool false Send X-Client-Cert-Fingerprint: the SHA-256 digest of the DER certificate, lower-case hex.

These headers ride the existing identity-trust model: all four default names are in the baseline identity denylist, so they are stripped from every request whose peer is not a proxy the host trusts (whether or not the host enables passthrough), and a custom subjectHeader is added to that host's own strip set. gpm sets them after the strip, only from a certificate the handshake actually verified; in optional mode a certless request reaches the upstream with no identity headers at all.

minTLSVersion is a per-host floor selected by SNI at handshake time. The edge already negotiates TLS 1.2 or 1.3 per client (1.2 is the default floor); set "1.3" only on hosts where every client supports it (drops 1.2: old smart TVs / embedded clients / legacy scripts may then fail to connect). Leave it unset for public hosts to keep the widest client compatibility.

Fleet default Per-host override
settings.tls.minVersion sets the floor for every host that leaves minTLSVersion unset, plus stream hosts and an unknown SNI. minTLSVersion here wins in either direction: "1.3" under the default fleet floor, and "1.2" under a "1.3" fleet floor.

DNSSyncPolicy (dns): lanDirect publishes each of the host's domains as a local CNAME on the LAN resolver (Pi-hole), so internal clients reach the edge directly instead of hairpinning through the WAN address; publicCname publishes them in the authoritative public zone (Cloudflare). Both default false: nothing is published unless asked for, and an opted-out host omits the dns key from its API responses entirely rather than returning an empty object. The backends themselves are configured once, in settings.dnsSync; a policy flag with its backend disabled publishes nothing, and the UI says so inline while leaving the toggle usable: setting the flag before the backend exists is legitimate staging (the host is the declaration; the syncer publishes once it is wired), so it is not refused.

HostTimeouts (timeouts): connectSeconds caps establishing the TCP/TLS connection to the upstream; readSeconds caps time awaiting the upstream's response headers (time-to-first-byte). Both are whole seconds (0-3600); 0/unset means no override. A host with any override uses its own cloned transport (its own connection pool), so a custom timeout never affects another host's keep-alive reuse; hosts without an override share the default pooled transport. readSeconds bounds only time-to-first-byte, so it does not cut off a slow streaming / SSE / websocket body once headers have arrived.

Location (locations[]) is a path-scoped override. Matching is longest-prefix; the request path is forwarded unchanged unless stripPrefix is set. Everything a location adds is appended to the host-wide chain rather than replacing it, so a location is always at least as restrictive as its host.

Field Type Default Required Description
path string none yes Path prefix this override applies to.
upstream Upstream host's backend no Single backend for this path. Mutually exclusive with upstreamGroupRef; with neither, the host's backend (including an upstream group) is inherited.
upstreamGroupRef string host's backend no UpstreamGroup name for this path. Mutually exclusive with upstream.
stripPrefix bool false no Remove this location's matched prefix before forwarding: /app/foo reaches the backend as /foo, and /app (or /app/) as /. A root location (path: /) strips nothing.
middlewares []string none no Middleware names appended to the host's.
accessLists []string none no Access-list names appended to the host's.
auth AuthMiddleware none no Inline auth gate for this path, stacked on top of the host's (below).
rateLimit RateLimitMiddleware none no Inline rate limit for this path, stacked on top of the host's (below).

stripPrefix is applied inside the whole security chain: rate limiting, the access list, the bouncer, auth and guards all still evaluate the original client path (the path the location itself matched on), and only the request that reaches the backend is shortened. The query string is forwarded untouched.

# grafana.example.com/metrics/... served by a Grafana that expects to be at /
name: tools
domains: [tools.example.com]
upstream: { scheme: http, host: 10.0.0.40, port: 80 }
locations:
  - path: /metrics
    stripPrefix: true
    upstream: { scheme: http, host: 10.0.0.41, port: 3000 }

Compression (compression) gzip-compresses eligible response bodies from this host's upstream, using only the standard library's compress/gzip (via a pooled sync.Pool of writers).

Field Type Default Notes
enabled bool false Off means byte-for-byte today's behaviour: no compression.
minBytes int 1024 Smallest response body gzip bothers with. The body is buffered up to this size before the compress/pass-through decision is made, so a response that never reaches it is sent uncompressed.
types []string text/html, text/plain, text/css, text/csv, application/json, application/javascript, text/javascript, application/xml, text/xml, image/svg+xml Response Content-Types (media type only; charset etc. ignored) eligible for compression.

Compression honours the client's Accept-Encoding and is skipped outright when: the request is HEAD; the client sent no Accept-Encoding: gzip; the upstream already set Content-Encoding (never double-encoded); the response Content-Type doesn't match types; the body stays under minBytes; the status is 204, 304, or 101 (protocol switch); or the response is text/event-stream or otherwise starts streaming (the handler flushes before the compress decision is made, this is also what keeps a WebSocket upgrade, which is hijacked rather than written through, untouched). A compressed response gets Content-Encoding: gzip, Vary: Accept-Encoding, and has Content-Length stripped (the compressed size isn't known up front). BREACH: compressing a response whose size depends on attacker-controlled input reflected alongside a secret (e.g. a CSRF token) can leak that secret through the compressed size; that trade-off is why compression is opt-in per host rather than default-on; hosts serving that shape of response should leave it off.

name: app
domains: [app.example.com]
upstream: {scheme: http, host: backend, port: 8080}
tls: {certificateRef: wildcard, forceSSL: true}
dns: {lanDirect: true, publicCname: true}
middlewares: [require-sso]
compression: {enabled: true}
locations:
  - path: /metrics
    accessLists: [internal-only]      # /metrics also requires the internal CIDR

Inline auth and rate limit (auth / rateLimit)

A proxy host or location may carry an auth or rateLimit block directly, with no Middleware object and no reference to attach. The block has the same shape, the same validation rules and compiles to the same proxy-listener handler as the middleware of that kind, so behaviour, metrics and error pages are identical.

  • Direct is for a handful of hosts. Gating one host by SSO is one block on that host, not three objects across three pages.
  • Middleware objects remain the reuse path. One gate shared by a fleet is still one Middleware every host references.
  • Both may be set. There is no mutual exclusivity: a host can run an inline block and reference a middleware of the same kind, and every gate must pass.
  • Inline runs first. An inline block sits at the same chain position as its middleware kind, just outside the referenced ones of that kind.
  • A location stacks on its host. The host's inline block still applies on the location's path; the location's own is added to it.
Key Type Default Required Description
auth AuthMiddleware none no Identical to a type: auth middleware's auth spec: identityProvider, mode, requiredRoles, allowFrom, clientCertRoles, basic. Every mode works inline, basic included, so auth.basic.realm and auth.basic.users[].username/.passwordHash are valid here, see BasicAuthSpec. A plaintext password field on a PUT is hashed server-side on a host or location exactly as on a middleware. See AuthMiddleware for every field rule.
rateLimit RateLimitMiddleware none no Identical to a type: rate-limit middleware's rateLimit spec: requestsPerSecond or requests+window, burst, allowFrom, blockFor. See RateLimitMiddleware.

Validation is the middleware's, verbatim: identityProvider must resolve to an existing IdentityProvider (a dangling name is a load-time error, and the proxy listeners fail closed with 503 if one ever reaches it), allowFrom is refused in oidc and forward-auth mode, clientCertRoles is client-cert only, and a rate limit must set exactly one of the two rate forms.

The five-host shape, with no middleware objects at all:

# config/proxy-hosts/grafana.yaml
name: grafana
domains: [grafana.example.com]
upstream: {scheme: http, host: 192.0.2.20, port: 3000}
tls: {certificateRef: wildcard, forceSSL: true}
auth:
  identityProvider: authentik      # the only other object you need
  mode: forward-auth
  requiredRoles: [admin]
rateLimit:
  requests: 100
  window: 1m
  allowFrom: [10.0.0.0/8]          # the LAN is never throttled
locations:
  - path: /api
    rateLimit: {requestsPerSecond: 5}   # tighter on the API path only

Deprecated fields

Still parsed so existing YAML keeps loading. They are gone from the UI, the OpenAPI schema and the tables above, and set nothing at runtime.

Field Status Reason
websocketsUpgrade Deprecated, ignored WebSocket upgrades always work: the proxy forwards the Upgrade handshake, and the compression and header-strip layers both special-case a 101. The field is retained only so existing YAML keeps loading.
tls.http2 Deprecated, ignored HTTP/2 is always offered: the listener advertises h2,http/1.1 via ALPN unconditionally, so there is nothing per-host to switch.