Gateway Operations
Bootstrap from zero
Section titled “Bootstrap from zero”Local HTTP-only loop:
gateway.svc config generate quickstart > .gateway.yamlgateway.svc config generate spa-http -d config/mkdir -p public && echo '<h1>hello</h1>' > public/index.htmlgateway.svc -f .gateway.yaml -L debugcurl 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:
gateway.svc config exposure -f .gateway.yamlIt 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.
Hot reload
Section titled “Hot reload”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.
Observability
Section titled “Observability”- Metrics listener (
kisaimetrics, default127.0.0.1:8088): Prometheus/metricsand Go pprof/debug/pprof/*. There is no health endpoint on the data plane: probe a route or the metrics listener. - Logs:
log: stdoutor fluentdhost:port.loglevelcontrols the gateway;router.loglevelindependently 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 (dashboardexample), 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=…). Themeili.gateway.exposuregauge carries the same table, andmeili.gateway.refused_requests{route, endpoint, reason}counts requests refused by path patterns,sourcesor the cross-origin write check.
Knowing what you are running
Section titled “Knowing what you are running”gateway.svc versiongateway 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.
Routing warnings at boot and reload
Section titled “Routing warnings at boot and reload”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.
| Warning | What it means |
|---|---|
| A domainmap, endpoint or staticpath is bound by no route | It was declared and nothing references it |
| One local staticpath is bound by two routes | The file server starts once per route, so the two contend for the same port |
| Two routes claim the same entrypoint, host and path | The router picks by priority, and the loser never sees a request |
| An endpoint publishes the whole of a sensitive backend beyond the internal zone | It has exposure: both or external but no allow list |
| A route lists a sensitive endpoint, and its entrypoint and domainmap declare no zone | The gateway cannot tell whether the route is public: declare zone |
No gatewayrole under zerotrust | Backends 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.
Failure modes
Section titled “Failure modes”| Symptom | Cause / action |
|---|---|
| Every request on a domain → 400 | Domain 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 proxy | The 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 files | They’re in config.path instead of config.router.dir |
| Reload rejected | Duplicate names / unresolved references / protected passthrough paths / unknown certresolver. The error names the offender; last-known-good stays live |
| Browser shows a self-signed cert | ACME 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 404 | It 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 502 | The server’s certificate does not carry the backend’s identity URI |
| TCP/UDP routes skip tenant headers | L4 routes bypass the HTTP middleware chain by design. No CEPT attribution |
Every request on a route answers 503 with rate_limit_unavailable | The rate-limit store is unreachable and that profile is set to on_store_failure: deny. See Rate limiting |