server-up/docs/beveiliging.md
Ramon bc55252267
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m34s
v0.7.70-beta - invulmenu bij installeren, geheimen naar .env
Invulmenu:
- Vaste stappen (Basis, Verbinden, Instellingen, Toegang, Controleren) in
  plaats van één lange lijst. Eén stap per groep werkt niet: de mediane app
  heeft één groep, arr-stack eenentwintig. Lege stappen worden overgeslagen.
- Volgende controleert de verplichte velden van die stap; die melding kwam
  eerder pas bij het installeren.
- Geavanceerde opties achter één schakelaar die op elke stap zichtbaar is,
  plus 'Alles op één pagina' voor het oude gedrag. Beide onthouden.

Geheimen naar .env:
- compose_transform.geheimen_naar_env() vervangt geheime waarden door
  ${NAAM} en schrijft ze naar .env met rechten 0600. Werkt ook midden in
  een database-URL. Poorten en paden blijven leesbaar in compose.
- .serverup.json bevat geen geheimen meer en krijgt ook 0600; daar stonden
  ze wereldleesbaar in. Het herconfiguratieformulier leest ze terug uit
  .env via een bewaarde veld-naar-sleutel-koppeling.
- docs/beveiliging.md legt uit wat dit niet oplost: docker inspect toont de
  waarde nog steeds.

Bewerken:
- Laatste stap toont het gerenderde resultaat met jouw waarden en is
  bewerkbaar; compose_override/env_override gaan mee bij het installeren.
  Ongeldige YAML wordt geweigerd voordat er een map bestaat.
- De bestaande editor heeft tabbladen (compose/.env) en biedt herstarten na
  opslaan. De .env-PUT weigert een regel zonder '=' en logt in audit.

Inloggegevens:
- /api/stacks/<n>/credentials plus paneel op de stackkaart: adres,
  gebruikersnaam, wachtwoord achter een oog-knop, kopieerknop. Alleen voor
  beheerders, elk bekijken komt in het auditlog.
- Nieuw 'credential'-kenmerk in template.json met naamherkenning als
  terugval, zodat de bestaande 96 sjablonen meteen werken.

Herstel van een regressie uit v0.7.60: acht inlogwachtwoorden hadden hun
genereerknop verloren toen die aan 'generate' werd gehangen. Terug, behalve
waar een verzonnen waarde fout is (WireGuard-sleutel, externe tokens).

De menulogica wordt getest door de echte component uit index.html in Node uit
te voeren; slaat zichzelf over waar node ontbreekt. 2295 tests groen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
2026-07-28 21:31:51 +02:00

158 lines
6.2 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.
## 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.
- **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.