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

17 KiB
Raw Permalink Blame History

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)
    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):

    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:

    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):

    # 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.

    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
  8. Setze Permissions: ssh rebreak-mdm "chmod 600 /opt/nanomdm/certs/push.pem"
  9. 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.