Docs

Security model

What powder protects, what you have to protect, and what leaves your boxes. Shared responsibility for a self-hosted deploy tool.

Powder runs on your servers and you are responsible for them. This page says where powder helps and where it does not. To report a vulnerability, see SECURITY.md.

Powder has root on your servers

Powder deploys over SSH with a key you install in authorized_keys on each server (see Self-hosting), usually for root. That is by design: it runs Docker, writes under /opt/apps and reads logs. Anyone who controls powder controls those servers. Treat the control plane like a root credential.

Protect the control plane

  • One admin. There is no signup. The first GitHub login on an empty install becomes the admin, and POWDER_ADMIN_USER_IDS pins admin ids from then on. Once any id is set, no new user can be created through login. Set it right after first login, and keep the GitHub account behind it protected (2FA).
  • GitHub OAuth. Sign-in asks for the repo, admin:repo_hook, read:user and user:email scopes, because powder installs deploy keys and webhooks. The stored GitHub tokens are encrypted at rest.
  • Single tenant. Non-admin sessions get a 403 on every API route. This is always on; the old POWDER_SINGLE_TENANT variable is ignored (a note is logged at startup if it is set).
  • HTTPS. Serve the dashboard over HTTPS only. Sessions last 30 days.
  • Network. The compose file binds powder to 127.0.0.1:3050. Put a reverse proxy in front of that port, firewall everything else, and if you can, keep the dashboard on a VPN or Tailscale instead of the open internet.
  • SSH key. The private key mounted into the containers is the key to every server. Use one key for powder, restrict who can read it on the host, and remove it from authorized_keys on a server you no longer manage. Deploy git traffic checks GitHub's published host keys strictly.

Protect the database and the encryption key

Deploy keys, webhook secrets, GitHub tokens and app env vars are stored encrypted (AES-256-GCM) in powder's Postgres, with the key in POWDER_ENCRYPTION_KEY.

  • Whoever has both the database and POWDER_ENCRYPTION_KEY can read every stored secret. Keep them apart, and do not expose Postgres to the network.
  • Every stored secret depends on that key. If you lose it, they are lost: you have to re-enter env vars and regenerate keys and webhooks.
  • Rotating it is manual. Nothing re-encrypts existing rows for you. Back the key up somewhere other than the box.
  • BETTER_AUTH_SECRET signs sessions. Keep it secret and rotate it if it leaks (everyone is signed out).

What a deployed repo can do on the box

When you connect a repo, powder clones it on your server and builds its Dockerfile there with Docker. Build steps run on your box, and the container runs there afterwards. Powder does not sandbox the app beyond what Docker does. Connect only code you trust.

At import, powder renders the app's runtime compose file and validates it before anything is created. It refuses:

  • these service keys: privileged, pid, cap_add, devices, security_opt, ipc, userns_mode, uts, cgroup_parent
  • network_mode: host
  • volumes_from
  • bind mounts outside /opt/apps/<app> (relative, absolute or ~ paths)
  • a dockerfilePath that is absolute, contains .. or uses characters other than letters, digits, ., _, / and -, and a container port outside 1-65535

That validation covers what powder generates at import. It does not read your Dockerfile or your source, and it does not stop a build step from doing what Docker lets a build do. Everything else about the app is your choice when you connect it.

Webhooks

Each repo gets its own webhook secret and URL token. Powder checks the X-Hub-Signature-256 HMAC-SHA256 signature of push and ping deliveries against that repo's secret with a constant-time comparison and rejects a bad signature with 401. Other events are ignored. Monorepo groups use a per-group secret the same way.

What powder does not protect against

  • A compromised GitHub account: it holds the admin login, the repos and the webhooks.
  • A malicious or compromised repo that you chose to deploy.
  • Root on the powder box or a managed server: it reads the key, the env file and the containers.
  • A leaked POWDER_ENCRYPTION_KEY together with a copy of the database.

Backups

If you configure S3-compatible storage, powder uploads encrypted Postgres dumps there, with the passphrase POWDER_BACKUP_KEY. Powder's S3 credentials stay with powder, and the server only gets expiring upload links. Restore is manual. Test a restore before you need it, and keep a copy of the passphrase elsewhere. See Backups.

Updates

Nothing updates automatically. Powder never pulls a new version of itself. Watch the repository releases and GitHub Security Advisories, and upgrade with bin/self-deploy.sh as described in Self-hosting.

Logs

Deploy output is stored and shown in the dashboard. It contains whatever your build and start commands print, which can include environment banners, paths and error messages with secrets in them. Treat deploy logs as sensitive, and keep your builds from printing secrets.

Telemetry and outbound calls

Powder has no required phone-home. Every call that leaves the box is an optional feature you control with an env var.

  • GitHub API: webhook setup, CI status, and the daily update check. The update check sends one request with a powder/<version> User-Agent and nothing else; disable it with POWDER_UPDATE_CHECK=off.
  • PostHog analytics: only if NEXT_PUBLIC_POSTHOG_KEY is set when the image is built. The dashboard sends events through its own /ingest path and identifies the signed-in user by id, email and name. Session recording is on with inputs masked, and exception capture is on.
  • HQ reporting: only when the POWDER_HQ_* variables are set (POWDER_HQ_INGEST_TOKEN plus the matching URL). Powder then posts deploy results, warning cards and cron results to the endpoints you configured, fail-silent.
  • Anthropic: only with ANTHROPIC_API_KEY, and only when you click "diagnose with AI" on a failed CI run. That sends the last 6000 characters of the GitHub Actions job log of that run, after redacting URLs with credentials, *KEY/*TOKEN/*SECRET/*PASSWORD assignments, bearer tokens and common token prefixes. The log is wrapped as untrusted data, the reply is coerced to capped strings and shown labelled "model-generated, verify before running". Calls time out after 30 seconds and are capped per day by POWDER_LLM_DAILY_MAX (default 50).
  • Notification webhook: only with POWDER_NOTIFY_WEBHOOK_URL. Powder POSTs JSON {event, app, server, message, url, at} for deploy_failed and app_down (the message carries the first 500 characters of the deploy error). Discord and Slack incoming webhooks work as the target.
  • Alerts: POWDER_NOTIFY_WEBHOOK_URL is the only alert channel powder has. There is no email and no other notifier: with the variable unset, deploy failures and app-down events are visible only in the dashboard.
  • Your own S3-compatible storage: backups upload there when BACKUP_S3_* is set.
  • Your own apps: uptime probes and HTTP crons request the URLs you configure, plus the SSH connections to your servers.