Security
A console that runs commands
has to be accountable for them
Control Panel is privileged operations tooling. Every deployment of it belongs in your trusted computing base — so this page tells you exactly what it enforces, what it records, and what it does not yet defend against.
Authentication & roles
Role-based access control, enforced in Go, not in the interface
Every /api/* route runs through server-side authentication. Hiding a button is a nicety;
the gate is in the handler.
- Four roles, each including the last — viewer, operator, deployer, admin.
- OIDC single sign-on with the authorization-code flow and PKCE, mapping an IdP group or claim to a panel role. Local users stay available for air-gapped installs.
- Session timeouts — 30 minutes idle and 12 hours absolute by default.
- "Remember me" stores only a SHA-256 of the session token in a
0600file, so the file cannot be replayed as a cookie. Signing out deletes the entry. - No users configured? The panel runs in legacy open mode on loopback only and refuses to bind a non-loopback address.
| Role | Read | Restart | Deploy | Console |
|---|---|---|---|---|
| viewer | yes | no | no | no |
| operator | yes | yes | no | no |
| deployer | yes | yes | yes | no |
| admin | yes | yes | yes | audited |
The execution surface
Narrowing what the browser is allowed to ask for
Named server-side actions
Docker service operations are requested by name — start_services, with an environment and a
list. The server builds the command from its own configuration and validates the arguments. The client
never supplies a shell string on that path.
Filesystem sandbox
File reads and writes are confined to the app directory, the OS temp directory, configured environment
roots and an explicit allow-list. config/users.json and config/known_hosts are
never reachable over HTTP.
CSRF and origin checks
Mutating calls from foreign web origins are rejected, so a page you happen to visit cannot drive a panel bound to localhost — the classic localhost-CSRF and DNS-rebinding route.
SSH host-key verification
Remote host keys are pinned in config/known_hosts, trust-on-first-use by default and
pre-provisioned only with CP_SSH_STRICT=1. A changed host key is always an error, never a
silent accept.
Secrets by reference
Credential fields can be a secretRef resolved server-side at use-time —
env:, file: or vault: (KV v2) — so no secret material has to live
in a config file. PEM content is materialised to a 0600 temp file automatically.
Redaction everywhere
Credential-looking values are stripped from audit lines and from GET /api/config and
/api/environments responses. API tokens are stored as SHA-256 hashes; the plaintext is shown
once, at creation.
Transport & deployment
Bind it narrowly, terminate TLS, tell it about your proxy
The default bind is loopback. Anything wider requires authentication to be configured — the server will not start otherwise.
- HTTPS directly with
-tls-cert/-tls-key, or behind a reverse proxy withCP_TRUSTED_ORIGINS. - Set
CP_TRUSTED_PROXIESwhen proxied — otherwise every request looks like it came from the proxy, one attacker's failed logins throttle everyone, and the audit trail records the proxy instead of the operator. - Security headers on responses, and a single-instance-per-port guard so two servers cannot fight over the same socket.
- Scoped API tokens — least role, specific environments, an expiry, revocable from the admin UI or the API.
# users first — the bind is refused without them ./control-panel -adduser ops -role operator CP_MODE=offline \ CP_ADDR=0.0.0.0:8099 \ CP_TRUSTED_ORIGINS=https://panel.example.com \ CP_TRUSTED_PROXIES=private \ CP_SSH_STRICT=1 \ CP_AUDIT_LOG=/var/log/control-panel/audit.log \ CP_METRICS_TOKEN="$SCRAPE_TOKEN" \ ./control-panel -tls-cert cert.pem -tls-key key.pem
Hosted workspaces
What our infrastructure holds — and what it deliberately does not
Held in your workspace
- Environment definitions, service lists and dashboard layout, scoped to your tenant.
- Monitoring history, deployment history and your audit log.
- Credentials you choose to store — or nothing at all, if you use
secretRefagainst your own Vault.
Not registered at all
- A shell on the host the workspace runs on.
- The host filesystem, and disk cleanup of it.
- Anything that reads the machine we operate — a workspace admin is a stranger to it. Those routes exist only in a self-hosted install, where the admin already owns the box.
Threat model
Known limitations, stated plainly
A security page that lists only strengths is a marketing page. These are the current edges, the same ones we state in the security pack we send on request.
Shell-command authorization is a policy tier, not a parser
Named server-side actions cover docker service operations. Windows, bastion and
artifact flows still send command strings, which the server classifies by role. Viewers cannot execute
anything at all; between operator, deployer and admin the classification is a trust boundary for
already-trusted users and an audit boundary — not an injection-proof parser. Grant admin
sparingly.
Shell and upload endpoints run with the server process's privileges
There is no per-user OS sandbox inside the panel's own process. Run it as a dedicated, least-privileged account, and treat operator and above as trusted roles.
Ordinary sessions live in memory
A restart signs those users out. Only "remember me on this machine" sessions persist — as SHA-256 hashes, expiring after 7 days idle and 30 days absolute. Deleting that file forces everyone to sign in again.
The legacy in-UI admin/viewer gate still exists
It is superseded by server-side authentication and remains only for compatibility with older configurations. Do not rely on it as a control.
A build with no licence server configured can never be revoked
Revocation reaches a panel by fetching a signed list, which needs a reachable server. Fully air-gapped installs are therefore issued short-dated licences instead, so expiry does the work revocation cannot.
Installing an update replaces the running binary
The in-app updater is the one feature that rewrites the install, so it is fenced
accordingly: the artifact's SHA-256 is published in the release manifest and mandatory — an entry with
no checksum, a malformed one, or a download URL that is not https is refused, both when the
release is published and again by every panel that reads it. The download is written under a temporary
name and only renamed once the hash matches, so a failed or tampered download cannot be found later and
run, and a remembered installer is re-hashed before reuse. The artifact is itself a signed installer
that Windows and macOS check again before running it. Nothing installs unattended: an administrator
presses the button, and the check, the install and every settings change are audited. Set
CP_UPDATES=0 to disable the outbound check entirely.
Coordinated disclosure
Found something? Tell us privately first.
Please do not open a public issue. Email us with:
- a description of the issue and its impact,
- reproduction steps or a proof of concept,
- the commit or release you tested.
You will get an acknowledgement within 5 business days. Please allow a reasonable window for a fix before public disclosure. We credit reporters who want to be credited.
For your security review
We answer questionnaires, provide an SBOM and signed artifacts for release builds, and will walk your reviewer through the hardening checklist on a call.
Hardening checklist
Enable authentication, terminate TLS, keep and forward the audit log, pin SSH host keys, keep secrets out of config, and grant admin sparingly. Each step is in the docs with the exact flags.
Bring it to your reviewer
Most objections to an operations console are about accountability. This one keeps the receipts.