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
169 lines
6.5 KiB
Python
169 lines
6.5 KiB
Python
"""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
|