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.
Your Hetzner token, which opens your whole project, read and write
Encrypted
The k3s server token, in every version a kept snapshot still needs
Encrypted
The k3s agent token
Encrypted
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 yourself
Encrypted
The secret options of add-ons, such as DNS tokens
Encrypted
A sealed kubeconfig, until it is collected
Encrypted, for at most 5 minutes; the portal cannot open the seal
pbx-agent’s keys
Public keys only
Enrollment tokens
Hashes only
Your API keys In progress
Hashes only; a key is shown once, when it is made
The audit log of API and MCP calls In progress
The 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 tokens
They are made on the node and sealed to the key of whoever asked
/etc/rancher/k3s/k3s.yaml, the admin file on the server
The portal never reads it
pbx-agent’s private keys
They are made on the node and never leave it
The contents of your snapshots
They 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.
A 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 node
At 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 requests
Every 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 secrets
Every 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 data
User 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 releases
A 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 kubeconfigs
A 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 options
Go 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.
Limit
What it does
Team keys
A 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.
Scopes
k3s:read, k3s:write, k3s:access, k3s:destructive. Every call needs exactly one; a key holds only what its agent needs.
The person behind the key
The 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 calls
Creating, 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.
Kubeconfigs
Sealed to the agent’s own key; a kubeconfig asked for through a key can be collected only by that key.
Rate limits
120 calls per minute per key, 20 of them calls that change something.
Audit log
Every 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 could
Why
Create, change and delete anything in your Hetzner project, not only the cluster’s resources
The 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 manages
Issuing kubeconfigs is one of pbx-agent’s operations
Queue a restore of an older snapshot, which resets the cluster’s state
Restore 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 target
pbx-agent applies the desired state the portal sends
Detach or delete the cluster
Delete is the provisioner’s job
Open your snapshots
With the server token and portal-held S3 keys
It could not
Why
Run an arbitrary command on your server
pbx-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 node
The 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 yourself
The 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.
Tested 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 admins
Whenever 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 cluster
Every 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 asked
Every operation shows who asked for it, mine included, on the cluster’s Operations tab.
Typed confirmations
For a restore, a detach and a delete.
Short lifetimes
Kubeconfigs live at most 24 hours.
What you can do
Keep 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.