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.

4
roles enforced server-side on every route
JSONL
append-only audit log, SIEM-ready
0
secrets required in config — resolve from Vault, env or file
Ed25519
licence signatures, verified offline

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 0600 file, 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.
roles · what each one may do
RoleReadRestartDeployConsole
vieweryesnonono
operatoryesyesnono
deployeryesyesyesno
adminyesyesyesaudited
logs/audit.log — one JSON object per line
{"ts":"12:05:12Z","user":"k.som",
"action":"service.restart","env":"PRODUCTION",
"target":"gateway","result":"ok"}
{"ts":"12:05:44Z","user":"operator",
"action":"fs.write","result":"denied"}

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 with CP_TRUSTED_ORIGINS.
  • Set CP_TRUSTED_PROXIES when 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.
A hardened start
# 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 secretRef against 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.
If that boundary is not enough for your data, do not take our word for it — take the binary. Self-hosting puts every one of those rows on a machine you control, with a licence that never phones home.

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.