Skip to content
Talk to our solutions team

Gateway Operations

Local HTTP-only loop:

Terminal window
gateway.svc config generate quickstart > .gateway.yaml
gateway.svc config generate spa-http -d config/
mkdir -p public && echo '<h1>hello</h1>' > public/index.html
gateway.svc -f .gateway.yaml -L debug
curl http://localhost:8443/

Production: start from the bootstrap profile (zoned entrypoints:, port 443, zerotrust: true, cert flags), keep routing in config.router.dir or on config.svc. Before the first start and before every deploy, print what the configuration publishes and confirm the external surface is what you meant:

Terminal window
gateway.svc config exposure -f .gateway.yaml

It applies every load-time check and exits non-zero when the gateway would refuse the configuration. See Exposure.

Run flags: -f/--config, -p/--port, -l/--log, -L/--loglevel, --sid, --datacenter, --cluster, --zerotrust, --svccert/--svckey/--rootca, --jwt_public_key, --config_url/--config_apikey, --servicediscovery_url/--servicediscovery_apikey.

The platform supervisor manages it as service gateway with default state up.

Routing config reloads without restart, from either source:

  • Local dir: recursive filesystem watch; any write/create/remove/rename triggers a debounced reload.
  • Remote: config.svc pushes over SSE.

Reload semantics:

  • The update is overlaid onto the last-known-good config, so a routing-only push can never wipe listener/boot keys.
  • On reload failure the previous config is re-applied; if that fails too, the process exits: the supervisor restarts it clean.
  • In-flight requests finish on the old configuration.
  • Static router config cannot change at runtime: entrypoints, ACME resolvers, and provider settings apply at boot only: changing them requires a restart. Everything in the routing plane (domainmaps, backends, endpoints, routes, staticpaths, LB profiles, passthrough dynamic config) is hot.

Backend server lists additionally update live via the service-discovery watcher (default 5s poll) without any config push.

  • Metrics listener (kisaimetrics, default 127.0.0.1:8088): Prometheus /metrics and Go pprof /debug/pprof/*. There is no health endpoint on the data plane: probe a route or the metrics listener.
  • Logs: log: stdout or fluentd host:port. loglevel controls the gateway; router.loglevel independently dials the embedded router’s verbosity.
  • Access logs / tracking: an anonymous-tracker cookie and access-log events publish asynchronously (fire-and-forget) to configured endpoints; disabled on internal traffic; non-fatal when unset.
  • Dashboard: expose the router dashboard only through a normal, kisai-auth-gated route (dashboard example), the insecure API mode is a protected key and rejected at boot.
  • Exposure: after every load the log lists what each zone publishes (exposure zone=…). The meili.gateway.exposure gauge carries the same table, and meili.gateway.refused_requests{route, endpoint, reason} counts requests refused by path patterns, sources or the cross-origin write check.
Terminal window
gateway.svc version
gateway 2.0.0 …
traefik (vendored fork): v3.7.15 (351a5ab1e884)

The second line is the upstream release the embedded router was built from, with its pinned commit. Check it when triaging anything that looks like proxy behaviour rather than routing behaviour, and when an upstream advisory names a version range.

The expensive failure in a gateway is the quiet one: the config parses, the process starts, and a route is simply not there. An advisory pass at boot and on every reload names the shapes that load without routing anything.

WarningWhat it means
A domainmap, endpoint or staticpath is bound by no routeIt was declared and nothing references it
One local staticpath is bound by two routesThe file server starts once per route, so the two contend for the same port
Two routes claim the same entrypoint, host and pathThe router picks by priority, and the loser never sees a request
An endpoint publishes the whole of a sensitive backend beyond the internal zoneIt has exposure: both or external but no allow list
A route lists a sensitive endpoint, and its entrypoint and domainmap declare no zoneThe gateway cannot tell whether the route is public: declare zone
No gatewayrole under zerotrustBackends that read the caller from the client certificate see the gateway, not the client
An endpoint’s CORS allows *Its cross-origin write check is off

These are warnings rather than errors. Each shape is legal and some are deliberate, so the reload carries on; read them as a list of things to confirm you meant.

Observability does not gate the data plane

Section titled “Observability does not gate the data plane”

A metrics listener that fails to start, or that is slow to stop during a reload, is logged and the gateway keeps routing. A taken metrics port or a bad kisaimetrics: block does not stop the boot, and a slow shutdown of that listener does not fail a reload.

SymptomCause / action
Every request on a domain → 400Domain missing from all domainmaps: or domains: written as a scalar instead of a list (caught with a targeted error at load)
Nothing matches behind another proxyThe fronting proxy sends its own Host (e.g. localhost); add its IP to the entrypoint’s trustedips so X-Forwarded-Host is honored
Boot fails parsing routing filesThey’re in config.path instead of config.router.dir
Reload rejectedDuplicate names / unresolved references / protected passthrough paths / unknown certresolver. The error names the offender; last-known-good stays live
Browser shows a self-signed certACME issuance failed: check the certresolver is referenced by the route, ACME logs, :80 reachability for HTTP-01, and that acme.json is writable
Boot error “internal.port required”Legacy listener mode needs internal.port, or switch to an explicit entrypoints: list
Boot or reload refused: “route … is external … lists endpoint …, whose exposure is internal”A sensitive backend (vault, horus, config, meta) or an internal endpoint is on a public route. Remove it from that route, or give the endpoint exposure: both and an allow list. See Exposure
Boot refused: “zone internal but binds every interface”Bind the internal entrypoint to its private or loopback address
A path under an endpoint’s prefix answers 404It is not in the endpoint’s allow list, matches its deny list, or uses a method not listed
A browser POST answers 403 “cross-origin request refused”The page’s origin is not the endpoint’s own and not trusted: add it to the CORS origins or csrf.trustedorigins
Requests to a pinned backend answer 502The server’s certificate does not carry the backend’s identity URI
TCP/UDP routes skip tenant headersL4 routes bypass the HTTP middleware chain by design. No CEPT attribution
Every request on a route answers 503 with rate_limit_unavailableThe rate-limit store is unreachable and that profile is set to on_store_failure: deny. See Rate limiting