Skip to content

chore: harden VPS deploy user — rootless Docker or narrowed sudoers #129

@GitAddRemote

Description

@GitAddRemote

Tech Story

As a platform engineer, I want the deploy user on the VPS to have the minimum permissions needed to run Docker — and nothing more — so that a compromised deploy SSH key cannot be used to escalate to root or damage unrelated services on the server.

ELI5 Context

Why is this a security concern?
The deploy SSH key is stored in GitHub Secrets and used by GitHub Actions on every deployment. If that key were ever leaked — via a misconfigured workflow, a compromised GitHub account, or a supply chain attack on a GitHub Action — an attacker with that key should only be able to do deploy-related things, not wipe the server or read other users' files.

What is rootless Docker?
Normally, the Docker daemon runs as root. Even if your container runs as a non-root user, the Docker socket (/var/run/docker.sock) is a root-owned resource. Anyone with access to it can effectively become root on the host. Rootless Docker runs the entire Docker daemon as an unprivileged user — no root involved at any step. It's the cleanest solution.

What is a sudoers restriction?
If rootless Docker isn't viable, the fallback is to give the deploy user sudo access to only specific commands — exactly the docker compose commands it needs. This is done by creating a file in /etc/sudoers.d/. It's less elegant than rootless Docker but still far better than NOPASSWD: ALL.

How do I know which option to use?
Try Option A first. Run dockerd-rootless-setuptool.sh check on the VPS — if it passes all checks, use Option A. If it reports kernel feature gaps (common on older Linode kernels), use Option B.

Technical Elaboration

Pre-check: which option is viable?

SSH into the VPS as the deploy user and run:

curl -fsSL https://get.docker.com/rootless | sh --dry-run

If output is clean, proceed with Option A. If errors about newuidmap or kernel namespaces appear, use Option B.


Option A — Rootless Docker (preferred)

On the VPS, logged in as the deploy user:

# Install rootless Docker
curl -fsSL https://get.docker.com/rootless | sh

# Add to ~/.bashrc (so PATH is set correctly on SSH login)
echo 'export PATH=/home/deploy/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
source ~/.bashrc

# Enable Docker to start on boot without a login session
loginctl enable-linger deploy

# Start the rootless Docker service
systemctl --user enable docker
systemctl --user start docker

# Verify
docker run hello-world

Update bootstrap-vps.sh from #107 to include these steps so the setup is reproducible.

In .github/workflows/release.yml: remove any sudo prefix from docker compose commands in SSH steps.


Option B — Narrowed sudoers (fallback)

On the VPS, logged in as root:

# Create sudoers file for deploy user
cat > /etc/sudoers.d/deploy << 'SUDOEOF'
deploy ALL=(ALL) NOPASSWD: /usr/bin/docker compose -f /opt/station/docker-compose.prod.yml *
deploy ALL=(ALL) NOPASSWD: /usr/bin/docker compose -f /opt/station/docker-compose.staging.yml *
deploy ALL=(ALL) NOPASSWD: /usr/bin/docker exec station-postgres-1 pg_dump *
deploy ALL=(ALL) NOPASSWD: /usr/bin/docker exec station-postgres-1 psql *
SUDOEOF

chmod 440 /etc/sudoers.d/deploy

# Validate — a bad sudoers file can lock you out
visudo -c -f /etc/sudoers.d/deploy

# Remove any NOPASSWD: ALL line if it exists
visudo

Verify the restriction works:

# As deploy user — should work:
sudo docker compose -f /opt/station/docker-compose.prod.yml ps

# As deploy user — should fail:
sudo apt install anything

New file: infra/docs/vps-setup.md

Document the chosen option with:

  1. Prerequisites — Linode VPS running Ubuntu 22.04+, deploy user exists (from Tech Story: VPS baseline provisioning (Nginx, Certbot, deploy user) #107)
  2. Which option was chosen and why — record the result of the pre-check
  3. Exact commands run — copy from the option above
  4. Verification steps — what to run to confirm it's working
  5. How to reproduce on a fresh VPS — step-by-step from zero

Definition of Done

  • Pre-check run — result documented in infra/docs/vps-setup.md
  • Either Option A or Option B implemented and tested — choice documented with reasoning
  • Option A: docker run hello-world succeeds without sudo as deploy user; systemctl --user status docker shows active
  • Option B: sudo docker compose ... ps works; sudo rm -rf / is denied; no NOPASSWD: ALL in sudoers
  • Deploy workflow runs end-to-end without sudo issues — verified by pushing a test release branch
  • infra/scripts/bootstrap-vps.sh updated with chosen approach
  • infra/docs/vps-setup.md written with exact commands, verification steps, and reproduction guide
  • .github/workflows/release.yml SSH commands updated to remove or add sudo as appropriate

Dependencies

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions