Skip to content

Security model

What the PaaSbox portal holds for your clusters, how each part checks the others, and what an attacker could do with a leaked database, a leaked key or a running portal under their control. The reasons behind the design are on How it works; this page lists the facts.

How PaaSbox Clusters worksAs built, and run end to end in a lab on 2026-10-10; PaaSbox Clusters is not bookable yet. The PaaSbox portal calls the Hetzner API with your token to create the cluster in your own Hetzner project: a server, a private network and a firewall. On the server, pbx-agent, a program and not an AI model, listens on no port: about every 30 seconds it calls the portal over outbound HTTPS, reports the node’s state and receives the desired state and at most one operation. The portal never opens a connection to your server. Snapshots of the cluster’s state go from the node straight to an S3 bucket you own and never pass through the portal.PaaSbox portalhub · provisioneryour token, encryptedYour Hetzner projectYour serverpbx-agentk3s · Garden Linuxlistens on no portprivate networkfirewallHetzner API,your tokenHTTPS, outboundevery 30 sYour S3 bucketsnapshots, straightfrom the nodeThe agent opens every connection: outbound HTTPS,about every 30 s. The portal never connects tothe server.The portal calls the Hetzner API with your token tocreate and delete what belongs to the cluster.Snapshots go from the node straight to your bucket,never through the portal.
ConnectionOpened byWhat it carries
pbx-agent → the portal, HTTPS, about every 30 secondspbx-agent; it listens on no portThe node’s state; the desired state and at most one operation back
The portal → the Hetzner APIThe provisioner, the only part that uses your Hetzner tokenCreating and deleting the cluster’s resources in your project
k3s on the node → your bucketk3sSnapshots; they never pass through the portal
You or your agent → the Kubernetes API, port 6443YouOnly from the address ranges you chose
Anyone → ports 80 and 443AnyoneYour ingress

The portal never connects to your server. The firewall opens nothing else.

HoldsHow
Your Hetzner token, which opens your whole project, read and writeEncrypted
The k3s server token, in every version a kept snapshot still needsEncrypted
The k3s agent tokenEncrypted
The Hetzner token used inside the cluster (a separate one, or a copy of the first)Encrypted
The S3 keys of your snapshot bucket, unless you hold them yourselfEncrypted
The secret options of add-ons, such as DNS tokensEncrypted
A sealed kubeconfig, until it is collectedEncrypted, for at most 5 minutes; the portal cannot open the seal
pbx-agent’s keysPublic keys only
Enrollment tokensHashes only
Your API keys In progressHashes only; a key is shown once, when it is made
The audit log of API and MCP calls In progressThe arguments with every secret taken out

The encrypted fields are encrypted in the portal’s database with keys kept outside it. The secrets the portal sends to pbx-agent are sealed so that only that cluster’s pbx-agent can open them.

Never holds
Admin kubeconfigs or their tokensThey are made on the node and sealed to the key of whoever asked
/etc/rancher/k3s/k3s.yaml, the admin file on the serverThe portal never reads it
pbx-agent’s private keysThey are made on the node and never leave it
The contents of your snapshotsThey go from the node to your bucket
Your workload data or your workloads’ Secrets

The server token and the S3 keys together could open a snapshot, and a snapshot contains every Secret in your cluster. That is the price of a restore without you at the keyboard. If you hold the bucket’s keys yourself, the portal never sees them and cannot read your snapshots; a restore then works only while the cluster’s API is up, and a restore onto a new server would need your keys. In progress Keys you hold yourself have not run on a real cluster yet. See Set up backups.

MechanismWhat it does
EnrollmentA new server’s user data holds a single-use enrollment token, valid for 30 minutes and bound to the cluster and the node’s name. The portal checks through the Hetzner API that the server carries the cluster’s label and the node’s name, and checks the hash of the image the node actually booted against the release. Only then does it hand out the node’s configuration.
Keys made on the nodeAt its first start pbx-agent creates a signing key and an encryption key. The private halves never leave the server. The portal rotates them every 90 days.
Signed requestsEvery request after the enrollment is signed over the method, the path, a timestamp and a hash of the body. The portal refuses a clock skew over 60 seconds and a signature it saw in the last 120 seconds.
Sealed secretsEvery secret the portal sends is encrypted to the node’s key. pbx-agent refuses a document in which a secret arrives in clear.
Nothing lasting in user dataUser data stays readable from inside the server for its whole life. It holds only pbx-agent’s download address and checksum, the portal’s address, the cluster’s ID, the node’s name and the enrollment token, which is useless once used or expired. The node image blocks pods from the metadata address.
Signed releasesA release is signed with a key I keep offline: never on the portal, never in CI. pbx-agent carries two public keys, compiled in, and refuses a release neither signed. It updates itself only to a version named in such a release, and goes back to the previous version if the new one does not reach the portal within 10 minutes. In progress That return has not fired on a real server yet.
Sealed kubeconfigsA kubeconfig is made on the node and encrypted to a key that only the asker holds: a key your browser makes for that one request, or an agent’s own key. The portal passes the sealed copy on once and cannot read it.
Secret add-on optionsGo into Kubernetes Secrets, which k3s encrypts at rest, never into other objects.

In progress The REST API and MCP tools are code, not run against a real cluster yet. Guardrails for agents says what each limit does and does not stop.

LimitWhat it does
Team keysA team admin makes a key for one team, with an expiry of 30 days, 90 days, a year or none, and can revoke it at once. At most 25 active keys per team.
Scopesk3s:read, k3s:write, k3s:access, k3s:destructive. Every call needs exactly one; a key holds only what its agent needs.
The person behind the keyThe key acts as the person who made it, who must still be an active member of the team. A scope narrows what that person may do; it never widens it.
Destructive callsCreating, deleting, detaching and restoring a cluster need k3s:destructive, a team admin as the key’s owner, and the cluster’s name typed as confirmation.
KubeconfigsSealed to the agent’s own key; a kubeconfig asked for through a key can be collected only by that key.
Rate limits120 calls per minute per key, 20 of them calls that change something.
Audit logEvery MCP call, and every REST call that changes something or hands out a kubeconfig, refused calls included: who, through which key, what, with which arguments (secrets taken out), and how it ended. Team admins read it in the portal.

Ciphertexts, public keys, and the hashes of enrollment tokens and API keys. The keys that decrypt the ciphertexts are not in the database. The audit log’s arguments, which carry no secrets.

An attacker in control of the running portal couldWhy
Create, change and delete anything in your Hetzner project, not only the cluster’s resourcesThe portal holds your Hetzner token. This is why your clusters belong in a project of their own.
Ask pbx-agent for an admin kubeconfig, and so act as cluster-admin on every cluster it managesIssuing kubeconfigs is one of pbx-agent’s operations
Queue a restore of an older snapshot, which resets the cluster’s stateRestore is one of pbx-agent’s operations
Change the settings the portal manages: add-ons and their options from the cluster’s release, the snapshot schedule, and, if the portal holds the S3 keys, the snapshot targetpbx-agent applies the desired state the portal sends
Detach or delete the clusterDelete is the provisioner’s job
Open your snapshotsWith the server token and portal-held S3 keys
It could notWhy
Run an arbitrary command on your serverpbx-agent has a fixed list of operations and no channel for commands
Install a release I did not sign, or a version of pbx-agent not named in such a release, on a running nodeThe release key is not on the portal
Read /etc/rancher/k3s/k3s.yaml, your workloads’ data, or snapshots in a bucket whose keys you hold yourselfThe portal never receives them

A server created while the portal is compromised is another matter: it downloads the pbx-agent named in its user data, checked against a checksum the portal writes there.

CaseOutcomeWhat prevents worse
An enrollment token is stolenA rogue machine tries to enrollThe token works once, for 30 minutes, for one node name; the server’s ID and labels are checked through the Hetzner API; the booted image is checked
A pod reads the metadata addressIt gets no answer: the node image blocks pods from itEven with the block gone, it would find only a used or expired token
A request is replayed or interceptedReuse of a request, disclosure of a secretTLS, signed requests with a timestamp, the replay cache, sealed secret fields
An API key leaks In progressThe holder acts within the key’s scopes, as its ownerScopes, the owner’s team role, typed names and a team admin for destructive calls, rate limits, the audit log; revoke the key on the API keys page
Your account is taken overThe attacker uses the portal as youTyped confirmations, mails to the team’s admins, short kubeconfig lifetimes
Misuse by meOperations queued by meEvery operation shows who requested it, staff included; restores and kubeconfigs are mailed to your admins
PaaSbox disappearsThe portal and its DNS zone go awayThe cluster keeps running; you have root, the server token on the server’s disk and the snapshots in your bucket; see Run without PaaSbox
LimitWhat it does
No command channelAs above
Offline-signed releasesTested in a lab, then on my own clusters, then on the early channel, before they reach stable. Two failures among the first five upgrades to a release pause it.
Mail to your team’s adminsWhenever a restore or a kubeconfig is requested, with who asked for it, staff marked as staff. Upgrade failures, missed snapshots, expiring certificates and a cluster that has not reported for 24 hours are mailed too.
A name in your clusterEvery kubeconfig is a ServiceAccount named after the person it was issued to, in the namespace pbx-access, so its calls reach the Kubernetes API under that name. The node keeps no Kubernetes audit log of those calls.
Who askedEvery operation shows who asked for it, mine included, on the cluster’s Operations tab.
Typed confirmationsFor a restore, a detach and a delete.
Short lifetimesKubeconfigs live at most 24 hours.
What you can doKeep the clusters in a Hetzner project of their own, hold the bucket’s keys yourself, narrow the API’s address ranges, give each agent a key with only the scopes it needs, and keep the bucket in a project or at a provider the Hetzner token does not reach.

One further protection is an idea for a later version, neither built nor scheduled: your approval, outside the portal, before a restore or an admin kubeconfig is issued.

WhatHow
pbx-agentRuns as root, because a restore must stop k3s and start it again while the Kubernetes API is down. It listens on nothing.
DiagnosticsCollected only after you allowed it for that one request; never Secrets, tokens or environment files.
Secretsk3s encrypts Secrets at rest.
The root file systemRead-only, part of the image, whose checksum the signed release names.
Port 22Closed by the firewall; the image has no root password. Get root on the server has the ways in.