The failure mode of a misconfigured gateway is someone else spending your money. Its defaults are chosen accordingly — several things you might expect to be optional are not.
Authentication is mandatory#
- Tokens are compared with
subtle.ConstantTimeCompare, so a wrong token is indistinguishable from a missing one by timing. SKYL_AUTH_TOKENSadds labelled rotation tokens aslabel:token,.... The label appears in logs and metrics; the token never does. That is what makes rotation possible without downtime./healthz,/readyzand/metricssit outside the boundary, because the things that call them cannot hold a token.
What is never logged or returned#
- Provider keys are never logged and never appear in an error body.
- Upstream error bodies are not forwarded verbatim — they can echo request
content back to a caller who should not see it. Errors are classified and
re-emitted with a
kind. - Request and response bodies are never logged. Structured logging records method, path, status, duration, request ID and the caller label only.
SKYL_INCLUDE_RAWdefaults to false, unlike the library whereResponse.Rawis always populated — the same data, but here it crosses a trust boundary.
The middleware stack#
- 1.RequestIDA correlation ID per request, echoed back as X-Request-Id.
- 2.RecovererA panicking handler returns 500 rather than killing the process.
- 3.Structured logging (log/slog)Method, path, status, duration and request ID only — never headers, never bodies. The caller label from SKYL_AUTH_TOKENS is included; the token itself is not.
- 4.CORSOnly when SKYL_ALLOWED_ORIGINS is set.
- 5.Bearer authenticationConstant-time comparison via subtle.ConstantTimeCompare. Skipped only for /healthz, /readyz and /metrics.
- 6.ThrottleOnly when SKYL_MAX_CONCURRENT is set.
Denial of service#
Bound concurrency
SKYL_MAX_CONCURRENT=64SKYL_MAX_CONCURRENT=64An unbounded gateway converts a traffic spike into a provider rate-limit incident — and then into a 429 for every caller, including the ones that would have succeeded.
Bound request duration
SKYL_REQUEST_TIMEOUT=120s
SKYL_ATTEMPT_TIMEOUT=60s
SKYL_MAX_RETRIES=3SKYL_REQUEST_TIMEOUT=120s
SKYL_ATTEMPT_TIMEOUT=60s
SKYL_MAX_RETRIES=3Applied inside each handler rather than at the router, so a streaming response is not severed mid-generation.
CORS#
Deployment#
Run it on a private network. It is an internal service. If it must face the internet, put a reverse proxy with TLS and rate limiting in front.
The published image is gcr.io/distroless/static:nonroot: statically linked, no
shell, no package manager, configuration entirely from the environment, logs to
stdout.
A hardened deployment
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
env:
- name: SKYL_AUTH_TOKEN
valueFrom: { secretKeyRef: { name: skyl, key: auth-token } }
- name: ANTHROPIC_API_KEY
valueFrom: { secretKeyRef: { name: skyl, key: anthropic } }
- name: SKYL_MAX_CONCURRENT
value: "64"
- name: SKYL_METRICS
value: "true"securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
env:
- name: SKYL_AUTH_TOKEN
valueFrom: { secretKeyRef: { name: skyl, key: auth-token } }
- name: ANTHROPIC_API_KEY
valueFrom: { secretKeyRef: { name: skyl, key: anthropic } }
- name: SKYL_MAX_CONCURRENT
value: "64"
- name: SKYL_METRICS
value: "true"Graceful shutdown#
On SIGTERM the server drains: /readyz starts failing so a load balancer
pulls the instance, in-flight requests finish, then the process exits. Probe
/readyz for readiness — probing /healthz instead means traffic keeps
arriving at a draining instance, and every request in flight is a paid
generation.
What an authenticated caller can do#
Worth stating plainly, because it defines your blast radius. A caller with a valid token can:
- Send any prompt to any registered provider, at your expense.
- Choose any model string, including expensive ones.
- Read the list of registered providers and their models.
They cannot read your provider credentials, reach a provider you have not registered, or see another caller's traffic.
So a gateway token is roughly as sensitive as a provider key with a spending
cap. Treat rotation as routine — which is what SKYL_AUTH_TOKENS is for.
Troubleshooting#
Can I disable auth for local development?
No. Set SKYL_AUTH_TOKEN=local-dev-token instead — one line, and it keeps the
production and development paths identical.
A caller is spending too much
Give each caller its own labelled token via SKYL_AUTH_TOKENS, then attribute
usage by the label in your metrics. The gateway does not enforce per-caller
quotas; put that in a proxy in front if you need it.
I need the client IP for rate limiting
Read it from a header your own trusted proxy sets, and do the rate limiting there. The gateway deliberately does not trust forwarding headers.