Skip to content
skyl

Security and deployment

The gateway proxies paid APIs. A misconfiguration spends your money.

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_TOKENS adds labelled rotation tokens as label:token,.... The label appears in logs and metrics; the token never does. That is what makes rotation possible without downtime.
  • /healthz, /readyz and /metrics sit 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_RAW defaults to false, unlike the library where Response.Raw is always populated — the same data, but here it crosses a trust boundary.

The middleware stack#

  1. 1.RequestIDA correlation ID per request, echoed back as X-Request-Id.
  2. 2.RecovererA panicking handler returns 500 rather than killing the process.
  3. 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. 4.CORSOnly when SKYL_ALLOWED_ORIGINS is set.
  5. 5.Bearer authenticationConstant-time comparison via subtle.ConstantTimeCompare. Skipped only for /healthz, /readyz and /metrics.
  6. 6.ThrottleOnly when SKYL_MAX_CONCURRENT is set.

Denial of service#

Bound concurrency

SKYL_MAX_CONCURRENT=64
SKYL_MAX_CONCURRENT=64

An 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=3
SKYL_REQUEST_TIMEOUT=120s
SKYL_ATTEMPT_TIMEOUT=60s
SKYL_MAX_RETRIES=3

Applied 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.

Edit this page on GitHub