Wingsmith
Plan

Security

The operator's security model: principals, gateway listeners, trust boundaries, enrolment tokens, permissions, local mode and known limitations.

This page is the security model of a Wingsmith install: who can do what, where trust is decided, and what is not protected yet. To report a vulnerability, see Security reporting.

Wingsmith is pre-release and built for one organisation operating its own clusters. Several of the limitations below are known and not yet fixed. Plan around them.

Trust boundaries

ComponentRunsTrusts
Plane apiCentral, behind your ingressCallers presenting a valid admin session, tenant credential or gateway service token
Plane hubCentral, behind your ingressOperators presenting a per-cluster enrolment token
Plane reactorCentral, no listenerThe database
PostgreSQLCentralThe plane processes only. It holds all declared state and every credential hash
GatewayCentral, in the request pathDepends on the listener. See Gateway listeners
Operator, agentOne each per clusterThe plane and gateway they are configured to dial

Clusters only dial out. Neither the plane nor the gateway opens a connection into a cluster or holds a kubeconfig. See Networking.

Principals

The plane resolves every request to one of four principal kinds. There are no roles or scopes beyond these.

PrincipalCredentialCan do
AdminPassword, then a session token (hgrs1.) and refresh token (hgrr1.)Everything
TenantTenant console token (hgrt1.)Read-only view of what that tenant is served. Only GET /v1/serving/*
OperatorPer-cluster enrolment token (hgro1.)Open a hub session and write observed state for that one cluster
GatewayHANGAR_GATEWAY_SERVICE_TOKENGET /v1/gateway/tenants, GET /v1/endpoints/resolved, POST /v1/clusters/tunnel-auth. No admin rights

Admin

The single human operator of the installation, using the console or hgr. Wingsmith assumes one operator, or a small team sharing the role. There are no per-user accounts, no SSO and no audit trail by person.

POST /v1/auth/login returns a short-lived session token and a server-side refresh token.

SettingDefault
HANGAR_SESSION_TTL_SECONDS900 (15 minutes)
HANGAR_REFRESH_TTL_SECONDS604800 (7 days), absolute

Outside HANGAR_APP_ENV=local, admin auth is always on, and the plane refuses to start with HANGAR_ADMIN_AUTH=disabled or without the admin password hash and the session signing secret. A misconfigured deployment fails closed.

Generate the password hash with the plane image you built:

docker run --rm -it registry.example.com/hangar/plane:<tag> python -m app.auth hash

Set it on the api, with a random signing secret:

env:
  - name: HANGAR_ADMIN_PASSWORD_HASH
    value: "<bcrypt hash>"
  - name: HANGAR_SESSION_SIGNING_SECRET
    valueFrom:
      secretKeyRef:
        name: hangar-plane
        key: session-signing-secret

Operator

Each enrolment token is minted per cluster and binds the session to that cluster. An operator can only write observed state for the cluster its token names. Operator tokens are accepted by the hub only and refused on the HTTP API. There is no shared fleet-wide operator token.

Gateway

Set HANGAR_GATEWAY_SERVICE_TOKEN to the same random value on the plane api and on the gateway. Outside local, an unset token means no gateway principal resolves at all, and the gateway exits at boot without it.

Gateway listeners

The gateway decides trust by which listener accepted the connection. Every listener states its zone; there is no default.

ListenerAuthenticationExposure
Internal, :8080None. Reaching it is the proof of trustClusterIP only, with a NetworkPolicy
Public, HANGAR_GATEWAY_PUBLIC_LISTENTenant from the Host header, plus that tenant’s API key when it declares auth: api_keyHTTPS ingress on a wildcard host. Off unless set
Tunnel, :8443Cluster enrolment token, verified with the planeTCP passthrough, TLS terminated by the gateway

A caller on the internal listener can reach every enabled tenant’s models, including tenants that require an API key on the public listener. Without a NetworkPolicy, any pod in the cluster can do this. Your own edge authenticates and bills its users, then calls this listener. Policy example: Networking.

On the public listener:

  • A tenant declaring auth: none gets 401 unless HANGAR_GATEWAY_PUBLIC_ALLOW_NO_AUTH=true.
  • Only tenants with ingress: public and endpoints with visibility: public are served. A private endpoint, the plane’s default, is served on the internal listener alone.
  • Another tenant’s model reads as 404, not 403.

On the tunnel listener, a verified token is reused for about a minute and re-verified about once a minute for open tunnels. A token you rotate or revoke therefore loses its tunnels in a little over two minutes. If the plane is unreachable, open tunnels stay up, and a dial whose token was verified within the last 15 minutes is still accepted. Restarting the gateway replicas drops the cache at once.

Enrolment tokens

One token serves three uses:

  1. The operator authenticates to the hub with it.
  2. The agent authenticates to the gateway tunnel listener with it.
  3. The metrics agent authenticates to remote_write with it, unless you give the chart other metrics credentials.

Whoever reads the token from any of those pods can open a hub session and write observed state for that cluster, establish tunnels for it, and write metrics under its label. Protect the Secret:

  • Keep it in a Kubernetes Secret and restrict get on that Secret to the operator and agent ServiceAccounts.
  • Rotate it with POST /v1/clusters/{ref}/enrolment-token. This is an admin action and the old token is revoked immediately. Update the cluster’s Secret and restart the operator and agent pods.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: read-enrolment-token
  namespace: hangar-system
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["<enrolment token secret name>"]
    verbs: ["get"]

Enrolment itself is on Enrol a cluster.

Operator permissions

The operator’s RBAC is cut to what the install allows.

  • Read-only cluster: no write access outside the release namespace. Which operations the plane may request comes from cluster.allowedOperations, which is empty by default.
  • Managed cluster: the plane decides what runs in the workload namespace, including engine and benchmark images. Anyone with plane admin can run code there.

Narrow that with allowlists in the chart values:

operator:
  images:
    allowedPrefixes:
      - registry.example.com/approved/
  benchmark:
    images:
      - registry.example.com/harness/aiperf:<tag>
      - registry.example.com/harness/evalscope:<tag>

allowedPrefixes applies to every engine, frontend and prefetch image, matched at a path, tag or digest boundary, and the operator refuses anything else. Include the per-model frontend’s registry too, or no model renders. benchmark.images is matched exactly; empty keeps the operator’s defaults. Without an allowlist, treat plane admin as equivalent to write access to the workload namespace. Chart settings: Chart values.

Request protections

Failed sign-ins

Failed sign-ins are budgeted per caller address and account, so one caller spending its budget locks out itself, not the operator. Behind a proxy, set HANGAR_TRUSTED_PROXIES to the proxy addresses or CIDRs, or every caller shares the proxy’s budget. Addresses in that list have their X-Forwarded-For believed when charging failures.

Cross-site checks

  • A POST, PUT, PATCH or DELETE whose Origin is neither the plane’s own nor in HANGAR_ALLOWED_ORIGINS is refused with 403.
  • A write whose body is not JSON is refused with 415.
  • Every response carries Content-Security-Policy: frame-ancestors 'none' and X-Frame-Options: DENY.

Set HANGAR_ALLOWED_ORIGINS only if the console is served from a different origin than the api.

What the gateway logs

The gateway keeps no request or response bodies in its logs. The access line carries the request id, method, path (never the query string), status, outcome, timings, token counts, tenant and a 12-character key-hash prefix. It never carries a body, a header value or a model answer.

A discarded upstream error is logged as a summary: status, the engine’s type and code, body length and whether it was truncated. The full body is logged only under HANGAR_GATEWAY_LOG_UPSTREAM_BODIES, which must stay off in production. An engine’s error quotes the request it refused.

Local mode

HANGAR_APP_ENV=local is for a laptop. Never expose it to a network. compose.yml in the repository runs this way.

  • A request with no credential resolves to the admin principal.
  • The API binds 127.0.0.1, and answers only loopback, HANGAR_PUBLIC_HOST or a host in HANGAR_ALLOWED_ORIGINS; other Host values get 403.
  • The password hash and session signing secret are not required.
  • The gateway service token defaults to a fixed, public value.

HANGAR_APP_ENV defaults to production in the plane image, so a plane started with no environment fails closed.

Known limitations