Security model
How Kwerft protects the console, acts on Kubernetes as each user, isolates projects, stores secrets and records shells, for anyone evaluating it.
This page summarizes Kwerft’s security model for people evaluating it: what protects the first setup, how the console reaches Kubernetes, how projects are isolated, where secrets live, and what is still open. Kwerft is pre-beta; the known gaps are listed at the end.
First setup
- There are no default passwords. After the install, the console is unusable until someone presents the setup token that the installer wrote to
/etc/kwerft/setup-tokenon the server (root only, mode 0600). Presenting it proves control of the machine. - The token is single-use, valid for 24 hours and only until the owner account exists. The cluster only ever holds its SHA-256 hash; the token itself never leaves the server’s disk. Attempts are rate-limited.
- The Kubernetes API listens on the private network only. The host firewall lets in SSH, HTTP, HTTPS and WireGuard from the internet, nothing else.
Identity and sign-in
- Passwords are hashed with argon2id and must be at least 12 characters.
- Second factors: passkeys (also for passwordless sign-in), authenticator apps (TOTP) and single-use recovery codes, which exist only while a passkey or authenticator app does. Adding or removing a factor asks for the password again.
- An owner can require two-factor sign-in. Members without a factor are then held to the enrolment pages after their password; nobody is locked out, and nobody can remove their last factor.
- Single sign-on uses OpenID Connect with the code flow, PKCE, state and nonce. Accounts are matched by verified email once, then by the provider’s subject. Only members and invited people sign in, unless an owner or admin enables auto-join for allowed email domains. A member’s own second factor is still asked for after the provider.
- Sessions are
__Host-cookies, HttpOnly, ending after 7 days idle or 30 days at most. Every write must come from the console’s own origin. Passwords, second factors, setup tokens and invites are rate-limited. - Client addresses (for rate limits, the audit log and the firewall’s lock-out check) are taken from the
X-Real-Ipheader only when the connection comes from a node, where the ingress runs; otherwise from the connection itself.
Acting as the user: impersonation
The console never uses its own permissions for what a user does. Every write, and every read of a single object, goes to Kubernetes as that user: user kwerft:<email> in the group kwerft:role:<role>. Kubernetes RBAC is the final gate, so a bug in the console cannot give a user more than their role allows.
- The console’s service account may impersonate only these users and groups, never
system:masters. - List views and monitoring (projects, apps, jobs, volumes, domains, metrics, log search, alerts, recordings) read from a cache, confined in the console to the projects the user reaches.
- Kwerft’s test suite runs against a real Kubernetes API server and proves that a developer in one project cannot read another project’s pods, logs, secrets, metrics, alerts, builds or traffic, through Kubernetes or through any console list, search or stream, also with project-limited API tokens.
Roles and projects
Owners and admins manage everything. Developers and viewers get, cluster-wide, only read access to cluster-level objects such as project names, Git connections, alert rules and channels. Everything inside a project comes from role bindings that Kwerft writes into that project’s namespace: for every member when the project is open to the team, or only for the listed members when it is limited to them. Role changes apply at once, also to open shells and log streams; removing a member ends their sessions. See Access for the matrix.
API tokens and kubectl
- API tokens (
kwft_…) are 256-bit, shown once and stored as SHA-256 hashes. Each has a role cap (never above the user’s current role), an optional project restriction and an expiry (90 days by default, a year at most). They cannot manage accounts, tokens, members or sign-in settings, and cannot open shells. Failed token attempts block the client address after 20 failures in 15 minutes. - Downloaded kubeconfigs point at the console’s proxy
/k8s/, which impersonates the token’s user. It refuses Secrets, exec, attach, port-forward, proxying, protocol upgrades and--as, and audits every write.
Network defaults
- Each project is a namespace with a Pod Security level (
baselineby default) and, by default, a default-deny Cilium policy: other projects cannot reach it. - Each app accepts traffic only from the ingress for its public hostnames, the platform, and the apps it names. Outbound internet access is HTTPS-only by default.
- Traffic between nodes is encrypted with WireGuard.
- Plain HTTP only answers with a redirect to HTTPS (and Let’s Encrypt’s HTTP-01 challenges).
- Internal services without authentication of their own are fenced in by network policy: the log store accepts only its collector and the console, Hubble’s flow relay only the console and the nodes, the build registry only build pods, the console and the nodes.
- The host firewall is a separate nftables table that protects HTTP(S), SSH from the private network and the cluster’s own traffic from being closed in the console. Changes that remove access roll back after 60 seconds unless confirmed, even with the console down. See Network.
Secrets at rest
- k3s runs with secrets encryption on: Kubernetes Secrets are encrypted in etcd.
- Authenticator-app secrets in the console’s database are encrypted with AES-GCM under a data key kept in the Secret
kwerft-data-key. Owners rotate it under Settings › Data key with their password; Kwerft then re-encrypts everything and retires the old key. Back the key up together with the database. - Credentials Kwerft stores for you (the Hetzner DNS and Cloud API tokens, Git connection credentials, notification channel secrets, the single sign-on client secret) are write-only: owners and admins can replace them, nobody can read them back through the console, and no role reads Kubernetes Secrets through it.
Builds
Git builds run as rootless BuildKit jobs in their own namespace, the only one with relaxed Pod Security (rootless BuildKit needs it), under a resource quota. Build pods may reach the internet and the registry, but no private addresses, the cloud metadata service or anything else in the cluster. Pull requests from forks are never built. Apps run their built image pinned by digest.
Shells and the audit log
- Shells need the developer role or higher, and only reach containers in project namespaces, never the platform’s. Each shell needs a valid session and the console’s own origin, closes after 15 minutes idle and 1 hour at most, and a user can have three at once.
- Every shell is recorded as an asciicast (terminal output only: keystrokes are not recorded, since unechoed input is mostly passwords). Recordings are stored with mode 0600, kept 90 days, and only owners and admins can play or download them; each playback is audited.
- The audit log records sign-ins, membership and role changes, every change to apps and jobs, token use and refusals, and shell sessions, with the client address. It is append-only from the console.
Remote clusters and joining nodes
- Remote clusters run an agent that dials out to the console over TLS, verifying the console’s certificate, and authenticates with a per-cluster token stored only as a hash. The agent carries requests only to its own cluster’s API server, so remote API servers are never exposed. Rotating the token disconnects the old agent.
- Join commands hold a signed, expiring join token, not a Kubernetes credential. Worker nodes receive a short-lived k3s bootstrap token; control-plane join commands, which hand out the cluster’s server token, are for owners only.
Supply chain
Releases are built in CI as multi-arch images and a Helm chart on GHCR, with an SPDX SBOM next to the image; the published install script comes with SHA-256 checksums. Image signing (cosign) is prepared but opt-in per release. The installer verifies the Helm download’s checksum and pins every component’s version at the top of the script.
Known gaps
Kwerft is pre-beta. These are known and not yet solved:
- No secret store yet. App environment variables are stored in the App resource, readable by everyone with access to the project. Secrets referenced by apps have to be created as root on the server. A project-scoped secret store with write-only values is planned before the public beta.
- No backups yet. Backups to Hetzner Object Storage are planned before the public beta; until then, back up the data key and your volumes yourself.
- The build registry has no authentication. A build in one project could push images into another project’s repository; running apps are protected by digest pinning. Per-project registry credentials are planned.
- Some names are visible to every role: project names, alert rule names and notification channel names.
- The console runs as a single replica and is not highly available yet.
- A remote Hetzner Cloud cluster created from the console receives the console’s Cloud API token, since Hetzner tokens cannot be scoped. Use separate Hetzner projects for separate trust boundaries.
- The license is not decided yet. It will be before the public beta; all dependencies are Apache-2.0 or MIT.