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
226 lines
9.1 KiB
Markdown
226 lines
9.1 KiB
Markdown
# 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`:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```nginx
|
|
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_DIR` of de git-cache kunnen wijzen.
|
|
- **Git-URL's** — alleen `http(s)://`, `ssh://` en `git@host:pad`. Git's
|
|
`ext::`-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.json` en `secret.key` staan 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_BOOT` staat standaard **uit**. Repo's worden dus niet vanzelf
|
|
bijgewerkt; je synchroniseert zelf wanneer je dat wilt.
|
|
- Zet per repo een `commit` in de config om hem op een specifieke commit vast te
|
|
zetten:
|
|
|
|
```json
|
|
{"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:
|
|
|
|
```yaml
|
|
# docker-compose.yml — te delen, te committen
|
|
environment:
|
|
- TZ=Europe/Amsterdam
|
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
```
|
|
|
|
```bash
|
|
# .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.
|
|
|
|
```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 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.
|
|
|
|
## Een probleem melden
|
|
|
|
Vind je een beveiligingsprobleem, meld het dan via de repository-issues met zo
|
|
veel mogelijk details over de reproductie.
|