Skip to content

Security model

Security is layered and opt-in: with nothing configured, Cornus is a pass-through suitable for local development. Each layer below is enabled by configuration and fails closed once enabled — malformed policy is a hard startup error, an empty identity is denied where identity is required, and a misconfigured verifier rejects rather than passes. This page explains the model; the security and authentication guide covers the setup and hardening recipes.

Authentication

Authentication is a middleware seam around the whole HTTP surface, wired inside the telemetry handler so rejected requests are still traced. With no verifier configured it is a pass-through. When enabled, /healthz and /readyz stay open; every other route requires bearer auth, except GET/HEAD under /v2/* when CORNUS_REGISTRY_ANONYMOUS_PULL is set. Verifier configuration is environment-driven:

VariableMethod
CORNUS_AUTH_TOKENopaque full-access bearer token, constant-time compared
CORNUS_JWT_HS256_SECRETHS256 JWT
CORNUS_JWT_PUBLIC_KEYRS256/ES256 JWT from a PEM public key
CORNUS_JWT_JWKS_FILE / _URLJWKS with kid selection and rotation (asymmetric only)
CORNUS_JWT_ISSUER / _AUDIENCEoptional registered-claim checks
CORNUS_CARETAKER_TOKENscoped static token accepted only on the caretaker attach endpoint
CORNUS_AUTH_KEYSTORE / CORNUS_AUTHORIZED_KEYSSSH-public-key proof exchanged for a short-lived scoped JWT

JWT verification binds each key to its allowed algorithm set — alg: none, algorithm confusion, and public-key-as-HMAC are all rejected — and stores the caller identity on the request context for authorization. Client-token issuance (cornus token issue) is an operator/CLI action, not a general HTTP minting endpoint. Separately, an installation-wide signing key exists only while client auth is enabled: Cornus uses it to mint short-lived registry:push and registry:pull credentials for its own build/deploy dataplane. Those credentials are accepted before operator verifiers, marked internal in request context, and issued only for known co-located registry hosts. Kubernetes stores a 12-hour pull credential in the ownerless namespace cornus-registry-pull Secret and a supervised loop refreshes it every four hours; host backends mint per pull. Kubernetes caretaker sidecars get a scoped credential (valid for the caretaker attach endpoint only), sourced from a Kubernetes Secret, rather than carrying a full-access token into every pod.

The registry additionally speaks docker login: on /v2/* (only), HTTP Basic is accepted with the same credentials as the password — the static token or a JWT — and the username ignored (docker login -u token -p $CORNUS_TOKEN), feeding the identical verifier chain. The registry's 401 challenge is Basic realm="cornus" rather than Bearer: Cornus has no token service, so a Bearer challenge would send docker to a nonexistent token realm, while Basic makes stock docker/podman retry with the stored login. Non-registry routes still challenge with Bearer, and a caretaker-scoped credential framed as Basic is still rejected on the registry.

SSH-public-key client authentication is opt-in. When configured, the server offers purpose-bound SSH signing challenges for enrollment and session minting; SHA-1 RSA signatures are rejected. Enrollment rotates a 0600 one-time secret and stores public keys only. Session proof returns a short-lived scoped JWT with the enrolled name as its identity. Writable enrollment is for one replica; multi-replica deployments use declarative CORNUS_AUTHORIZED_KEYS and CORNUS_AUTH_KEYSTORE=none.

TLS and mTLS identity

TLS serving is built into cornus serve via --tls-cert/--tls-key, served through reloading callbacks that re-read the files when their modification time advances — so an external rotator (cert-manager, Vault, SPIFFE) can renew a mounted cert in place with no restart.

mTLS client-cert identity is an additional authentication method: a verified client certificate is a full credential whose CommonName is the caller identity, taking precedence over a bearer token. The hub uses this same authenticated identity, so hub reach/register policy keys on a credential the spoke cannot forge.

Authorization

Per-identity API authorization sits on top of authentication as a configure-to-enforce matrix: CORNUS_API_POLICY maps identity to allowed actions (build, deploy, exec, push, pull, gc). Unset means allow-all; once configured, a caller must be listed for the requested action, and an empty identity is denied — so enforcement effectively requires a JWT sub or an mTLS CommonName. Pure reads (deploy status, logs, registry pull) stay open by default, governed by authentication rather than per-identity authorization. Two refinements:

  • exec is its own action. Exec/attach is allowed if the policy allows exec or deploy — deploy implies exec, so the action's value is exec-only identities that can shell into a running workload without being able to apply or delete one.
  • Registry pull authorization is opt-in. When any rule explicitly mentions the pull action (a "*" wildcard does not count), registry GET/HEAD require it. An explicit pull policy wins over CORNUS_REGISTRY_ANONYMOUS_PULL — an anonymous caller carries no identity and is denied — and the server warns at startup when both are configured.

Separately, the deploy backends enforce a workload privilege policy as defense in depth: the host backends reject Privileged and host bind sources unless CORNUS_ALLOW_PRIVILEGED / CORNUS_ALLOW_BIND_SOURCES opt them in, and the kubernetes backend default-denies user-requested privileged workloads while allowing the Cornus-owned injected sidecars that genuinely need privilege for kernel 9P mounts or network redirection.

Trust boundaries

Several boundaries documented with their subsystems are worth collecting in one place:

  • The remote-build export is read-only and confined. A remote builder gets 9P access to exactly the context, dockerfile, and named-context directories — no .., no symlink escape, no writes, and .dockerignore is enforced before bytes leave the caller. See the build engine.
  • Session ids are capabilities. A deploy-attach session id is unguessable and travels inside authenticated streams, never in URLs; the mount relay publishes only its digest.
  • Egress policy is re-evaluated at every hop. The caretaker, the server, and the client each check the routing policy, so a compromised pod cannot upgrade its own routing; sessionless egress is honored only for the operator-gated gateway route. See client-side egress.
  • Hub policy keys on verified identity. Under mTLS a spoke's identity comes from the client certificate, not its own declaration. See the hub.
  • The in-pod Docker endpoint requires an explicit operator grant. The docker caretaker role is enabled only when a dedicated client-scoped token Secret is configured, because it grants the workload deploy-engine access. See the Docker endpoint.

Released under the Apache-2.0 License.