chahinebrini 8f2ef2cc98 feat(mdm,vip): MDM-VPN-Pivot + Layer-2-Country-Curated + Custom-Domain-Refactor
MDM-VPN-Pivot (Phase F.2 done):
- ops/mdm/profiles/rebreak-iphone-protection.mobileconfig auf v5 mit
  com.apple.vpn.managed Payload + OnDemandUserOverrideDisabled. iPhone-User
  kann ReBreak-VPN-Profile nicht entfernen und "Bedarf verbinden"-Toggle
  ist disabled. allowEnablingRestrictions empirisch widerlegt für FC-Toggle-
  Lock — out.
- DEV-removable Variante als Test-Profile dazu.
- Bootstrap-Tool (rebreak-supervise.sh) + Supervision-Identity-Setup-Doc.
- PHASES.md updated mit empirischen Befunden.

App-side MDM-Detect (Pfad-a Banner-Logic):
- modules/rebreak-protection: getDeviceState() returnt mdmManaged via
  Heuristik NETunnelProviderManager.count > 1 (App selbst kann nur einen
  eigenen erstellen, MDM-Push fügt einen zweiten hinzu).
- DeviceLayers.mdmManaged?: boolean Type.
- blocker.tsx: lockedIn-Bedingung erweitert um mdmManaged. Bei MDM-managed
  iPhones wird der App-Lock-Card (FC-Authorization-Toggle UI) ausgeblendet
  weil der per-App FC-Toggle nicht lockbar ist und durch den MDM-VPN-Layer
  redundant.

Layer-2-Country-Curated-Pivot:
- backend: vip-swap.post.ts raus, suggest.post.ts rein. Curated-domains
  durch admin (separate Tabelle/Pfad), getrennt von User-Custom-Domains.
- Admin-APIs für curated-domain Pflege (index.get + [id].patch).
- seed-country-blocklists Script für initiale Curated-Domain-Liste.
- protection/webcontent-domains.get refactored für Country-Curated-Pfad.
- Migration drop_vip_swap_fields.sql + schema.prisma adjusted.
- docs/concepts/layer2-country-pivot.md mit Architektur + Decision-Trail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 07:11:47 +02:00

323 lines
17 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.

# MDM Setup — Phasen
## Revisions-Log
| Datum | Was geändert |
|-------------|-----------------------------------------------------------------------------------------------|
| 2026-05-10 | Initial: Phasen AG mit Factory-Reset-Approach für Phase F |
| 2026-05-24 | Phase F pivotiert auf Backup-Sandwich (TechLockdown-Stil); Scope erweitert um DNS-/VPN-Lock |
| 2026-05-24-late | DEV-Test zeigt: VPN-Restrictions blocken Rebreak-eigene NEVPNManager-Calls. Scope korrigiert: VPN-Restrictions raus, DNS bleibt als Fallback-Layer. Saubere MDM-VPN-Lösung als Phase F.2 |
## Phase A ✅ Server-Bootstrap
Erledigt vor 2026-05-10.
- apt-update + apt-upgrade
- Pakete installiert: nginx, postgresql, docker.io, certbot, python3-certbot-nginx, ufw, fail2ban
- UFW konfiguriert: 22/tcp, 80/tcp, 443/tcp erlaubt, default-deny
- fail2ban aktiv (SSH-Brute-Force-Schutz)
- DNS: IONOS A-Record `mdm.rebreak.org` → 178.105.101.137
## Phase B ✅ TLS-Zertifikat
Erledigt vor 2026-05-10.
- `certbot --nginx -d mdm.rebreak.org` ausgeführt
- Cert liegt in `/etc/letsencrypt/live/mdm.rebreak.org/`
- certbot.timer (systemd) erneuert automatisch
## Phase C ✅ NanoMDM Container + nginx-Vhost
Erledigt 2026-05-10.
**Was gemacht wurde:**
1. PostgreSQL-Datenbank `nanomdm` mit User `nanomdm` und Passwort aus `/root/.nanomdm_db_pass` angelegt
2. `ALTER USER nanomdm WITH PASSWORD '...'` explizit gesetzt (scram-sha-256 braucht explizites Passwort)
3. `pg_hba.conf` ergänzt für Docker-Netze (172.17.0.0/16, 172.18.0.0/16)
4. `listen_addresses` in `postgresql.conf` auf `localhost,172.17.0.1,172.18.0.1` erweitert
5. MDM CA generiert: `ca.key` + `ca.crt` in `/opt/nanomdm/certs/`
6. `/opt/nanomdm/.env` mit `NANOMDM_DB_PASS` geschrieben (chmod 600)
7. `/opt/nanomdm/docker-compose.yml` mit `network_mode: host` (kritisch, sonst postgres nicht erreichbar wegen NAT-Masquerade)
8. `docker compose up -d` — Container läuft, `starting server listen=127.0.0.1:9000` bestätigt
9. nginx-Vhost `/etc/nginx/sites-available/mdm.rebreak.org` geschrieben + in sites-enabled symlinkt
10. `nginx -t && systemctl reload nginx`
11. Externer Verify: `curl -sI https://mdm.rebreak.org/``HTTP/2 404` von nanomdm (korrekt, kein 502)
**Bekannte Tücken aus diesem Setup:**
- `micromdm/nanomdm` auf Docker Hub existiert nicht. Korrektes Image: `ghcr.io/micromdm/nanomdm:latest`
- nanomdm v0.9 kennt `-storage postgres` nicht. Korrekt: `-storage pgsql` (bzw. `NANOMDM_STORAGE=pgsql`)
- Docker-Compose-Netzwerk (172.18.x) geht via NAT durch Host — Postgres sieht externe IP als Source. Lösung: `network_mode: host` im Compose, dann verbindet nanomdm direkt zu `127.0.0.1:5432`
- nginx 1.24 kennt `http2 on;` nicht (das ist nginx 1.25+). Korrekt: `listen 443 ssl http2;`
## Phase D ✅ Apple Push CSR generiert
Erledigt 2026-05-10.
```
openssl req -newkey rsa:2048 -nodes \
-keyout /opt/nanomdm/certs/push.key \
-out /opt/nanomdm/certs/push.csr \
-subj '/CN=ReBreak MDM Push/O=Raynis/C=DE'
chmod 600 /opt/nanomdm/certs/push.key
```
CSR-Content liegt in `/opt/nanomdm/certs/push.csr`. Der private Key `push.key` verlässt den Server nie.
## Phase D.0.5 ✅ mdmcert.download Signing-Request
Erledigt 2026-05-10.
**Warum dieser Schritt notwendig ist:**
Apple Push Notification Service (APNS) für MDM akzeptiert keine rohen CSRs von Self-Hostern direkt im Apple Push Portal. Apple verlangt, dass die CSR von einem akkreditierten MDM-Vendor signiert wird. Self-Hoster ohne Apple-MDM-Vendor-Status nutzen `mdmcert.download` — ein Service des MicroMDM-Teams, der die CSR mit einem akzeptierten Vendor-Key gegen-signiert und encrypted per Email zurückschickt.
**Was passiert:**
1. Wir schicken unseren CSR base64-encoded + eine Encryption-Cert an `https://mdmcert.download/api/v1/signrequest`
2. mdmcert.download signiert ihn mit ihrem Apple-akkreditierten Vendor-Key
3. Sie verschlüsseln das Ergebnis mit unserer Encryption-Cert (PKCS7) und senden es per Email an `hello@chahine-brini.com`
4. Das entschlüsselte Ergebnis (nicht der raw CSR, nicht das `.b64.p7`) wird im Apple Push Portal hochgeladen
**Was gemacht wurde:**
1. Encryption-Keypair auf dem MDM-Server generiert:
- Cert: `/opt/nanomdm/certs/mdmcert-encryption.crt` (public, wird an mdmcert.download geschickt)
- Key: `/opt/nanomdm/certs/mdmcert-encryption.key` (chmod 600, verlässt Server nie)
```bash
openssl req -new -newkey rsa:2048 -nodes \
-keyout /opt/nanomdm/certs/mdmcert-encryption.key \
-x509 -days 365 \
-out /opt/nanomdm/certs/mdmcert-encryption.crt \
-subj '/CN=ReBreak mdmcert encryption'
chmod 600 /opt/nanomdm/certs/mdmcert-encryption.key
```
2. Signing-Request an mdmcert.download abgeschickt (shared public API-Key aus micromdm-Source, öffentlich dokumentiert):
```bash
PUSH_CSR_B64=$(base64 -w0 /opt/nanomdm/certs/push.csr)
ENC_CRT_B64=$(base64 -w0 /opt/nanomdm/certs/mdmcert-encryption.crt)
curl -X POST https://mdmcert.download/api/v1/signrequest \
-H "Content-Type: application/json" \
-H "User-Agent: micromdm/certhelper" \
-d "{\"csr\":\"$PUSH_CSR_B64\",\"email\":\"hello@chahine-brini.com\",\"key\":\"<shared-api-key>\",\"encrypt\":\"$ENC_CRT_B64\"}"
```
Antwort: `{"result":"success"}`
**Naechster Schritt:** Email von mdmcert.download bei `hello@chahine-brini.com` prüfen. Anhang-Name hat Format `mdm_signed_request.YYYYMMDD_HHMMSS_NNN.plist.b64.p7`. Dann weiter mit Phase D.0.7.
**Technische Details (wichtig fuer Decrypt):**
- Der Dateiname endet auf `.b64.p7` — irreführend. Der tatsächliche Inhalt ist **hex-encoded PKCS7**, nicht base64. (Quelle: micromdm/micromdm cmd/mdmctl/mdmcert.download.go, Decrypt-Pfad)
- Der Decrypt-Befehl (`openssl cms` oder PKCS7-Tooling) muss zuerst hex→binary decodieren, dann PKCS7 mit dem mdmcert-encryption.key entschlüsseln
## Phase D.0.7 ⏳ Signed CSR entschlüsseln
**Voraussetzung:** Email von mdmcert.download mit Anhang empfangen (Phase D.0.5 abgeschlossen)
**Wer:** Chahine schickt den Anhang per `scp` auf den MDM-Server. Oder Backyard entschlüsselt wenn Anhang auf den Server kopiert wurde.
**Schritte:**
1. Anhang von Email speichern (z.B. `mdm_signed_request.20260510_XXXXXX.plist.b64.p7`)
2. Datei auf Server kopieren:
```bash
scp ~/Downloads/mdm_signed_request.*.plist.b64.p7 rebreak-mdm:/opt/nanomdm/certs/signed_request.p7
```
3. Hex→Binary dekodieren + PKCS7 entschlüsseln (micromdm-Tooling macht beides intern):
```bash
# Hex-String aus der Datei zu Binary konvertieren
xxd -r -p /opt/nanomdm/certs/signed_request.p7 > /opt/nanomdm/certs/signed_request.der
# PKCS7 mit unserem Encryption-Key entschlüsseln
openssl cms -decrypt \
-in /opt/nanomdm/certs/signed_request.der \
-inform DER \
-inkey /opt/nanomdm/certs/mdmcert-encryption.key \
-recip /opt/nanomdm/certs/mdmcert-encryption.crt \
-out /opt/nanomdm/certs/push_request.plist
```
4. Ergebnis `/opt/nanomdm/certs/push_request.plist` prüfen — sollte eine Apple Plist-Datei sein.
```bash
head -5 /opt/nanomdm/certs/push_request.plist
# Erwartete Ausgabe: <?xml version="1.0" ... oder PEM-ähnliches Format
```
5. DIESE Datei (`push_request.plist`) wird bei https://identity.apple.com hochgeladen (Phase D.1).
**Status:** Wartet auf Email-Empfang bei `hello@chahine-brini.com`
## Phase D.1 ⏳ Apple Push Cert — Benutzeraktion
**Voraussetzung:** Phase D.0.7 abgeschlossen (entschlüsseltes Plist `push_request.plist` auf Server)
**Wer:** Chahine (muss mit Apple-ID eingeloggt sein, die als MDM-Zertifikats-Owner gelten soll)
**WICHTIG: NICHT den raw push.csr oder die `.b64.p7`-Datei hochladen.**
Hochgeladen wird die entschlüsselte `push_request.plist` aus Phase D.0.7.
**Schritte:**
1. Phase D.0.7 abschliessen — `push_request.plist` auf Server entschlüsselt
2. Datei lokal runterladen: `scp rebreak-mdm:/opt/nanomdm/certs/push_request.plist ~/Downloads/`
3. Oeffne https://identity.apple.com/pushcert/ im Browser (einloggen mit Apple-ID)
4. Klicke "Create a Certificate"
5. Lade `push_request.plist` hoch (NICHT push.csr, NICHT die `.b64.p7`-Datei)
6. Download das ausgestellte Zertifikat (`.pem` oder `.cer`)
7. Kopiere es auf den Server: `scp ~/Downloads/MDMCertificate.pem rebreak-mdm:/opt/nanomdm/certs/push.pem`
7. Setze Permissions: `ssh rebreak-mdm "chmod 600 /opt/nanomdm/certs/push.pem"`
8. Informiere Backyard-Agent fuer Phase E
**Wichtig:**
- Das Zertifikat ist an die Apple-ID geknuepft, mit der es erstellt wurde
- Gueltigkeitsdauer: 1 Jahr
- Bei Renewal: GLEICHEN `push.key` verwenden (kein neues keypair generieren)
- Wenn push.key verloren geht: alle Geraete muessen re-enrollen
## Phase D.2 ✅ NanoMDM mit Push-Cert konfiguriert
Erledigt 2026-05-10.
**Was gemacht wurde:**
1. NanoMDM API-Key generiert (32-char-hex), in `/opt/nanomdm/.env` (`NANOMDM_API=`) + `/root/.nanomdm_admin_pass` (chmod 600)
2. Container force-recreated mit neuem env-file
3. Postgres-Schema von https://raw.githubusercontent.com/micromdm/nanomdm/main/storage/pgsql/schema.sql geladen + applied (8 tables: devices, push_certs, commands, etc.) — fehlte aus initial-setup
4. Push-Cert via `PUT /v1/pushcert` (basic-auth) hochgeladen → in `push_certs` table
5. Verify: Topic `com.apple.mgmt.External.816a2d4a-4ce1-4b44-9264-2831b891206a`, valid bis 2027-05-10
6. External smoke-test: `curl -u nanomdm:<key> https://mdm.rebreak.org/version` → `{"version":"v0.9.0"}` ✅
**Bekannte Tücke:** Initial-setup hat das postgres-schema nicht angewendet. NanoMDM-Container hat keine eingebaute migrate-step. Schema muss manuell via `psql -f schema.sql` geladen werden bevor erster API-call funktioniert.
## Phase E ⏸ Email-Distribution an Ina — geparkt (User-Decision 2026-05-10)
**Status: PARKED — alles server-side ready, Versand verschoben.**
User-Entscheidung: PIN-Versand an Ina jetzt nicht — wird später nachgeholt. iPhone-Enrollment kann ohne laufen (MASTER-PIN ist Recovery-Backup, nicht Voraussetzung für enrollment).
Server-Status:
- ✅ MASTER-Recovery-PIN auf Server: `/root/.nanomdm_master_pin` (chmod 600)
- ✅ Ina-Email-Draft auf Server: `/root/INA_EMAIL_DRAFT.md` (chmod 600)
- ✅ Resend-API-Key auf Server: `/root/.resend_api_key` (chmod 600)
- ⏸ Resend-Domain-Verify ungetan — Versand würde fehlschlagen ohne `chahine-brini.com` oder `rebreak.org` verified
Reaktivierung: User sagt „Phase E GO", wir verifizieren Domain in Resend, senden, fertig. Files bleiben bis dahin auf Server.
## Phase F ⏳ Device-Enrollment via Backup-Sandwich
**Revidiert 2026-05-24** — alter Plan (Factory-Reset + Apple Configurator) war User-Friction-Killer. Niemand reset sich freiwillig sein iPhone. Neuer Plan: Backup-Sandwich-Approach wie TechLockdown / iMazing Configurator Edition.
Phase F ist NICHT mehr auf Phase E blockiert (Ina-Email-Distribution kann nachgeholt werden).
### Mechanismus
```
1. Backup (idevicebackup2 encrypted) → vollständig auf Mac
2. Supervise (cfgutil prepare) → wiped Gerät, Supervised-Flag wird gesetzt
3. Restore (idevicebackup2 restore) → Daten zurück, Supervised-Flag bleibt persistent
4. Enroll (mobileconfig install) → via QR-Code aus Rebreak-App, OTA über mdm.rebreak.org
```
Find-My-Disable ist Voraussetzung für Step 2 (Activation Lock blockt sonst den Wipe). Apple-ID-Passwort des Users wird live abgefragt — nicht automatisierbar.
### Komponenten dieser Phase
- `ops/mdm/bootstrap-tool/` — Bash-Scripts orchestrieren Backup → Supervise → Restore auf User-Mac (Mac-only Phase 1; Windows = Phase 2 via iMazing-Lizenz oder libimobiledevice-Erweiterung)
- `ops/mdm/profiles/rebreak-iphone-protection.mobileconfig` — Profil-Template mit den unten genannten Restrictions
- `backend/server/api/mdm/enroll.get.ts` — User-spezifisches signed Profil ausliefern, plus QR-Code-Endpoint
- `apps/rebreak-native/lib/mdm.ts` + `app/(protection)/mdm-setup.tsx` — Lyra-geführter Onboarding-Flow in der App
### Scope (erweitert 2026-05-24, revidiert 2026-05-24-late nach DEV-Test)
Profil enthält:
| Restriction | Wirkung |
|----------------------------------------------|--------------------------------------------------------------|
| `allowAppRemoval = false` | Rebreak (und alle anderen Apps) nicht löschbar via Long-Press — zeigt nur "Vom Home-Screen entfernen", App bleibt in Mediathek (verifiziert auf TechLockdown-supervisem iPhone 2026-05-24) |
| `PayloadRemovalDisallowed = true` | Profil nicht via Settings → Allgemein → VPN/Geräteverwaltung entfernbar |
| `allowEraseContentAndSettings = false` | User kann iPhone nicht via Settings → Reset wipen |
| `allowUIConfigurationProfileInstallation = false` | User kann keine konkurrierenden Profile installieren |
| DNS-Settings-Payload (DoH) | System-DNS auf `dns.rebreak.org/dns-query` gelocked — always-on Fallback-Schicht |
**VPN-Restrictions bewusst RAUS** (Test-Befund 2026-05-24):
`allowVPNCreation=false` blockt auch Rebreak-eigene `NEVPNManager`-Aufrufe ("Permission denied"). Apple unterscheidet im API-Call nicht zwischen User und App. Konsequenz:
- App-VPN (Rebreak NEPacketTunnel) bleibt App-managed + user-toggleable — wie heute
- MDM-DNS-Payload ist always-on Fallback: auch wenn User Rebreak-VPN ausschaltet, DNS-Filter greift weiterhin
- Bypass-Vektor: User installiert 3rd-Party-VPN (z.B. ExpressVPN). Akzeptiert für Prototype — 5-min-Friktion, trifft planenden Rückfall nicht impulsiven
- Saubere Lösung wäre **Phase F.2**: MDM-pushed-VPN mit `ProviderBundleIdentifier=org.rebreak.app.PacketTunnelExtension`, dann braucht App-Code kein eigenes `NEVPNManager.saveToPreferences` mehr → echtes "VPN nur via MDM"
Bewusste Trade-offs:
- `allowAppRemoval=false` ist GLOBAL — kein per-Bundle-ID-Lock möglich ohne MDM-managed-Convert (zusätzlicher InstallApplication-Command, Phase F.5 später). Für Prototype akzeptiert: User der sich self-bindet darf auch andere Support-Apps nicht löschen — Feature, kein Bug.
- Determinierter User kann via zweitem Mac unsupervisen (ABM-ADE wäre der einzige echte Hard-Lock, ist aber strukturell nicht erreichbar für Consumer-iPhones). Akzeptabel für DiGA-Sucht-Kontext: wir hoben Friktion, nicht Festung.
Bewusst NICHT im Scope:
- KEIN App-Store-Block (Casino-Apps gibt's eh nicht im iOS-App-Store)
- KEINE Web-Content-Filter-Payload (Browser-Casinos werden vom Rebreak-NEFilter geblockt)
- KEINE Restriktionen die nicht direkt mit Casino-Bypass-Prevention zu tun haben
### Hardware-/Tool-Voraussetzungen
- Mac mit macOS (User-Mac, NICHT Server-Mac) — für cfgutil + libimobiledevice
- USB-Kabel iPhone↔Mac
- Apple Configurator 2 (kostenlos, Mac App Store) — für `cfgutil` CLI
- libimobiledevice via `brew install libimobiledevice` — für `idevicebackup2`
- Supervision-Identity einmalig generiert via cfgutil (persistent, gleicher Mac reused)
- iPhone mit deaktiviertem Find-My (live während Setup)
### Akzeptanz-Test (M2)
Auf einem physischen Test-iPhone nach kompletter Sandwich-Sequenz:
- [ ] `Settings → Allgemein → Info` zeigt "Dieses iPhone wird verwaltet/beaufsichtigt"
- [ ] Long-Press auf Rebreak-Icon → kein "App löschen" mehr
- [ ] `Settings → VPN → Rebreak` → Toggle disabled / nicht entfernbar
- [ ] `Settings → Allgemein → VPN, DNS und Gerätemanagement` → Profil zeigt "Nicht entfernbar"
- [ ] Daten/Apps/Login-States/iMessage-History intakt nach Sandwich
- [ ] Rebreak-App erkennt MDM-Enrollment-Status via Backend-Check und unlockt Pro/Legend-Schutz-UI
## Phase G ⏳ iPad-Enrollment (optional, später)
Identisch zu Phase F, gleicher flow:
1. iPad via USB-C mit Mac verbinden
2. Apple Configurator 2 → Supervised-Mode → factory-reset
3. MDM-enrollment-profile von `https://mdm.rebreak.org/enroll`
4. ReBreak-iOS app installieren (läuft nativ auf iPad)
5. Verifyieren: ReBreak nicht entfernbar, MDM-profile nicht entfernbar
**Aufwand:** ~30min nach Phase F. Apple Push Cert deckt iPad mit ab (kein zusätzlicher cert nötig).
**Voraussetzung:** Phase F erfolgreich getestet auf iPhone.
## Phase H ⏳ MacBook-Enrollment (optional, später)
Anders als iPhone/iPad weil:
- **Kein ReBreak-Mac-app** existiert → MDM-profile muss eigene Blocking-Mechanik mitbringen
- Lösung: **Web-Content-Filter-Payload** im profile (DNS/URL-blocklist auf OS-Ebene)
- Mac-Supervised-Mode: factory-reset des MacBook nötig (analog iPad), via Apple Configurator 2 + USB-C
**Schritte:**
1. ReBreak-Blocklist (~208k domains) als Web-Content-Filter-Payload formattieren
- Payload-type: `com.apple.webcontent-filter`
- oder `com.apple.dnsSettings.managed` für DNS-level-block
2. MDM-profile assemblen mit:
- `allowMDMProfileRemoval=false` (braucht supervised-mode)
- Web-Content-Filter mit Casino-Blocklist
- Optional: `allowSafariAutoFill=false` (verhindert auto-login auf bekannten casino-sites)
3. MacBook factory-reset → Apple Configurator 2 → supervised-mode → MDM-enrollment
4. Verify: Casino-domain im Browser → blocked
**Aufwand:** ~1 Tag (blocklist-conversion + profile-assembly + test). Plus factory-reset-zeit.
**Voraussetzung:**
- Phase F+G erfolgreich
- User explizites GO (factory-reset MacBook = großer Schritt)
- Backup von wichtigen MacBook-Daten
**Tradeoff:** Kein ReBreak-Mac-app = nur URL-blocking, keine SOS-features, kein Lyra, keine Community auf Mac. Wer ReBreak-features auf Mac will, braucht später entweder native Mac-app (s. `ops/mac-version-research.md`) oder Browser-Extension.