diff --git a/paseo/.env.example b/paseo/.env.example index 2f3591c..bdfaa43 100644 --- a/paseo/.env.example +++ b/paseo/.env.example @@ -2,7 +2,8 @@ # # cp .env.example .env -# Password for the daemon API and web UI. +# Password for the daemon API and web UI. Doubles as the paseo user's login +# password, so it is also what `sudo` asks for inside a terminal. # Generate one with: openssl rand -base64 24 PASEO_PASSWORD= diff --git a/paseo/Dockerfile b/paseo/Dockerfile index 877c073..dc9b6ce 100644 --- a/paseo/Dockerfile +++ b/paseo/Dockerfile @@ -7,11 +7,13 @@ FROM ghcr.io/getpaseo/paseo:latest # --- system packages ------------------------------------------------------- -# Everyday shell tooling. git and curl are already in the base image. +# Everyday shell tooling. git and curl are already in the base image; sudo is +# not, despite Debian's base-passwd shipping an (empty) sudo group. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - less jq unzip zip lsof psmisc ugrep bfs zsh \ - && rm -rf /var/lib/apt/lists/* + less jq unzip zip lsof psmisc ugrep bfs zsh sudo \ + && rm -rf /var/lib/apt/lists/* \ + && usermod -aG sudo paseo # --- python ---------------------------------------------------------------- # Debian 12 only carries 3.11, so fetch a standalone CPython build with uv @@ -49,3 +51,10 @@ RUN install -d -m 0755 /etc/apt/keyrings \ # it is not a Node wrapper. Do not swap it for the `curl | bash` installer, # which writes to $HOME/.local. Last because it changes most often. RUN npm install -g @anthropic-ai/claude-code + +# --- entrypoint ------------------------------------------------------------ +# Wraps the image's entrypoint to set the paseo user's password while still +# root -- see entrypoint.sh. tini stays in front of it, as in the base image. +# Last so that editing the script rebuilds only this layer. +COPY --chmod=0755 entrypoint.sh /usr/local/bin/paseo-sudo-entrypoint +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/paseo-sudo-entrypoint"] diff --git a/paseo/README.md b/paseo/README.md index 50e58b9..af3321c 100644 --- a/paseo/README.md +++ b/paseo/README.md @@ -31,7 +31,7 @@ the old entry is cached in `localStorage`. | Variable | Purpose | | --- | --- | -| `PASEO_PASSWORD` | Web UI and API login. Generate with `openssl rand -base64 24`. | +| `PASEO_PASSWORD` | Web UI and API login, and the `paseo` user's `sudo` password. Generate with `openssl rand -base64 24`. | | `PASEO_HOSTNAMES` | Domains allowed to reach the daemon, comma-separated. Your domain must be listed. | | `PASEO_TRUSTED_PROXIES` | Set to `uniquelocal`, or the UI loads but never connects. | | `PASEO_LABEL` | Container hostname. Paseo shows it as the host label in the UI; without it you get a random container ID. | @@ -71,7 +71,7 @@ but anything written outside `$HOME` (`chsh`, `apt install`) is lost on rebuild. | `gh` | GitHub's signed apt repo | Debian does not package it | | Go (`GO_VERSION`) | Official go.dev tarball | Debian 12 ships 1.19 | | Python (`PYTHON_VERSION`) | `uv python install` | Debian 12 ships 3.11 | -| `less jq unzip zip lsof psmisc ugrep bfs zsh` | apt | — | +| `less jq unzip zip lsof psmisc ugrep bfs zsh sudo` | apt | — | Bump a language with a build arg, e.g. `--build-arg GO_VERSION=1.27.1`. Add other agent providers to the `npm install` line: @@ -89,6 +89,14 @@ RUN npm install -g @anthropic-ai/claude-code @openai/codex opencode-ai a notice at startup. Rebuild to update. - The image stays root on purpose: the entrypoint chowns the volumes, then drops to the `paseo` user (uid 1000) with `gosu`. +- `sudo` prompts for `PASEO_PASSWORD`. `entrypoint.sh` wraps the image's own + entrypoint to set it — it has to run at start, since baking a password into a + layer would commit the secret and `/etc/shadow` is not on a volume, so it + reverts on every recreate. Leave `PASEO_PASSWORD` empty and the account stays + locked, which means no `sudo`. +- `sudo` resets `PATH` to its `secure_path`, which does not include + `/usr/local/go/bin`. `sudo go ...` therefore fails; use `sudo env PATH="$PATH" + go ...` or the full path. - Nothing installs into `$HOME`. That is `/home/paseo`, a volume mount that hides anything baked in at build time — hence `/opt/python` and `/usr/local/go` rather than the defaults. diff --git a/paseo/entrypoint.sh b/paseo/entrypoint.sh new file mode 100755 index 0000000..2711c21 --- /dev/null +++ b/paseo/entrypoint.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Sets the paseo user's login password from PASEO_PASSWORD, then hands off to +# the image's own entrypoint. +# +# Has to happen at start, not build: the password is a secret and would +# otherwise be baked into a layer. Has to happen here, not in a later hook: the +# base entrypoint ends in `exec gosu paseo`, so it never comes back, and by then +# we are no longer root and cannot write /etc/shadow. And it has to run every +# time -- /etc/shadow sits in the image, not in the /home/paseo volume, so it +# reverts on every container recreate. +set -euo pipefail + +if [[ "$(id -u)" == "0" && -n "${PASEO_PASSWORD:-}" ]]; then + # Piped rather than passed as an argument: arguments are visible in ps. + # chpasswd splits on the first colon, so a colon in the password is fine. + printf 'paseo:%s\n' "$PASEO_PASSWORD" | chpasswd +fi + +# With PASEO_PASSWORD unset the account keeps its locked password and sudo just +# refuses. The base entrypoint already warns about the missing variable. +exec /usr/local/bin/paseo-docker-entrypoint "$@"