feat(paseo): give the paseo user sudo, authenticated with PASEO_PASSWORD

The base image installs no sudo and leaves paseo out of the sudo group, so
add both. The password cannot be baked in at build time -- it is a secret and
would land in a layer -- and it cannot be set after the base entrypoint, which
ends in `exec gosu paseo` and never returns. Wrap that entrypoint instead and
set the password while still root, on every start: /etc/shadow lives in the
image, not on the /home/paseo volume, so it reverts on each recreate.
This commit is contained in:
tiennm99 committed 2026-09-16 17:21:11 +07:00
1 parent 914474953f
commit cb1d38f147
4 files changed
+45 -6

No files matched your search

+2 -1
View File
@@ -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=
+12 -3
View File
@@ -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"]
+10 -2
View File
@@ -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.
+21
View File
@@ -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 "$@"