Domain Controller auf Windows Server 2012 R2 koennen TLS 1.2 per Registry deaktiviert haben. ad.min_tls_version (Default 1.2) erlaubt es, fuer solche Altsysteme auf 1.0/1.1 herunterzugehen; das Portal warnt dann bei jedem Start. Die Zertifikatspruefung bleibt in jedem Fall aktiv — fuer LDAP gibt es weiterhin keine Insecure-Option. Im Legacy-Modus kommen lediglich die CBC-SHA256-Suiten hinzu, die Go als unsicher einstuft; RC4, 3DES und Suiten ohne Forward Secrecy werden nie angeboten. README dokumentiert zusaetzlich die beiden anderen 2012-R2-Stolpersteine: SHA-1-signierte DC-Zertifikate (seit Go 1.24 nicht mehr umgehbar) und RSA-Schluesselaustausch (nur ueber GODEBUG=tlsrsakex=1). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
494 lines
18 KiB
Markdown
494 lines
18 KiB
Markdown
# vpnportal — VPN-Konfig-Portal für OPNsense
|
|
|
|
Self-Service-Webportal, über das Mitarbeitende ihre persönliche
|
|
OpenVPN-Client-Konfiguration von einer OPNsense-Firewall herunterladen.
|
|
Anmeldung ausschließlich gegen das Active Directory des Kunden.
|
|
|
|
Das Portal ist ein reiner **Read-only-Verteiler**: Es erstellt, ändert oder
|
|
widerruft keine Zertifikate und schreibt nichts auf die Firewall. Ein einzelnes
|
|
statisch gelinktes Binary plus eine YAML-Datei, keine Datenbank.
|
|
|
|
---
|
|
|
|
## Voraussetzungen
|
|
|
|
| | |
|
|
|---|---|
|
|
| Betriebssystem | Linux, `amd64` oder `arm64` (Raspberry Pi) |
|
|
| Firewall | OPNsense mit Plugin `os-openvpn-client-export` (siehe `docs/opnsense-api.md`) |
|
|
| Verzeichnis | Active Directory mit LDAPS (636) oder StartTLS (389) |
|
|
| Zeit | NTP ist **Pflicht** — ohne korrekte Uhr werden Zertifikatslaufzeiten falsch bewertet |
|
|
| TLS | Serverzertifikat für das Portal (eigene CA genügt) |
|
|
|
|
Nicht unterstützt: Windows, Multi-Domain-Forests, mehrere OPNsense-Instanzen
|
|
pro Portal.
|
|
|
|
---
|
|
|
|
## Installation in unter 30 Minuten
|
|
|
|
### 1. OPNsense vorbereiten
|
|
|
|
1. Plugin `os-openvpn-client-export` installieren
|
|
(*System → Firmware → Plugins*).
|
|
2. Gruppe anlegen (*System → Access → Groups*), z. B. `vpnportal-api`, und ihr
|
|
**ausschließlich** das Privileg **„VPN: OpenVPN Client Export"** zuweisen.
|
|
3. Benutzer anlegen (*System → Access → Users*), z. B. `svc-vpnportal`, dieser
|
|
Gruppe zuordnen, kein Shell-Zugang.
|
|
4. Für diesen Benutzer einen API-Key erzeugen; Key und Secret notieren.
|
|
|
|
> **Niemals einen Admin-Key verwenden.** Das Portal braucht genau ein Privileg.
|
|
> `vpnportal check` meldet ausdrücklich, wenn die Berechtigung fehlt.
|
|
|
|
### 2. Active Directory vorbereiten
|
|
|
|
1. Unprivilegierten Dienstbenutzer anlegen, z. B.
|
|
`svc-vpnportal@firma.local`. Lesezugriff genügt; keine besonderen Rechte.
|
|
2. Sicherheitsgruppe für den VPN-Zugang anlegen oder benennen, z. B.
|
|
`VPN-Users`. Verschachtelte Gruppen werden aufgelöst.
|
|
3. Sicherstellen, dass die Domain Controller **über ihre Hostnamen**
|
|
erreichbar sind — das LDAPS-Zertifikat wird gegen den Hostnamen geprüft.
|
|
IP-Adressen in `ad.servers` werden abgelehnt.
|
|
4. Wird eine interne CA verwendet, deren Zertifikat als PEM bereitstellen.
|
|
|
|
### 3. Portal installieren
|
|
|
|
```bash
|
|
# Archiv entpacken
|
|
tar -xzf vpnportal-<version>-linux-amd64.tar.gz -C /tmp/vpnportal
|
|
install -m 0755 /tmp/vpnportal/vpnportal /usr/local/bin/vpnportal
|
|
|
|
# Konfiguration anlegen
|
|
install -d -m 0750 /etc/vpnportal
|
|
install -m 0640 /tmp/vpnportal/config.example.yaml /etc/vpnportal/config.yaml
|
|
$EDITOR /etc/vpnportal/config.yaml
|
|
|
|
# TLS-Material ablegen
|
|
install -m 0644 portal.crt /etc/vpnportal/portal.crt
|
|
install -m 0600 portal.key /etc/vpnportal/portal.key
|
|
```
|
|
|
|
Dateirechte werden **erzwungen** — das Portal startet nicht, wenn sie zu weit
|
|
gefasst sind:
|
|
|
|
| Datei | Höchstens |
|
|
|---|---|
|
|
| `config.yaml` | `0640` |
|
|
| Secret-Dateien | `0640` |
|
|
| `portal.key` | `0600` |
|
|
| Log-Verzeichnis | `0750` |
|
|
|
|
### 4. Prüfen
|
|
|
|
```bash
|
|
vpnportal check --config /etc/vpnportal/config.yaml
|
|
```
|
|
|
|
Gibt eine Checkliste aus und benennt jeden Fehler konkret:
|
|
|
|
```
|
|
✓ Konfiguration
|
|
gelesen und plausibel
|
|
✓ Dateirechte
|
|
Konfiguration, Secrets und Log-Verzeichnis sind ausreichend geschützt
|
|
✗ OPNsense-Verbindung
|
|
der API-Benutzer hat keine Berechtigung — auf der OPNsense das Privileg
|
|
"VPN: OpenVPN Client Export" zuweisen
|
|
```
|
|
|
|
Einen konkreten Benutzer durchspielen (ohne dessen Passwort):
|
|
|
|
```bash
|
|
vpnportal check --test-auth mmueller
|
|
```
|
|
|
|
### 5. Als Dienst einrichten
|
|
|
|
```bash
|
|
install -m 0644 /tmp/vpnportal/vpnportal.service /etc/systemd/system/
|
|
systemctl daemon-reload
|
|
systemctl enable --now vpnportal
|
|
systemctl status vpnportal
|
|
```
|
|
|
|
Die mitgelieferte Unit ist gehärtet (`DynamicUser`, `ProtectSystem=strict`,
|
|
leeres `CapabilityBoundingSet`, `SystemCallFilter`). Wer einen festen
|
|
Systembenutzer bevorzugt, ersetzt `DynamicUser=yes` durch:
|
|
|
|
```ini
|
|
User=vpnportal
|
|
Group=vpnportal
|
|
```
|
|
|
|
und legt den Benutzer mit
|
|
`useradd --system --no-create-home --shell /usr/sbin/nologin vpnportal` an.
|
|
|
|
---
|
|
|
|
## Secrets
|
|
|
|
Jedes Secret lässt sich auf drei Wegen setzen. **Rangfolge: Umgebungsvariable
|
|
> `*_file` > Wert in der Konfigurationsdatei.**
|
|
|
|
| Secret | Feld | Datei-Variante | Umgebungsvariable |
|
|
|---|---|---|---|
|
|
| OPNsense-Key | `opnsense.api_key` | `api_key_file` | `VPNPORTAL_OPNSENSE_API_KEY` |
|
|
| OPNsense-Secret | `opnsense.api_secret` | `api_secret_file` | `VPNPORTAL_OPNSENSE_API_SECRET` |
|
|
| AD-Passwort | `ad.bind_password` | `bind_password_file` | `VPNPORTAL_AD_BIND_PASSWORD` |
|
|
|
|
Werden Feld und `*_file` gleichzeitig gesetzt, bricht der Start mit einer
|
|
Fehlermeldung ab — stillschweigendes Übergehen wäre gefährlicher.
|
|
|
|
Mit systemd empfiehlt sich `LoadCredential` (schon in der Unit vorbereitet):
|
|
|
|
```bash
|
|
install -d -m 0700 /etc/vpnportal/secrets
|
|
printf '%s' 'DAS-AD-PASSWORT' > /etc/vpnportal/secrets/ad-bind-password
|
|
chmod 600 /etc/vpnportal/secrets/ad-bind-password
|
|
```
|
|
|
|
In der `config.yaml` dann:
|
|
|
|
```yaml
|
|
ad:
|
|
bind_password_file: "/run/credentials/vpnportal.service/ad-bind-password"
|
|
```
|
|
|
|
Die Konfiguration wird bewusst **nicht** verschlüsselt: der Schlüssel läge
|
|
daneben, das wäre Sicherheitstheater. Schutz sind Dateirechte und die
|
|
Härtung der Unit.
|
|
|
|
---
|
|
|
|
## Zertifikatszuordnung
|
|
|
|
Welches Firewall-Zertifikat zu welchem Benutzer gehört, entscheidet
|
|
`matching`. `{username}` ist der **aus dem AD gelesene** `sAMAccountName` in
|
|
Kleinschreibung — nie die Eingabe des Benutzers. Der Vergleich ignoriert
|
|
Groß- und Kleinschreibung.
|
|
|
|
```yaml
|
|
matching:
|
|
cn_pattern: "{username}" # CN == sAMAccountName (Standard)
|
|
# cn_pattern: "{username}@firma.de" # CN ist eine E-Mail-Adresse
|
|
```
|
|
|
|
Für Bestandsumgebungen, die sich nicht in ein Template fassen lassen, gibt es
|
|
`cn_regex` als Notnagel. Er schließt `cn_pattern` aus:
|
|
|
|
```yaml
|
|
matching:
|
|
cn_regex: "^(vpn-)?{username}(-[0-9]+)?$"
|
|
```
|
|
|
|
`{username}` wird vor dem Kompilieren quotiert eingesetzt, ein Benutzername
|
|
mit Sonderzeichen kann das Muster also nicht verändern.
|
|
|
|
> **Vorsicht:** Eine zu weit gefasste Regex kann fremde Zertifikate zuordnen.
|
|
> Nach jeder Änderung mit `check --test-auth` und einem Testbenutzer prüfen.
|
|
|
|
Zusätzlich zur Zuordnung gilt immer: **revozierte und abgelaufene Zertifikate
|
|
werden nie ausgeliefert.** Die Zertifikatsliste wird bei jeder Anfrage frisch
|
|
von der Firewall geholt, eine Revozierung greift also sofort.
|
|
|
|
---
|
|
|
|
## Betrieb
|
|
|
|
### Monitoring
|
|
|
|
`GET /healthz` antwortet ohne Anmeldung und ohne sensible Details:
|
|
|
|
```json
|
|
{"ok":true,"opnsense":true,"directory":true,"sessions":3}
|
|
```
|
|
|
|
HTTP 200 wenn `ok`, sonst 503. Ergebnisse werden 30 Sekunden gecacht, häufiges
|
|
Abfragen belastet die Backends also nicht.
|
|
|
|
Für Zabbix/CheckMK genügt der HTTP-Status; wer feiner prüfen will, wertet
|
|
`opnsense` und `directory` einzeln aus.
|
|
|
|
### Logs lesen
|
|
|
|
Alles geht parallel nach stdout (systemd-Journal) und in die Audit-Datei:
|
|
|
|
```bash
|
|
journalctl -u vpnportal -f
|
|
|
|
# Wer hat wann welche Konfiguration bezogen?
|
|
jq -r 'select(.event=="config_download") | "\(.ts) \(.user) \(.vpn_instance)"' \
|
|
/var/log/vpnportal/audit.log
|
|
|
|
# Fehlanmeldungen einer IP zählen
|
|
jq -r 'select(.event=="login_failed") | .src_ip' /var/log/vpnportal/audit.log \
|
|
| sort | uniq -c | sort -rn
|
|
```
|
|
|
|
### Neustart
|
|
|
|
Ein Neustart beendet **alle** Sitzungen — Sessions liegen ausschließlich im
|
|
RAM. Das ist beabsichtigt; weitere Daten gehen dabei nicht verloren.
|
|
|
|
---
|
|
|
|
## Audit-Log
|
|
|
|
JSON Lines, eine Zeile pro Ereignis, Zeitstempel RFC 3339 mit Zeitzone:
|
|
|
|
```json
|
|
{"ts":"2026-08-14T09:32:11+02:00","event":"login_success","user":"mmueller","src_ip":"10.1.20.34","session":"a3f9"}
|
|
{"ts":"2026-08-14T09:32:40+02:00","event":"config_download","user":"mmueller","src_ip":"10.1.20.34","session":"a3f9","vpn_instance":"VPN Homeoffice","cert_cn":"mmueller","cert_expiry":"2027-03-01","format":"ovpn"}
|
|
```
|
|
|
|
**Audit-Ereignisse:** `login_success`, `login_failed` (mit `reason`), `logout`,
|
|
`session_expired`, `config_download`, `download_denied`
|
|
|
|
**Betriebsereignisse:** `no_cert_found`, `opnsense_unreachable`,
|
|
`ldap_failover`, `rate_limited`, `startup`
|
|
|
|
**`reason` bei `login_failed`:** `invalid_credentials`, `account_disabled`,
|
|
`account_locked`, `password_expired`, `password_change_required`,
|
|
`not_in_vpn_group`, `user_not_found`, `backend_unavailable`
|
|
|
|
### Was niemals im Log steht
|
|
|
|
- **Passwörter.** Wird versehentlich ein Passwort ins Benutzernamenfeld
|
|
getippt, erscheint statt des Namens `"<unknown>"` — der Klartextname wird
|
|
nur protokolliert, wenn der Benutzer im AD tatsächlich gefunden wurde.
|
|
- Konfigurationsinhalte, Zertifikate, private Schlüssel.
|
|
- Vollständige Session-Tokens (nur eine 4-stellige Hash-Korrelations-ID).
|
|
- Vollständige CN-Listen der Firewall (nur auf `debug`-Level, das im
|
|
Normalbetrieb aus bleibt).
|
|
|
|
### Rotation und Weiterleitung
|
|
|
|
Die Rotation ist eingebaut und größenbasiert (`max_size_mb`, `max_backups`,
|
|
`compress`) — das schont SD-Karten. Wer stattdessen externes `logrotate`
|
|
nutzt, schickt danach ein `SIGHUP`:
|
|
|
|
```
|
|
/var/log/vpnportal/audit.log {
|
|
daily
|
|
rotate 90
|
|
compress
|
|
postrotate
|
|
systemctl kill -s HUP vpnportal
|
|
endscript
|
|
}
|
|
```
|
|
|
|
> **Für revisionssichere Aufbewahrung das Log an ein zentrales Logsystem
|
|
> weiterleiten** (über das Journal oder Syslog). Empfohlene Aufbewahrung:
|
|
> 90 Tage, kundenseitig anpassbar.
|
|
|
|
Das Log enthält personenbezogene Daten und ist entsprechend zu schützen. Die
|
|
Datei wird mit `0600` angelegt, das Verzeichnis darf höchstens `0750` haben.
|
|
|
|
---
|
|
|
|
## Raspberry Pi
|
|
|
|
- **SSD statt SD-Karte.** Das Portal schreibt zwar nur das Audit-Log, aber
|
|
SD-Karten sterben trotzdem.
|
|
- **Gerät in einen abschließbaren Netzwerkschrank.** Das Speichermedium trägt
|
|
die Zugangsdaten zu Firewall und Verzeichnis.
|
|
- **NTP prüfen** (`timedatectl status`). `vpnportal check` vergleicht die
|
|
Systemzeit mit der Firewall und warnt ab zwei Minuten Abweichung.
|
|
|
|
---
|
|
|
|
## Fehlersuche
|
|
|
|
| Symptom | Wahrscheinliche Ursache | Kommando |
|
|
|---|---|---|
|
|
| Start bricht mit `chmod`-Hinweis ab | Datei- oder Verzeichnisrechte zu weit | Der Hinweis nennt den nötigen Befehl |
|
|
| Start bricht mit unbekanntem Feld ab | Tippfehler in der `config.yaml` | Die Meldung nennt das Feld |
|
|
| Niemand kann sich anmelden | Dienstkonto, Gruppe oder DC-Erreichbarkeit | `vpnportal check` |
|
|
| Ein einzelner Benutzer nicht | Gruppenmitgliedschaft oder Schreibweise | `vpnportal check --test-auth <benutzer>` |
|
|
| „Keine VPN-Konfiguration hinterlegt" | `cn_pattern` passt nicht zum CN | Audit-Feld `pattern` im Ereignis `no_cert_found` lesen |
|
|
| „Dienst vorübergehend nicht verfügbar" | Firewall nicht erreichbar oder CA falsch | `vpnportal check` |
|
|
| Anmeldung dauert lang und schlägt fehl | Erster DC tot, Failover greift | Ereignis `ldap_failover` im Log |
|
|
| „Zu viele Anmeldeversuche" | Rate-Limit greift | Ereignis `rate_limited`; die Wartezeit läuft von allein ab |
|
|
|
|
Für die Fehlersuche lässt sich `logging.level` vorübergehend auf `debug`
|
|
setzen — dann werden auch die verglichenen CNs protokolliert. **Im
|
|
Normalbetrieb wieder auf `info` zurückstellen.**
|
|
|
|
### Alte Domain Controller (Windows Server 2012 R2 und älter)
|
|
|
|
Fehlendes TLS 1.3 ist **kein** Problem — das Portal verlangt mindestens
|
|
TLS 1.2 und handelt automatisch herunter. Server 2012 R2 beherrscht TLS 1.2
|
|
für Schannel standardmäßig. Scheitert die Verbindung trotzdem, ist fast immer
|
|
eine dieser drei Ursachen im Spiel. Die Fehlermeldung von `vpnportal check`
|
|
sagt, welche:
|
|
|
|
| Fehlermeldung enthält | Ursache | Behebung |
|
|
|---|---|---|
|
|
| `protocol version not supported`, `handshake failure` | TLS 1.2 ist auf dem DC per Registry deaktiviert | `ad.min_tls_version: "1.0"` setzen (siehe unten) |
|
|
| `insecure algorithm SHA1-RSA`, `certificate signed by unknown authority` | DC-Zertifikat ist mit SHA-1 signiert | **Zertifikat neu ausstellen** — siehe Kasten |
|
|
| `tls: no cipher suite supported by both` | DC bietet nur RSA-Schlüsselaustausch | `GODEBUG=tlsrsakex=1` in der Unit setzen |
|
|
|
|
**TLS-Mindestversion herabsetzen:**
|
|
|
|
```yaml
|
|
ad:
|
|
min_tls_version: "1.0" # Default ist 1.2
|
|
```
|
|
|
|
Das Portal warnt dann bei jedem Start. Die **Zertifikatsprüfung bleibt aktiv** —
|
|
für LDAP gibt es bewusst keine Möglichkeit, sie abzuschalten. Herabgesetzt
|
|
werden nur die zulässigen Protokollversionen und die angebotenen
|
|
Cipher-Suiten; RC4 und 3DES werden auch dann nie angeboten.
|
|
|
|
> **SHA-1-Zertifikate lassen sich nicht mehr umgehen.** Go 1.24 hat den
|
|
> Notausgang `GODEBUG=x509sha1=1` ersatzlos entfernt. Ist das LDAPS-Zertifikat
|
|
> des Domain Controllers mit SHA-1 signiert — bei ADCS-Vorlagen aus der
|
|
> 2012-R2-Ära der Normalfall — hilft nur, es mit SHA-256 neu auszustellen.
|
|
> Prüfen lässt sich das so:
|
|
>
|
|
> ```bash
|
|
> openssl s_client -connect dc01.firma.local:636 -showcerts </dev/null 2>/dev/null \
|
|
> | openssl x509 -noout -text | grep -i "signature algorithm"
|
|
> ```
|
|
>
|
|
> Steht dort `sha1WithRSAEncryption`, muss das Zertifikat getauscht werden.
|
|
|
|
**RSA-Schlüsselaustausch erzwingen** (letzte Rettung, keine Forward Secrecy).
|
|
Go schaltet diese Suiten seit 1.22 ab; sie lassen sich nur global per GODEBUG
|
|
reaktivieren. In der systemd-Unit:
|
|
|
|
```ini
|
|
[Service]
|
|
Environment=GODEBUG=tlsrsakex=1
|
|
```
|
|
|
|
### Passwort abgelaufen
|
|
|
|
Das Portal kann keine Passwörter ändern. Ist ein AD-Passwort abgelaufen
|
|
(AD-Code 532), sieht der Benutzer als einzige Ausnahme eine konkrete Meldung
|
|
und muss das Passwort an seinem Arbeitsplatz ändern. Verlangt das AD eine
|
|
Passwortänderung bei der nächsten Anmeldung (Code 773), erscheint die
|
|
generische Meldung — dieser Fall lässt sich am Portal nicht unterscheiden
|
|
kommunizieren, ohne Kontoinformationen preiszugeben.
|
|
|
|
---
|
|
|
|
## Bekannte Grenzen
|
|
|
|
- **Kein WireGuard** (für v2 geplant, braucht ein eigenes Schlüsselkonzept).
|
|
- **Kein Self-Service für Zertifikate** — Erstellen, Erneuern und Widerrufen
|
|
bleiben auf der Firewall.
|
|
- **Keine Admin-Oberfläche.** Logs per SSH/Monitoring, `/healthz` für
|
|
externe Überwachung.
|
|
- **Keine Mandantenfähigkeit**, eine OPNsense-Instanz pro Portal.
|
|
- **Single-Domain.** Referrals werden ignoriert, Multi-Domain-Forests sind
|
|
nicht unterstützt.
|
|
- **Kein RADIUS und keine lokalen Portalbenutzer.**
|
|
- **Nur Deutsch.** Die Textstruktur ist für i18n vorbereitet, aber nur
|
|
Deutsch ist befüllt.
|
|
|
|
---
|
|
|
|
## Update
|
|
|
|
```bash
|
|
systemctl stop vpnportal
|
|
install -m 0755 vpnportal /usr/local/bin/vpnportal
|
|
vpnportal check --config /etc/vpnportal/config.yaml
|
|
systemctl start vpnportal
|
|
```
|
|
|
|
Sitzungen gehen dabei verloren, das ist beabsichtigt.
|
|
|
|
Es gibt **kein Auto-Update**. `portal.update_check` ist standardmäßig
|
|
**aus**; eingeschaltet baut das Portal beim Start eine ausgehende
|
|
HTTPS-Verbindung zur Release-URL auf und meldet abweichende Versionen im
|
|
Log. Es lädt nichts herunter und installiert nichts.
|
|
|
|
---
|
|
|
|
## Bauen aus dem Quelltext
|
|
|
|
Voraussetzung: **Go 1.25 oder neuer**. Keine CGO-Abhängigkeiten, kein npm,
|
|
kein Build-Step für das Frontend — Templates und Assets liegen per `go:embed`
|
|
im Binary.
|
|
|
|
```bash
|
|
make test # alle Tests
|
|
make race # Tests mit Race-Detector
|
|
make lint # go vet + gofmt
|
|
make build # Binary nach dist/
|
|
make release # Archive für linux-amd64 und linux-arm64 inkl. SHA256SUMS
|
|
```
|
|
|
|
### Go auf dem Zielhost installieren
|
|
|
|
Die Go-Pakete der Distributionen sind für dieses Projekt zu alt (Debian 12:
|
|
1.19, Ubuntu 24.04: 1.22, Raspberry Pi OS: 1.19). `apt install golang` genügt
|
|
also nicht — Go kommt aus dem offiziellen Tarball:
|
|
|
|
```bash
|
|
uname -m # x86_64 -> amd64, aarch64 -> arm64
|
|
|
|
# Aktuelle Version auf https://go.dev/dl/ nachsehen, hier als Beispiel 1.26.5
|
|
curl -LO https://go.dev/dl/go1.26.5.linux-amd64.tar.gz
|
|
sudo rm -rf /usr/local/go
|
|
sudo tar -C /usr/local -xzf go1.26.5.linux-amd64.tar.gz
|
|
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile
|
|
export PATH=$PATH:/usr/local/go/bin
|
|
go version
|
|
```
|
|
|
|
Der erste Build lädt die vier Abhängigkeiten von `proxy.golang.org` — der
|
|
Host braucht dafür einmalig Internetzugang. Soll er dauerhaft ohne Netz
|
|
auskommen, entweder das Binary auf einer anderen Maschine bauen
|
|
(`make release`) oder einmalig `go mod vendor` ausführen und das
|
|
`vendor/`-Verzeichnis mitliefern.
|
|
|
|
### Integrationstests gegen eine echte OPNsense
|
|
|
|
Die Feldnamen der Export-API sind versionsabhängig. Vor einer Auslieferung
|
|
sind die in `docs/opnsense-api.md` dokumentierten Annahmen einmal gegen eine
|
|
echte Instanz zu bestätigen:
|
|
|
|
```bash
|
|
export OPNSENSE_URL="https://fw01.firma.local"
|
|
export OPNSENSE_KEY="..." OPNSENSE_SECRET="..."
|
|
export OPNSENSE_REVOKED_CN="testuser-revoked" # optional
|
|
go test -tags integration ./internal/opnsense/ -v
|
|
```
|
|
|
|
Ohne gesetzte Variablen überspringen sich diese Tests selbst.
|
|
|
|
### Oberfläche ohne Backends ansehen
|
|
|
|
```bash
|
|
go test -tags uidemo ./cmd/vpnportal/ -run TestUIDemo -v -timeout 0
|
|
# http://127.0.0.1:8080 — Anmeldung mit beliebigen Daten
|
|
# VPNPORTAL_UIDEMO_EMPTY=1 zeigt den Zustand "kein Zertifikat"
|
|
```
|
|
|
|
---
|
|
|
|
## Sicherheitsdesign in Kürze
|
|
|
|
- HTTPS-Pflicht am Portal; TLS-Verifikation gegen Firewall und Verzeichnis
|
|
immer aktiv. Die Insecure-Option existiert nur für die Firewall und warnt
|
|
bei jedem Start; für LDAP gibt es sie gar nicht.
|
|
- LDAP-Injection-Schutz durch konsequentes `ldap.EscapeFilter`.
|
|
- CSRF-Token auf allen zustandsändernden Anfragen **einschließlich Downloads**.
|
|
Downloads sind ausschließlich `POST`, es gibt keine GET-Download-Links.
|
|
- **Jeder Download wird serverseitig neu autorisiert.** Die Auswahl in der
|
|
Oberfläche ist keine Berechtigung: zwischen Anzeige und Klick kann ein
|
|
Zertifikat revoziert worden sein.
|
|
- Konfigurationen werden gestreamt, nie zwischengespeichert; alle Seiten nach
|
|
der Anmeldung tragen `Cache-Control: no-store`.
|
|
- Rate-Limiting pro Benutzername **und** pro Quell-IP mit exponentiellem
|
|
Backoff, gedeckelt bei fünf Minuten. Bewusst **kein harter Lockout** —
|
|
sonst könnte ein Dritter Kollegen dauerhaft aussperren.
|
|
- Keine Benutzer-Enumeration: gleiche Meldung und angeglichene Antwortzeit für
|
|
„Benutzer existiert nicht" und „Passwort falsch".
|
|
- Security-Header auf jeder Antwort: HSTS, `nosniff`, `no-referrer`,
|
|
`X-Frame-Options: DENY` und eine CSP ohne jedes `unsafe-*`.
|
|
- Die Oberfläche ist ohne JavaScript vollständig bedienbar.
|