docs: handleiding + functionele + technische specificatie
Drie nieuwe markdown-bestanden onder /docs: - handleiding.md — voor de dagelijkse gebruiker: eerste login, modules, veelvoorkomende taken (Caddy editen, sprint mergen via flow), wat expliciet niet vanuit de UI kan, log-locaties bij incidenten, veiligheidsadvies. - specs/functional.md — wat de app doet: scope per module met acceptatiecriteria, flow state-machine (pending/running/success/ failed/cancelled/timeout), hard limits (1 actieve flow, 64KB log knippen, 24u session), expliciete buiten-scope-lijst. - specs/technical.md — hoe het werkt: 3-process architectuur (dashboard container + agent op host + Postgres), stack-tabel met versies en redenen, data-model (User/Session/FlowRun/FlowStep), auth-flow met CSRF, agent-protocol over SSE, security-eigenschappen per laag. Lengtes pragmatisch gekozen — geen completeness-fetisj, wel genoeg om iemand die nieuw is in de codebase binnen 30 min te oriënteren. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
44e9280de1
commit
fda7be3214
3 changed files with 545 additions and 0 deletions
128
docs/handleiding.md
Normal file
128
docs/handleiding.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Handleiding — Ops Dashboard
|
||||
|
||||
Voor de dagelijkse beheerder van een single-host server-stack (Docker, systemd, Git, Caddy, Postgres). Deze handleiding beschrijft *hoe* je de app gebruikt, niet *hoe* hij werkt — voor dat laatste zie [`specs/technical.md`](./specs/technical.md).
|
||||
|
||||
## Eerste inlog
|
||||
|
||||
1. Open `https://<jouw-host>/` (bv. `https://ops.jp-visser.nl`).
|
||||
2. Je wordt doorgestuurd naar `/login`. Vul de admin-credentials in die je tijdens deploy in `SEED_USER_EMAIL` / `SEED_USER_PASSWORD` hebt gezet.
|
||||
3. Na succesvolle login zit je 24 uur ingelogd via een HttpOnly-cookie. Daarna opnieuw inloggen.
|
||||
|
||||
Wachtwoord vergeten? Geen reset-flow in de app — los op met een SQL-update:
|
||||
|
||||
```bash
|
||||
# Genereer een nieuwe bcrypt-hash voor je nieuwe wachtwoord
|
||||
docker run --rm -e PW='<nieuw-wachtwoord>' node:22-alpine sh -c '
|
||||
cd /tmp && npm init -y >/dev/null 2>&1 && npm install --silent bcryptjs >/dev/null 2>&1
|
||||
node -e "console.log(require(\"bcryptjs\").hashSync(process.env.PW, 12))"
|
||||
'
|
||||
# Plak in psql:
|
||||
docker exec -it scrum4me-postgres psql -U scrum4me -d ops_dashboard \
|
||||
-c "UPDATE \"User\" SET pwd_hash = '<hash>' WHERE email = '<jouw-email>';"
|
||||
```
|
||||
|
||||
## Dashboard (home)
|
||||
|
||||
Vijf live status-widgets, auto-refresh ~5 sec:
|
||||
|
||||
| Widget | Toont |
|
||||
|---|---|
|
||||
| **Docker** | Aantal draaiende containers + lijst |
|
||||
| **Git** | Branch en uncommitted-status per geconfigureerd repo-pad |
|
||||
| **systemd** | Service-status (active/inactive/failed) per geconfigureerde unit |
|
||||
| **Caddy** | TLS-certs met dichtstbijzijnde expiratiedatum (geel = <30 dagen) |
|
||||
| **Audit** | Laatste flow-run met timestamp en exit-status |
|
||||
|
||||
Klik een widget aan om naar de detail-pagina te gaan.
|
||||
|
||||
## Modules
|
||||
|
||||
### `/docker` — Containers
|
||||
|
||||
Tabel van `docker ps` met auto-refresh. Klik op een container-naam voor detail (logs, image, ports, status).
|
||||
|
||||
Read-only — geen start/stop/restart vanuit de UI. Voor wijzigingen: een **Flow** (zie hieronder).
|
||||
|
||||
### `/git` — Repositories
|
||||
|
||||
Per geconfigureerd pad in `REPO_PATHS` (env var): branch, uncommitted-files (M/A/D/??), laatste 3 commits. Klik door voor diff-viewer.
|
||||
|
||||
Read-only — pulls/commits gaan via een Flow.
|
||||
|
||||
### `/systemd` — Services
|
||||
|
||||
Lijst van services uit `SYSTEMD_UNITS` (env var). Toont status, laatste log-regels. Klik door voor full journal-tail van die unit.
|
||||
|
||||
Restart-knop: alleen voor units die in `commands.yml` zijn whitelisted én in `sudoers.d/ops-agent` met `NOPASSWD` staan.
|
||||
|
||||
### `/caddy` — Reverse-proxy & TLS
|
||||
|
||||
Toont de actieve Caddyfile (syntax-highlighted) plus alle TLS-certs (subject + expiry). "Edit"-knop opent een editor — opslaan **valideert** de Caddyfile via `caddy validate` voordat het wordt geschreven.
|
||||
|
||||
### `/flows` — Multi-step deployments
|
||||
|
||||
Twee voor-gedefinieerde flows:
|
||||
|
||||
- **Update Scrum4Me website** — pull main, build container, restart, smoke-test
|
||||
- **Update Caddy config** — schrijf nieuwe Caddyfile, valideer, restart Caddy, verifieer dat alle hostnames nog reageren
|
||||
|
||||
Een flow draait stap-voor-stap met **dry-run** als standaard. Na dry-run zie je per stap wat het gaat doen. Klik "Run" om echt uit te voeren. Tijdens executie zie je live stdout/stderr per stap.
|
||||
|
||||
### `/audit` — Flow-runs
|
||||
|
||||
Chronologische lijst van alle gestarte flows: starttijd, duur, exit-status, wie 'm startte. Klik door voor de volledige output (stdout/stderr per stap).
|
||||
|
||||
### `/settings/backups` — Backups
|
||||
|
||||
Postgres backup-management:
|
||||
|
||||
- Lijst van bestaande dump-bestanden in `/srv/scrum4me/backups`
|
||||
- "Backup now"-knop maakt een dump met timestamp-naam
|
||||
- Restore-runbook (handmatige stappen — geen automatische restore vanuit UI om de blast-radius klein te houden)
|
||||
|
||||
## Veelvoorkomende taken
|
||||
|
||||
### Container hangt — wat nu?
|
||||
|
||||
1. `/docker` → klik container-naam → bekijk logs
|
||||
2. Diagnose? Open een SSH-sessie en gebruik `docker logs`, `docker exec` etc. (Niet vanuit de UI — dat is buiten scope.)
|
||||
3. Restart nodig? Voeg de container toe aan `commands.yml` whitelist (op de host) + run via `/flows`
|
||||
|
||||
### Caddy-config wijzigen
|
||||
|
||||
1. `/caddy` → "Edit"
|
||||
2. Pas Caddyfile aan in de editor
|
||||
3. Save → app draait `caddy validate` → bij succes wordt het geschreven en Caddy herstart
|
||||
4. Verifieer in `/caddy` dat het cert-overzicht klopt
|
||||
|
||||
> Voor breaking changes (verkeerde syntax of niet-bestaande site): de validate-stap blokkeert. Bij twijfel: maak eerst een backup van `/srv/scrum4me/caddy/Caddyfile`.
|
||||
|
||||
### Sprint mergen via flow
|
||||
|
||||
`/flows/update-scrum4me-web` — kies branch (default `main`), klik dry-run, lees wat het doet, klik "Run". Stap-output stream live. Na success: smoke-test verifieert dat de homepage 200 geeft.
|
||||
|
||||
## Wat kan **niet** vanuit de UI
|
||||
|
||||
- SSH-toegang of arbitrary shell-commando's (alleen whitelisted commands.yml-keys)
|
||||
- User-management (één admin via seed; multi-user is buiten scope)
|
||||
- Container starten met andere image of args (alleen restart van bestaande)
|
||||
- Wachtwoord reset (SQL-update vereist)
|
||||
- Cert handmatig forceren (Caddy doet auto-ACME)
|
||||
|
||||
## Logs voor incident-response
|
||||
|
||||
| Component | Log-locatie |
|
||||
|---|---|
|
||||
| Dashboard app | `docker logs scrum4me-ops-dashboard` |
|
||||
| ops-agent | `journalctl -u ops-agent -f` |
|
||||
| Caddy | `docker logs scrum4me-caddy` |
|
||||
| Postgres | `docker logs scrum4me-postgres` |
|
||||
|
||||
Audit-trail van wat-doet-wie-wanneer: tabel `FlowRun` + `FlowStep` in de `ops_dashboard` database, of via `/audit` in de UI.
|
||||
|
||||
## Veiligheidsadvies
|
||||
|
||||
- Houd port 3099 (ops-agent) **niet** open naar de buitenwereld. UFW-regel scoped op `172.18.0.0/16`. Zie [`runbooks/post-install.md`](./runbooks/post-install.md).
|
||||
- Roteer `OPS_AGENT_SECRET` jaarlijks: nieuw secret in `.env` én `/etc/ops-agent/secret`, dan beide herstarten.
|
||||
- Voeg geen wildcards toe in `sudoers.d/ops-agent` — elke `systemctl`-actie moet een expliciete service-naam zijn.
|
||||
- `commands.yml` is single source of truth voor wat de agent mag — alles wat niet in de whitelist staat, kan een aanvaller niet uitvoeren ook al heeft hij het secret.
|
||||
147
docs/specs/functional.md
Normal file
147
docs/specs/functional.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# Functionele specificatie — Ops Dashboard
|
||||
|
||||
## Doel
|
||||
|
||||
Eén web-UI waarmee de eigenaar van een single-host server-stack (Docker + systemd + Git-checkouts + Caddy + Postgres) dezelfde operaties kan uitvoeren die anders in een SSH-terminal gebeuren — met audit-log, herhaalbare flows en minder typefouten.
|
||||
|
||||
**Schaal:** één host, één admin-gebruiker. Multi-host/team is buiten scope.
|
||||
|
||||
## Gebruikers en rollen
|
||||
|
||||
| Rol | Beschrijving | Hoeveelheid |
|
||||
|---|---|---|
|
||||
| **admin** | Volle toegang tot alle modules en flows. Single account, geseed via env. | 1 |
|
||||
|
||||
Geen RBAC, geen tenant-isolatie, geen "view-only"-modus. Wie inlogt kan alles.
|
||||
|
||||
## Functionele scope per module
|
||||
|
||||
### Dashboard (`/`)
|
||||
|
||||
5 live status-widgets met auto-refresh ~5s:
|
||||
|
||||
| Widget | Data-bron | Indicator |
|
||||
|---|---|---|
|
||||
| Docker | `docker ps --format json` | Count van running containers, lijst (naam + status) |
|
||||
| Git | `git status --short --branch` per pad in `REPO_PATHS` | Branch + dirty-vlag |
|
||||
| systemd | `systemctl is-active <unit>` per item in `SYSTEMD_UNITS` | Active / Inactive / Failed |
|
||||
| Caddy | `caddy admin-cmd certificates` (of equiv. shell-output parse) | Aantal certs + dichtstbijzijnde expiry |
|
||||
| Audit | DB-query op `FlowRun` desc | Laatste run + status |
|
||||
|
||||
**Acceptatie:**
|
||||
- Widget laadt < 1 s na page-load
|
||||
- Auto-refresh werkt in achtergrond zonder volledig herrenderen
|
||||
- Bij fout (agent down, command faalt): widget toont rood errorblok, niet de hele page
|
||||
|
||||
### Auth (`/login`)
|
||||
|
||||
- Email + wachtwoord (single user)
|
||||
- 5 failed attempts in 1 minuut → 429 rate-limit per IP
|
||||
- Succesvolle login → session-cookie 24u, HttpOnly, SameSite=strict, Secure (production)
|
||||
- `/api/auth/logout` invalideert sessie en wist cookie
|
||||
|
||||
### Docker (`/docker`)
|
||||
|
||||
- Lijst running containers (CONTAINER ID, IMAGE, COMMAND, CREATED, STATUS, PORTS, NAMES)
|
||||
- Auto-refresh elke 5s
|
||||
- `/docker/[name]` → detail-page met logs (laatste 200 regels), image-info, environment
|
||||
- **Geen** start/stop/restart vanuit UI — alleen via flows
|
||||
|
||||
### Git (`/git`)
|
||||
|
||||
- Per pad in `REPO_PATHS`: huidige branch, ahead/behind count, modified-files-count, laatste 3 commits
|
||||
- `/git/[repo]` → diff-viewer voor uncommitted changes + commit-historie laatste 20
|
||||
|
||||
### systemd (`/systemd`)
|
||||
|
||||
- Per unit in `SYSTEMD_UNITS`: active/inactive/failed, last-changed-timestamp
|
||||
- `/systemd/[unit]` → laatste 100 journal-regels van die unit, met level-filter
|
||||
- **Restart-actie**: alleen voor units die expliciet in `sudoers.d/ops-agent` met NOPASSWD staan
|
||||
|
||||
### Caddy (`/caddy`)
|
||||
|
||||
- Toon huidige `/srv/scrum4me/caddy/Caddyfile` met syntax-highlighting
|
||||
- Toon alle uitgegeven certs (subject, issuer, expiry, dichtstbijzijnde eerst)
|
||||
- Geel-warning bij expiry < 30 dagen, rood bij < 7 dagen
|
||||
- `/caddy/edit` → editor met save-knop; save valideert via `caddy validate` voor commit en restart van caddy-container
|
||||
|
||||
### Flows (`/flows`)
|
||||
|
||||
Twee voor-gedefinieerde flows in YAML in `ops-agent/flows.example/`:
|
||||
|
||||
| Flow | Stappen |
|
||||
|---|---|
|
||||
| `update_scrum4me_web` | git pull → npm run build → docker compose up -d --build → smoke-test op homepage |
|
||||
| `update_caddy_config` | write nieuw Caddyfile → caddy validate → docker compose restart caddy → check cert renewal |
|
||||
|
||||
Per flow:
|
||||
- Dry-run default (toont alleen wat het zou doen)
|
||||
- "Run"-knop voert echt uit; toont live SSE-stream van stdout/stderr per stap
|
||||
- Bij stap-fail: stop, markeer FlowRun als `failed`, latere stappen niet uitgevoerd
|
||||
- Bij success: FlowRun = `success`, totaalduur opgeslagen
|
||||
|
||||
### Audit (`/audit`)
|
||||
|
||||
- Lijst van alle `FlowRun` records, default 50 laatste, sort desc op `started_at`
|
||||
- Filter op status, datumrange, flow-name
|
||||
- `/audit/[flow_run_id]` → volledige output per stap, scrollable
|
||||
|
||||
### Settings/Backups (`/settings/backups`)
|
||||
|
||||
- Lijst van `.sql.gz` bestanden in `/srv/scrum4me/backups`, met size + mtime
|
||||
- "Backup now"-knop → maakt nieuwe dump met `pg_dumpall` voor alle databases
|
||||
- Restore: **handmatig vanuit terminal** — UI toont alleen de stappen als runbook
|
||||
|
||||
## State-machine flows
|
||||
|
||||
```
|
||||
┌────────┐
|
||||
│ pending │
|
||||
└────┬────┘
|
||||
│ (start request)
|
||||
┌────▼────┐
|
||||
│ running │
|
||||
└────┬────┘
|
||||
┌────┼────┬────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
success failed cancelled timeout (>30min)
|
||||
```
|
||||
|
||||
`pending` → `running`: bij ontvangst start-request
|
||||
`running` → `success`: alle stappen exit-code 0
|
||||
`running` → `failed`: een stap exit-code ≠ 0
|
||||
`running` → `cancelled`: user klikt cancel
|
||||
`running` → `timeout`: na 30 min nog steeds running (cleanup-job)
|
||||
|
||||
## Hard limits
|
||||
|
||||
- Max 1 actieve flow tegelijk (lock-file in `/var/run/agent/`); 2e start-request → 409 Conflict
|
||||
- Stdout/stderr per stap geknipt op 64 KB om audit-log niet te laten exploderen
|
||||
- Session-TTL hard 24 uur, geen "remember me"
|
||||
- Auto-refresh max 1 keer per 5 seconden om agent niet te overbelasten
|
||||
|
||||
## Niet-functionele eisen
|
||||
|
||||
| Eis | Doel |
|
||||
|---|---|
|
||||
| First load < 1s | Single-user, lokale Postgres, geen onnodige round-trips |
|
||||
| Module-page TTI < 2s | Server-side render met direct agent-call, geen client-fetch waterval |
|
||||
| Audit-trail volledig | Elke flow-start logt user + tijdstempel + args; elke command-execution logt exit + duration |
|
||||
| Geen geheime data in URLs | Tokens in headers, secrets nooit in query-params |
|
||||
| CSP strict | `script-src 'self' 'unsafe-inline'`; geen externe CDNs |
|
||||
| HTTPS-only in productie | Caddy auto-ACME; cookies Secure-flag in prod-mode |
|
||||
|
||||
## Buiten scope
|
||||
|
||||
- Meerdere admins / RBAC
|
||||
- Meerdere hosts / cluster-management
|
||||
- Custom container starten (alleen restart bestaande)
|
||||
- Real-time alerts (geen pager, geen email)
|
||||
- Externe monitoring-integratie (Grafana/Prometheus/Sentry)
|
||||
- Wachtwoord-reset-flow / SSO
|
||||
|
||||
## Verwante documenten
|
||||
|
||||
- [Technische specificatie](./technical.md) — hoe het werkt
|
||||
- [Handleiding](../handleiding.md) — hoe je het gebruikt
|
||||
- [Post-install runbook](../runbooks/post-install.md) — eerste deploy
|
||||
270
docs/specs/technical.md
Normal file
270
docs/specs/technical.md
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# Technische specificatie — Ops Dashboard
|
||||
|
||||
## Architectuur in één plaatje
|
||||
|
||||
```
|
||||
┌────────────────┐ HTTPS ┌──────┐ HTTP ┌─────────────────┐
|
||||
│ Browser (jou) ├─────────►│Caddy ├────────►│ ops-dashboard │
|
||||
│ │ │ :443 │ │ Next.js 16 :3000│
|
||||
└────────────────┘ └──────┘ └────┬────────┬───┘
|
||||
│ │
|
||||
HMAC HTTP │ │ TCP/SQL
|
||||
:3099 │ │
|
||||
┌───────────────▼┐ │
|
||||
│ ops-agent │ │
|
||||
│ Fastify on host│ │
|
||||
│ spawn/exec │ │
|
||||
└───┬────────────┘ │
|
||||
│ │
|
||||
┌───────┴───────┐ ┌───────▼────────┐
|
||||
│ Whitelisted │ │ Postgres 17 │
|
||||
│ host commands │ │ db=ops_dashb.. │
|
||||
│ docker/git/etc │ └────────────────┘
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
Drie processen, één host:
|
||||
|
||||
1. **ops-dashboard** — Next.js app in Docker, op compose-bridge, exposed via Caddy
|
||||
2. **ops-agent** — Node/Fastify service direct op host (geen container), heeft sudoers + docker.sock access
|
||||
3. **postgres** — Docker container, dezelfde als die Scrum4Me al gebruikt; ops-dashboard heeft eigen DB `ops_dashboard`
|
||||
|
||||
## Stack
|
||||
|
||||
| Laag | Technologie | Versie | Reden |
|
||||
|---|---|---|---|
|
||||
| App framework | Next.js | 16.2 (App Router) | RSC server-side fetching matched onze "render with agent data" patroon |
|
||||
| UI library | React | 19 | Bundled bij Next 16 |
|
||||
| Styling | Tailwind CSS | 4 | Utility-first; geen custom design system |
|
||||
| UI primitives | `@base-ui/react` | 1.4 | Headless components, geen Radix-lock-in |
|
||||
| Code highlighting | shiki | 1.29 | Server-side highlighting in Caddyfile view |
|
||||
| Database ORM | Prisma | 7.8 (via `@prisma/adapter-pg`) | Same as Scrum4Me; één skill om beide te onderhouden |
|
||||
| Auth (password) | bcryptjs | 3 | Geen native bindings nodig |
|
||||
| Session | Custom in `lib/session.ts` | — | Eenvoudig: token in DB, hash in cookie |
|
||||
| Agent | Fastify | 5 | Lichtgewicht, native SSE-streaming |
|
||||
| Agent whitelist | js-yaml | 4 | Read-only configfile |
|
||||
|
||||
## Deploy-topologie
|
||||
|
||||
| Component | Locatie | Beheer |
|
||||
|---|---|---|
|
||||
| ops-dashboard | Docker container `scrum4me-ops-dashboard`, image `ops-dashboard:latest` | `docker compose` in `/srv/scrum4me/compose/docker-compose.yml` |
|
||||
| ops-agent | systemd unit `ops-agent.service`, host-binary `/opt/ops-agent/dist/index.js` | systemd, geïnstalleerd via `deploy/ops-agent/setup.sh` |
|
||||
| Caddyfile-route | Block in `/srv/scrum4me/caddy/Caddyfile` | Handmatig, na add restart Caddy-container |
|
||||
| Database | Postgres-container `scrum4me-postgres`, db `ops_dashboard` | Hergebruik bestaande container |
|
||||
| Backups | `/srv/scrum4me/backups/*.sql.gz` | Cron of handmatig via UI |
|
||||
|
||||
Caddy routeert `ops.jp-visser.nl` → service-naam `ops-dashboard:3000` op compose-bridge.
|
||||
|
||||
## Data-model
|
||||
|
||||
```
|
||||
User
|
||||
├── id cuid (string PK)
|
||||
├── email unique
|
||||
├── pwd_hash bcrypt $2b$12$...
|
||||
└── created_at
|
||||
|
||||
Session
|
||||
├── id cuid (PK)
|
||||
├── user_id → User
|
||||
├── token_hash sha256 hex (cookie waarde wordt gehashed opgeslagen)
|
||||
└── expires_at 24h na create
|
||||
|
||||
FlowRun
|
||||
├── id cuid (PK)
|
||||
├── user_id → User
|
||||
├── flow_name string (bv. "update_scrum4me_web")
|
||||
├── status enum: pending|running|success|failed|cancelled
|
||||
├── started_at
|
||||
├── finished_at nullable
|
||||
└── (1:N) FlowStep
|
||||
|
||||
FlowStep
|
||||
├── id cuid (PK)
|
||||
├── flow_run_id → FlowRun (cascade delete)
|
||||
├── step_index int
|
||||
├── name string (zoals in YAML flow-definitie)
|
||||
├── exit_code int nullable
|
||||
├── stdout text (max 64KB, geknipt)
|
||||
├── stderr text (max 64KB, geknipt)
|
||||
├── started_at
|
||||
└── finished_at nullable
|
||||
```
|
||||
|
||||
Migrations in `prisma/migrations/`. Seed in `prisma/seed.ts` (creëert eerste admin uit `SEED_USER_*`).
|
||||
|
||||
## Auth-flow
|
||||
|
||||
```
|
||||
1. Browser GET /login
|
||||
← Set-Cookie: csrf_token=<uuid>; SameSite=strict; httpOnly=false
|
||||
← HTML form
|
||||
|
||||
2. Browser POST /api/auth/login
|
||||
Headers:
|
||||
Cookie: csrf_token=<uuid>; ops_session=...
|
||||
x-csrf-token: <uuid> ← double-submit CSRF check
|
||||
Body: { email, password }
|
||||
|
||||
3. Server:
|
||||
a. proxy.ts CSRF check (cookie==header)
|
||||
b. /api/auth/login route:
|
||||
- rate-limit per IP (5/min)
|
||||
- prisma.user.findUnique({ email })
|
||||
- bcrypt.compare(password, user.pwd_hash)
|
||||
c. Bij succes:
|
||||
- generateSessionToken (32 bytes hex)
|
||||
- prisma.session.create({ token_hash: sha256(token), expires_at: now+24h })
|
||||
- Set-Cookie ops_session=<token>; HttpOnly; SameSite=strict; Secure (in prod)
|
||||
|
||||
4. Browser GET /<any-protected-path>
|
||||
Server: proxy.ts → als geen ops_session cookie → redirect /login
|
||||
Anders: getCurrentUser() leest cookie, hashed, prisma.session.findUnique({ token_hash })
|
||||
```
|
||||
|
||||
CSRF: double-submit cookie pattern. CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy via proxy.ts response-headers.
|
||||
|
||||
## Agent-protocol
|
||||
|
||||
Dashboard → agent communicatie via `lib/agent-client.ts`:
|
||||
|
||||
```
|
||||
POST http://172.18.0.1:3099/agent/v1/exec
|
||||
Headers:
|
||||
Authorization: Bearer <OPS_AGENT_SECRET>
|
||||
Content-Type: application/json
|
||||
Body:
|
||||
{ command_key: "docker_ps", args?: string[], stdin?: string }
|
||||
```
|
||||
|
||||
Response: SSE stream
|
||||
|
||||
```
|
||||
event: stdout
|
||||
data: {"data": "<chunk>"}
|
||||
|
||||
event: stderr
|
||||
data: {"data": "<chunk>"}
|
||||
|
||||
event: exit
|
||||
data: {"code": 0}
|
||||
```
|
||||
|
||||
Agent server-side flow per call:
|
||||
1. `req.body.command_key` → lookup in `/etc/ops-agent/commands.yml`
|
||||
2. Bij hit: spawn `def.cmd[0]` met `def.cmd.slice(1) ++ args` (geen shell, geen interpolatie)
|
||||
3. Stream stdout/stderr chunks naar SSE
|
||||
4. Bij `child.close`: write `event: exit`, end response
|
||||
5. Bij `child.error`: write `event: error`, end response
|
||||
6. Bij `reply.raw.close` (client-disconnect): `child.kill()`
|
||||
7. Audit-log naar journalctl: `{audit:true, command_key, args, exit_code, duration_ms}`
|
||||
|
||||
`commands.yml` voorbeeld:
|
||||
|
||||
```yaml
|
||||
docker_ps:
|
||||
cmd: ["docker", "ps", "--format", "json"]
|
||||
description: "List running containers"
|
||||
|
||||
git_status:
|
||||
cmd: ["git", "status", "--short", "--branch"]
|
||||
cwd_pattern: true # args[0] = cwd, rest = command args
|
||||
description: "Git status in a repo"
|
||||
|
||||
systemctl_restart_caddy:
|
||||
cmd: ["sudo", "/usr/bin/systemctl", "restart", "caddy"]
|
||||
description: "Restart caddy service"
|
||||
```
|
||||
|
||||
Geen `command_key` in whitelist → 403 Forbidden.
|
||||
|
||||
## Flows engine
|
||||
|
||||
YAML-definitie in `ops-agent/flows.example/*.yml`:
|
||||
|
||||
```yaml
|
||||
name: update_scrum4me_web
|
||||
description: Pull main, build, restart container, verify
|
||||
steps:
|
||||
- name: Pull latest main
|
||||
command_key: git_pull
|
||||
args: ["/srv/scrum4me/repos/Scrum4Me", "main"]
|
||||
precondition: git_status_clean
|
||||
- name: Build container
|
||||
command_key: docker_compose_build
|
||||
args: ["scrum4me-web"]
|
||||
- name: Restart
|
||||
command_key: docker_compose_up
|
||||
args: ["-d", "scrum4me-web"]
|
||||
- name: Smoke test
|
||||
command_key: curl_status
|
||||
args: ["https://scrum4me.jp-visser.nl"]
|
||||
expect_exit_code: 0
|
||||
```
|
||||
|
||||
Runner (`ops-agent/src/lib/flow-runner.ts`):
|
||||
- Sequential, fail-fast
|
||||
- Per stap: check preconditions, spawn, capture stdout/stderr, store in FlowStep
|
||||
- Bij dry-run: vervang `spawn` door log van `def.cmd ++ args`
|
||||
- Bij echte run: stream via SSE naar dashboard `/api/flows/run` route
|
||||
|
||||
## Realtime in de UI
|
||||
|
||||
Niet via WebSocket of Server-Sent Events op de dashboard-side. Auto-refresh wordt server-rendered (`export const dynamic = 'force-dynamic'`) met client-side `useEffect(setInterval, 5000)` om `router.refresh()` te triggeren.
|
||||
|
||||
Flow-execution: client opent `EventSource` op `/api/flows/run/[id]` die de SSE van de agent doorstuurt.
|
||||
|
||||
## Configuratie
|
||||
|
||||
Verplicht in `.env`:
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgresql://USER:PASS@postgres:5432/ops_dashboard
|
||||
OPS_AGENT_URL=http://172.18.0.1:3099
|
||||
OPS_AGENT_SECRET=<hex-32-bytes>
|
||||
SEED_USER_EMAIL=admin@example.com
|
||||
SEED_USER_PASSWORD=<sterk-wachtwoord>
|
||||
```
|
||||
|
||||
Optioneel:
|
||||
|
||||
```bash
|
||||
SYSTEMD_UNITS=scrum4me-web,ops-agent # comma-separated
|
||||
REPO_PATHS=/srv/scrum4me/repos/Scrum4Me,… # comma-separated absolute paths
|
||||
```
|
||||
|
||||
Bij start: app valideert dat verplichte env vars gezet zijn; faalt fast met duidelijke error.
|
||||
|
||||
## Security-eigenschappen
|
||||
|
||||
| Eigenschap | Implementatie |
|
||||
|---|---|
|
||||
| Wachtwoord-hashing | bcrypt 12 rounds |
|
||||
| Session-cookie | HttpOnly, SameSite=strict, Secure in prod, 24u TTL |
|
||||
| CSRF | Double-submit cookie pattern, validated in `proxy.ts` voor POSTs |
|
||||
| CSP | Strict in response headers — geen inline scripts behalve Next.js internals met nonce |
|
||||
| Agent-auth | HMAC via Bearer-token (`OPS_AGENT_SECRET`) — symmetrisch |
|
||||
| Command-injection | `spawn(bin, args, {shell: false})` — geen shell-interpolatie ooit |
|
||||
| Whitelist | `commands.yml` is single source of truth voor wat draaibaar is |
|
||||
| Sudo | `sudoers.d/ops-agent` met absolute paden + service-namen, geen wildcards |
|
||||
| Audit | Elke `/agent/v1/exec` call logt naar journalctl met `{audit:true, …}` markeer |
|
||||
| Rate-limit | Login 5/min/IP; agent per-secret zonder rate-limit (single-user trust) |
|
||||
| Bind | Agent bindt op `0.0.0.0:3099`; UFW staat alleen `172.18.0.0/16` toe |
|
||||
|
||||
## Niet-functionele eigenschappen
|
||||
|
||||
| Eigenschap | Specificatie |
|
||||
|---|---|
|
||||
| Geen multi-tenancy | Eén user-row in DB, app verifieert alleen "is er een geldig session-record"; geen `WHERE user_id = ?` filter (single-tenant) |
|
||||
| Geen retry/queue | Failed flows blijven failed; user moet handmatig opnieuw klikken |
|
||||
| Geen migrations-automation | `prisma migrate deploy` is **niet** in de boot-flow; doe je expliciet bij elke deploy |
|
||||
| Geen graceful shutdown | Container SIGTERM → in-flight requests verloren; geen drain |
|
||||
| Logging | Stdout/stderr van containers via `docker logs`; agent via `journalctl -u ops-agent`; geen aggregator |
|
||||
|
||||
## Open punten
|
||||
|
||||
- **Echte caddyfile-grammar** (IDEA-061) — nu nginx-fallback
|
||||
- **Multi-user / RBAC** — buiten scope, mogelijk later
|
||||
- **Rate-limit op agent** — voor multi-user toekomst nodig
|
||||
- **Real-time alerts** — momenteel pull-based, push naar Slack/Tailscale-only nog niet
|
||||
Loading…
Add table
Add a link
Reference in a new issue