Skip to content

Security model

Private reporting

Report suspected vulnerabilities to ff0x@this-is-fine.io . Send a minimal reproduction, the affected version, the impact and sanitized logs. Do not put credentials, exploit details or private cluster configuration in public issues, and do not email live API keys, preauth keys, kubeconfigs, private keys or complete Secret objects — ask for an encrypted channel if sensitive evidence is needed.

The latest minor release line and the preceding one receive security fixes; use the latest patch of your line. Disclosure is coordinated with the reporter, normally within 90 days. Remediation targets are 7 days for critical and 30 days for high-severity findings (best effort, not an SLA). Advisories are published in the release notes and as Radicle issues labelled security.

Trust model

Actor or boundaryControlsLimit
Kubernetes administratorOperator installation, RBAC, CRDs, bootstrap Secret, base policyCan read every credential and replace workloads; the operator cannot defend against its own administrator.
Headscale administratorNode identities, grants, route approvals, API keysAPI keys are bearer credentials with full control-server access, not per-controller capabilities.
Tailnet writerControl endpoints, bootstrap Secret reference, policy inputsAdministrative. Do not delegate cluster-scoped Tailnet writes to tenants.
TailnetEgress / TailnetIngress writerAgent identity, tag, backends, allowFromNot a hostile-tenant boundary: custom tags and cross-namespace backends require trusted writers; allowFrom is an allowlist, not isolation.
TailnetGateway writerPersistent kernel identity, image pins, routes and retained resourcesAdministrative. Explicit activation creates a NET_ADMIN workload; never delegate this cluster-scoped CR to tenants.
Agent podEphemeral tsnet identity, reusable preauth key, no Kubernetes clientContainer compromise exposes the key and that identity’s network access until the key expires.
Operator podCredentials, workloads, status, Headscale policyNeeds broad namespace access by design; a compromised operator is a cluster and tailnet incident.
Tailnet peerAccess per Headscale policy and per-CR identityThe operator authenticates nodes, not application users.
HTTP backendIn mode: http the agent terminates frontend TLS; backend.tls enables verified HTTPS upstreamWithout backend.tls, the cluster hop is plain HTTP. With it, frontend and backend TLS remain separate sessions.
RegistryRelease images and charts are signed with the CI’s fixed Cosign keyA valid signature says who built it, not that it is safe to run; verify the immutable digest you deploy.

Backend HTTPS uses only the selected namespace-local CA bundle, with chain, hostname and validity verification. There is no system-root fallback or insecure mode. Protect writes to referenced CA ConfigMaps as trust-policy changes. After a projected CA change, new requests use the new transport and old idle connections close. Established streams continue; deleting a CA is not immediate revocation of existing connections. Service references can still resolve to administrator-controlled endpoints or ExternalName destinations; they are not an SSRF isolation boundary.

Credentials

  • The bootstrap API key stays in the operator-namespace Secret you create; the operator only reads it and mints its own working key, rotated at half-life after the replacement has been verified.
  • Agent preauth keys are reusable until expiry. Protect Secret read access and restrict CR writers.
  • Image-pull Secrets are referenced by name in the agent’s namespace; credentials are never copied.
  • Status, Events, metrics and logs never contain whole credentials; Headscale error bodies are withheld because they can echo them. Tests cover known fixture credentials and malformed inputs.
  • If a credential leaks, revoke or rotate it at the issuer. Deleting a log line does not revoke a token.

Workloads, RBAC and network

Operator and agent pods run as non-root numeric users with RuntimeDefault seccomp, no privilege escalation, all capabilities dropped and a read-only root filesystem. Agents keep a writable /state and do not mount a ServiceAccount token.

The kernel-mode TailnetGateway is different: Tailscale uses UID 0, only NET_ADMIN, and /dev/net/tun; CoreDNS uses UID 65532 and only NET_BIND_SERVICE. Its projected API token can get/update/patch only its pre-created State Secret. It cannot create state or list Secrets. The operator reads private state keys in memory to verify public identity. Auth/completion receipt ConfigMaps are privileged trust anchors: protect their create/update/delete rights as strictly as Secrets. Replacing all retained evidence can forge provenance; immutability alone does not prevent this. The operator may delete the Gateway’s StatefulSet, PDB, ServiceAccount, Role and RoleBinding and patch tailnetgateways for its finalizer. Deletion happens only with deletionPolicy: Delete, only for objects whose UID the receipt recorded or that carry the operator’s gateway label, and every call is UID-preconditioned; the CR patch writes metadata only. The Secret/ConfigMap delete rights already existed.

The operator holds cluster-wide permissions on the workloads and Secrets it manages for tenant agents, plus cert-manager Certificate get/create/patch/delete for issuerRef ingresses. Kubernetes RBAC cannot limit create by resourceNames, so the ClusterRole/Role split in the chart is an audit aid, not reduced privilege. The exact default grants are listed in the RBAC reference ; task rbac-check fails when the chart drifts from it.

The operator NetworkPolicy is opt-in because API-server and Headscale CIDRs are site-specific. Generated agent NetworkPolicies restrict inbound traffic to consumerNamespaces and the operator namespace (TCP 9080 for /status, /targets, /metrics); they do not constrain outbound traffic, which userspace tsnet needs for control, DERP and UDP. Do not expose port 9080 or the operator’s 8080 outside the cluster. Leader election serialises reconciliation; it is not a fence against in-flight writes from a paused replica.

Supply chain

Every release publishes one multi-arch image (linux/amd64, linux/arm64) and the Helm chart, each signed with the CI broker’s fixed Cosign key, together with an SPDX SBOM and a SLSA v1 provenance attestation per platform. Images are scanned for HIGH/CRITICAL findings before a versioned release is accepted; govulncheck runs on every build.

The public key is deploy/cosign.pub . Signatures are not uploaded to a transparency log, so verification passes --insecure-ignore-tlog. Verify the digest you are about to deploy, not a tag:

cosign verify --key deploy/cosign.pub --insecure-ignore-tlog docker.io/ff0x/tailnet-operator@sha256:<digest>
cosign verify-attestation --key deploy/cosign.pub --insecure-ignore-tlog --type spdxjson \
  docker.io/ff0x/tailnet-operator@sha256:<digest>
cosign verify --key deploy/cosign.pub --insecure-ignore-tlog \
  oci.this-is-fine.io/charts/tailnet-operator:<version>

The production image is built from scratch and contains only the two static binaries.

Radicle release record

Each vX.Y.Z tag also has a Radicle release (collaborative object dev.radicle.artifact), created and signed by a repository delegate and listed under Releases in Radicle Explorer. It registers deploy/cosign.pub as an artifact (BLAKE3 CID) and carries the signed image and chart digests as metadata (image, chart, verify), so the key, the release commit and the published digests are bound together independently of the registry:

rad clone rad:z46fGxuhyvDV57psA8s6Q6PdHJrsX && cd tailnet-operator
rad-artifact show v0.18.0 --pretty          # creator must be a delegate; shows image/chart digests
test "$(rad-artifact cid deploy/cosign.pub)" = "<cid from show>"

Then run the verify command from the metadata against the digest you deploy.

Source: docs/explanation/security-model.md at v0.18.0 — found a mistake? Tell us.