chore: harden VPS deploy user with rootless Docker#156
Merged
Conversation
Replace docker group membership (root-equivalent socket access) with a sudoers file that permits only 'docker compose' and 'docker exec'. - bootstrap-vps.sh: replace usermod -aG docker with /etc/sudoers.d/deploy-docker - All deploy/backup/restore scripts: prefix docker compose calls with sudo - infra/docs/vps-setup.md: document Option B (implemented) and Option A (rootless Docker upgrade path) with pre-check, install, and verification steps
Contributor
There was a problem hiding this comment.
Pull request overview
This PR aims to harden VPS deployments by removing deploy’s membership in the docker group and instead granting narrowly-scoped sudo permissions for Docker operations used by the deploy/backup/restore scripts, plus documenting the approach.
Changes:
- Add a
/etc/sudoers.d/deploy-dockerrule inbootstrap-vps.shand stop adding the deploy user to thedockergroup. - Prefix deploy/backup/restore/staging Docker invocations with
sudo. - Add
infra/docs/vps-setup.mddocumenting the narrowed-sudoers approach and a rootless Docker upgrade path.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| infra/scripts/bootstrap-vps.sh | Replaces docker-group membership with a sudoers-based permission model for Docker. |
| infra/scripts/deploy.sh | Uses sudo docker compose for production deploy actions. |
| infra/scripts/deploy-staging.sh | Uses sudo docker compose for staging deploy actions. |
| infra/scripts/staging-up.sh | Uses sudo docker compose for bringing staging up and checking status. |
| infra/scripts/staging-down.sh | Uses sudo docker compose for bringing staging down. |
| infra/scripts/backup-db.sh | Uses sudo docker compose exec for Postgres dumps. |
| infra/scripts/restore-db.sh | Uses sudo docker compose to stop/start backend and restore DB via psql. |
| infra/docs/vps-setup.md | Documents the VPS deploy-user hardening approach and rootless Docker alternative. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
VPS pre-check confirmed user namespace support (unprivileged_userns_clone=1, unshare works). uidmap package was absent but is installable via apt, so rootless Docker is viable. - bootstrap-vps.sh: add uidmap + dbus-user-session to apt installs, replace sudoers block with rootless Docker setup (loginctl enable-linger, .bashrc DOCKER_HOST/PATH, rootless install via runuser, systemctl --user enable/start) - Deploy scripts: revert sudo prefix — rootless Docker needs no sudo - infra/docs/vps-setup.md: document Option A as implemented, record pre-check results, verification steps, and security properties table
Step-by-step guide for migrating an existing VPS (with live containers) from the root Docker daemon to rootless, including postgres data preservation via pg_dump/restore and docker group removal.
- Remove deploy user from docker group on re-run (gpasswd -d) so a previous bootstrap that added docker group membership is cleaned up - Use absolute paths in deploy.sh, deploy-staging.sh, staging-up.sh, and staging-down.sh instead of relying on cd; eliminates ambiguity and makes invocation context-independent
- Add DOCKER_HOST self-initialization to all deploy/backup/restore scripts so they work correctly in cron and non-interactive SSH sessions - Fix root-ownership risk on ~deploy/.bashrc by chowning after append - Clarify bootstrap-vps.sh comment: deploy user has no access to root Docker socket, which still exists at /var/run/docker.sock for system use - Fix self-contradictory newuidmap table row in vps-setup.md - Add rootless-docker-migration.md documenting the live station-bot migration including AppArmor profile requirement and post-mortem
- Fix .bashrc comment: DOCKER_HOST only affects interactive/login shells, not cron — non-interactive scripts set it themselves - Fix hardcoded UID in .bashrc heredoc: use quoted heredoc so $(id -u) evaluates dynamically at login rather than being baked in at bootstrap time - Add AppArmor profile creation to bootstrap-vps.sh for Ubuntu 24.04+ where unprivileged user namespaces are restricted by default - Update vps-setup.md 'fully automated' claim to mention AppArmor handling - Fix Phase 3 pg_dump command in migration runbook to use targeted grep extraction instead of set -a source, which fails on non-strict env files
- backup-db.sh, restore-db.sh: replace set -a/source with grep-based variable extraction to avoid failures when env file contains values with spaces (e.g. APP_NAME=STATION BACKEND) - bootstrap-vps.sh: tighten hardening comment — leaked key cannot escalate to root or access other users' containers, but can still affect deploy-user-owned resources - vps-setup.md: qualify overview security claim to match actual rootless Docker guarantees - rootless-docker-migration.md: fix "inaccessible to every other user" — root can still access the socket; correct to "non-root users"
- backup-db.sh: add `|| true` to BACKUP_HEALTHCHECK_URL grep so a missing key doesn't abort the script under set -e - bootstrap-vps.sh: derive AppArmor profile filename and rootlesskit binary path from DEPLOY_HOME instead of hard-coded /home/deploy, so the profile correctly targets the configured deploy user
- backup-db.sh, restore-db.sh: add || true to all required-var grep substitutions so missing keys reach the explicit :? error messages instead of silently aborting under set -euo pipefail - restore-db.sh: add trap to remove temp file on EXIT so interrupted restores don't accumulate large artifacts in /tmp - vps-setup.md Phase 3: use explicit DOCKER_HOST=unix:///var/run/docker.sock for the pg_dump step; rootless installer often switches CLI context immediately, making "don't source .bashrc" insufficient - vps-setup.md Phase 5: use explicit DOCKER_HOST for rootless socket on the psql restore step to remove ambiguity - bootstrap-vps.sh: skip rootless install if daemon is already healthy; clean up partial installs before retrying to avoid installer getting stuck
- vps-setup.md: remove PATH from the bootstrap-vps.sh description bullet; only DOCKER_HOST is written to .bashrc with the APT-based install - rootless-docker-migration.md: fix cut -d= -f2 to cut -d= -f2- in the post-mortem Issue 1 example snippet to match the hardened parsing used throughout this PR
- rootless-docker-migration.md: remove PATH from the "What changed" bullet; only DOCKER_HOST is written to .bashrc with the APT-based install, ~/bin is not needed
- bootstrap-vps.sh: set XDG_RUNTIME_DIR explicitly for all systemctl --user invocations via runuser; without it systemctl cannot reach the user's D-Bus/systemd instance in non-interactive contexts (common error: failed to connect to bus)
- restore-db.sh: set BACKEND_STOPPED=1 before docker compose stop rather than after; if stop exits non-zero under set -e the EXIT trap fires immediately and would have seen flag=0, skipping the restart — setting it first ensures the cleanup handler always attempts to bring the backend back up on any failure at or after the stop step
- vps-setup.md: rewrite overview to accurately state that a leaked key gives SSH access and arbitrary container execution as the deploy user; the key hardening property is preventing root escalation via Docker, not restricting to deploy-related operations only - vps-setup.md: replace misleading "Survive deploy key compromise" table row with "Blast radius of leaked key: Full host (root) → Deploy user only" which accurately describes what rootless Docker actually achieves - rootless-docker-migration.md: tighten the Why section — a compromised key cannot escalate to root or access other users' containers via Docker, but blast radius is correctly scoped to the deploy user's namespace
- rootless-docker-migration.md Issue 4: replace hard-coded curl|sh install paths (/home/deploy/bin/dockerd-rootless-setuptool.sh, rm -f /home/deploy/bin/dockerd) with APT-based equivalents; dockerd-rootless-setuptool.sh is in /usr/bin and dockerd is not placed in ~/bin with the APT install — cleanup is now the setup tool uninstall + rm -rf ~/.local/share/docker, with a note for anyone cleaning up an old curl|sh install - rootless-docker-migration.md Prerequisites: add historical record callout to clarify the apt block reflects the original curl|sh migration and that the recommended method now includes docker-ce-rootless-extras
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #129
Summary
bootstrap-vps.sh: addsuidmap+dbus-user-sessionprerequisites, enables linger, setsDOCKER_HOST/PATHin.bashrc, installs rootless Docker viarunuser, enables and starts the user systemd servicedeploy.sh,deploy-staging.sh,backup-db.sh,restore-db.sh,staging-up.sh,staging-down.sh): no changes to docker compose calls — rootless Docker requires no sudoinfra/docs/vps-setup.md: documents rootless Docker as the implemented approach, records pre-check results, verification steps, and a before/after security properties tablePre-check results
unprivileged_userns_clone1✓newuidmapuidmapapt packageunshare --userTest plan
bootstrap-vps.shon the VPSsystemctl --user status dockershows active as deploy userdocker run hello-worldsucceeds without sudo as deploy userls /var/run/docker.sockis denied for deploy usergroupsdoes not includedockerfor deploy user