fix: image uid
All checks were successful
build / image (push) Successful in 1m15s

This commit is contained in:
Antoine Pelletier 2026-07-30 00:14:55 +02:00
parent 34e1ae34db
commit fb47979787
3 changed files with 22 additions and 25 deletions

View file

@ -55,22 +55,25 @@ COPY --from=ghcr.io/amacneil/dbmate:2 /usr/local/bin/dbmate /usr/local/bin/dbmat
COPY db/migrations/ /app/db/migrations/
# debian:*-slim ships without /etc/nsswitch.conf, and glibc then cannot turn a
# uid back into a user record. libvirt does exactly that lookup before connecting
# and gives up with "Failed to find user record for uid", whatever the socket
# permissions are. Restore the plain file-based lookup.
# uid back into a user record. Restore the plain file-based lookup.
RUN printf 'passwd: files\ngroup: files\nshadow: files\nhosts: files dns\n' \
> /etc/nsswitch.conf
# Unprivileged, but it must land in the host's libvirt group to reach the
# socket: `group_add` in the compose file does that at run time.
RUN useradd --system --create-home --uid 10001 app
# Runs as root, on purpose. libvirt resolves the calling uid to a user record
# before it even opens the socket, and refused to do so for a custom uid here
# ("Failed to find user record for uid") even with /etc/passwd and the libvirt
# group both in place. Root always resolves.
#
# The privilege actually gained is small: any process that can reach the libvirt
# socket already starts and destroys the VMs of this host, which is the whole
# point of this container. It holds no other mount and no secret beyond the
# database password.
WORKDIR /app
COPY --from=backend /build/target/release/app-template /usr/local/bin/vm-selector
COPY --from=frontend /build/dist/ /app/frontend/
COPY docker/config.yml /etc/app-template/config.yml
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \

View file

@ -196,24 +196,22 @@ cargo sqlx prepare # dev database must be up and migrated
### Portainer stack
Deploy `docker-compose.yml` and set the four stack variables it documents at the top
(`IMAGE`, `POSTGRES_PASSWORD`, `LIBVIRT_GID`, `APP_PORT`). Get the group id from the
host:
```bash
getent group libvirt | cut -d: -f3
```
Deploy `docker-compose.yml` and set the stack variables it documents at the top
(`IMAGE`, `POSTGRES_PASSWORD`, `APP_PORT`).
Then create a webhook on the stack, **tick "Re-pull image"**, and put its url in the
`PORTAINER_WEBHOOK` secret of the repository. Without the re-pull, the webhook
recreates the containers from the layer already on disk and nothing changes.
The two things that make libvirt reachable from inside the container:
What makes libvirt reachable from inside the container: `/var/run/libvirt` is
bind-mounted from the host — the whole directory, since the socket is `libvirt-sock`
or `virtqemud-sock` depending on the host's setup — and the container runs as root.
- `/var/run/libvirt` bind-mounted from the host — the whole directory, since the
socket is `libvirt-sock` or `virtqemud-sock` depending on the host's setup
- the container in the host's `libvirt` group (`group_add`) — without it the socket
is visible and every call is denied
Running as root is not laziness about the group: with a custom uid, libvirt resolves
the caller to a user record before opening the socket and refused to do so here even
with `/etc/passwd` correct and the `libvirt` group added through `group_add`
(`Failed to find user record for uid '10001'`). What root actually adds is small —
anything that reaches this socket already controls every VM on the host.
### Domain name

View file

@ -4,8 +4,6 @@
# Environment variables to set in the Portainer stack:
# IMAGE registry.tibiscuit.ch/vm-selector:latest
# POSTGRES_PASSWORD anything, it never leaves the internal network
# LIBVIRT_GID the host's libvirt group id, from:
# getent group libvirt | cut -d: -f3
# APP_PORT host port for the reverse proxy to point at (default 3010)
services:
@ -59,10 +57,8 @@ services:
# runs the monolithic libvirtd or the split daemons, the app needs
# libvirt-sock or virtqemud-sock.
- /var/run/libvirt:/var/run/libvirt
# Reaching that socket requires being in the host's libvirt group. Without
# this the socket is visible and every call is denied.
group_add:
- '${LIBVIRT_GID:?see the header of this file}'
# No `group_add`: the container runs as root (see the Dockerfile for why), so
# it needs no group membership to reach the socket.
depends_on:
db:
condition: service_healthy