v0.10.00-beta - Server Up draait niet meer als root
Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 2m8s
Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 2m8s
- bij de installatie wordt gevraagd onder welk account Server Up draait: een nieuw systeemaccount 'serverup', het account waarmee je werkt, een bestaand account, of root zoals voorheen (--user NAAM slaat de vraag over) - nieuw docker-entrypoint.sh: start als root, zet /data, stacks en backups klaar, en zakt met setpriv af naar SU_UID:SU_GID met de capabilities die nodig blijven (chown, dac_override, fowner - alle drie al in Docker's standaardset) - appdata blijft daarbij bewust ongemoeid; die mappen zijn van de apps zelf - de gid van de docker-socket wordt uit de socket zelf gelezen, zodat het account niet in de docker-groep hoeft (dat zou root op de host geven) - installatieformulier vult PUID/PGID met de ids waaronder Server Up draait in plaats van de 1000 die 44 sjablonen blind noemen - een onleesbaar bestand laat de rest van de backup niet meer sneuvelen; wat ontbreekt komt in het log en in de metadata (skipped) - terugzetten herstelt het eigenaarschap van appdata, dat tarfile met filter="data" laat vallen - CI controleert in een echte container dat setpriv bestaat, dat er wordt afgezakt, dat de socket-gid meekomt, dat /data schrijfbaar is en dat SU_UID=0 root laat blijven - SU_UID=0 in .env is de ontsnappingsklep naar het oude gedrag Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
This commit is contained in:
parent
f4b652e182
commit
fc2e80fbda
16 changed files with 998 additions and 9 deletions
|
|
@ -42,6 +42,15 @@ PORT=5000
|
|||
# waar hij stond.
|
||||
#BASE_DIR=/opt/serverup
|
||||
|
||||
# Onder welk account Server Up draait. install.sh vult dit in; 0 (of leeg) is
|
||||
# root, zoals vóór v0.10. De container start altijd als root — dat moet, want
|
||||
# het docker-volume is van root — en zakt daarna af naar deze uid/gid.
|
||||
#
|
||||
# Loop je ergens tegenaan, zet ze dan allebei op 0 en draai `docker compose up -d`.
|
||||
# Zie docs/beveiliging.md voor wat dit wel en niet oplevert.
|
||||
#SU_UID=0
|
||||
#SU_GID=0
|
||||
|
||||
# Zet op 1 zodra alles via HTTPS loopt: de sessiecookie krijgt dan de
|
||||
# Secure-vlag.
|
||||
#SU_HTTPS=1
|
||||
|
|
|
|||
|
|
@ -162,6 +162,49 @@ jobs:
|
|||
docker compose build --pull
|
||||
docker tag "server-up:${VERSION}" "server-up:${CHANNEL}" || true
|
||||
|
||||
# Server Up draait niet meer als root. Dat hangt aan het entrypoint en aan
|
||||
# `setpriv`, en beide zijn alleen in een echte container te toetsen — de
|
||||
# pytest-suite kan er niet bij. Zonder deze stap merk je een regressie hier
|
||||
# pas als iemands installatie niet meer opkomt.
|
||||
- name: Niet-root modus controleren
|
||||
run: |
|
||||
set -euo pipefail
|
||||
IMG="server-up:${VERSION}"
|
||||
|
||||
echo "1. setpriv aanwezig in het image"
|
||||
docker run --rm --entrypoint setpriv "$IMG" --help >/dev/null
|
||||
echo " ok"
|
||||
|
||||
echo "2. zakt af naar de opgegeven gebruiker"
|
||||
uit=$(docker run --rm -e SU_UID=1000 -e SU_GID=1000 \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock "$IMG" id)
|
||||
echo " $uit"
|
||||
case "$uit" in
|
||||
uid=1000*) ;;
|
||||
*) echo "FOUT: draait niet als uid 1000"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "3. zit in de groep van de docker-socket"
|
||||
sok_gid=$(stat -c %g /var/run/docker.sock)
|
||||
case "$uit" in
|
||||
*"$sok_gid"*) echo " gid $sok_gid aanwezig" ;;
|
||||
*) echo "FOUT: gid $sok_gid van de socket ontbreekt"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "4. kan schrijven in /data"
|
||||
docker run --rm -e SU_UID=1000 -e SU_GID=1000 \
|
||||
-v "su-nietroot-proef-${VERSION}:/data" "$IMG" \
|
||||
sh -c 'touch /data/proef && echo " ok"'
|
||||
docker volume rm "su-nietroot-proef-${VERSION}" >/dev/null
|
||||
|
||||
echo "5. zonder SU_UID blijft het root — de ontsnappingsklep"
|
||||
uit0=$(docker run --rm "$IMG" id)
|
||||
echo " $uit0"
|
||||
case "$uit0" in
|
||||
uid=0*) ;;
|
||||
*) echo "FOUT: SU_UID=0 hoort root te blijven"; exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
|
|
|||
50
CHANGELOG.md
50
CHANGELOG.md
|
|
@ -1,3 +1,53 @@
|
|||
# v0.10.00-beta — Server Up draait niet meer als root
|
||||
|
||||
Er stond `user: "0:0"` in `docker-compose.yml` en nergens waarom. Dat is nu een
|
||||
keuze die je bij de installatie maakt.
|
||||
|
||||
**Bij het installeren wordt gevraagd onder welk account het moet draaien**: een
|
||||
nieuw systeemaccount `serverup` (aanbevolen, wordt aangemaakt zonder shell en
|
||||
zonder wachtwoord), het account waarmee je werkt, een bestaand account uit een
|
||||
lijst, of root zoals voorheen. Met `--user NAAM` of `--yes` sla je de vraag over.
|
||||
Een bestaande installatie krijgt de vraag één keer bij `--update`; daarna blijft
|
||||
je keuze staan.
|
||||
|
||||
**De container start nog steeds als root en zakt daarna af.** Dat moet: Docker
|
||||
maakt het `su-data`-volume als root aan, en een container die meteen als een
|
||||
gewone gebruiker start komt niet eens tot zijn configuratie. Het nieuwe
|
||||
`docker-entrypoint.sh` zet `/data`, `stacks` en `backups` klaar en gebruikt dan
|
||||
`setpriv` — niet `gosu`, want een proces dat met `setuid` afzakt verliest zijn
|
||||
capabilities, en die zijn hier nodig. `appdata` blijft ongemoeid: die mappen zijn
|
||||
van de apps zelf, en een `chown` daaroverheen breekt precies de containers die
|
||||
als hun eigen uid draaien.
|
||||
|
||||
Het afgezakte proces houdt `CHOWN`, `DAC_OVERRIDE` en `FOWNER`. Alle drie zitten
|
||||
al in de standaardset van Docker, dus de container krijgt geen enkel recht bij —
|
||||
er gaan er alleen af. Wat dat wél en níét oplevert staat in
|
||||
`docs/beveiliging.md`; kort gezegd beschermt het tegen bugs en ongelukken, niet
|
||||
tegen iemand die de interface overneemt. Die heeft de docker-socket, en dat
|
||||
blijft root. Een socket-proxy helpt daar niet tegen, omdat het dóél van deze app
|
||||
is om containers met willekeurige bind mounts aan te maken.
|
||||
|
||||
**Twee rechten-fouten die hierdoor aan het licht kwamen, zijn ook verholpen:**
|
||||
|
||||
- Het installatieformulier zette `PUID`/`PGID` op wat het sjabloon toevallig
|
||||
noemde — 44 sjablonen staan blind op `1000`. Nu krijgen ze de ids waaronder
|
||||
Server Up draait, zodat appdata van hem is en hij hem kan inpakken.
|
||||
- Eén onleesbaar bestand liet de héle backup falen: de `tar.add` van de
|
||||
appdata-map zat in dezelfde `try` als de rest. Nu gaat de rest gewoon mee en
|
||||
komt wat ontbreekt in het log én in de metadata (`skipped`) te staan. Een
|
||||
archief dat compleet lijkt maar het niet is, is erger dan een archief dat zegt
|
||||
wat het mist.
|
||||
- Terugzetten gaf alles aan wie uitpakte: `tarfile` met `filter="data"` (PEP 706)
|
||||
laat uid en gid uit het archief vallen. De appdata krijgt nu de `PUID`/`PGID`
|
||||
uit de metadata terug, anders die van Server Up zelf.
|
||||
|
||||
De CI controleert vanaf nu in een echte container dat `setpriv` bestaat, dat er
|
||||
wordt afgezakt, dat de gid van de docker-socket meekomt, dat `/data` schrijfbaar
|
||||
is, en dat `SU_UID=0` root laat blijven.
|
||||
|
||||
**Terug naar de oude situatie:** zet `SU_UID=0` en `SU_GID=0` in je `.env` en
|
||||
draai `docker compose up -d`.
|
||||
|
||||
# v0.9.00-beta — Eén hoofdmap, en je data mag mee verhuizen
|
||||
|
||||
**Server Up stelde `/opt/serverup` voor, wat je ook instelde.** Dat was geen
|
||||
|
|
|
|||
15
Dockerfile
15
Dockerfile
|
|
@ -11,9 +11,14 @@ ARG SU_VERSION=dev
|
|||
LABEL org.opencontainers.image.title="Server Up" \
|
||||
org.opencontainers.image.version="${SU_VERSION}" \
|
||||
org.opencontainers.image.source="https://git.example.com/bes-r/server-up"
|
||||
# util-linux levert `setpriv`, waarmee het entrypoint naar een gewone gebruiker
|
||||
# afzakt zonder de capabilities te verliezen die Server Up nodig heeft. Het is
|
||||
# een Essential-pakket en dus al aanwezig, maar expliciet noemen zorgt dat een
|
||||
# uitgeklede basis-image het niet stilletjes wegbezuinigt.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git openssh-client curl tar gzip ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
git openssh-client curl tar gzip ca-certificates util-linux passwd \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& setpriv --help >/dev/null
|
||||
|
||||
# Install Docker CLI + docker-compose inside container
|
||||
RUN DPKG_ARCH=$(dpkg --print-architecture) \
|
||||
|
|
@ -69,6 +74,12 @@ ENV PYTHONUNBUFFERED=1 PORT=5000 HOME=/root \
|
|||
SU_CONFIG=/data/config.json SU_AUDIT=/data/audit.db SU_GIT_CACHE=/data/git \
|
||||
SU_SECRET=/data/secret.key \
|
||||
GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/data/known_hosts"
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
EXPOSE 5000
|
||||
HEALTHCHECK --interval=30s --timeout=8s --start-period=15s CMD curl -fs http://localhost:5000/healthz || exit 1
|
||||
# Het entrypoint start als root, zet /data klaar en zakt daarna af naar
|
||||
# SU_UID:SU_GID. Zonder die variabelen verandert er niets: dan blijft het root.
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD ["python","app.py"]
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.9.00-beta
|
||||
0.10.00-beta
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ services:
|
|||
image: ${SU_IMAGE:-server-up}:${SU_TAG:-latest}
|
||||
container_name: server-up
|
||||
hostname: server-up
|
||||
# De container *start* als root — dat moet, want Docker maakt het su-data
|
||||
# volume als root aan en daar moet het entrypoint eerst in kunnen. Het zakt
|
||||
# daarna af naar SU_UID:SU_GID (zie docker-entrypoint.sh). Laat je die leeg,
|
||||
# dan blijft Server Up als root draaien zoals voorheen.
|
||||
user: "0:0"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
|
@ -36,6 +40,11 @@ services:
|
|||
- SU_AUDIT=/data/audit.db
|
||||
- SU_GIT_CACHE=/data/git
|
||||
- SU_CONTAINER=server-up
|
||||
# Onder welk account Server Up draait. install.sh vult dit in; leeg of 0
|
||||
# betekent root. De container krijgt hiermee géén extra rechten — er gaan
|
||||
# er alleen af. Zie docs/beveiliging.md.
|
||||
- SU_UID=${SU_UID:-0}
|
||||
- SU_GID=${SU_GID:-0}
|
||||
# Dezelfde waarde als de mount hierboven, zodat de app zijn standaard
|
||||
# paden (stacks, appdata, backups) onder de juiste map zoekt. Zonder dit
|
||||
# mount je /srv terwijl de app /opt/serverup blijft voorstellen.
|
||||
|
|
|
|||
112
docker-entrypoint.sh
Executable file
112
docker-entrypoint.sh
Executable file
|
|
@ -0,0 +1,112 @@
|
|||
#!/bin/sh
|
||||
# Start Server Up onder een gewone gebruiker in plaats van als root.
|
||||
#
|
||||
# Waarom dit niet gewoon `user:` in compose is: Docker maakt het `su-data`-volume
|
||||
# als root aan. Een container die meteen als uid 1000 start kan daar niet in
|
||||
# schrijven en komt niet eens tot de configuratie. Dit script begint dus als
|
||||
# root, zet klaar wat klaargezet moet worden, en zakt daarna pas af.
|
||||
#
|
||||
# En waarom `setpriv` en niet `gosu`: een proces dat met setuid afzakt verliest
|
||||
# zijn capabilities. Server Up moet daarna nog steeds bestanden kunnen chownen
|
||||
# (bij het verhuizen van mappen en het terugzetten van backups) en appdata van
|
||||
# andere gebruikers kunnen lezen (bij het maken van backups). `setpriv` kan die
|
||||
# rechten als *ambient* capability meegeven, zodat ze de overgang overleven.
|
||||
#
|
||||
# SU_UID leeg of 0 → alles blijft zoals het was: draaien als root.
|
||||
set -e
|
||||
|
||||
log() { printf ' %s\n' "$1" >&2; }
|
||||
|
||||
SU_UID="${SU_UID:-0}"
|
||||
SU_GID="${SU_GID:-$SU_UID}"
|
||||
|
||||
case "$SU_UID:$SU_GID" in
|
||||
*[!0-9:]*)
|
||||
log "SU_UID/SU_GID moeten getallen zijn ('$SU_UID:$SU_GID'); ik negeer ze."
|
||||
exec "$@" ;;
|
||||
esac
|
||||
|
||||
# ── Als root draaien: het gedrag van vóór deze versie ────────────────────────
|
||||
if [ "$SU_UID" = "0" ] || [ -z "$SU_UID" ]; then
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
# Zelf al niet-root? Dan is er niets om af te zakken en missen we de rechten om
|
||||
# iets klaar te zetten. Gewoon doorgaan is beter dan weigeren.
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
log "Draait al als uid $(id -u); SU_UID wordt genegeerd."
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
# ── Een naam bij het nummer ──────────────────────────────────────────────────
|
||||
# git en ssh willen een echte gebruiker in /etc/passwd met een $HOME, anders
|
||||
# komt er "No such device or address" uit een clone.
|
||||
if ! getent group "$SU_GID" >/dev/null 2>&1; then
|
||||
groupadd --gid "$SU_GID" serverup 2>/dev/null || true
|
||||
fi
|
||||
if ! getent passwd "$SU_UID" >/dev/null 2>&1; then
|
||||
useradd --uid "$SU_UID" --gid "$SU_GID" --no-create-home \
|
||||
--home-dir /data --shell /usr/sbin/nologin serverup 2>/dev/null || true
|
||||
fi
|
||||
GEBRUIKER="$(getent passwd "$SU_UID" | cut -d: -f1)"
|
||||
GEBRUIKER="${GEBRUIKER:-$SU_UID}"
|
||||
|
||||
# ── Wat van ons is, overnemen ────────────────────────────────────────────────
|
||||
# /data is het volume met de configuratie, het auditlog en de git-cache.
|
||||
chown -R "$SU_UID:$SU_GID" /data 2>/dev/null || log "Kon /data niet overnemen."
|
||||
cp -n /root/.gitconfig /data/.gitconfig 2>/dev/null || true
|
||||
chown "$SU_UID:$SU_GID" /data/.gitconfig 2>/dev/null || true
|
||||
|
||||
# De stacks en de backups maakt Server Up zelf aan, dus die mogen van hem zijn.
|
||||
#
|
||||
# appdata NIET. Die mappen zijn van de apps: postgres draait als 999, een
|
||||
# linuxserver-image als de PUID die jij hebt ingevuld. Er een chown overheen
|
||||
# halen breekt precies die containers. Server Up leest ze via CAP_DAC_OVERRIDE.
|
||||
if [ -n "$BASE_DIR" ]; then
|
||||
for sub in stacks backups; do
|
||||
[ -d "$BASE_DIR/$sub" ] || continue
|
||||
chown -R "$SU_UID:$SU_GID" "$BASE_DIR/$sub" 2>/dev/null \
|
||||
|| log "Kon $BASE_DIR/$sub niet overnemen."
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Bij de docker-socket kunnen ──────────────────────────────────────────────
|
||||
# De gid daarvan verschilt per host, dus die lezen we hier uit in plaats van hem
|
||||
# in compose te moeten hardcoderen. De gebruiker wordt lid van die groep, want
|
||||
# `setpriv --groups` en `--init-groups` sluiten elkaar uit — via /etc/group
|
||||
# krijgen we allebei.
|
||||
# Overschrijfbaar voor rootless Docker en Podman, die de socket elders zetten.
|
||||
SOCK="${SU_DOCKER_SOCK:-/var/run/docker.sock}"
|
||||
if [ -S "$SOCK" ]; then
|
||||
SOCK_GID="$(stat -c %g "$SOCK" 2>/dev/null || echo '')"
|
||||
if [ -n "$SOCK_GID" ] && [ "$SOCK_GID" != "$SU_GID" ]; then
|
||||
SOCK_GROEP="$(getent group "$SOCK_GID" | cut -d: -f1)"
|
||||
if [ -z "$SOCK_GROEP" ]; then
|
||||
SOCK_GROEP=dockersock
|
||||
groupadd --gid "$SOCK_GID" "$SOCK_GROEP" 2>/dev/null || true
|
||||
fi
|
||||
usermod -aG "$SOCK_GROEP" "$GEBRUIKER" 2>/dev/null \
|
||||
|| log "Kon $GEBRUIKER niet in groep $SOCK_GROEP zetten."
|
||||
fi
|
||||
else
|
||||
log "Geen docker-socket op $SOCK; Server Up kan straks niets starten."
|
||||
fi
|
||||
|
||||
# ── Afzakken ─────────────────────────────────────────────────────────────────
|
||||
# chown → eigenaarschap meenemen bij het verhuizen van mappen en het
|
||||
# terugzetten van backups
|
||||
# dac_override, fowner → appdata lezen die van een andere gebruiker is
|
||||
#
|
||||
# Alle drie zitten al in de standaardset van Docker; er komt hier dus geen
|
||||
# enkel recht bij dat de container niet al had. Wat weggaat is de rest van root.
|
||||
CAPS="chown,dac_override,fowner"
|
||||
|
||||
if ! command -v setpriv >/dev/null 2>&1; then
|
||||
log "setpriv ontbreekt in dit image; ik draai als root verder."
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
log "Server Up draait als $GEBRUIKER ($SU_UID:$SU_GID)."
|
||||
export HOME=/data
|
||||
exec setpriv --reuid "$SU_UID" --regid "$SU_GID" --init-groups \
|
||||
--inh-caps "+$CAPS" --ambient-caps "+$CAPS" "$@"
|
||||
|
|
@ -144,11 +144,79 @@ Een externe wachtwoordkluis (Vault, Infisical) raden we hier af: Server Up zou
|
|||
dan afhangen van een app die het zelf beheert, en als die niet draait start er
|
||||
niets meer.
|
||||
|
||||
## Onder welk account Server Up draait
|
||||
|
||||
Tot v0.9 draaide Server Up als root. Vanaf v0.10 kies je bij de installatie een
|
||||
account — standaard een nieuw systeemaccount `serverup` zonder shell en zonder
|
||||
wachtwoord — en zakt de container daarnaartoe af.
|
||||
|
||||
```bash
|
||||
sh install.sh --user serverup # aanbevolen, wordt zo nodig aangemaakt
|
||||
sh install.sh --user ramon # een bestaand account
|
||||
sh install.sh --user root # de oude situatie
|
||||
```
|
||||
|
||||
Zonder `--user` wordt ernaar gevraagd, met een lijst van bruikbare accounts. De
|
||||
keuze belandt als `SU_UID`/`SU_GID` in je `.env`. Een bestaande installatie
|
||||
bijwerken met `--update` vraagt het één keer; daarna blijft je keuze staan.
|
||||
|
||||
### Waarom de container tóch als root start
|
||||
|
||||
Docker maakt het `su-data`-volume als root aan. Een container die meteen als een
|
||||
gewone gebruiker start, kan daar niet in schrijven en komt niet eens tot zijn
|
||||
configuratie. `docker-entrypoint.sh` begint daarom als root, zet `/data` en de
|
||||
mappen `stacks` en `backups` klaar, en zakt dan af met `setpriv`.
|
||||
|
||||
`appdata` blijft daarbij bewust ongemoeid. Die mappen zijn van de apps zelf —
|
||||
postgres draait als 999, een linuxserver-image als de PUID die jij hebt
|
||||
ingevuld. Er een `chown` overheen halen breekt precies die containers.
|
||||
|
||||
### Wat het wel en niet oplevert
|
||||
|
||||
Het afgezakte proces houdt drie capabilities: `CHOWN`, `DAC_OVERRIDE` en
|
||||
`FOWNER`. Die heeft het nodig om appdata van andere gebruikers in te pakken voor
|
||||
een backup, en om het eigenaarschap goed te zetten bij het verhuizen van mappen
|
||||
en het terugzetten van een backup. Alle drie zitten al in de standaardset van
|
||||
Docker: de container krijgt dus geen enkel recht bij, er gaan er alleen af.
|
||||
|
||||
**Wees niet gerust op meer dan dit.** Met `DAC_OVERRIDE` kan het proces nog
|
||||
steeds elk bestand onder de gekoppelde mappen lezen en schrijven, en het heeft
|
||||
nog steeds de docker-socket. Wat je wint:
|
||||
|
||||
- bestanden die Server Up aanmaakt zijn van jouw account, niet van root
|
||||
- een echte uid om op te filteren in systeemlogs
|
||||
- geen poorten onder 1024, geen kernelmodules, geen systeemtijd, geen mount
|
||||
|
||||
Wat je **niet** wint: bescherming tegen iemand die Server Up overneemt. Die heeft
|
||||
de socket, en dat blijft root op de host. Zie hieronder.
|
||||
|
||||
### Zet je account niet in de `docker`-groep
|
||||
|
||||
Dat lijkt handig maar geeft dat account root op de machine: wie bij de socket kan,
|
||||
start een container die `/` mount. Het is ook niet nodig — het entrypoint leest de
|
||||
gid van de socket zelf uit en maakt de gebruiker daar in de container lid van.
|
||||
|
||||
### Terug naar root
|
||||
|
||||
Loop je ergens tegenaan, zet dan in je `.env`:
|
||||
|
||||
```
|
||||
SU_UID=0
|
||||
SU_GID=0
|
||||
```
|
||||
|
||||
en draai `docker compose up -d`. Alles werkt dan zoals vóór v0.10.
|
||||
|
||||
## Wat níét is afgedekt
|
||||
|
||||
- **De docker-socket zelf.** Server Up heeft volledige toegang tot de daemon; dat
|
||||
is inherent aan wat het doet. Een socket-proxy die alleen bepaalde endpoints
|
||||
toelaat werkt niet, omdat compose vrijwel alles nodig heeft.
|
||||
toelaat werkt hier niet: het dóél van deze app is containers met willekeurige
|
||||
bind mounts aanmaken, dus een proxy die `POST /containers/create` doorlaat laat
|
||||
de ontsnapping gewoon door. Voor een monitoringtool als Traefik is zo'n proxy
|
||||
wél zinvol; voor een Docker-beheerder niet.
|
||||
- **Niet als root draaien lost dit niet op.** Zie hierboven: het beschermt tegen
|
||||
bugs en ongelukken, niet tegen misbruik van de interface.
|
||||
- **Rate limiting op de API** buiten de login-lockout om. Zet er zo nodig een
|
||||
proxy met rate limiting voor.
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ schrijven én `docker` aanroepen, dan blijft alles onder je eigen account.
|
|||
--bind ADRES Waarop de poort luistert (standaard: 127.0.0.1)
|
||||
--branch NAAM Branch om te installeren (standaard: main)
|
||||
--token TOKEN Toegangstoken, als je repo niet openbaar is
|
||||
--user NAAM Account waaronder Server Up draait ('serverup' wordt zo nodig
|
||||
aangemaakt; 'root' is de oude situatie)
|
||||
--admin NAAM Maak meteen een beheerdersaccount met deze naam
|
||||
--admin-password-file PAD
|
||||
Lees het wachtwoord uit een bestand
|
||||
|
|
@ -84,6 +86,30 @@ sh install.sh --token jouw-forgejo-token
|
|||
|
||||
---
|
||||
|
||||
## Onder welk account het draait
|
||||
|
||||
Bij de installatie wordt gevraagd onder welk account Server Up moet draaien:
|
||||
|
||||
```
|
||||
Onder welk account moet Server Up draaien?
|
||||
Het draait nu nog als root; dat hoeft niet voor alles.
|
||||
|
||||
1) Een nieuw account 'serverup' aanmaken — aanbevolen
|
||||
Systeemaccount zonder shell en zonder wachtwoord.
|
||||
2) ramon — het account waarmee je nu werkt
|
||||
3) Een bestaand account kiezen
|
||||
4) Als root draaien — zoals voorheen
|
||||
```
|
||||
|
||||
Met `--user NAAM` of `--yes` sla je de vraag over; dan wordt het `serverup`. De
|
||||
keuze komt als `SU_UID`/`SU_GID` in je `.env` en blijft bij een `--update` staan.
|
||||
|
||||
Zet dat account **niet** in de `docker`-groep. Dat geeft het root op je machine
|
||||
en is niet nodig — de container regelt de toegang tot de socket zelf. Wat dit
|
||||
wel en niet oplevert staat in [beveiliging.md](beveiliging.md).
|
||||
|
||||
---
|
||||
|
||||
## Waar je gegevens staan
|
||||
|
||||
`--dir` bepaalt waar Server Up zélf staat. Waar je *stacks, appdata en backups*
|
||||
|
|
|
|||
198
install.sh
198
install.sh
|
|
@ -31,6 +31,13 @@ BEHEERDER_WW_BESTAND=""
|
|||
# blijft staan, zodat een --update je instellingen niet terugzet.
|
||||
BIND_EXPLICIET=0
|
||||
POORT_EXPLICIET=0
|
||||
# Onder welk account Server Up draait. Leeg = nog niet gekozen; 0 = root.
|
||||
GEBRUIKER=""
|
||||
GEBRUIKER_EXPLICIET=0
|
||||
SU_UID=""
|
||||
SU_GID=""
|
||||
SU_NAAM=""
|
||||
BASE_DIR="${BASE_DIR:-}"
|
||||
|
||||
# ── Uitvoer ──────────────────────────────────────────────────────────────────
|
||||
# Kleuren alleen als we naar een terminal schrijven; in een pipe of logbestand
|
||||
|
|
@ -86,6 +93,9 @@ Gebruik: install.sh [opties]
|
|||
maar zet er dan wel een reverse proxy met TLS voor.
|
||||
--branch NAAM Branch om te installeren (standaard: ${BRANCH})
|
||||
--token TOKEN Toegangstoken, als je git-repo niet openbaar is
|
||||
--user NAAM Account waaronder Server Up draait ('serverup' wordt zo
|
||||
nodig aangemaakt; 'root' is de oude situatie). Zonder deze
|
||||
optie wordt ernaar gevraagd.
|
||||
--admin NAAM Maak meteen een beheerdersaccount met deze naam
|
||||
--admin-password-file PAD
|
||||
Lees het wachtwoord uit een bestand. Kan ook via de
|
||||
|
|
@ -120,6 +130,8 @@ while [ $# -gt 0 ]; do
|
|||
--bind) BIND="${2:?--bind vereist een adres}"; BIND_EXPLICIET=1; shift 2 ;;
|
||||
--branch) BRANCH="${2:?--branch vereist een naam}"; shift 2 ;;
|
||||
--token) TOKEN="${2:?--token vereist een waarde}"; shift 2 ;;
|
||||
--user) GEBRUIKER="${2:?--user vereist een accountnaam}"
|
||||
GEBRUIKER_EXPLICIET=1; shift 2 ;;
|
||||
--admin) BEHEERDER="${2:?--admin vereist een gebruikersnaam}"; shift 2 ;;
|
||||
--admin-password-file)
|
||||
BEHEERDER_WW_BESTAND="${2:?--admin-password-file vereist een pad}"; shift 2 ;;
|
||||
|
|
@ -319,6 +331,152 @@ zet_env_regel() {
|
|||
$SUDO sh -c "sed -i '/^$2=/d' '$1' && printf '%s=%s\n' '$2' '$3' >> '$1'"
|
||||
}
|
||||
|
||||
# ── Onder welk account draait Server Up? ─────────────────────────────────────
|
||||
# Server Up start als root — dat moet, want het docker-volume is van root — en
|
||||
# zakt daarna af naar dit account. Zie docs/beveiliging.md voor wat dat wel en
|
||||
# niet oplevert.
|
||||
|
||||
uid_van() { getent passwd "$1" 2>/dev/null | cut -d: -f3; }
|
||||
gid_van() { getent passwd "$1" 2>/dev/null | cut -d: -f4; }
|
||||
|
||||
ingelogde_gebruiker() {
|
||||
# Wie de installatie aanstuurt. Onder sudo is dat niet 'root' maar degene
|
||||
# die het aanriep; zonder sudo valt logname terug op de terminal.
|
||||
naam="${SUDO_USER:-}"
|
||||
[ -n "$naam" ] || naam="$(logname 2>/dev/null || echo '')"
|
||||
[ "$naam" = "root" ] && naam=""
|
||||
printf '%s' "$naam"
|
||||
}
|
||||
|
||||
bruikbare_gebruikers() {
|
||||
# Gewone accounts: uid 1000–60000 en een echte shell. Systeemaccounts
|
||||
# (daemon, www-data, nobody) horen hier niet tussen — die hebben eigen
|
||||
# rechten en zouden verwarrend zijn om uit te kiezen.
|
||||
awk -F: '$3 >= 1000 && $3 < 60000 &&
|
||||
$7 !~ /(nologin|false|sync)$/ { print $1 " (uid " $3 ")" }' \
|
||||
/etc/passwd 2>/dev/null
|
||||
}
|
||||
|
||||
maak_serverup_account() {
|
||||
# Systeemaccount: geen shell, geen wachtwoord, geen thuismap. Het bestaat
|
||||
# alleen om eigenaar te zijn van bestanden.
|
||||
if [ -n "$(uid_van serverup)" ]; then
|
||||
log "Account 'serverup' bestaat al"
|
||||
return 0
|
||||
fi
|
||||
if [ "$DROOG" -eq 1 ]; then
|
||||
printf ' [droog] account serverup aanmaken (systeemaccount, geen shell)\n'
|
||||
return 0
|
||||
fi
|
||||
if heeft useradd; then
|
||||
$SUDO useradd --system --no-create-home --shell /usr/sbin/nologin serverup \
|
||||
|| fout "Kon het account 'serverup' niet aanmaken."
|
||||
elif heeft adduser; then
|
||||
$SUDO adduser --system --no-create-home --shell /usr/sbin/nologin serverup \
|
||||
|| fout "Kon het account 'serverup' niet aanmaken."
|
||||
else
|
||||
fout "Geen useradd of adduser gevonden. Maak zelf een account en geef het op met --user NAAM."
|
||||
fi
|
||||
goed "Account 'serverup' aangemaakt"
|
||||
}
|
||||
|
||||
zet_gebruiker() {
|
||||
# zet_gebruiker NAAM — vertaal een naam naar SU_UID/SU_GID.
|
||||
SU_NAAM="$1"
|
||||
if [ "$1" = "root" ]; then
|
||||
SU_UID=0; SU_GID=0
|
||||
waarschuw "Server Up draait als root; dat is de oude situatie."
|
||||
return 0
|
||||
fi
|
||||
_uid="$(uid_van "$1")"
|
||||
if [ -z "$_uid" ]; then
|
||||
if [ "$1" = "serverup" ]; then
|
||||
maak_serverup_account
|
||||
_uid="$(uid_van serverup)"
|
||||
fi
|
||||
if [ -z "$_uid" ] && [ "$DROOG" -eq 1 ] && [ "$1" = "serverup" ]; then
|
||||
# Alleen 'serverup' zouden we zélf hebben aangemaakt; dat het nu
|
||||
# nog ontbreekt is dus geen fout. Een andere naam die niet bestaat
|
||||
# is een typefout, en die hoor je juist bij een proefdraai te horen.
|
||||
SU_UID=""; SU_GID=""
|
||||
log "Zou draaien als 'serverup' (uid wordt bij het aanmaken toegekend)"
|
||||
return 0
|
||||
fi
|
||||
[ -n "$_uid" ] || fout "Account '$1' bestaat niet. Maak het aan of kies een ander."
|
||||
fi
|
||||
SU_UID="$_uid"
|
||||
SU_GID="$(gid_van "$1")"
|
||||
SU_GID="${SU_GID:-$SU_UID}"
|
||||
goed "Server Up draait als $1 ($SU_UID:$SU_GID)"
|
||||
# Bewust géén lidmaatschap van de docker-groep: wie daarin zit kan een
|
||||
# container starten die / mount, en is dus root op deze machine. De
|
||||
# container leest de gid van de socket zelf uit en heeft dit niet nodig.
|
||||
log "Niet toegevoegd aan de docker-groep — dat zou root-toegang geven en is niet nodig."
|
||||
}
|
||||
|
||||
kies_gebruiker() {
|
||||
[ "$GEBRUIKER_EXPLICIET" -eq 1 ] && { zet_gebruiker "$GEBRUIKER"; return 0; }
|
||||
# Wat er al staat wint: bij --update wordt kies_bind niet aangeroepen, dus
|
||||
# zonder dit zou een bestaande installatie elke keer opnieuw vragen.
|
||||
lees_env_terug "$DOEL/.env"
|
||||
[ -n "$SU_UID" ] && return 0
|
||||
if [ "$JA" -eq 1 ] || [ ! -e /dev/tty ]; then
|
||||
zet_gebruiker serverup
|
||||
return 0
|
||||
fi
|
||||
|
||||
_ingelogd="$(ingelogde_gebruiker)"
|
||||
stap "Account"
|
||||
printf ' Onder welk account moet Server Up draaien?\n'
|
||||
printf ' Het draait nu nog als root; dat hoeft niet voor alles.\n\n'
|
||||
printf " 1) Een nieuw account 'serverup' aanmaken — aanbevolen\n"
|
||||
printf ' Systeemaccount zonder shell en zonder wachtwoord.\n'
|
||||
if [ -n "$_ingelogd" ]; then
|
||||
printf ' 2) %s — het account waarmee je nu werkt\n' "$_ingelogd"
|
||||
else
|
||||
printf ' 2) (geen ingelogd account gevonden)\n'
|
||||
fi
|
||||
printf ' 3) Een bestaand account kiezen\n'
|
||||
printf ' 4) Als root draaien — zoals voorheen\n\n'
|
||||
printf ' Keuze [1]: '
|
||||
read -r keuze </dev/tty || keuze=""
|
||||
case "$keuze" in
|
||||
''|1) zet_gebruiker serverup ;;
|
||||
2) [ -n "$_ingelogd" ] || fout "Er is geen ingelogd account gevonden; kies 1, 3 of 4."
|
||||
zet_gebruiker "$_ingelogd" ;;
|
||||
3) printf '\n Bruikbare accounts:\n'
|
||||
bruikbare_gebruikers | sed 's/^/ /'
|
||||
printf '\n Naam: '
|
||||
read -r naam </dev/tty || naam=""
|
||||
naam="$(printf '%s' "$naam" | tr -d '[:space:]')"
|
||||
[ -n "$naam" ] || fout "Geen naam opgegeven."
|
||||
zet_gebruiker "$naam" ;;
|
||||
4) zet_gebruiker root ;;
|
||||
*) fout "Onbekende keuze: $keuze" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
zet_mappen_klaar() {
|
||||
# De hoofdmap en de twee mappen die Server Up zélf beheert alvast op de
|
||||
# juiste eigenaar zetten, zodat de eerste start niet op rechten struikelt.
|
||||
#
|
||||
# appdata blijft er bewust buiten: die mappen zijn van de apps (postgres
|
||||
# draait als 999, een linuxserver-image als jouw PUID). Er een chown
|
||||
# overheen halen breekt precies die containers.
|
||||
[ "${SU_UID:-0}" = "0" ] && [ "$DROOG" -eq 0 ] && return 0
|
||||
[ "${SU_NAAM:-root}" = "root" ] && return 0
|
||||
_basis="${BASE_DIR:-/opt/serverup}"
|
||||
if [ "$DROOG" -eq 1 ]; then
|
||||
printf ' [droog] %s, %s/stacks en %s/backups op %s zetten (appdata blijft ongemoeid)\n' \
|
||||
"$_basis" "$_basis" "$_basis" "$SU_NAAM"
|
||||
return 0
|
||||
fi
|
||||
$SUDO mkdir -p "$_basis/stacks" "$_basis/backups" "$_basis/appdata"
|
||||
$SUDO chown "$SU_UID:$SU_GID" "$_basis"
|
||||
$SUDO chown -R "$SU_UID:$SU_GID" "$_basis/stacks" "$_basis/backups"
|
||||
goed "Mappen klaargezet onder $_basis"
|
||||
}
|
||||
|
||||
lees_env_terug() {
|
||||
# Wat er in .env staat is de waarheid, niet wat dit script als standaard
|
||||
# heeft. Zonder dit klopt bij --update de wachtlus en het slotadres niet
|
||||
|
|
@ -338,6 +496,19 @@ lees_env_terug() {
|
|||
_inhoud="$($_lees "$1" 2>/dev/null || true)"
|
||||
_poort="$(printf '%s\n' "$_inhoud" | sed -n 's/^PORT=//p' | tail -1)"
|
||||
_bind="$(printf '%s\n' "$_inhoud" | sed -n 's/^BIND=//p' | tail -1)"
|
||||
# SU_UID/SU_GID en BASE_DIR overnemen, zodat --update niet opnieuw vraagt
|
||||
# onder welk account het moet draaien en niet de verkeerde mappen chownt.
|
||||
if [ -z "$SU_UID" ]; then
|
||||
_uid="$(printf '%s\n' "$_inhoud" | sed -n 's/^SU_UID=//p' | tail -1)"
|
||||
case "$_uid" in ''|*[!0-9]*) ;; *) SU_UID="$_uid"
|
||||
_gid="$(printf '%s\n' "$_inhoud" | sed -n 's/^SU_GID=//p' | tail -1)"
|
||||
case "$_gid" in ''|*[!0-9]*) SU_GID="$_uid" ;; *) SU_GID="$_gid" ;; esac ;;
|
||||
esac
|
||||
fi
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
_basis="$(printf '%s\n' "$_inhoud" | sed -n 's/^BASE_DIR=//p' | tail -1)"
|
||||
case "$_basis" in /*) BASE_DIR="$_basis" ;; esac
|
||||
fi
|
||||
if [ "$POORT_EXPLICIET" -eq 0 ]; then
|
||||
case "$_poort" in ''|*[!0-9]*) ;; *) POORT="$_poort" ;; esac
|
||||
fi
|
||||
|
|
@ -373,6 +544,19 @@ schrijf_env() {
|
|||
fi
|
||||
gewijzigd=1
|
||||
fi
|
||||
# Het account is een keuze die je net gemaakt hebt; die hoort er altijd
|
||||
# in te komen, ook bij een --update van een installatie die nog als root
|
||||
# draaide.
|
||||
if [ -n "$SU_UID" ]; then
|
||||
if [ "$DROOG" -eq 1 ]; then
|
||||
printf ' [droog] SU_UID=%s SU_GID=%s in bestaande .env zetten\n' \
|
||||
"$SU_UID" "$SU_GID"
|
||||
else
|
||||
zet_env_regel "$env_bestand" SU_UID "$SU_UID"
|
||||
zet_env_regel "$env_bestand" SU_GID "$SU_GID"
|
||||
fi
|
||||
gewijzigd=1
|
||||
fi
|
||||
if [ "$gewijzigd" -eq 1 ]; then
|
||||
goed "Bestaande .env bijgewerkt (BIND=$BIND, PORT=$POORT)"
|
||||
else
|
||||
|
|
@ -383,7 +567,8 @@ schrijf_env() {
|
|||
fi
|
||||
|
||||
if [ "$DROOG" -eq 1 ]; then
|
||||
printf ' [droog] .env aanmaken met BIND=%s PORT=%s\n' "$BIND" "$POORT"
|
||||
printf ' [droog] .env aanmaken met BIND=%s PORT=%s, draaiend als %s\n' \
|
||||
"$BIND" "$POORT" "${SU_NAAM:-root}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
|
@ -394,6 +579,10 @@ schrijf_env() {
|
|||
fi
|
||||
$SUDO sh -c "sed -i '/^BIND=/d;/^PORT=/d' '$env_bestand'"
|
||||
$SUDO sh -c "printf 'BIND=%s\nPORT=%s\n' '$BIND' '$POORT' >> '$env_bestand'"
|
||||
if [ -n "$SU_UID" ]; then
|
||||
zet_env_regel "$env_bestand" SU_UID "$SU_UID"
|
||||
zet_env_regel "$env_bestand" SU_GID "$SU_GID"
|
||||
fi
|
||||
goed "Aangemaakt: $env_bestand (BIND=$BIND, PORT=$POORT)"
|
||||
}
|
||||
|
||||
|
|
@ -656,9 +845,14 @@ case "$ACTIE" in
|
|||
controleer_systeem
|
||||
controleer_docker
|
||||
haal_broncode
|
||||
# Bestaande installaties draaien nog als root. Eén keer vragen of dat zo
|
||||
# moet blijven; wie al gekozen heeft, wordt niet opnieuw lastiggevallen
|
||||
# (kies_gebruiker ziet SU_UID uit .env staan).
|
||||
kies_gebruiker
|
||||
# Ook bij bijwerken: expliciete --bind/--port doorvoeren, en anders
|
||||
# overnemen wat er al staat.
|
||||
schrijf_env
|
||||
zet_mappen_klaar
|
||||
start
|
||||
wacht_op_gereed || true
|
||||
printf '\n'
|
||||
|
|
@ -689,9 +883,11 @@ case "$ACTIE" in
|
|||
controleer_systeem
|
||||
controleer_docker
|
||||
kies_bind
|
||||
kies_gebruiker
|
||||
beheerder_vooraf
|
||||
haal_broncode
|
||||
schrijf_env
|
||||
zet_mappen_klaar
|
||||
start
|
||||
wacht_op_gereed || true
|
||||
maak_beheerder
|
||||
|
|
|
|||
|
|
@ -1471,6 +1471,14 @@ def _annotate_fields(fields: list[dict]) -> list[dict]:
|
|||
# data dus ergens anders terecht dan waar je hem verwacht.
|
||||
if naam == "appdata_dir" and f.get("type") != "int":
|
||||
f["default"] = appdata
|
||||
# En voor PUID/PGID: 44 sjablonen zetten die blind op 1000. Draait
|
||||
# Server Up onder een ander account, dan maakt de app zijn bestanden
|
||||
# als een gebruiker die Server Up daarna niet meer kan inpakken voor
|
||||
# een backup. Zijn eigen ids zijn het enige voorstel dat altijd klopt.
|
||||
if naam == "puid":
|
||||
f["default"] = os.getuid()
|
||||
elif naam == "pgid":
|
||||
f["default"] = os.getgid()
|
||||
if f.get("type") == "int" and "port" in naam:
|
||||
try:
|
||||
gewenst = int(f.get("default") or 0)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ een geprepareerd archief buiten de stackmap schrijven.
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
|
|
@ -91,6 +92,36 @@ def _stack_meta(stack: str) -> dict:
|
|||
return {}
|
||||
|
||||
|
||||
def _voeg_toe(tar, map_: Path, arcnaam: str, log_fn=None) -> list[str]:
|
||||
"""Pak een appdata-map in en meld wat er niet mee kon.
|
||||
|
||||
Eén onleesbaar bestand liet eerder de héle backup falen: de `tar.add` van de
|
||||
map zat in dezelfde `try` als de rest. Dat is precies het scenario waarin je
|
||||
een backup het hardst nodig hebt — Server Up draait niet meer als root, dus
|
||||
appdata van een app met een eigen uid kan buiten bereik liggen.
|
||||
|
||||
Retourneert de paden die zijn overgeslagen. Die gaan in het log én in de
|
||||
metadata: een archief dat er compleet uitziet maar het niet is, is erger dan
|
||||
een archief dat zegt wat het mist.
|
||||
"""
|
||||
gemist: list[str] = []
|
||||
# Bewust bestand voor bestand in plaats van `tar.add` te laten recursen: die
|
||||
# breekt af op de eerste fout, en wat er dan al in het archief zat zou bij
|
||||
# een tweede poging dubbel meegaan.
|
||||
tar.add(map_, arcname=arcnaam, recursive=False)
|
||||
for p in sorted(map_.rglob("*")):
|
||||
rel = p.relative_to(map_)
|
||||
try:
|
||||
tar.add(p, arcname=f"{arcnaam}/{rel}", recursive=False)
|
||||
except (OSError, tarfile.TarError) as e:
|
||||
gemist.append(str(rel))
|
||||
if log_fn and len(gemist) <= 5:
|
||||
log_fn(f"Overgeslagen (niet leesbaar): {rel} — {e}")
|
||||
if gemist and log_fn:
|
||||
log_fn(f"{len(gemist)} bestand(en) niet ingepakt; zie de metadata.")
|
||||
return gemist
|
||||
|
||||
|
||||
def appdata_mappen(stack: str) -> list[Path]:
|
||||
"""De mappen waar deze stack zijn gegevens in bewaart.
|
||||
|
||||
|
|
@ -285,6 +316,7 @@ def create(stack: str, reden: str = "handmatig", log_fn=None,
|
|||
if log_fn:
|
||||
log_fn(f"Backup maken van {stack}…")
|
||||
|
||||
overgeslagen: list[str] = []
|
||||
try:
|
||||
# Python's tarfile in plaats van het tar-commando: de returncode van dat
|
||||
# commando werd eerder genegeerd, zodat een mislukte backup als succes
|
||||
|
|
@ -299,7 +331,8 @@ def create(stack: str, reden: str = "handmatig", log_fn=None,
|
|||
for m in data_mappen:
|
||||
if log_fn:
|
||||
log_fn(f"Appdata: {m.name}")
|
||||
tar.add(m, arcname=f"appdata/{m.name}")
|
||||
overgeslagen.extend(
|
||||
_voeg_toe(tar, m, f"appdata/{m.name}", log_fn=log_fn))
|
||||
for dp in dumps:
|
||||
tar.add(dp, arcname=f"dumps/{dp.name}")
|
||||
except Exception as e:
|
||||
|
|
@ -309,10 +342,17 @@ def create(stack: str, reden: str = "handmatig", log_fn=None,
|
|||
meta = {"stack": stack, "file": naam, "ts": time.time(),
|
||||
"size": doel.stat().st_size, "reason": reden,
|
||||
"appdata": [m.name for m in data_mappen],
|
||||
"dumps": [dp.name for dp in dumps]}
|
||||
"dumps": [dp.name for dp in dumps],
|
||||
# Leeg is het normale geval. Staat hier iets in, dan is dit archief
|
||||
# niet compleet en moet je dat kunnen zien vóór je het terugzet.
|
||||
"skipped": overgeslagen}
|
||||
_schrijf_meta(doel, meta)
|
||||
if log_fn:
|
||||
log_fn(f"Backup klaar: {naam} ({_leesbaar(meta['size'])})")
|
||||
if overgeslagen:
|
||||
log_fn(f"Backup klaar: {naam} ({_leesbaar(meta['size'])}) — "
|
||||
f"{len(overgeslagen)} bestand(en) ontbreken")
|
||||
else:
|
||||
log_fn(f"Backup klaar: {naam} ({_leesbaar(meta['size'])})")
|
||||
|
||||
if cfg.load().get("BACKUP_VERIFY", True):
|
||||
if log_fn:
|
||||
|
|
@ -622,11 +662,44 @@ def restore(stack: str, bestandsnaam: str, log_fn=None) -> tuple[bool, str]:
|
|||
return True, f"'{stack}' teruggezet uit {bestandsnaam}."
|
||||
|
||||
|
||||
def _verwachte_eigenaar(waarden: dict) -> tuple[int, int]:
|
||||
"""Van wie hoort de teruggezette appdata te zijn?
|
||||
|
||||
`tarfile` met `filter="data"` (PEP 706) laat uid en gid uit het archief
|
||||
vallen — alles wordt van het proces dat uitpakt. Draait Server Up onder een
|
||||
eigen account, dan krijgt de app zijn gegevens terug als een gebruiker die
|
||||
hij niet is, en start hij niet meer. De PUID/PGID uit de metadata is wat de
|
||||
app verwacht; staat die er niet, dan is onze eigen uid het beste antwoord.
|
||||
"""
|
||||
def _getal(sleutel, terugval):
|
||||
try:
|
||||
return int(str(waarden.get(sleutel)).strip())
|
||||
except (TypeError, ValueError):
|
||||
return terugval
|
||||
return _getal("puid", os.getuid()), _getal("pgid", os.getgid())
|
||||
|
||||
|
||||
def _zet_eigenaar(wortel: Path, uid: int, gid: int, log_fn=None) -> bool:
|
||||
"""Zet map en inhoud op deze eigenaar. False als dat niet mocht."""
|
||||
try:
|
||||
os.chown(wortel, uid, gid, follow_symlinks=False)
|
||||
for p in wortel.rglob("*"):
|
||||
os.chown(p, uid, gid, follow_symlinks=False)
|
||||
return True
|
||||
except (OSError, NotImplementedError) as e:
|
||||
if log_fn:
|
||||
log_fn(f"Let op: eigenaar van {wortel.name} kon niet op {uid}:{gid} "
|
||||
f"gezet worden ({e}). De app kan mogelijk niet bij zijn "
|
||||
f"gegevens.")
|
||||
return False
|
||||
|
||||
|
||||
def _zet_appdata_terug(stack: str, bron: Path, log_fn=None):
|
||||
"""De appdata-tak uit het archief terug naar de appdata-map."""
|
||||
if not bron.is_dir():
|
||||
return
|
||||
waarden = (_stack_meta(stack).get("values") or {})
|
||||
uid, gid = _verwachte_eigenaar(waarden)
|
||||
basis = Path(str(waarden.get("appdata_dir")
|
||||
or cfg.load().get("DATA_DIR") or "")).resolve()
|
||||
if basis == Path("/") or not basis.parent.exists():
|
||||
|
|
@ -641,8 +714,9 @@ def _zet_appdata_terug(stack: str, bron: Path, log_fn=None):
|
|||
if doel.exists():
|
||||
shutil.rmtree(doel, ignore_errors=True)
|
||||
shutil.move(str(m), str(doel))
|
||||
_zet_eigenaar(doel, uid, gid, log_fn=log_fn)
|
||||
if log_fn:
|
||||
log_fn(f"Appdata teruggezet: {m.name}")
|
||||
log_fn(f"Appdata teruggezet: {m.name} ({uid}:{gid})")
|
||||
|
||||
|
||||
def _speel_dumps_in(stack: str, stackmap: Path, log_fn=None) -> list[str]:
|
||||
|
|
|
|||
|
|
@ -316,3 +316,91 @@ def _mislukt(stderr=b"fout"):
|
|||
def _gelukt(stdout=b""):
|
||||
import subprocess
|
||||
return subprocess.CompletedProcess([], 0, stdout, b"")
|
||||
|
||||
|
||||
# ── Rechten: wie is de eigenaar van wat er teruggezet wordt? ─────────────────
|
||||
# Server Up draait vanaf v0.10 niet meer als root. Twee gevolgen die eerder
|
||||
# geen rol speelden: appdata van een app met een eigen uid kan buiten bereik
|
||||
# liggen, en `tarfile` met filter="data" geeft alles aan wie uitpakt.
|
||||
|
||||
def test_een_onleesbaar_bestand_laat_de_rest_van_de_backup_staan(env, monkeypatch):
|
||||
"""Eén bestand waar we niet bij kunnen mocht niet de héle backup slopen —
|
||||
juist dan heb je hem nodig."""
|
||||
from core import backups, docker
|
||||
monkeypatch.setattr(docker, "_run", lambda *a, **k: _mislukt())
|
||||
_d, appdata = _stack_met_appdata(env)
|
||||
(appdata / "umami" / "tweede.txt").write_bytes(b"deze wel")
|
||||
|
||||
# Geen namaak: een bestand waar de eigenaar zelf niet in kan kijken. Precies
|
||||
# wat je krijgt als een container zijn data als een andere uid wegschrijft.
|
||||
onleesbaar = appdata / "umami" / "bestand.txt"
|
||||
onleesbaar.chmod(0o000)
|
||||
try:
|
||||
meta = backups.create("umami")
|
||||
finally:
|
||||
onleesbaar.chmod(0o644)
|
||||
|
||||
assert meta["skipped"] == ["bestand.txt"], meta
|
||||
with backups.tarfile.open(backups.backup_dir() / meta["file"]) as tar:
|
||||
namen = tar.getnames()
|
||||
assert "appdata/umami/tweede.txt" in namen, "de rest is ook weggevallen"
|
||||
assert "appdata/umami/bestand.txt" not in namen
|
||||
|
||||
|
||||
def test_wat_ontbreekt_staat_in_de_metadata(env, monkeypatch):
|
||||
"""Een archief dat compleet lijkt maar het niet is, is erger dan een
|
||||
archief dat zegt wat het mist."""
|
||||
from core import backups, docker
|
||||
monkeypatch.setattr(docker, "_run", lambda *a, **k: _mislukt())
|
||||
_stack_met_appdata(env)
|
||||
meta = backups.create("umami")
|
||||
assert meta["skipped"] == [], "een geslaagde backup meldt niets ontbrekends"
|
||||
assert "skipped" in backups.list_for("umami")[0]
|
||||
|
||||
|
||||
def test_teruggezette_appdata_krijgt_de_puid_van_de_stack(env, monkeypatch):
|
||||
"""filter="data" laat uid/gid vallen, dus zonder chown wordt alles van wie
|
||||
uitpakt — en start de app niet meer."""
|
||||
import json as _json
|
||||
from core import backups, docker
|
||||
monkeypatch.setattr(docker, "_run", lambda *a, **k: _mislukt())
|
||||
monkeypatch.setattr(docker, "compose_up", lambda *a, **k: 0)
|
||||
monkeypatch.setattr(docker, "compose_down", lambda *a, **k: 0)
|
||||
d, appdata = _stack_met_appdata(env)
|
||||
meta_pad = d / ".serverup.json"
|
||||
inhoud = _json.loads(meta_pad.read_text())
|
||||
inhoud["values"].update({"puid": "1005", "pgid": "1006"})
|
||||
meta_pad.write_text(_json.dumps(inhoud))
|
||||
|
||||
meta = backups.create("umami")
|
||||
gevraagd = []
|
||||
monkeypatch.setattr(backups.os, "chown",
|
||||
lambda p, u, g, **kw: gevraagd.append((str(p), u, g)))
|
||||
ok, msg = backups.restore("umami", meta["file"])
|
||||
|
||||
assert ok, msg
|
||||
assert gevraagd, "er is helemaal niet gechownd"
|
||||
assert all(u == 1005 and g == 1006 for _p, u, g in gevraagd), gevraagd
|
||||
|
||||
|
||||
def test_zonder_puid_valt_hij_terug_op_ons_eigen_account(env):
|
||||
from core import backups
|
||||
import os as _os
|
||||
assert backups._verwachte_eigenaar({}) == (_os.getuid(), _os.getgid())
|
||||
assert backups._verwachte_eigenaar({"puid": "1005", "pgid": "1006"}) == (1005, 1006)
|
||||
# Een sjabloon met onzin erin mag geen TypeError geven.
|
||||
assert backups._verwachte_eigenaar({"puid": "abc"}) == (_os.getuid(), _os.getgid())
|
||||
|
||||
|
||||
def test_chown_die_niet_mag_stopt_het_terugzetten_niet(env, monkeypatch, tmp_path):
|
||||
"""Draai je zonder de rechten om te chownen, dan is een waarschuwing beter
|
||||
dan een mislukt herstel."""
|
||||
from core import backups
|
||||
monkeypatch.setattr(backups.os, "chown",
|
||||
lambda *a, **k: (_ for _ in ()).throw(PermissionError("mag niet")))
|
||||
map_ = tmp_path / "appdata-umami"
|
||||
map_.mkdir()
|
||||
(map_ / "a.txt").write_text("x")
|
||||
regels = []
|
||||
assert backups._zet_eigenaar(map_, 1005, 1006, log_fn=regels.append) is False
|
||||
assert any("eigenaar" in r for r in regels), regels
|
||||
|
|
|
|||
169
tests/test_entrypoint.py
Normal file
169
tests/test_entrypoint.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
"""Het entrypoint dat naar een gewone gebruiker afzakt.
|
||||
|
||||
Dit script draait vóór de app en heeft geen vangnet: gaat het mis, dan start
|
||||
Server Up niet meer op. Het mag daarom onder geen enkele omstandigheid weigeren
|
||||
— hooguit als root doorgaan en zeggen waarom.
|
||||
|
||||
De root-tak is hier niet echt te draaien, dus die toetsen we met neppe
|
||||
hulpprogramma's op $PATH. Dat is geen simulatie van de uitkomst maar van de
|
||||
omgeving: het echte script loopt er onveranderd doorheen.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
WORTEL = Path(__file__).resolve().parent.parent
|
||||
SCRIPT = WORTEL / "docker-entrypoint.sh"
|
||||
|
||||
# De hulpprogramma's die het script als root aanroept. Ze schrijven wat ze
|
||||
# kregen naar $NEP_LOG, zodat de test kan zien wát er gevraagd is.
|
||||
NEPPERS = {
|
||||
"id": "echo 0",
|
||||
"chown": 'echo "chown $*" >> "$NEP_LOG"',
|
||||
"groupadd": 'echo "groupadd $*" >> "$NEP_LOG"',
|
||||
"useradd": 'echo "useradd $*" >> "$NEP_LOG"',
|
||||
"usermod": 'echo "usermod $*" >> "$NEP_LOG"',
|
||||
"getent": 'echo "getent $*" >> "$NEP_LOG"; exit 2',
|
||||
"stat": "echo 999",
|
||||
"setpriv": 'echo "setpriv $*" >> "$NEP_LOG"; exit 0',
|
||||
}
|
||||
|
||||
# Wat het script verder nog nodig heeft en wat we níét willen namaken.
|
||||
ECHTE = ("cut", "cp", "echo")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nep_omgeving(tmp_path):
|
||||
"""Een $PATH met alléén nepjes en het strikt noodzakelijke.
|
||||
|
||||
Bewust afgesloten in plaats van de nepjes ervóór te zetten: een test die een
|
||||
nep weghaalt om te zien wat er dan gebeurt, vond anders gewoon het echte
|
||||
programma van deze machine.
|
||||
"""
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
for naam, body in NEPPERS.items():
|
||||
p = bin_dir / naam
|
||||
p.write_text(f"#!/bin/sh\n{body}\n", encoding="utf-8")
|
||||
p.chmod(0o755)
|
||||
for naam in ECHTE:
|
||||
echt = shutil.which(naam)
|
||||
if echt:
|
||||
(bin_dir / naam).symlink_to(echt)
|
||||
log = tmp_path / "aanroepen.log"
|
||||
log.write_text("", encoding="utf-8")
|
||||
return {"bin": bin_dir, "log": log, "tmp": tmp_path}
|
||||
|
||||
|
||||
def draai(env_extra: dict, nep=None, argv=("echo", "APP-GESTART")):
|
||||
env = dict(os.environ)
|
||||
env.update(env_extra)
|
||||
if nep:
|
||||
env["PATH"] = str(nep["bin"])
|
||||
env["NEP_LOG"] = str(nep["log"])
|
||||
# Absoluut pad: subprocess zoekt `sh` anders in het afgeschermde $PATH.
|
||||
r = subprocess.run([shutil.which("sh") or "/bin/sh", str(SCRIPT), *argv],
|
||||
capture_output=True, text=True, env=env, timeout=30)
|
||||
return r
|
||||
|
||||
|
||||
# ── Het mag nooit weigeren ───────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.parametrize("env_extra, waarom", [
|
||||
({}, "zonder SU_UID"),
|
||||
({"SU_UID": "0"}, "expliciet root"),
|
||||
({"SU_UID": ""}, "lege SU_UID"),
|
||||
({"SU_UID": "abc"}, "geen getal"),
|
||||
({"SU_UID": "1000", "SU_GID": "x"}, "gid geen getal"),
|
||||
({"SU_UID": "1000"}, "al niet-root"),
|
||||
])
|
||||
def test_de_app_start_hoe_dan_ook(env_extra, waarom):
|
||||
r = draai(env_extra)
|
||||
assert r.returncode == 0, f"{waarom}: {r.stderr}"
|
||||
assert "APP-GESTART" in r.stdout, f"{waarom}: de app is niet gestart"
|
||||
|
||||
|
||||
def test_onzin_wordt_gemeld_en_niet_stil_genegeerd():
|
||||
r = draai({"SU_UID": "abc"})
|
||||
assert "getallen" in r.stderr
|
||||
|
||||
|
||||
def test_als_root_blijft_root_ongemoeid(nep_omgeving):
|
||||
"""SU_UID=0 mag niets klaarzetten en niets afzakken."""
|
||||
r = draai({"SU_UID": "0"}, nep=nep_omgeving)
|
||||
assert "APP-GESTART" in r.stdout
|
||||
assert nep_omgeving["log"].read_text() == "", "er is toch iets gewijzigd"
|
||||
|
||||
|
||||
# ── De tak die alleen als root loopt ─────────────────────────────────────────
|
||||
|
||||
def test_afzakken_zet_klaar_en_geeft_de_juiste_rechten(nep_omgeving, tmp_path):
|
||||
basis = tmp_path / "serverup"
|
||||
for sub in ("stacks", "appdata", "backups"):
|
||||
(basis / sub).mkdir(parents=True)
|
||||
|
||||
r = draai({"SU_UID": "1000", "SU_GID": "1000", "BASE_DIR": str(basis)},
|
||||
nep=nep_omgeving)
|
||||
assert r.returncode == 0, r.stderr
|
||||
regels = nep_omgeving["log"].read_text()
|
||||
|
||||
# De gebruiker krijgt een naam en een thuis, anders struikelt git erover.
|
||||
assert "useradd --uid 1000 --gid 1000" in regels
|
||||
assert "/data" in regels and "chown -R 1000:1000 /data" in regels
|
||||
|
||||
# Wat Server Up zelf aanmaakt, mag van hem zijn.
|
||||
assert f"chown -R 1000:1000 {basis}/stacks" in regels
|
||||
assert f"chown -R 1000:1000 {basis}/backups" in regels
|
||||
|
||||
# En appdata niet: die mappen zijn van de apps zelf. Een chown daaroverheen
|
||||
# breekt elke container die als zijn eigen uid draait.
|
||||
assert "appdata" not in regels, "appdata is van eigenaar veranderd"
|
||||
|
||||
setpriv = [r for r in regels.splitlines() if r.startswith("setpriv ")]
|
||||
assert setpriv, "er is niet afgezakt"
|
||||
assert "--reuid 1000" in setpriv[0] and "--regid 1000" in setpriv[0]
|
||||
# Zonder ambient caps verliest het proces bij het afzakken precies de
|
||||
# rechten die het voor verhuizen en backups nodig heeft.
|
||||
assert "--ambient-caps +chown,dac_override,fowner" in setpriv[0]
|
||||
assert "echo APP-GESTART" in setpriv[0], "de app wordt niet doorgegeven"
|
||||
|
||||
|
||||
def test_de_gid_van_de_docker_socket_wordt_overgenomen(nep_omgeving, tmp_path):
|
||||
"""Die verschilt per host; hardcoderen in compose werkt daarom niet."""
|
||||
sok = tmp_path / "docker.sock"
|
||||
s = socket.socket(socket.AF_UNIX)
|
||||
s.bind(str(sok))
|
||||
assert stat.S_ISSOCK(sok.stat().st_mode)
|
||||
|
||||
r = draai({"SU_UID": "1000", "SU_GID": "1000", "SU_DOCKER_SOCK": str(sok)},
|
||||
nep=nep_omgeving)
|
||||
s.close()
|
||||
assert r.returncode == 0, r.stderr
|
||||
regels = nep_omgeving["log"].read_text()
|
||||
# De nep-`stat` meldt gid 999; die groep bestaat niet (nep-getent faalt),
|
||||
# dus hij hoort aangemaakt te worden en de gebruiker erin te belanden.
|
||||
assert "groupadd --gid 999" in regels
|
||||
assert "usermod -aG dockersock" in regels
|
||||
|
||||
|
||||
def test_zonder_socket_start_hij_toch(nep_omgeving, tmp_path):
|
||||
r = draai({"SU_UID": "1000", "SU_GID": "1000",
|
||||
"SU_DOCKER_SOCK": str(tmp_path / "bestaat-niet.sock")},
|
||||
nep=nep_omgeving)
|
||||
assert r.returncode == 0
|
||||
assert "docker-socket" in r.stderr
|
||||
assert "setpriv " in nep_omgeving["log"].read_text()
|
||||
|
||||
|
||||
def test_zonder_setpriv_draait_hij_als_root_verder(nep_omgeving):
|
||||
"""Beter als root draaien dan helemaal niet opstarten."""
|
||||
(nep_omgeving["bin"] / "setpriv").unlink()
|
||||
r = draai({"SU_UID": "1000", "SU_GID": "1000"}, nep=nep_omgeving)
|
||||
assert r.returncode == 0
|
||||
assert "APP-GESTART" in r.stdout
|
||||
assert "setpriv ontbreekt" in r.stderr
|
||||
|
|
@ -520,3 +520,97 @@ def test_readme_en_docs_noemen_dezelfde_installatieregel():
|
|||
"raw/branch/main/install.sh")
|
||||
assert regel in (WORTEL / "README.md").read_text(encoding="utf-8")
|
||||
assert regel in (WORTEL / "docs" / "installeren.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# ── Onder welk account draait Server Up? ─────────────────────────────────────
|
||||
# Vanaf v0.10 draait Server Up niet meer als root. Welke gebruiker het wordt is
|
||||
# een keuze bij de installatie, en die keuze mag niet stilzwijgend verkeerd
|
||||
# uitpakken: een typefout in de naam hoort te stoppen, en 'root' moet blijven
|
||||
# kunnen voor wie problemen krijgt.
|
||||
|
||||
GEBRUIKER_FRAGMENT = (
|
||||
'DROOG=0; JA=0; GEBRUIKER_EXPLICIET=0; SU_UID=""; DOEL="$MAP"\n'
|
||||
'uid_van() { case "$1" in ramon) echo 1000 ;; serverup) echo 990 ;; esac; }\n'
|
||||
'gid_van() { case "$1" in ramon) echo 1000 ;; serverup) echo 990 ;; esac; }\n'
|
||||
'ingelogde_gebruiker() { printf "%s" "${NEP_INGELOGD-ramon}"; }\n'
|
||||
'bruikbare_gebruikers() { echo "ramon (uid 1000)"; }\n'
|
||||
'kies_gebruiker\n'
|
||||
'printf "UITKOMST uid=%s gid=%s naam=%s\\n" "$SU_UID" "$SU_GID" "$SU_NAAM"\n'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invoer,verwacht", [
|
||||
("\n", "uid=990 gid=990 naam=serverup"), # enter = aanbevolen
|
||||
("1\n", "uid=990 gid=990 naam=serverup"),
|
||||
("2\n", "uid=1000 gid=1000 naam=ramon"), # de ingelogde gebruiker
|
||||
("3\nramon\n", "uid=1000 gid=1000 naam=ramon"), # bestaand account
|
||||
("4\n", "uid=0 gid=0 naam=root"), # de oude situatie
|
||||
])
|
||||
def test_keuzemenu_voor_het_account(invoer, verwacht, tmp_path):
|
||||
leeg = tmp_path / "leeg"
|
||||
leeg.mkdir()
|
||||
uit = _in_terminal(f'MAP="{leeg}"\n' + GEBRUIKER_FRAGMENT, invoer, tmp_path)
|
||||
assert f"UITKOMST {verwacht}" in uit, uit
|
||||
|
||||
|
||||
def test_de_ingelogde_gebruiker_staat_erbij(tmp_path):
|
||||
leeg = tmp_path / "leeg"
|
||||
leeg.mkdir()
|
||||
uit = _in_terminal(f'MAP="{leeg}"\n' + GEBRUIKER_FRAGMENT, "1\n", tmp_path)
|
||||
assert "ramon — het account waarmee je nu werkt" in uit, uit
|
||||
|
||||
|
||||
def test_zonder_ingelogde_gebruiker_geen_loze_keuze(tmp_path):
|
||||
"""Draai je als root zonder sudo, dan is er niemand om voor te stellen."""
|
||||
leeg = tmp_path / "leeg"
|
||||
leeg.mkdir()
|
||||
uit = _in_terminal(f'MAP="{leeg}"\nNEP_INGELOGD=""\n' + GEBRUIKER_FRAGMENT,
|
||||
"2\n", tmp_path)
|
||||
assert "geen ingelogd account gevonden" in uit
|
||||
assert "UITKOMST" not in uit, "keuze 2 werd geaccepteerd zonder account"
|
||||
|
||||
|
||||
def test_een_naam_die_niet_bestaat_stopt(tmp_path):
|
||||
leeg = tmp_path / "leeg"
|
||||
leeg.mkdir()
|
||||
uit = _in_terminal(f'MAP="{leeg}"\n' + GEBRUIKER_FRAGMENT, "3\ntypfout\n", tmp_path)
|
||||
assert "bestaat niet" in uit
|
||||
assert "UITKOMST" not in uit
|
||||
|
||||
|
||||
def test_een_eerdere_keuze_wordt_niet_opnieuw_gevraagd(tmp_path):
|
||||
"""Bij --update mag het script je niet elke keer opnieuw lastigvallen."""
|
||||
bestaand = tmp_path / "bestaand"
|
||||
bestaand.mkdir()
|
||||
# Bewust een uid die geen enkele menukeuze oplevert: met 990 (serverup) zou
|
||||
# deze test ook slagen als het menu tóch verscheen en enter werd gekozen.
|
||||
(bestaand / ".env").write_text("BIND=127.0.0.1\nPORT=5000\nSU_UID=1234\nSU_GID=1234\n")
|
||||
uit = _in_terminal(f'MAP="{bestaand}"\n' + GEBRUIKER_FRAGMENT, "", tmp_path)
|
||||
assert "UITKOMST uid=1234 gid=1234" in uit, uit
|
||||
assert "Onder welk account" not in uit, "er werd toch gevraagd"
|
||||
|
||||
|
||||
def test_de_docker_groep_wordt_bewust_overgeslagen():
|
||||
"""Wie in de docker-groep zit kan een container starten die / mount, en is
|
||||
daarmee root op de host. De container heeft het niet nodig: die leest de gid
|
||||
van de socket zelf uit."""
|
||||
tekst = SCRIPT.read_text(encoding="utf-8")
|
||||
for regel in tekst.splitlines():
|
||||
kaal = regel.strip()
|
||||
if kaal.startswith("#") or kaal.startswith("log ") or kaal.startswith("printf"):
|
||||
continue
|
||||
assert "usermod" not in kaal or "docker" not in kaal, \
|
||||
f"gebruiker wordt in de docker-groep gezet: {kaal}"
|
||||
assert "gpasswd" not in kaal, f"groepswijziging: {kaal}"
|
||||
|
||||
|
||||
def test_appdata_wordt_niet_van_eigenaar_veranderd():
|
||||
"""De appdata-mappen zijn van de apps zelf — postgres draait als 999, een
|
||||
linuxserver-image als jouw PUID. Een chown daaroverheen breekt ze."""
|
||||
tekst = SCRIPT.read_text(encoding="utf-8")
|
||||
for regel in tekst.splitlines():
|
||||
kaal = regel.strip()
|
||||
if kaal.startswith("#") or kaal.startswith("printf"):
|
||||
continue
|
||||
if "chown" in kaal:
|
||||
assert "appdata" not in kaal, f"appdata wordt gechownd: {kaal}"
|
||||
|
|
|
|||
|
|
@ -335,3 +335,35 @@ def test_installatieformulier_gebruikt_de_ingestelde_appdata(client, env, monkey
|
|||
headers={"X-CSRF-Token": csrf}).get_json()
|
||||
veld = next(f for f in d["fields"] if f["name"] == "appdata_dir")
|
||||
assert veld["default"] == "/srv/eigen/appdata"
|
||||
|
||||
|
||||
def test_puid_volgt_het_account_van_server_up(client, env):
|
||||
"""44 sjablonen zetten PUID/PGID blind op 1000. Draait Server Up onder een
|
||||
ander account, dan schrijft de app zijn data weg als een gebruiker die
|
||||
Server Up daarna niet kan inpakken voor een backup."""
|
||||
from core import git
|
||||
csrf = login(client)
|
||||
|
||||
bron = git.cache_dir("server-up") / "apps" / "lsioapp"
|
||||
(bron / "files").mkdir(parents=True)
|
||||
(bron / "template.json").write_text(json.dumps({
|
||||
"kind": "compose",
|
||||
"metadata": {"name": "Lsio", "description": "d", "tags": ["t"]},
|
||||
"variables": [{"title": "Algemeen", "items": [
|
||||
{"name": "service_name", "type": "str", "title": "Naam",
|
||||
"default": "lsioapp", "required": True},
|
||||
{"name": "puid", "type": "int", "title": "PUID", "default": 1000},
|
||||
{"name": "pgid", "type": "int", "title": "PGID", "default": 1000},
|
||||
]}],
|
||||
}), encoding="utf-8")
|
||||
(bron / "files" / "compose.yaml").write_text(
|
||||
"services:\n << service_name >>:\n image: x\n"
|
||||
" environment:\n - PUID=<< puid >>\n - PGID=<< pgid >>\n",
|
||||
encoding="utf-8")
|
||||
|
||||
d = client.post("/api/store/preview",
|
||||
json={"stack": "lsioapp", "repo_id": "server-up"},
|
||||
headers={"X-CSRF-Token": csrf}).get_json()
|
||||
velden = {f["name"]: f for f in d["fields"]}
|
||||
assert velden["puid"]["default"] == os.getuid()
|
||||
assert velden["pgid"]["default"] == os.getgid()
|
||||
|
|
|
|||
Loading…
Reference in a new issue