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/ COPY db/migrations/ /app/db/migrations/
# debian:*-slim ships without /etc/nsswitch.conf, and glibc then cannot turn a # 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 # uid back into a user record. Restore the plain file-based lookup.
# and gives up with "Failed to find user record for uid", whatever the socket
# permissions are. Restore the plain file-based lookup.
RUN printf 'passwd: files\ngroup: files\nshadow: files\nhosts: files dns\n' \ RUN printf 'passwd: files\ngroup: files\nshadow: files\nhosts: files dns\n' \
> /etc/nsswitch.conf > /etc/nsswitch.conf
# Unprivileged, but it must land in the host's libvirt group to reach the # Runs as root, on purpose. libvirt resolves the calling uid to a user record
# socket: `group_add` in the compose file does that at run time. # before it even opens the socket, and refused to do so for a custom uid here
RUN useradd --system --create-home --uid 10001 app # ("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 WORKDIR /app
COPY --from=backend /build/target/release/app-template /usr/local/bin/vm-selector COPY --from=backend /build/target/release/app-template /usr/local/bin/vm-selector
COPY --from=frontend /build/dist/ /app/frontend/ COPY --from=frontend /build/dist/ /app/frontend/
COPY docker/config.yml /etc/app-template/config.yml COPY docker/config.yml /etc/app-template/config.yml
USER app
EXPOSE 3000 EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ 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 ### Portainer stack
Deploy `docker-compose.yml` and set the four stack variables it documents at the top Deploy `docker-compose.yml` and set the stack variables it documents at the top
(`IMAGE`, `POSTGRES_PASSWORD`, `LIBVIRT_GID`, `APP_PORT`). Get the group id from the (`IMAGE`, `POSTGRES_PASSWORD`, `APP_PORT`).
host:
```bash
getent group libvirt | cut -d: -f3
```
Then create a webhook on the stack, **tick "Re-pull image"**, and put its url in the 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 `PORTAINER_WEBHOOK` secret of the repository. Without the re-pull, the webhook
recreates the containers from the layer already on disk and nothing changes. 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 Running as root is not laziness about the group: with a custom uid, libvirt resolves
socket is `libvirt-sock` or `virtqemud-sock` depending on the host's setup the caller to a user record before opening the socket and refused to do so here even
- the container in the host's `libvirt` group (`group_add`) — without it the socket with `/etc/passwd` correct and the `libvirt` group added through `group_add`
is visible and every call is denied (`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 ### Domain name

View file

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