- 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
9.1 KiB
Beveiliging
Server Up beheert de Docker-daemon. Wie toegang heeft tot de webinterface, kan containers starten met willekeurige volumes — en daarmee in de praktijk alles op de host. Behandel toegang tot Server Up dus als root-toegang tot de server.
Vanaf v0.5.00 is de interface standaard afgeschermd met een login.
Eerste start
Bij de eerste start is er nog geen account. Open de webinterface en maak er meteen een aan: zolang dat niet gebeurd is, kan iedereen die de pagina bereikt het beheerdersaccount claimen. De container laat daarom bij het opstarten een waarschuwing zien.
Standaard bindt docker-compose.yml de poort op 127.0.0.1, dus alleen vanaf de
server zelf bereikbaar. Wil je er van buitenaf bij, zet dan een reverse proxy met
TLS ervoor (zie onder) en pas BIND aan in je .env:
cd /opt/docker/server-up # of /opt/server-up op prod
cp .env.example .env # alleen de eerste keer
sed -i 's/^BIND=.*/BIND=0.0.0.0/' .env
docker compose up -d
.env staat in .gitignore en wordt door de deploy-workflow overgeslagen, dus
je instellingen blijven staan bij een update. Alle beschikbare sleutels staan
met uitleg in .env.example.
Authenticatiemodi
Instelbaar via PUT /api/auth/mode:
| Modus | Betekenis |
|---|---|
local (standaard) |
Gebruikersnaam + wachtwoord in Server Up zelf |
proxy |
Identiteit komt uit een header van je reverse proxy; geen lokale login meer |
both |
Beide; handig om SSO te testen zonder jezelf buiten te sluiten |
Wachtwoorden worden opgeslagen als scrypt-hash (n=2¹⁴) met een willekeurige salt. Na vijf mislukte pogingen is het account vijf minuten geblokkeerd.
SSO via een reverse proxy
Draai je Authelia, Authentik of Cloudflare Access, dan kan die de identiteit in
een header zetten (meestal Remote-User).
Belangrijk: die header wordt alleen vertrouwd als het bron-IP in
trusted_proxies staat. Zonder die lijst kan iedereen de header zelf meesturen
en zich voordoen als beheerder. Server Up weigert daarom modus proxy/both als
trusted_proxies leeg is.
Zorg er ook voor dat je proxy de header van binnenkomende requests wist en zelf opnieuw zet.
Reverse proxy met TLS
Server Up spreekt gewoon HTTP. Zet er een proxy voor die TLS afhandelt:
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Remote-User ""; # wis wat de client stuurt
}
Zet SU_HTTPS=1 in de omgeving zodra alles via TLS loopt; de sessiecookie krijgt
dan de Secure-vlag.
Wat er beveiligd is
- Sessies —
HttpOnly,SameSite=Strict, 12 uur geldig. - CSRF — elke POST/PUT/DELETE vereist de
X-CSRF-Token-header. Routes die de toestand wijzigen accepteren geen GET meer. - Padvalidatie — stack-, instantie- en repo-namen worden gecontroleerd, zodat
ze nooit buiten
LIBRARY_DIRof de git-cache kunnen wijzen. - Git-URL's — alleen
http(s)://,ssh://engit@host:pad. Git'sext::-transport voert een shell-commando uit en wordt geweigerd. - Templates — boilerplates renderen in een Jinja2-sandbox.
- Geheimen — git-tokens worden nooit teruggegeven door de API (alleen een
has_token-vlag);config.jsonensecret.keystaan op 0600. - Headers — CSP op
'self',X-Frame-Options: DENY,nosniff,Referrer-Policy: no-referrer. Alle front-end libraries worden meegeleverd, dus er gaat op runtime niets naar een CDN. - SSH — host-keys worden geverifieerd (
accept-new, opgeslagen in/data/known_hosts).
Externe repo's en modules
Een module is Python-code die Server Up uitvoert. Voeg alleen repo's toe die je vertrouwt. Twee knoppen om aan te draaien:
-
AUTO_SYNC_ON_BOOTstaat standaard uit. Repo's worden dus niet vanzelf bijgewerkt; je synchroniseert zelf wanneer je dat wilt. -
Zet per repo een
commitin de config om hem op een specifieke commit vast te zetten:{"id": "mijn-apps", "url": "https://…", "branch": "main", "commit": "a1b2c3d"}
Waar de wachtwoorden van je apps staan
Vul je bij het installeren een wachtwoord of sleutel in, dan komt die waarde
niet in het compose-bestand terecht maar in .env naast de stack:
# docker-compose.yml — te delen, te committen
environment:
- TZ=Europe/Amsterdam
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# .env — rechten 0600, alleen leesbaar voor de eigenaar
POSTGRES_PASSWORD=aB3dE6gH9jK2mN5p
Ook .serverup.json, waarin de gemaakte keuzes staan zodat je ze later kunt
wijzigen, bevat geen geheimen meer en krijgt 0600. Tot v0.7.60 stonden
wachtwoorden op allebei die plekken wereldleesbaar.
Wat dit wél oplost: het compose-bestand is deelbaar en te committen, alle geheimen staan op één plek, en die plek is afgeschermd.
Wat dit níét oplost: compose vult ${...} in bij het inlezen, dus de
container krijgt gewoon de letterlijke waarde en docker inspect toont hem nog
steeds. Wie de docker-socket kan bereiken, kan de wachtwoorden van je apps lezen
— maar die kan sowieso al elke container overnemen.
Wil je een geheim écht buiten docker inspect houden, dan is secrets: van
Compose de weg: de waarde staat dan in een bestand dat de container zelf inleest.
Dat vereist wel dat het image een *_FILE-variant kent. Postgres, MariaDB en
MySQL kunnen het; de meeste apps niet.
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.
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 hier niet: het dóél van deze app is containers met willekeurige
bind mounts aanmaken, dus een proxy die
POST /containers/createdoorlaat 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.
Een probleem melden
Vind je een beveiligingsprobleem, meld het dan via de repository-issues met zo veel mogelijk details over de reproductie.