Run the container as a fixed non-root uid #47

Open
opened 2026-10-22 21:16:20 +00:00 by james.oates · 0 comments
Owner

Current state

The image sets no USER, so ER runs as root (uid 0, gid 0), verified on the v2.0.0 candidate:

$ docker run --rm --entrypoint sh edge-router -c 'id; stat -c "%U:%G %a %n" /app/data'
uid=0(root) gid=0(root) ...
root:root 755 /app/data

This is unchanged by the Alpine/musl switch — the Debian image ran as root too. Raised by Roku via Axianthe while preparing the Kyoshi deployment.

Why it should change

ER has no need for uid 0 at runtime. Inside the container it binds HTTP_PORT (default 80) and 443, and privileged ports only matter because the image defaults to them — a non-root ER can bind 8080/8443 inside the container and be published on 80/443 by the runtime, which is how Roku's deployment is shaped anyway.

Against that, running as root means a container escape starts as root, and the unencrypted certificate store's protection is now entirely file-mode based, so the uid that owns it matters more than it used to.

Why not in v2.0.0

A uid change is a breaking change to every existing deployment's data volume, and v2.0.0 already carries the TPM/OpenBao removal and the per-name certificate migration. Bundling a third thing that fails at first start — rather than at build or config time — is how a release becomes un-rollbackable. Axianthe's call, and I agree.

Host-side changes a consumer must make first

Whatever uid is chosen, every bind-mounted or named-volume path ER writes has to be owned by it before the first start, because ER will not create or chown what it cannot write:

  • The data volume (/app/data, holds the SQLite database, its -wal/-shm sidecars, every certificate and key, and the ACME account key) — chown -R <uid>:<gid>. ER sets umask(077) before opening the database and corrects the main file's mode, but it cannot fix ownership; a foreign owner is a deliberate hard error.
  • Any *_FILE secret — HTTPREQ_USERNAME_FILE, HTTPREQ_PASSWORD_FILE, MASTER_API_KEY_FILE. ER reads these without inspecting mode or owner (see 79a7171), so a root-owned 0400 secret becomes unreadable the moment ER is not root. Roku's is exactly this: root-owned 0400. Either re-own it to the new uid or set the Compose secret's uid/mode.
  • Config and static mounts are read-only and need read access for the new uid, which 0644 already gives.
  • [[static_sites]] directories — read access for the new uid.
  • A /dev/log or journal socket, if a deployment wires one.

Decisions to make

  • A fixed numeric uid/gid, not a name, so it is stable across rebuilds and matchable on the host. Pick one and document it as part of the contract.
  • Whether the image chowns /app/data at build time (helps a fresh named volume, does nothing for an existing one or a bind mount).
  • Whether to keep the default ports at 80/443 or move them to 8080/8443 — moving them makes non-root work out of the box but is a second breaking change, so probably keep the ports and let the operator publish them.
  • A release note telling operators to chown before upgrading, and what happens if they do not: ER refuses to start with the key-store ownership error rather than running degraded.

For v2.0.0

Nothing to do in the code. The release notes must state the image's USER and uid explicitly — currently root/0 — so Roku and anyone else can prepare the host. Tracked here so that statement is a documented current state rather than an omission.

## Current state The image sets no `USER`, so ER runs as **root (uid 0, gid 0)**, verified on the v2.0.0 candidate: ``` $ docker run --rm --entrypoint sh edge-router -c 'id; stat -c "%U:%G %a %n" /app/data' uid=0(root) gid=0(root) ... root:root 755 /app/data ``` This is unchanged by the Alpine/musl switch — the Debian image ran as root too. Raised by Roku via Axianthe while preparing the Kyoshi deployment. ## Why it should change ER has no need for uid 0 at runtime. Inside the container it binds `HTTP_PORT` (default 80) and 443, and privileged ports only matter because the image defaults to them — a non-root ER can bind 8080/8443 inside the container and be published on 80/443 by the runtime, which is how Roku's deployment is shaped anyway. Against that, running as root means a container escape starts as root, and the unencrypted certificate store's protection is now entirely file-mode based, so the uid that owns it matters more than it used to. ## Why not in v2.0.0 A uid change is a breaking change to every existing deployment's data volume, and v2.0.0 already carries the TPM/OpenBao removal and the per-name certificate migration. Bundling a third thing that fails at first start — rather than at build or config time — is how a release becomes un-rollbackable. Axianthe's call, and I agree. ## Host-side changes a consumer must make first Whatever uid is chosen, every bind-mounted or named-volume path ER writes has to be owned by it *before* the first start, because ER will not create or chown what it cannot write: - **The data volume** (`/app/data`, holds the SQLite database, its `-wal`/`-shm` sidecars, every certificate and key, and the ACME account key) — `chown -R <uid>:<gid>`. ER sets `umask(077)` before opening the database and corrects the main file's mode, but it cannot fix ownership; a foreign owner is a deliberate hard error. - **Any `*_FILE` secret** — `HTTPREQ_USERNAME_FILE`, `HTTPREQ_PASSWORD_FILE`, `MASTER_API_KEY_FILE`. ER reads these without inspecting mode or owner (see 79a7171), so a root-owned 0400 secret becomes unreadable the moment ER is not root. Roku's is exactly this: root-owned 0400. Either re-own it to the new uid or set the Compose secret's `uid`/`mode`. - **Config and static mounts** are read-only and need read access for the new uid, which 0644 already gives. - **`[[static_sites]]` directories** — read access for the new uid. - **A `/dev/log` or journal socket**, if a deployment wires one. ## Decisions to make - A **fixed numeric uid/gid**, not a name, so it is stable across rebuilds and matchable on the host. Pick one and document it as part of the contract. - Whether the image `chown`s `/app/data` at build time (helps a fresh named volume, does nothing for an existing one or a bind mount). - Whether to keep the default ports at 80/443 or move them to 8080/8443 — moving them makes non-root work out of the box but is a second breaking change, so probably keep the ports and let the operator publish them. - A release note telling operators to chown before upgrading, and what happens if they do not: ER refuses to start with the key-store ownership error rather than running degraded. ## For v2.0.0 Nothing to do in the code. The release notes must state the image's `USER` and uid explicitly — currently root/0 — so Roku and anyone else can prepare the host. Tracked here so that statement is a documented current state rather than an omission.
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
IsoHex/edge-router#47
No description provided.