This document explains what devbox protects against, what it doesn't, and where the actual security boundaries are.
Devbox implements a network-centric security model, not local privilege separation.
What IS a security boundary:
- Tailscale ACL: Only authenticated Tailscale peers can reach the services (default deny)
- SSH key authentication: SSH password auth is disabled; only key-based login is allowed
- UFW firewall: Host-level firewall denies all inbound except SSH (on port 5522) and UFW-managed rules
- Authenticated peers only: Services are restricted to known Tailscale peers; public internet has zero access
What is NOT a security boundary:
- Local privilege separation: The
devuser in thedockergroup has root-equivalent access (can bind-mount/as root) - Sudoers rules alone: Restrictive sudoers rules are enforced but don't prevent docker-group privilege escalation
- Container isolation: Exegol and other containers run with relaxed security options (AppArmor disabled, minimal capabilities) intentionally for tool functionality; container escape may grant host access
- Credential file encryption: By default,
.devbox-credentialsis stored plaintext on disk; optional GPG/age encryption is available but not enforced
The dev user is in the docker group, which grants root-equivalent access on the host. This is by design:
# From docker group membership, an attacker can:
docker run -it --rm -v /:/host ubuntu /bin/bash
# Now inside the container:
chroot /host /bin/bash
# You now have root access on the hostThis is not a limitation of devbox; it's a fundamental Docker design. The docker socket provides unrestricted access to the Docker daemon, which allows binding the entire filesystem.
Acceptance of this risk:
- The security boundary is the Tailscale ACL + SSH hardening, not local privilege separation
- If the SSH key is compromised, the attacker has full root access
- If the Tailscale account is compromised, the attacker has full network access to all services
- Local privilege escalation (via docker, kernel exploit, etc.) is out of scope
Sudoers is configured with explicit NOPASSWD rules for necessary privileged commands:
dev ALL=(root) NOPASSWD: /usr/sbin/ufw, /usr/bin/tailscale, /usr/sbin/openvpn
dev ALL=(root) NOPASSWD: /bin/systemctl restart docker, /bin/systemctl reload ufw
dev ALL=(root) PASSWD: ALL
These rules enforce that certain commands (firewall, VPN, Tailscale) don't require entering a password repeatedly. They are not a security boundary but a convenience feature. The dev user's root-equivalent docker access supersedes any sudoers restrictions.
Tailscale network compromise:
- Attacker gains access to a Tailscale account and joins the network
- Mitigation: Tailscale ACL restricts access to known peers by IP
- Action required: Monitor Tailscale dashboard for unexpected peers; revoke compromised devices immediately
SSH key compromise (stolen, leaked):
- Attacker obtains the SSH private key (e.g., from a laptop backup)
- Mitigation: SSH key-only auth; password bruteforce is impossible
- Action required: Revoke the key immediately (remove from
authorized_keys), generate a new key, re-deploy
Upstream image registry compromise:
- Docker Hub, GitHub Container Registry, or Ollama upstream is compromised and returns malicious images
- Mitigation: ADR-0003 (digest pinning) ensures exact images are deployed; malicious image would need to match the pinned digest
- Action required: Weekly CI smoke test (ADR-0003) detects obvious breakage; cosign signature verification (ADR-0010) confirms build provenance
Setup.sh download compromise:
- Bun, Rustup, Tailscale, or other installer URLs are MITM'd or registry is compromised
- Mitigation: ADR-0005 (fetch_and_verify) pins SHA256 of installers; hash mismatch blocks installation
- Action required: Emergency
DEVBOX_ALLOW_UNVERIFIED=1override documented in docs/ops.md
Hand-edit clobbering (accidental data loss):
- Operator edits
~/docker/traefik/traefik.ymland re-runssetup.sh, losing the edits - Mitigation: ADR-0013 (pre-rsync snapshots) backs up
~/docker/before rsync - Action required: Restore from
~/.local/share/devbox/backups/<timestamp>/(documented in docs/ops.md)
Physical access:
- Attacker has physical access to the server
- Why out-of-scope: No security can protect against physical access (disk can be removed, BIOS can be reset)
- Recommendation: Server should be in a physically secure location (data center, locked cabinet)
Kernel exploits / zero-days:
- Attacker finds a CVE in the Linux kernel and exploits it to gain root
- Why out-of-scope: No application-level mitigation possible; requires kernel patches
- Recommendation: Keep the OS patched; subscribe to Ubuntu security mailing list
Container escape:
- Attacker runs code in a container (Exegol, Ollama, etc.) and exploits a Docker/containerd bug to reach the host
- Why out-of-scope: Container security is a shared responsibility of Docker, kernel, and application configuration
- Partially mitigated: Security options (
no-new-privileges,cap_drop: ALL, selectivecap_add) reduce attack surface; however, Exegol intentionally disables AppArmor for tool functionality - Recommendation: Keep Docker and the kernel patched; don't run untrusted container images
Exegol upstream compromise:
- ThePorgs/Exegol project is compromised and releases a malicious image
- Why out-of-scope: Exegol is a pentesting container intentionally running arbitrary tools; any additional trust verification is minimal value
- Accepted risk: Documented in ADR-0012; operator accepts that Exegol is untrusted-by-design
- Recommendation: Review Exegol releases periodically; pin a specific known-good digest if reproducibility is critical
GitHub Actions compromise:
- GitHub Actions infrastructure is compromised; malicious
weekly-rebuild.ymlis deployed - Why partially out-of-scope: GitHub Actions is infrastructure you don't control
- Partially mitigated: SLSA provenance (ADR-0010) attests the build environment; operator can verify against GitHub's public logs
- Recommendation: Review cosign signatures carefully; if suspicious, re-build and sign the release locally
Supply-chain attack on CI dependencies:
- A GitHub Actions action (e.g.,
cosign-installer,slsa-github-generator) is compromised - Why out-of-scope: Actions are third-party code; no devbox-level mitigation possible
- Partially mitigated: All action versions are pinned (ADR-0010); known-good versions are committed
- Recommendation: Audit action code before updating; monitor for security announcements
-
Immediately revoke the compromised key:
# On the server, remove the old key ssh-keygen -f ~/.ssh/authorized_keys -R "$(cat ~/.ssh/authorized_keys)" # Or manually edit ~/.ssh/authorized_keys and remove the public key
-
Generate a new SSH key (on your laptop):
ssh-keygen -t ed25519 -C "devbox@$(hostname)" -f ~/.ssh/devbox -N "passphrase"
-
Deploy the new key:
# Temporarily enable password auth or use an alternate access method # Then update authorized_keys with the new public key
-
Verify access works with the new key:
ssh -i ~/.ssh/devbox dev@devbox -
Disable the old key completely and destroy it:
shred -u ~/.ssh/devbox.old
-
From the Tailscale web console, revoke the device:
- Go to https://login.tailscale.com/admin/machines
- Find the device and click "Delete"
-
On the server, run Tailscale auth again:
sudo tailscale logout sudo tailscale up -
Verify the new device appears in the console and has the correct IP
-
Monitor the Tailscale network for unexpected new devices
If docker compose up -d --wait fails on a service, check:
# Review recent digest changes
git log --oneline services/ | head -10
# Compare current lockfile with what CI generated
docker compose -f services/traefik/docker-compose.yml -f services/traefik/docker-compose.lock.yml config | grep image
# Check if the service is obviously broken
docker compose logs traefik | tail -50
# If suspicious, revert the last commit
git revert HEAD
docker compose up -dBy default, .devbox-credentials (NOPASSWD file for Ansible/helper tools) is stored plaintext in ~/.devbox-credentials. This file contains:
- Nextcloud admin credentials
- Ollama API keys (if any)
- Redis password
- Other service credentials
Risk: If the disk is readable by another user or compromised, credentials are exposed.
Mitigation:
- File is readable only by
dev:dev(0600 permissions) - Umask is set to 077 when creating the file (ADR-0005)
- If GPG keys are present, the file is encrypted to
~/.devbox-credentials.gpgautomatically
Recommendation:
- If credentials are sensitive, enable GPG encryption (
gpg --list-keysmust return at least one key) - Rotate credentials regularly
- Do not share the server with untrusted users
- Use Tailscale SSH (if available) instead of long-lived SSH keys where possible
If ENABLE_HTTPS=true, OVH API credentials are read from:
${XDG_CONFIG_HOME:-$HOME/.config}/devbox/ovh.env
This file must contain:
OVH_ENDPOINT="ovh-eu"
OVH_APP_KEY="..."
OVH_APP_SECRET="..."
OVH_CONSUMER_KEY="..."
Risk: Credentials are plaintext; if the file is readable, credentials are exposed.
Mitigation:
- File permissions are enforced to 0600 (readable only by owner)
- File is NOT tracked in git (added to
.gitignore) - File is NOT backed up by default (Scenario E in pre-mortem)
Recommendation:
- Restrict
/etc/sudoers.d/so onlydevcan read it (already done) - Encrypt the file locally using GPG or age if storing on a shared system
- Rotate OVH credentials periodically
- Use dedicated OVH accounts for this infrastructure (not personal accounts)
From the pre-mortem: if setup.sh is interrupted (SIGINT) between credential file creation and chmod 600, the plaintext credentials file could be left world-readable.
Mitigations:
umask 077at the top of setup.sh (default umask creates 0700 files)install -m 0600 /dev/null ~/.devbox-credentials(atomic permissions before any content)trap 'shred -u "${CREDS_FILE}" 2>/dev/null || true' INT TERM(cleanup on signal)
Verification:
# Check for world-readable credential files
find ~ -maxdepth 1 -name '.devbox-credentials*' -perm /044
# Should return nothing (empty)Run this after every install to verify the security model is in place:
# 1. Verify SSH key-only auth
sudo sshd -T | grep -E "^passwordauthentication|^pubkeyauthentication"
# Should show: passwordauthentication no, pubkeyauthentication yes
# 2. Verify UFW is active and denies by default
sudo ufw status
# Should show: Status: active, Default: deny (incoming), allow (outgoing), disabled (routed)
# 3. Verify docker group membership
id dev
# Should show: groups=...,docker,...
# 4. Verify Tailscale is connected
tailscale status | grep -E "^[[:space:]]+.*online"
# 5. Verify .env file permissions
ls -la ~/docker/*/.env
# Should show: -rw------- (0600)
# 6. Verify sudoers whitelist
sudo -l
# Should show only ufw, tailscale, openvpn, systemctl commands allowed NOPASSWD
# 7. Verify no credential files are world-readable
find ~ -maxdepth 1 -name '*.devbox-credentials*' -perm /044 || echo "OK: no world-readable creds"
# 8. Verify Traefik middleware are wired at entryPoint level (ADR-0006)
docker exec traefik cat /etc/traefik/traefik.yml | grep -A 5 "entryPoints:" | head -20- ADR-0011: Docker group privilege model (detailed explanation)
- ADR-0010: Cosign keyless and supply-chain verification
- ADR-0005: Verified downloads and SHA pinning
- ADR-0003: Image digest pinning
- docs/ops.md: Incident response runbook and backup/restore procedures
- CONTRIBUTING.md: Contributing to devbox securely