rebreak-monorepo/ops/mdm/bootstrap-tool/rebreak-supervise.sh
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

408 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# rebreak-supervise.sh
# --------------------
# Backup-Sandwich-Bootstrap für iPhone-Supervision ohne sichtbaren Daten-Verlust.
#
# 1. idevicebackup2 encrypted Backup -> ~/.rebreak-supervise/backups/<UDID>/
# 2. cfgutil prepare --supervised -> wiped iPhone, Supervised-Flag setzen
# 3. idevicebackup2 restore -> Daten zurück, Supervised-Flag persistiert
# 4. cfgutil install-profile -> ReBreak-Schutz-Profil installieren
#
# Voraussetzungen (siehe README.md):
# - macOS
# - Apple Configurator 2 (App Store) + cfgutil im PATH
# - libimobiledevice (brew install libimobiledevice)
# - Supervision-Identity einmalig generiert (siehe SUPERVISION-IDENTITY-SETUP.md)
# - iPhone via USB-C verbunden, Find-My DEAKTIVIERT, Code entsperrt
# - Vertrauenshandshake (Trust-Dialog auf iPhone) bestätigt
#
# CLI:
# rebreak-supervise.sh [--dry-run] [--state-dir DIR] [--profile PATH] [--resume]
#
# Status: PROTOTYPE. Einige Steps (cfgutil-Aufruf, Verify-Pfad) sind erst auf
# physischem Test-iPhone final verifiziert. Markierungen "VERIFY ON DEVICE" im
# Code zeigen wo Hardware-in-Loop noch nachgehärtet werden muss.
set -euo pipefail
# ------------------------------------------------------------------------------
# Konfiguration + Defaults
# ------------------------------------------------------------------------------
STATE_DIR="${REBREAK_STATE_DIR:-$HOME/.rebreak-supervise}"
PROFILE_PATH_DEFAULT="$(cd "$(dirname "$0")/.." && pwd)/profiles/rebreak-iphone-protection.mobileconfig"
PROFILE_PATH=""
DRY_RUN=0
RESUME=0
CFGUTIL="/Applications/Apple Configurator.app/Contents/MacOS/cfgutil"
SUPERVISION_IDENTITY_P12="${SUPERVISION_IDENTITY_P12:-$STATE_DIR/supervision-identity.p12}"
SUPERVISION_IDENTITY_PASS_FILE="${SUPERVISION_IDENTITY_PASS_FILE:-$STATE_DIR/supervision-identity.pass}"
# Farben (nur wenn TTY)
if [[ -t 1 ]]; then
C_RESET="\033[0m"; C_RED="\033[1;31m"; C_GREEN="\033[1;32m"
C_YELLOW="\033[1;33m"; C_BLUE="\033[1;34m"; C_DIM="\033[2m"
else
C_RESET=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""; C_DIM=""
fi
# ------------------------------------------------------------------------------
# Helpers: Logging
# ------------------------------------------------------------------------------
LOG_FILE=""
log() { printf "%b\n" "$1" | tee -a "${LOG_FILE:-/dev/null}"; }
ok() { log "${C_GREEN}${C_RESET} $1"; }
warn() { log "${C_YELLOW}${C_RESET} $1"; }
err() { log "${C_RED}${C_RESET} $1" >&2; }
info() { log "${C_BLUE}${C_RESET} $1"; }
dim() { log "${C_DIM} $1${C_RESET}"; }
die() { err "$1"; exit "${2:-1}"; }
confirm() {
local prompt="$1"
[[ "$DRY_RUN" == 1 ]] && { dim "[dry-run] auto-yes: $prompt"; return 0; }
read -r -p "$(printf "%b" "${C_YELLOW}?${C_RESET} $prompt [y/N] ")" reply
[[ "$reply" =~ ^[yY]$ ]]
}
run() {
# Wraps a command: in dry-run, just echo. Otherwise execute.
if [[ "$DRY_RUN" == 1 ]]; then
dim "[dry-run] $*"
return 0
fi
"$@"
}
# ------------------------------------------------------------------------------
# State-Management: simples JSON-ähnliches Format pro UDID
#
# Datei: $STATE_DIR/state-<UDID>.env (key=value, source-bar)
# Keys: STEP_PREFLIGHT_AT, STEP_BACKUP_AT, BACKUP_PATH,
# STEP_SUPERVISE_AT, STEP_RESTORE_AT, STEP_PROFILE_AT
# ------------------------------------------------------------------------------
state_file_for() { printf "%s/state-%s.env" "$STATE_DIR" "$1"; }
state_load() {
local f; f="$(state_file_for "$1")"
if [[ -f "$f" ]]; then
# shellcheck disable=SC1090
source "$f"
fi
}
state_set() {
# state_set UDID KEY VALUE
local f; f="$(state_file_for "$1")"
local key="$2"; local val="$3"
# In-Memory-Update
eval "$key=\"\$val\""
# Persist (re-write each time, ist klein)
local tmpf="${f}.tmp"
{
for k in STEP_PREFLIGHT_AT STEP_BACKUP_AT BACKUP_PATH STEP_SUPERVISE_AT STEP_RESTORE_AT STEP_PROFILE_AT BACKUP_PASSWORD_FILE; do
local v="${!k:-}"
[[ -n "$v" ]] && printf "%s=%q\n" "$k" "$v"
done
} > "$tmpf"
mv "$tmpf" "$f"
chmod 600 "$f"
}
# ------------------------------------------------------------------------------
# Step 0: Argumente parsen
# ------------------------------------------------------------------------------
usage() {
cat <<EOF
Usage: $(basename "$0") [options]
Options:
--dry-run Befehle nur loggen, nicht ausführen
--state-dir DIR State-Verzeichnis (default: \$HOME/.rebreak-supervise)
--profile PATH Pfad zur .mobileconfig (default: ../profiles/rebreak-iphone-protection.mobileconfig)
--resume Bei vorhandenem State: bereits-erledigte Steps überspringen
-h, --help Diese Hilfe
Environment:
REBREAK_STATE_DIR Wie --state-dir
SUPERVISION_IDENTITY_P12 Pfad zur Supervision-Identity (default: STATE_DIR/supervision-identity.p12)
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--state-dir) STATE_DIR="$2"; shift 2 ;;
--profile) PROFILE_PATH="$2"; shift 2 ;;
--resume) RESUME=1; shift ;;
-h|--help) usage; exit 0 ;;
*) err "Unbekannte Option: $1"; usage; exit 2 ;;
esac
done
PROFILE_PATH="${PROFILE_PATH:-$PROFILE_PATH_DEFAULT}"
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
LOG_FILE="$STATE_DIR/log-$(date +%Y%m%d-%H%M%S).txt"
touch "$LOG_FILE"
log ""
log "${C_BLUE}═══════════════════════════════════════════════════${C_RESET}"
log "${C_BLUE} ReBreak iPhone Supervise Bootstrap${C_RESET}"
log "${C_BLUE}═══════════════════════════════════════════════════${C_RESET}"
log "State-Dir: $STATE_DIR"
log "Profile: $PROFILE_PATH"
log "Log: $LOG_FILE"
[[ "$DRY_RUN" == 1 ]] && warn "DRY-RUN-Modus aktiv — keine Aktion wird durchgeführt"
log ""
# ------------------------------------------------------------------------------
# Step 1: PREFLIGHT — Dependencies, Identity, Profil, Device-Detection
# ------------------------------------------------------------------------------
info "[1/5] Preflight"
# Tools
for bin in idevice_id ideviceinfo idevicebackup2; do
command -v "$bin" >/dev/null 2>&1 \
|| die "Fehlt: $bin → brew install libimobiledevice"
done
ok "libimobiledevice tools im PATH"
[[ -x "$CFGUTIL" ]] || die "cfgutil nicht gefunden: $CFGUTIL → Apple Configurator 2 aus App Store installieren"
ok "cfgutil gefunden"
# Supervision-Identity
if [[ ! -f "$SUPERVISION_IDENTITY_P12" ]]; then
die "Supervision-Identity fehlt: $SUPERVISION_IDENTITY_P12
→ siehe SUPERVISION-IDENTITY-SETUP.md (einmaliger Setup-Step)"
fi
ok "Supervision-Identity vorhanden"
# Profil
[[ -f "$PROFILE_PATH" ]] || die "Profil nicht gefunden: $PROFILE_PATH"
if ! plutil -lint "$PROFILE_PATH" >/dev/null 2>&1; then
die "Profil ist kein gültiges Plist: $PROFILE_PATH"
fi
ok "Profil ist gültig"
# Connected device(s)
UDIDS="$(idevice_id -l 2>/dev/null || true)"
if [[ -z "$UDIDS" ]]; then
die "Kein iPhone via USB erkannt. Stelle sicher:
- USB-C-Kabel ist eingesteckt
- iPhone ist entsperrt
- 'Diesem Computer vertrauen?' wurde auf dem iPhone bestätigt"
fi
# Bei mehreren: erste, ggf. später interaktiv erweitern
UDID="$(echo "$UDIDS" | head -n1)"
log ""
info "Gerät: $UDID"
DEVICE_NAME="$(ideviceinfo -u "$UDID" -k DeviceName 2>/dev/null || echo "?")"
IOS_VERSION="$(ideviceinfo -u "$UDID" -k ProductVersion 2>/dev/null || echo "?")"
ACTIVATION="$(ideviceinfo -u "$UDID" -k ActivationState 2>/dev/null || echo "?")"
log "Name: $DEVICE_NAME"
log "iOS: $IOS_VERSION"
log "Activation: $ACTIVATION"
# Activation-Check
if [[ "$ACTIVATION" != "Activated" ]]; then
warn "ActivationState ist '$ACTIVATION' — Backup könnte scheitern"
fi
# State laden falls --resume
state_load "$UDID"
if [[ "$RESUME" == 1 ]]; then
[[ -n "${STEP_BACKUP_AT:-}" ]] && ok "[resume] Backup bereits erledigt: $STEP_BACKUP_AT"
[[ -n "${STEP_SUPERVISE_AT:-}" ]] && ok "[resume] Supervise bereits erledigt: $STEP_SUPERVISE_AT"
[[ -n "${STEP_RESTORE_AT:-}" ]] && ok "[resume] Restore bereits erledigt: $STEP_RESTORE_AT"
[[ -n "${STEP_PROFILE_AT:-}" ]] && ok "[resume] Profil bereits installiert: $STEP_PROFILE_AT"
fi
state_set "$UDID" STEP_PREFLIGHT_AT "$(date -Iseconds)"
log ""
# ------------------------------------------------------------------------------
# Step 2: BACKUP (encrypted)
# ------------------------------------------------------------------------------
if [[ "$RESUME" == 1 && -n "${STEP_BACKUP_AT:-}" ]]; then
info "[2/5] Backup übersprungen (resume)"
else
info "[2/5] Backup (idevicebackup2, encrypted)"
BACKUP_ROOT="$STATE_DIR/backups/$UDID"
mkdir -p "$BACKUP_ROOT"
# Encryption-Passwort: generieren wenn nicht vorhanden, persistieren
BACKUP_PASSWORD_FILE="${BACKUP_PASSWORD_FILE:-$STATE_DIR/backup-pass-$UDID.txt}"
if [[ ! -f "$BACKUP_PASSWORD_FILE" ]]; then
if [[ "$DRY_RUN" != 1 ]]; then
openssl rand -base64 24 > "$BACKUP_PASSWORD_FILE"
chmod 600 "$BACKUP_PASSWORD_FILE"
ok "Backup-Passwort generiert: $BACKUP_PASSWORD_FILE"
warn "WICHTIG: dieses Passwort wird zum Restore gebraucht. Sicher aufheben."
else
dim "[dry-run] würde openssl rand -base64 24 > $BACKUP_PASSWORD_FILE"
fi
fi
BACKUP_PASSWORD="$([[ -f "$BACKUP_PASSWORD_FILE" ]] && cat "$BACKUP_PASSWORD_FILE" || echo "DRYRUN")"
warn "Backup kann je nach iPhone-Größe 15-60 Minuten dauern."
warn "Diese Session NICHT abbrechen, USB-Kabel NICHT abziehen."
confirm "Backup jetzt starten?" || die "Abgebrochen vom User" 0
# Encryption aktivieren falls noch nicht
# idevicebackup2 will die Backup-Encryption-Konfiguration auf dem Gerät selbst setzen
# VERIFY ON DEVICE: ob 'encryption on' idempotent ist oder vorher 'encryption off' nötig
run idevicebackup2 -u "$UDID" -i backup encryption on "$BACKUP_PASSWORD" "$BACKUP_ROOT" \
|| warn "Encryption-Setup hat returned non-zero — möglicherweise bereits aktiv, fahre fort"
# Backup
run idevicebackup2 -u "$UDID" -i backup "$BACKUP_ROOT"
state_set "$UDID" BACKUP_PATH "$BACKUP_ROOT"
state_set "$UDID" BACKUP_PASSWORD_FILE "$BACKUP_PASSWORD_FILE"
state_set "$UDID" STEP_BACKUP_AT "$(date -Iseconds)"
ok "Backup fertig: $BACKUP_ROOT"
fi
log ""
# ------------------------------------------------------------------------------
# Step 3: SUPERVISE — WIPED das Gerät, setzt Supervised-Flag
# ------------------------------------------------------------------------------
if [[ "$RESUME" == 1 && -n "${STEP_SUPERVISE_AT:-}" ]]; then
info "[3/5] Supervise übersprungen (resume)"
else
info "[3/5] Supervise (cfgutil prepare)"
warn "DIESER SCHRITT WIPED DAS GERÄT."
warn "Backup MUSS in Step 2 erfolgreich gewesen sein."
warn "Find-My ist deaktiviert? Apple-ID-Passwort eingegeben? Falls nein: JETZT abbrechen."
confirm "Wirklich fortfahren mit Wipe+Supervise?" || die "Abgebrochen vom User" 0
# cfgutil-Aufruf — VERIFY ON DEVICE: exakte Syntax + ECID vs UDID
# Apple-Doku-Stand: cfgutil unterstützt --ecid; UDID-Filter via --ecid in 0x-Hex
# Für unsupervised+activated devices ist der einfachste Weg: alle connected devices
# (es sollte nur eins angeschlossen sein zu diesem Zeitpunkt)
run "$CFGUTIL" \
--foreach \
prepare \
--supervised \
--organization-name "ReBreak" \
--supervisor-host-certs "$SUPERVISION_IDENTITY_P12"
state_set "$UDID" STEP_SUPERVISE_AT "$(date -Iseconds)"
ok "Supervise-Aufruf abgesetzt. Gerät reboots gerade — warte auf Wieder-Verbindung."
fi
log ""
# ------------------------------------------------------------------------------
# Step 4: WAIT FOR RECONNECT + RESTORE
# ------------------------------------------------------------------------------
if [[ "$RESUME" == 1 && -n "${STEP_RESTORE_AT:-}" ]]; then
info "[4/5] Restore übersprungen (resume)"
else
info "[4/5] Restore (warte auf Re-Verbindung, dann idevicebackup2 restore)"
if [[ "$DRY_RUN" != 1 ]]; then
log "Warte auf iPhone-Reconnect (max 5 min)..."
for i in $(seq 1 60); do
if idevice_id -l 2>/dev/null | grep -q "$UDID"; then
ok "iPhone ist wieder verbunden"
break
fi
sleep 5
[[ $i -eq 60 ]] && die "Timeout: iPhone nicht innerhalb 5 min wieder verbunden"
done
# iOS-Setup-Assistant muss der User auf dem iPhone bis zum Punkt "Vom Backup wiederherstellen?"
# bringen — oder wir restoren direkt via libimobiledevice (was wir hier tun)
# VERIFY ON DEVICE: ob restore direkt nach Wipe geht (vor Setup-Assistant) oder
# erst nach Setup-Assistant + initial-activation
warn "iPhone zeigt jetzt 'Hallo'/Setup-Assistant."
warn "Folge der Anleitung in README.md → 'Post-Supervise iPhone-Setup'."
warn "Sobald iPhone die 'Mit Computer verbinden'-Stage erreicht: hier weiter."
confirm "iPhone ist im Recovery-Setup-Stadium und bereit für Restore?" \
|| die "Abgebrochen vom User" 0
fi
BACKUP_PASSWORD="$(cat "$BACKUP_PASSWORD_FILE")"
# VERIFY ON DEVICE: '-i restore' nimmt $BACKUP_ROOT als positional arg
# Encryption-Passwort wird via stdin oder Env erwartet — idevicebackup2 v1.3+ unterstützt --password
run idevicebackup2 -u "$UDID" -i restore \
--system --reboot \
--password "$BACKUP_PASSWORD" \
"$BACKUP_PATH"
state_set "$UDID" STEP_RESTORE_AT "$(date -Iseconds)"
ok "Restore abgesetzt. iPhone reboots."
fi
log ""
# ------------------------------------------------------------------------------
# Step 5: INSTALL PROFIL + VERIFY
# ------------------------------------------------------------------------------
info "[5/5] Profil installieren + Verify"
if [[ "$DRY_RUN" != 1 ]]; then
log "Warte auf iPhone-Reconnect post-restore (max 10 min)..."
for i in $(seq 1 120); do
if idevice_id -l 2>/dev/null | grep -q "$UDID"; then
ok "iPhone ist wieder verbunden"
break
fi
sleep 5
[[ $i -eq 120 ]] && die "Timeout: iPhone nicht innerhalb 10 min wieder verbunden"
done
warn "User muss iOS jetzt entsperren + Setup-Assistant abschließen falls noch nicht."
confirm "iPhone ist entsperrt und im Home-Screen?" || die "Abgebrochen vom User" 0
fi
# Profil installieren via cfgutil
# VERIFY ON DEVICE: 'cfgutil install-profile <path>' Syntax
run "$CFGUTIL" --foreach install-profile "$PROFILE_PATH"
state_set "$UDID" STEP_PROFILE_AT "$(date -Iseconds)"
# Verify supervised
if [[ "$DRY_RUN" != 1 ]]; then
IS_SUPERVISED="$("$CFGUTIL" --foreach get isSupervised 2>/dev/null || echo "?")"
log "Supervised-State (cfgutil get isSupervised): $IS_SUPERVISED"
fi
log ""
log "${C_GREEN}═══════════════════════════════════════════════════${C_RESET}"
log "${C_GREEN} Bootstrap fertig${C_RESET}"
log "${C_GREEN}═══════════════════════════════════════════════════${C_RESET}"
log ""
log "Manueller Verify auf dem iPhone:"
log " 1. Settings → Allgemein → Info → 'Dieses iPhone wird beaufsichtigt' sichtbar?"
log " 2. Long-Press auf Rebreak-Icon → kein 'App löschen' mehr?"
log " 3. Settings → VPN → Rebreak → Toggle disabled?"
log " 4. Settings → Allgemein → VPN, DNS und Gerätemanagement → Profil 'Nicht entfernbar'?"
log ""
log "Backend-Enrollment (separater Step):"
log " → in der Rebreak-App: Profil-Tab → 'Schutz aktivieren' → QR scannen"
log ""
log "Logs: $LOG_FILE"
log "State: $(state_file_for "$UDID")"
log "Backup: $BACKUP_PATH"
log "Backup-Pass: $BACKUP_PASSWORD_FILE ${C_YELLOW}(sicher aufheben!)${C_RESET}"