Files
DDI/docs/ARCHITECTURE.md

251 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Architektur
Dieses Dokument beschreibt den Aufbau der DDI-Suite (Arbeitstitel **ddi-suite**). Die Begründungen der einzelnen Entscheidungen stehen in den [ADRs](adr/README.md), der Auftrag in [PROJECT_BRIEF.md](PROJECT_BRIEF.md).
## 1. Überblick
```
Browser ──HTTPS :8443──▶ Management-Server (Go) ◀──▶ PostgreSQL
│ REST-API (OpenAPI), Web-UI (eingebettet), Auth/RBAC,
│ Audit, Lizenz (Keygen), Config-Generator, interne CA,
│ Bundle-Signatur
│
│ gRPC/mTLS :8444 (Agent verbindet sich ausgehend)
┌────────────┴─────────────────────┐
▼ ▼
ddi-agent-dns + BIND 9 ddi-agent-dhcp + Kea DHCPv4
- Bundle verifizieren - Bundle verifizieren
- prüfen, aktivieren, Health - prüfen, aktivieren, Health
- Rollback bei Fehler - Rollback bei Fehler
- Leases melden
```
Die DNS- und DHCP-Engines (BIND 9, Kea) sind bewährte Fremdsoftware. Die Suite erzeugt deren Konfiguration und steuert sie, ersetzt sie aber nicht.
### Grundsätze
1. **Datenebene unabhängig.** DNS und DHCP laufen weiter, wenn Server, Netzwerk oder Lizenz ausfallen. Ein Agent behält immer die letzte funktionierende Konfiguration.
2. **Deterministische, typisierte Konfigurationserzeugung** ohne Text-Templates ([0014](adr/0014-konfigurationserzeugung.md)).
3. **Kein Anwenden ohne Prüfung:** Signatur → Syntaxtest → atomare Aktivierung → Health-Check → sonst Rollback.
4. **Ausgehende Verbindungen vom Agent**, mTLS mit eigener CA ([0007](adr/0007-server-agent-grpc-mtls.md), [0008](adr/0008-interne-ca-enrollment.md)).
5. **Alles Umgebungsspezifische aus Konfiguration**, Namen zentral ([0018](adr/0018-zentrale-benennung.md)).
## 2. Repository
Monorepo mit einem Go-Modul `git.beugsiud.de/beug.k/DDI` ([0002](adr/0002-monorepo-ein-go-modul-lizenzgrenzen.md)).
| Pfad | Inhalt | Lizenz |
| --- | --- | --- |
| `proto/` | gRPC-Definitionen (`ddi/agent/v1`), generierter Go-Code unter `proto/gen/` | Apache-2.0 |
| `internal/` | Von Server und Agents gemeinsam genutzter Code (Benennung, Build-Info, Logging) | Apache-2.0 |
| `agent/` | Agent-Kern (`core`), Rollen (`role/dns`, `role/dhcp`), Binaries (`cmd/…`) | Apache-2.0 |
| `server/` | Management-Server: `cmd/ddi-server`, `api/` (OpenAPI), `internal/…` (Domäne, Speicher, Generator, CA, Lizenz) | proprietär |
| `web/` | Frontend (React, TS, Vite), eingebettet über `web/embed.go` | proprietär |
| `deploy/` | systemd-Units, Beispielkonfigurationen, `compose/` für Entwicklung und Tests | Apache-2.0 |
| `scripts/` | `install.sh`, Build-, Release- und Prüfskripte | Apache-2.0 |
| `tools/` | `keygen-setup` (Lizenzserver einrichten, nicht Teil der Auslieferung) | proprietär |
| `test/` | Integrations- und End-to-End-Tests mit echten BIND-/Kea-Containern | Apache-2.0 |
| `docs/` | Dokumentation (Deutsch) und ADRs | – |
Abhängigkeitsregel (per `depguard` geprüft): `agent/`, `internal/` und `proto/` importieren nie `server/`.
## 3. Management-Server
### 3.1 Schichten
```
server/cmd/ddi-server Befehle serve, migrate, bootstrap-admin, gen-master-key
server/api OpenAPI-Spezifikation (eingebettet)
server/internal/app Verdrahtung, Hintergrundjobs, Metriken
server/internal/config YAML-Datei + Umgebungsvariablen
server/internal/httpserver Listener, Health, Middleware (Request-ID, Log, Header, Recover)
server/internal/httpapi REST-API: Routentabelle, Sitzung/CSRF/Rechte/Schreibschutz, OIDC-Flow
server/internal/service Anwendungsfälle; jede Änderung schreibt ihr Audit in derselben Transaktion
server/internal/ipam Fachlogik Netze/DHCP-Bereiche/Adressen (rein): Validierung, effektive Pools,
Auslastung, Adresskarte, Änderungsvorschau
server/internal/auth Argon2id, TOTP, Tokens, Rate-Limiter, Rollen und Rechte
server/internal/license LicenseProvider, KeygenProvider, Lizenzdateien, Zustandsautomat, Manager
server/internal/secrets AES-256-GCM mit Master-Key (Schlüssel-IDs, Rotation)
server/internal/store PostgreSQL-Repositories (pgx), Migrationen (goose, embed)
server/internal/configgen reine Generatoren für BIND und Kea (derive, bindgen, keagen)
server/internal/agentapi gRPC-Dienst für Agents (Enrollment, Stream, Bundles, Leases), mTLS-Prüfung
server/internal/agenthub offene Agent-Streams: Push neuer Revisionen, Trennen beim Sperren
server/internal/events Ereignisbus für Live-Aktualisierungen (SSE unter /api/v1/events)
server/internal/textdiff Zeilen-Diff (Myers) im Unified-Format für Revisionen
server/internal/pki Agent-CA, Bundle-Signaturschlüssel, Zertifikate ausstellen und prüfen
internal/bundle Bundle-Manifest, Signatur und Prüfung (Ed25519), von Server und Agent genutzt
internal/agentpki Agent-ID im Zertifikat, CA-Fingerprint, TLS-Einstellungen der Agents
agent/core Agent-Kern: Konfiguration, Zustand, Enrollment, Verbindung, Ablauf, Leases
agent/role/dns, agent/role/dhcp Rollen für BIND und Kea
```
Fachlogik (`ipam`, `auth`, `license`) kennt weder HTTP noch SQL. `service` orchestriert Transaktionen; Handler und Repositories sind Adapter um sie herum.
### 3.2 Datenmodell (Kern)
Umgesetzt in M1 (`server/internal/store/migrations`): `app_settings` (u. a. Installationszeitpunkt, OIDC mit verschlüsseltem Secret), `users`, `sessions` (nur Token-Hash), `audit_log`, `networks`, `dhcp_pools` (bis Migration 00006), `addresses` (Typen `static`, `reserved`, `manual_dhcp`, `dynamic`), `saved_filters`, `license_state` (verschlüsselt).
Die Datenbank erzwingt die Kernregeln selbst:
- keine überlappenden Netze (`EXCLUDE USING gist (cidr inet_ops WITH &&)`), Gateway im Netz
- MAC Pflicht bei `manual_dhcp`, eine Reservierung pro MAC und Netz
- IP eindeutig je Netz, Name (FQDN) eindeutig je Zone für Einträge mit DNS-Records
- Adresse liegt im Netz (Trigger), Host-Adresse statt Präfix
Optimistisches Sperren über `version`; alle Änderungen an den Adressen eines Netzes sperren die Netzzeile und sind dadurch serialisiert.
M3 (Migration 00003): `agents`, `enrollment_tokens`, `pki_keys` (verschlüsselt), `config_revisions` und `config_revision_files` (unveränderlich per Trigger), `deployments`, `dhcp_leases`; Einzelheiten in [ADR 0021](adr/0021-agent-strecke-m3.md). M4 (Migration 00004): `agents.target_revision`, offene Push-Deployments, `config_revisions.source` (Generator-Eingabe für Rollbacks); Entwürfe ergeben sich aus Audit-Log und Konfigurationsvergleich ([ADR 0022](adr/0022-ausrollen-pull-push-m4.md)), eine eigene `pending_change`-Tabelle entfällt. Ab M5 folgen `zone` (`forward`/`reverse`, Modus `managed`/`dynamic`), `record` und `cluster` (`dns`/`dhcp`).
**DHCP pro Adresse ([ADR 0025](adr/0025-dhcp-pro-adresse-pruefungen-ui.md), Migration 00006):** Pools werden nicht mehr gepflegt; `dhcp_pools` entfällt. Der DHCP-Bereich eines Netzes mit `dhcp_enabled` ist die Menge seiner `dynamic`-Einträge (nur IPv4, nicht die Gateway-Adresse); jede ununterbrochene Folge davon wird ein Kea-Pool (`ipam.Ranges`, in der API als abgeleitetes `Network.pools`). Damit liegen `static`-, `reserved`- und `manual_dhcp`-Einträge automatisch außerhalb dessen, was Kea vergibt.
| Typ | DHCP | DNS (A/AAAA, PTR) |
| --- | --- | --- |
| `static` | nein | ja |
| `reserved` | nein (gesperrt) | nein |
| `manual_dhcp` | Reservierung MAC → IP | ja |
| `dynamic` | IP gehört zum DHCP-Bereich (ohne MAC) | ja (ab Generator-Eingabe Version 3) |
`dynamic`-Einträge ohne Hostnamen erhalten beim Anlegen (Formular, CSV, Bereichsoperation, Typwechsel, Massenänderung) einen zufälligen Namen `dyn-` + 8 Zeichen `[a-z0-9]`; der eindeutige FQDN-Index umfasst `static`, `manual_dhcp` und `dynamic`. Leases ändern nur vorhandene Einträge (Lease-Felder, vom Client gemeldeter Name in `lease_hostname`, bei `dynamic` die MAC), nie den Hostnamen, und legen keine Einträge an. Bereichsoperationen (`POST /networks/{id}/range`) legen bzw. löschen bis zu 65.536 Einträge in einer Anweisung mit einem Audit-Eintrag.
### 3.3 Änderungsfluss
```
UI/API-Änderung ──▶ Validierung ──▶ DB (Fachdaten + pending_change + Audit)
│ „X Änderungen warten auf Ausrollen“
Ausrollen (Benutzer) ───────────────────▶│
▼
Generator (rein) ──▶ ConfigRevision N (strukturiert + Artefakte + Diff zu N-1)
▼
Bundle je Agent (Manifest + Artefakte, Ed25519-signiert)
▼
auto_pull: Agent fragt im Intervall nach │ Push: Kommando „apply N“ über Stream
▼
Agent wendet an ──▶ Deployment-Ergebnis (Erfolg/Fehler/Rollback) ──▶ Server ──▶ UI live
```
Ein Rollback im UI erzeugt eine **neue** Revision mit altem Inhalt ([0009](adr/0009-signierte-config-bundles.md)).
### 3.4 Lizenz
Zustandsautomat `trial → valid ⇄ grace → read_only` ([0016](adr/0016-lizenzierung-keygen.md)). `read_only` sperrt zentral alle schreibenden API-Aufrufe und das Ausrollen. Agents und Datenebene sind nie betroffen.
## 4. Agents
### 4.1 Kern und Rollen
```go
// Vereinfachte Schnittstellen (Implementierung ab M3, Standalone ab M7).
type ConfigSource interface { // woher kommt die Soll-Konfiguration?
Desired(ctx context.Context) (<-chan VerifiedBundle, error)
Report(ctx context.Context, r DeploymentResult) error
}
type Role interface { // wie wird sie angewendet?
Validate(ctx context.Context, stagingDir string) error // named-checkconf / kea-dhcp4 -t
Activate(ctx context.Context, stagingDir string) error // atomar umschalten + reload
Healthy(ctx context.Context) error // Testabfrage / Control-Socket
}
```
`ServerSource` (gRPC) kommt in M3, `FileSource` (YAML, Standalone) in M7. Der Kern ist für beide identisch ([0010](adr/0010-agent-binaries-pro-rolle.md)).
### 4.2 Anwenden
1. Bundle empfangen, **Signatur** und Agent-ID prüfen, Revision > zuletzt angewendete.
2. Artefakte nach `revisions/<n>/` schreiben (atomar), SHA-256 gegen Manifest prüfen.
3. **Staging** erzeugen, Syntaxprüfung (`named-checkconf -z`, `named-checkzone`, `kea-dhcp4 -t`).
4. **Aktivieren:** Dateien atomar ersetzen, `rndc reconfig`/`reload` bzw. Kea `config-reload` über den Control-Socket.
5. **Health-Check:** DNS-Testabfrage gegen eine gerade ausgerollte Zone bzw. Kea `status-get`/`config-get`-Vergleich.
6. Fehler in 3–5 → vorige funktionierende Revision wiederherstellen, erneut aktivieren, Fehler mit Details melden.
### 4.3 DNS-Rolle
Eine generierte Include-Datei plus Zonendateien der managed Zonen. Dynamische Zonen werden angelegt, aber deren Daten nie überschrieben; die Records, die DDI darin verwaltet (Adresstabelle, Records der Zone), gleicht der DNS-Agent per `nsupdate` mit dem Systemschlüssel `ddi-agent-update` ab ([0026](adr/0026-eintraege-in-dynamischen-zonen.md)). Primary/Secondary mit NOTIFY und TSIG-gesichertem AXFR/IXFR (M5). Details: [0004](adr/0004-dns-engine-bind9.md).
### 4.4 DHCP-Rolle
Vollständige `kea-dhcp4.conf`, Pools aus den Folgen von `dynamic`-Einträgen (ADR 0025), Reservierungen aus `manual_dhcp`. Leases über `lease_cmds` (`lease4-get-page`), daraus Änderungsereignisse an den Server. HA über `libdhcp_ha.so` (M5). Nur frei verfügbare Kea-Bestandteile ([0005](adr/0005-dhcp-engine-kea.md)).
## 5. Kommunikation und Sicherheit
| Verbindung | Richtung | Port (Standard) | Schutz |
| --- | --- | --- | --- |
| Web-UI/REST-API | Browser → Server | 8443/tcp | TLS 1.2+, Sitzungscookie (`Secure`, `HttpOnly`, `SameSite=Lax`), CSRF-Token |
| Agent-Protokoll | Agent → Server | 8444/tcp | TLS 1.3, mTLS (interne CA), Enrollment per Einmal-Token + CA-Hash |
| Lizenzprüfung | Server → Keygen | 443/tcp | TLS, signierte Antworten (sofern Edition unterstützt) |
| DNS | Clients → BIND | 53/udp+tcp | – |
| Zonentransfer | Secondary ↔ Primary | 53/tcp | TSIG |
| DHCP | Clients/Relays → Kea | 67/udp (Antwort 68/udp) | – |
| Kea-HA | Kea ↔ Kea | 8001/tcp (`ha_port`) | HTTP mit Basic-Auth, noch ohne TLS (siehe [SECURITY.md](SECURITY.md#6-was-nicht-geschützt-ist)) |
Vertrauenskette und Schlüssel:
- **Master-Key** (Datei, nicht in der DB) verschlüsselt alle Geheimnisse in der DB ([0015](adr/0015-geheimnisse-verschluesselt.md)).
- **CA-Schlüssel** signiert Server- und Agent-Zertifikate ([0008](adr/0008-interne-ca-enrollment.md)).
- **Bundle-Signaturschlüssel** (Ed25519, getrennt von der CA) signiert Konfigurationen ([0009](adr/0009-signierte-config-bundles.md)).
Bedrohungsmodell, Schlüsselverwaltung und Härtung: [SECURITY.md](SECURITY.md). Betrieb (Sicherung, Upgrade, Rotation, Fehlersuche): [OPERATIONS.md](OPERATIONS.md). Ports, Versionen und gemessene Richtwerte: [SYSTEM-REQUIREMENTS.md](SYSTEM-REQUIREMENTS.md).
## 6. Dateisystem auf den Zielsystemen
| Pfad | Zweck |
| --- | --- |
| `/usr/local/bin/ddi-server`, `ddi-agent-dns`, `ddi-agent-dhcp` | Binaries |
| `/etc/ddi-suite/` | Konfiguration (`server.yaml`, `agent-dns.yaml`, …), `master.key` (nur Server) |
| `/var/lib/ddi-suite/` | Zustand (Agent-Identität und Revisionen, [0011](adr/0011-agent-zustand-in-dateien.md)) |
| `/etc/bind/ddi-suite/` bzw. `/var/named/ddi-suite/` | vom DNS-Agent erzeugte BIND-Dateien |
| `/etc/kea/kea-dhcp4.conf` | vom DHCP-Agent erzeugte Kea-Konfiguration |
Jeder Dienst läuft unter einem eigenen Systembenutzer mit systemd-Härtung. Alle Namen kommen aus der zentralen Benennung.
## 7. Konfiguration
Server und Agents lesen eine YAML-Datei (Server: Pfad aus `DDI_CONFIG_FILE`, Beispiel `deploy/config/server.dev.yaml`). Unbekannte Schlüssel sind ein Fehler. Umgebungsvariablen mit Präfix `DDI_` überschreiben einzelne Werte, damit Container und Tests ohne Datei auskommen.
| Server-Variable | Bedeutung |
| --- | --- |
| `DDI_HTTP_LISTEN`, `DDI_TLS_CERT_FILE`, `DDI_TLS_KEY_FILE`, `DDI_PUBLIC_URL` | Listener, TLS, externe URL (OIDC-Redirect) |
| `DDI_DATABASE_URL` bzw. `DDI_DATABASE_URL_FILE` | PostgreSQL |
| `DDI_MASTER_KEY_FILE`, `DDI_STATE_DIR` | Master-Key, Zustandsverzeichnis |
| `DDI_LOG_LEVEL`, `DDI_LOG_FORMAT` | Logging |
| `DDI_SESSION_LIFETIME`, `DDI_SESSION_IDLE_TIMEOUT` | Sitzungen |
| `DDI_LICENSE_GRACE_PERIOD`, `KEYGEN_URL`, `KEYGEN_PRODUCT_ID` | Lizenz (siehe [LICENSING.md](LICENSING.md)) |
| `DDI_RETENTION_AUDIT_DAYS`, `DDI_RETENTION_REVISIONS_KEEP` | Aufbewahrung (`retention.audit_days`, `retention.revisions_keep`; 0 = alles behalten) |
`KEYGEN_ACCOUNT_ID` und `KEYGEN_PUBLIC_KEY` gelten nur für Builds ohne eingebettete Werte (Entwicklung). Dauerangaben verstehen Go-Notation und Tage (`14d`).
**Aufbewahrung** ([0024](adr/0024-auslieferung-installer-m6.md)): Ist `retention.audit_days` oder `retention.revisions_keep` gesetzt, räumt der Server fünf Minuten nach dem Start und danach täglich auf. Gelöscht werden Audit-Einträge, die älter als die angegebene Zahl Tage sind, und Revisionen jenseits der neuesten `revisions_keep` (samt Dateien). Nie gelöscht werden die neueste Revision, Revisionen, die ein Agent anwendet, als letzte funktionierende kennt oder als Ziel hat, und Revisionen mit offenen Deployments. Ein Aufräumlauf, der etwas löscht, schreibt den Audit-Eintrag `retention.cleanup` mit den Anzahlen.
## 8. Build, CI und Auslieferung
- `Makefile` als Einstieg: `build`, `test`, `lint`, `fmt`, `generate`, `web`, `dev-up`/`dev-down`, `release`.
- CI: Gitea Actions unter `.gitea/workflows/` ([0003](adr/0003-git-host-gitea-actions.md)). Geprüft werden Go (Format, `vet`, `golangci-lint`, Tests, `govulncheck`), Protobuf (`buf lint`, generierter Code aktuell), Web (ESLint, Typecheck, Build, Tests, `npm audit`), Shell (`shellcheck`), SPDX-Header und die Entwicklungsumgebung (Compose-Smoke-Test).
- Release bei Tag `v*`: Binaries für `linux/amd64` und `linux/arm64`, Tarballs, `.deb`/`.rpm` (nfpm), `SHA256SUMS` und – mit Secret `RELEASE_SIGNING_KEY` – die Ed25519-Signatur `SHA256SUMS.sig`, Upload an das Gitea-Release ([RELEASE.md](RELEASE.md)).
- Installer `scripts/install.sh` (M6) lädt die Artefakte und prüft die Prüfsummen.
## 9. Entwicklungsumgebung
`deploy/compose/compose.dev.yaml` startet PostgreSQL 17, BIND 9.20 und Kea DHCPv4 3.2 mit Beispielkonfigurationen. Server und Agents laufen in der Entwicklung direkt auf dem Host: `make run-server` (erzeugt beim ersten Mal `local/master.key`), `make dev-admin` legt den ersten Administrator an, `cd web && npm run dev` startet die Oberfläche mit Hot Reload. Siehe [README](../README.md).
## 10. Skalierung
Zielgröße der ersten Version sind bis ca. 50 Agents und einige zehntausend Adressen. Gemessene Werte für 5.000 und 50.000 Adressen stehen in [SYSTEM-REQUIREMENTS.md](SYSTEM-REQUIREMENTS.md#4-messungen). Damit es nach oben nicht verbaut ist, gilt:
- Tabellen werden serverseitig paginiert und gefiltert. Indizes auf `ip`, `mac`, `hostname`.
- Revisionen speichern strukturierte Daten plus Artefakte. Die Aufbewahrung ist konfigurierbar.
- Agent-Streams sind leichtgewichtig (ein Stream pro Agent). Lease-Meldungen laufen gebündelt.
- Mögliche spätere Schritte: Katalogzonen für Secondaries, inkrementelle Kea-Änderungen per `host_cmds` (frei ab Kea 3.0), mehrere Server-Instanzen hinter einem Load-Balancer (Sitzungen und Stream-Zuordnung über die Datenbank).
## 11. Meilensteine
| Meilenstein | Inhalt |
| --- | --- |
| M0 | Fundament: Repo, Makefile, Lint, CI, Compose, ADRs, dieses Dokument |
| M1 | Server-Kern: Schema, Auth, RBAC, Audit, Lizenz, REST-API, Netze/Adressen, UI-Grundgerüst |
| M2 | Config-Generator mit Golden- und Integrationstests |
| M3 | Agent verwaltet: Enrollment, mTLS, Signatur, Anwenden, Rollback, Leases |
| M4 | Pull/Push, Entwürfe, Diff, Live-Status, Rollback im UI |
| M5 | Cluster, Zonen, Records, dynamische Zonen, Kea-HA, Lease-Erfassung |
| M6 | Installer, Release, Pakete, Systemvoraussetzungen mit Messungen, Betriebs- und Sicherheitsdoku |
| M7 | (später) Standalone-Agent, DHCPv6, DNSSEC, Discovery |