feat(opnsense,certmatch): read-only Export-Client mit Streaming und CN-Zuordnung

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
Carsten Abele 2026-08-14 09:15:53 +02:00
parent 99ee8758cc
commit e49882b8a8
9 changed files with 1280 additions and 0 deletions

102
docs/opnsense-api.md Normal file
View file

@ -0,0 +1,102 @@
# OPNsense-API: genutzte Endpunkte und Annahmen
Das Portal spricht ausschließlich das Plugin **`os-openvpn-client-export`** an
und ausschließlich lesend.
> **Diese Annahmen sind mit `go test -tags integration ./internal/opnsense/`
> gegen eine echte Instanz zu bestätigen, bevor v1 ausgeliefert wird.**
> Die exakten Feldnamen und Antwortformen wechseln zwischen OPNsense-Versionen;
> der Client ist deshalb bewusst tolerant gebaut.
## Genutzte Endpunkte
| Methode | Pfad | Zweck |
|---|---|---|
| `GET` | `/api/openvpn/export/providers` | Liste der exportierbaren VPN-Instanzen |
| `GET` | `/api/openvpn/export/accounts/{vpnid}` | Zertifikate einer Instanz |
| `GET` | `/api/openvpn/export/download/{vpnid}/{format}/{certref}` | Konfiguration herunterladen |
Alle drei sind `GET`. Das Portal ruft **keinen** schreibenden Endpunkt auf und
übergibt **keine** Exportoptionen — die auf der Firewall hinterlegten
Einstellungen sind die Quelle der Wahrheit.
## Authentifizierung
HTTP Basic Auth mit API-Key als Benutzername und API-Secret als Passwort.
Der API-Benutzer braucht ausschließlich das Privileg
**„VPN: OpenVPN Client Export"** — niemals einen Admin-Key.
Bei ungültigem Key antwortet OPNsense je nach Version mit `401` oder mit
`200` und einer HTML-Loginseite. Der Client behandelt beides als
`ErrUnauthorized`; die HTML-Erkennung prüft sowohl den `Content-Type` als
auch ein führendes `<` im Body.
## Akzeptierte Feldnamen
Der Client akzeptiert mehrere Schreibweisen und nimmt den ersten
nicht-leeren Treffer (`internal/opnsense/types.go`, `rawAccount`):
| Bedeutung | Akzeptierte JSON-Felder |
|---|---|
| Common Name | `commonName`, `common_name` |
| Beschreibung | `description`, `descr` |
| Ablaufdatum | `validTo`, `valid_to`, `validto` |
| Revoziert | `isRevoked`, `revoked`, `is_revoked` |
| VPN-ID (Provider) | `vpnid`, sonst der Map-Schlüssel |
| Instanzname | `name`, `description`, sonst `VPN <id>` |
**Revoziert** gilt, sobald *eines* der drei Felder wahr meldet — im Zweifel
restriktiv. Als „wahr" zählen `true`, `1`, `"1"`, `"true"`, `"yes"`, `"on"`.
## Akzeptierte Datumsformate
`time.RFC3339`, `2006-01-02T15:04:05`, `2006-01-02 15:04:05`, `2006-01-02`,
`Jan _2 15:04:05 2006 MST` (OpenSSL-Stil), `060102150405Z` (ASN.1 UTCTime)
sowie Unix-Zeitstempel als Zahl.
Ein **nicht auswertbares oder fehlendes** Ablaufdatum führt *nicht* zum
Ausschluss des Zertifikats — die Revocation-Prüfung bleibt maßgeblich, und ein
unbekanntes Datum darf keine gültige Konfiguration blockieren. Der
Integrationstest `TestIntegrationAccountsExpiryIsParsed` schlägt fehl, wenn
*kein* Zertifikat ein auswertbares Datum liefert; das ist das Signal, die
Layout-Liste zu ergänzen.
## Antwortformen des Downloads
Zwei Varianten sind implementiert und werden am `Content-Type` unterschieden:
1. **Rohdaten** (`application/octet-stream` o. ä.) — wird direkt
durchgestreamt, nichts wird gepuffert oder auf Platte geschrieben.
Der Dateiname kommt aus `Content-Disposition`.
2. **Base64 in JSON** (`application/json`) — Feld `content` oder `data`,
optional `filename`. Lässt sich der Inhalt nicht Base64-dekodieren, wird
er als Klartext behandelt.
## Unterstützte Exportformate
Das Portal bietet genau zwei Formate an: `ovpn` (inline `.ovpn`) und
`viscosity`. Weitere Formate sind bewusst nicht wählbar.
## Mindestversion
**Beim ersten erfolgreichen Integrationstestlauf hier eintragen:**
- OPNsense: _(zu ermitteln)_
- `os-openvpn-client-export`: _(zu ermitteln)_
## Integrationstests ausführen
```bash
export OPNSENSE_URL="https://fw01.firma.local"
export OPNSENSE_KEY="..."
export OPNSENSE_SECRET="..."
export OPNSENSE_CA="/pfad/zur/firma-ca.pem" # optional
export OPNSENSE_REVOKED_CN="testuser-revoked" # optional, für den Revocation-Test
export OPNSENSE_VPNID="1" # optional, für den Export-Test
export OPNSENSE_CERT_REF="abc123" # optional, für den Export-Test
go test -tags integration ./internal/opnsense/ -v
```
Ohne gesetzte Variablen überspringen sich die Tests selbst; sie laufen
deshalb nie versehentlich in der normalen Testsuite mit.