Skip to content
Talk to our solutions team

Gateway security

The gateway is the public edge. Everything on this page is about one question: which decisions does a client get to influence, and which does the deployment make.

A domainmap binds hosts to a CEPT tuple. The tenancy headers a backend reads are stamped by the gateway from that mapping, so they describe where the request arrived, not what it asked for.

domainmaps:
- name: acme-prod
domains: [app.acme.com]
customer: acme
environment: prod
product: forge
tenant: main
  1. The gateway derives the routing domain from the request’s own Host.
  2. The mapper resolves that domain and sets X-Customer, X-Product, X-Env and X-Tenant.
  3. A domain in no domainmap is answered 400 rather than served against a default.

Because the tuple comes from the mapping, a client cannot select a tenant by sending a header. Any tenancy headers arriving from outside are replaced by the mapper’s own.

X-Forwarded-* is a claim about a request’s origin made by whatever sent it, so the gateway believes it only from a peer it was told to believe.

PeerBehaviour
Listed in the entrypoint’s trustedipsX-Forwarded-Host becomes the routing domain, and X-Real-Ip is carried through
Anything elseThe origin host and client IP are captured internally, then the whole X-Forwarded-* set is deleted before the request goes further

The capture step is why the gateway still knows the true origin behind a trusted proxy that rewrites Host, and the deletion step is why an untrusted client cannot use the same headers to pick a routing domain.

If the gateway sits behind a load balancer or another proxy, that peer’s address belongs in trustedips. Keep the list to the addresses that actually front the gateway.

An operator sometimes needs to act against another tenant. That is X-Target-Tenant, and it is constrained twice:

  • It is honoured only on a domainmap explicitly marked superadmin. On any other domain the header is refused and the refusal is logged.
  • When it is honoured, the gateway stamps X-SuperAdmin: true so the backend can tell an operator-initiated request from an ordinary one and audit it as such.

Mark as few domainmaps superadmin as the operation needs, and treat those hostnames as administrative surface.

The kisai-auth middleware verifies bearer tokens locally, against the public key in jwt_public_key. The identity service issues tokens and is the login redirect target; it is not consulted per request, so a request is not served on the strength of an unverified token when that service is unreachable.

endpoints:
- name: api
path: { prefix: /api }
backend: api-svc
middlewares: [kisai-auth, customer-domain-mapper]
  • A request without a valid token is redirected to loginurl when one is configured, and answered 403 otherwise.
  • publicPaths regexes exempt open paths. Keep them narrow and anchored: a pattern such as /health matches more than a pattern such as ^/health$.
  • Rotate jwt_public_key with the issuer.

Authentication at the edge is a filter, not a substitute for the entity access rules each service applies. A request that passes here still meets the callee’s own authorization.

Header values accept ${…} templates resolved per request. Two properties matter for safety:

  • CRLF in a rendered value is stripped, so a value carrying a newline cannot split a header or inject another one. This is the defence against request smuggling through a templated header.
  • A missing variable renders empty, and an empty header value strips the header. A template that does not resolve removes the header rather than forwarding a literal ${…} for a backend to misread.

Values are populated by middlewares that run earlier in the chain, so place customer-domain-mapper and kisai-auth before the header block that reads ${tenant.*} or ${auth.*}.

Staying close to upstream Traefik means edge hardening arrives with the version bump. The current build denies requests with an opaque request target, and stops h2c upgrade headers and request trailers being forwarded to a backend.

zerotrust: true turns on two things at once:

  • The internal listener requires a client certificate.
  • The gateway uses mTLS on its own leg to each backend.

It is independent of public TLS. A route can terminate a public certificate whether or not zerotrust is on, and a deployment can run zerotrust internally while serving the internet normally.

The TLS option names default and kisaitlsinternal are reserved, and passthrough refuses writes to them, so the hardened defaults that generated routes rely on stay as configured. Defining new option names is the supported way to add a stricter or a more permissive profile for a specific route.

SurfacePosture
Metrics and profilingIts own listener, bound to loopback by default. Never put it on the data plane
Router dashboardExpose it as an ordinary route behind kisai-auth. The unauthenticated dashboard mode is refused at boot
Backend TLS verificationThe global switch is protected. Scope any exception to one named transport
Router pluginsInterpreted Go inside the gateway process, so the passthrough path for them is closed

TCP and UDP entrypoints carry bytes, not requests, so there is no request for the middleware chain to act on and no per-request tenant attribution. Give an L4 port one tenant, by binding a listener per tenant. See Entrypoints.

  • Every public hostname is in a domainmap, and unmapped hosts are expected to answer 400.
  • trustedips lists only the proxies that actually front the gateway.
  • superadmin is set on the fewest domainmaps possible, and those hostnames are treated as admin surface.
  • jwt_public_key matches the current issuer key, and rotation is planned.
  • publicPaths patterns are anchored and reviewed.
  • The metrics listener is on loopback or an internal interface.
  • The dashboard, if exposed at all, is behind kisai-auth.
  • TLS is issued and renewing, and the ACME storage path is writable and backed up.
  • zerotrust matches the mesh posture you intend.
  • Any passthrough block has been reviewed as configuration a person wrote by hand.
  • The running binary’s Traefik version is the one you expect: gateway.svc version.