App container confinement (shell hardening)
The Hetzner host applies a confinement profile to every app on each deploy.
Browser sessions execute inside the selected app container, as an explicit
unprivileged UID/GID (32767 by default), never in a host shell. The default
workspace is terminal; shell only serves the frontend.
Enforcement
| Component | Host path or repository source |
|---|---|
| Runtime profile applier | scripts/paas-harden-app.sh → /usr/local/bin/paas-harden-app |
| Dokku pre-deploy hook | scripts/paas-hardening-pre-deploy.sh → plugin paas-hardening/pre-deploy |
| Seccomp profile | config/paas-app-seccomp.json → /etc/dokku/paas/seccomp-app.json |
| Operator shell helper | scripts/paas-app-shell.sh → /usr/local/bin/paas-app-shell |
| Initial installation | templates/bootstrap.sh.tftpl |
Deploy/run containers receive --cap-drop=ALL,
--security-opt=no-new-privileges:true and the vendored Moby default seccomp
profile. If that file is missing, the applier warns and Docker's built-in
default still applies. Shells force an unprivileged runtime user even if an
image's default changes; image authors must also choose an appropriate
non-root application user.
| Limit | Ordinary apps | WebUI |
|---|---|---|
| CPU | 1 vCPU | 1.5 vCPU |
| Memory (no extra swap) | 512 MiB | 1536 MiB |
| PIDs | 256 | 512 |
| Temporary filesystem | 256 MiB, noexec/nosuid/nodev | Same |
The writable container layer has no disk quota on the host's overlay2/ext4 storage. Monitor disk usage; a bounded temporary filesystem is not a full disk-exhaustion defense. Persistent host-backed storage and datastores require separate capacity management and backups.
Important trust exceptions
App containers do not mount the Docker socket or host process filesystem. The host shell backend does require Docker CLI/socket access through its Docker-group service user: that service is a privileged control-plane component, not an untrusted app.
The terminal workspace mounts a shared read-only deploy key at
/app/.ssh. Read-only prevents editing the mount, not use or theft of the
credential. It can administer every Dokku app, including inspecting app
configuration. Browser ?app= selection is not a Dokku permission boundary.
Never print/copy the private key or inspect unrelated apps.
Every Dokku app receives OPEN_TERMINAL_API_URL and the same
OPEN_TERMINAL_API_KEY. The private API on http://terminal.web:5000
uses the host-managed paas-terminal-api network, with no public domain,
proxy or published host port. Other Docker containers may still reach its
listener; protected routes require the shared key. Network privacy does not
provide per-app authorization. A compromised participating app can access the
same terminal workspace.
Browser shells start with a scrubbed environment. The installed systemd unit
allowlists OPENROUTER_API_KEY,OPENROUTER_MODEL from terminal runtime config.
It does not directly pass the private terminal API key. That omission does
not conceal secrets from terminal users: their UID matches the API process
and permits reading its environment, and the shared deploy key permits reading
app config. OpenRouter calls use an explicit API key, not server identity.
Only trusted operators should receive workspace, API or WebUI terminal access.
Verification and operation
On the host, inspect a known target container:
sudo docker inspect <app>.web.1
Check CapDrop, SecurityOpt, Memory, NanoCpus, PidsLimit and Binds;
do not publish the output, which can contain runtime credentials. In a shell,
id must be non-root and /var/run/docker.sock must be absent. Do not test
limits with resource-exhaustion payloads on a live shared host.
Profiles apply automatically to new apps and future deploys. An operator can
reapply with sudo paas-harden-app --all, followed by a deliberately scheduled
sudo dokku ps:rebuild <app> for each intended app (this replaces containers).
Use sudo paas-app-shell <app> for an operator-started unprivileged app shell.
Tune limits in the applier and redeploy deliberately.
See Web shell for authentication and audit details, and Getting started for deployment.