Engineering-Artikel

macOS-TCC-Rechte in Cloud-Mac-CI diagnostizieren und sicher zurücksetzen

macOS-TCC-Rechte in Cloud-Mac-CI diagnostizieren und sicher zurücksetzen

Ein und derselbe Befehl zur UI-Automatisierung funktioniert bei manueller Ausführung über SSH, kann innerhalb eines CI-Runners jedoch keine Screenshots erstellen, Fenster anklicken oder Ereignisse an eine andere Anwendung senden. Ein erneuter Joblauf hilft in der Regel nicht, denn die Ursache liegt nicht in der Skriptlogik, sondern in macOS Transparency, Consent, and Control – kurz TCC. TCC prüft nicht nur, welcher Benutzer einen Befehl ausführt, sondern auch, zu welcher grafischen Sitzung er gehört, welcher Prozess dafür verantwortlich ist und ob sich die Codeidentität dieses Prozesses geändert hat.

Zuerst einen TCC-Fehler erkennen

TCC-Probleme werden häufig mit einem defekten Treiber, einem nicht gestarteten Fenster oder einem Befehls-Timeout verwechselt. Teilen Sie den Job deshalb zunächst in zwei Ebenen auf: reine Kommandozeilenschritte und Schritte, die grafische Berechtigungen benötigen. Erst wenn Kompilierung, Repository-Zugriff und normale Netzwerkanfragen funktionieren, während Screenshots, Bedienungshilfensteuerung oder Automatisierungsereignisse separat fehlschlagen, lohnt sich eine Prüfung in Richtung TCC.

Erfassen Sie zuerst den Ausführungskontext:

id
whoami
printf 'console_user=%s
' "$(stat -f '%Su' /dev/console)"
printf 'uid=%s
' "$(id -u)"
launchctl print "gui/$(id -u)" >/tmp/gui-domain.txt 2>&1
ps -axo user,pid,ppid,command | grep -E 'runner|xcodebuild|osascript' | grep -v grep

Wenn der Benutzer von /dev/console nicht mit dem Runner-Benutzer übereinstimmt oder gui/<uid> überhaupt nicht existiert, steht dem Job keine verwendbare grafische Anmeldedomäne zur Verfügung. Berechtigungen wiederholt zurückzusetzen ist dann wirkungslos. Korrigieren Sie zuerst, wo und wie der Job gestartet wird.

„Per SSH ausführbar“ bedeutet lediglich, dass Shell, Pfade und Dateiberechtigungen funktionieren. Es beweist nicht, dass der betreffende Prozess über Rechte für Bildschirmaufnahme, Bedienungshilfen oder Anwendungsautomatisierung verfügt.

Ablehnenden Prozess im Systemprotokoll ermitteln

Fragen Sie unmittelbar nach dem reproduzierten Fehler nur einen kurzen Protokollzeitraum ab, damit die relevanten Einträge nicht zwischen unzusammenhängenden Meldungen untergehen:

log show --last 5m \
  --predicate 'subsystem == "com.apple.TCC"' \
  --style compact

Achten Sie vor allem auf den angeforderten Dienst, den Clientpfad, den verantwortlichen Prozess und das Ablehnungsergebnis. Suchen Sie nicht ausschließlich nach dem Namen des Skripts. Shell-Skripte werden häufig von einem Terminal, Runner, osascript oder Testhost gestartet. Tatsächlich autorisiert werden muss möglicherweise der übergeordnete Prozess, der das Skript ausführt.

Eine Identitäts-Baseline aus vier Merkmalen erstellen

Für jeden Cloud-Mac sollte eine Berechtigungs-Baseline ohne vertrauliche Schlüssel gespeichert werden. Sie muss mindestens den ausführenden Benutzer, die Startdomäne, den tatsächlichen Pfad der ausführbaren Datei und die Codesignaturidentität enthalten. Nach einem Runner-Upgrade oder dem Austausch einer Binärdatei werden die aktuellen Werte mit dieser Baseline verglichen.

RUNNER="/opt/ci/bin/runner"

ls -l "$RUNNER"
realpath "$RUNNER"
codesign -dv --verbose=4 "$RUNNER" 2>&1
codesign -dr - "$RUNNER" 2>&1
spctl --assess --type execute --verbose=4 "$RUNNER"

Ein identischer Pfad bedeutet nicht automatisch eine identische Identität. Das direkte Überschreiben einer Datei, der temporäre Download eines nicht signierten Werkzeugs oder die gemeinsame Verwendung desselben symbolischen Links durch verschiedene Versionen können verändern, welchen Client TCC erkennt. Robuster ist es, jede Version in einem eigenen Verzeichnis abzulegen und den Einstiegspunkt kontrolliert umzuschalten. Nach jeder Umschaltung muss die Signatur erneut geprüft werden.

Außerdem muss eindeutig sein, welcher Prozess als „verantwortlicher Prozess“ gilt. Startet beispielsweise der Runner eine Shell und diese anschließend osascript, um eine grafische Anwendung zu steuern, ist möglicherweise nicht das Skript im Repository das eigentliche Autorisierungsobjekt. Verfolgen Sie bei der Fehlersuche die PPID-Kette nach oben, statt mehreren unbeteiligten Werkzeugen gleichzeitig weitergehende Rechte zu erteilen.

Den Job in der richtigen grafischen Sitzung ausführen

Jobs, die den Desktop bedienen müssen, dürfen nicht als LaunchDaemon ausgeführt werden. Ein LaunchDaemon gehört zur Systemdomäne. Selbst wenn er unter einem bestimmten Benutzer gestartet wird, befindet er sich dadurch noch nicht in dessen Aqua-Sitzung. Besser ist es, den Runner als LaunchAgent dieses Benutzers zu registrieren und ihn nach der grafischen Anmeldung zu laden.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.example.ci-runner</string>
  <key>ProgramArguments</key>
  <array>
    <string>/opt/ci/bin/runner</string>
    <string>run</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>LimitLoadToSessionType</key>
  <string>Aqua</string>
  <key>StandardOutPath</key>
  <string>/Users/ci/Library/Logs/ci-runner.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/ci/Library/Logs/ci-runner-error.log</string>
</dict>
</plist>

Validieren Sie die Datei zunächst mit plutil -lint und laden Sie sie anschließend als Zielbenutzer:

plutil -lint "$HOME/Library/LaunchAgents/com.example.ci-runner.plist"
launchctl bootstrap "gui/$(id -u)" \
  "$HOME/Library/LaunchAgents/com.example.ci-runner.plist"
launchctl print "gui/$(id -u)/com.example.ci-runner"

Meldet bootstrap, dass die Domäne nicht gefunden wurde, prüfen Sie zuerst, ob für diesen Benutzer bereits eine aktive grafische Sitzung existiert. Erzwingen Sie den Job nicht in der Sitzung eines anderen Benutzers und kaschieren Sie eine falsche Zuordnung nicht durch einen einmaligen sudo-Aufruf.

Nur die tatsächlich benötigten Berechtigungen zurücksetzen

Ziehen Sie ein Zurücksetzen erst in Betracht, nachdem Benutzer, Sitzung und Prozessidentität geprüft wurden. Stoppen Sie zunächst den Runner, damit er während des Zurücksetzens nicht fortlaufend neue Anfragen erzeugt und die Diagnose erschwert. Anschließend setzt der betroffene Benutzer nur die relevanten Dienste zurück:

launchctl bootout \
  "gui/$(id -u)/com.example.ci-runner"

tccutil reset Accessibility
tccutil reset ScreenCapture
tccutil reset AppleEvents

Behandeln Sie diese drei Befehle nicht als festes Paket. Benötigt ein Job nur Bildschirmaufnahmen, wird ausschließlich ScreenCapture behandelt. Accessibility ist erst für die Bedienung der Oberfläche erforderlich. AppleEvents wird nur benötigt, wenn Automatisierungsereignisse an eine bestimmte Anwendung gesendet werden müssen. Lösen Sie nach dem Zurücksetzen in einer gültigen grafischen Sitzung einen minimalen Test aus, erteilen Sie die erforderlichen Freigaben und starten Sie den Runner erst danach neu.

Nach einer Änderung der Bildschirmaufnahmeberechtigung können ältere Prozesse weiterhin den vorherigen Berechtigungsstatus verwenden. Beenden und starten Sie den verantwortlichen Prozess daher vollständig neu, statt lediglich das Repository-Skript erneut auszuführen. Automatisierungsrechte können zudem getrennt nach Zielanwendung gespeichert werden. Aus „Anwendung A lässt sich steuern“ folgt daher nicht, dass sich auch Anwendung B steuern lässt.

Das direkte Löschen der TCC-Datenbank im Benutzerverzeichnis oder das Bearbeiten ihrer SQLite-Einträge ist keine zuverlässige Reparaturmethode. Dadurch werden Entscheidungen für weitere Anwendungen entfernt und der Wiederherstellungsprozess lässt sich nur schwer auditieren. Cloud-Macs von OrbVPS sind dedizierte physische Knoten, doch diese Exklusivität ändert nichts am Berechtigungsmodell von macOS. Das Prinzip der minimalen Rechte muss weiterhin auf den tatsächlichen Job und den verantwortlichen Prozess angewendet werden.

Die Wiederherstellung als Abnahmekriterium definieren

Nehmen Sie nach der Wiederherstellung nicht sofort die vollständige Pipeline wieder in Betrieb. Führen Sie zunächst einen Prüfjob aus, der genau einen Screenshot erstellt oder ein einzelnes Automatisierungsereignis sendet. Aktivieren Sie die Testaufgaben danach schrittweise. Für die Abnahme des Knotens sollten mindestens die folgenden Ergebnisse dokumentiert werden:

  1. Runner-Benutzer und Konsolenbenutzer stimmen überein.
  2. Die Domäne gui/<uid> existiert und der LaunchAgent hat den Status running.
  3. Der tatsächliche Runner-Pfad entspricht der erwarteten Version.
  4. Die Ausgabe von codesign stimmt mit der Baseline überein.
  5. Das TCC-Protokoll enthält keine weiteren Ablehnungen für den Zieldienst.
  6. Der minimale Prüfjob funktioniert auch nach einem Neustart des Runners.
  7. Nach Abschluss des vollständigen Jobs bleiben keine Testhosts oder Automatisierungsprozesse zurück.

Simulieren Sie abschließend einmal die Wiederherstellung nach einem Neustart des Knotens. Solange noch keine grafische Sitzung aufgebaut ist, darf der Runner keine Jobs annehmen. Er sollte einen Job nicht erst akzeptieren und anschließend bei der Bildschirmaufnahme oder Fenstersteuerung hängen bleiben. Am Einstiegspunkt des Jobs kann eine Sitzungsprüfung ergänzt werden, die bei einem Fehler eine eindeutige Diagnose ausgibt und den Job beendet:

uid="$(id -u)"
if ! launchctl print "gui/$uid" >/dev/null 2>&1; then
  printf '%s
' "No active GUI session for CI user"
  exit 75
fi

Die größte Schwierigkeit bei TCC-Fehlern ist nicht das Bestätigen einer Berechtigungsabfrage, sondern die Ermittlung des Prozesses, dem diese Berechtigung tatsächlich zugeordnet werden muss. Wenn ausführender Benutzer, LaunchAgent-Startdomäne, Pfad der ausführbaren Datei und Signaturidentität fest definiert sind, werden sporadische Berechtigungsfehler zu überprüfbaren und rücksetzbaren Betriebsbedingungen.

Häufig gestellte Fragen

Warum funktioniert ein Befehl per SSH, wird aber im CI-Dienst von TCC blockiert?

Die Ausführungen können unterschiedlichen Anmeldesitzungen, launchd-Domänen oder verantwortlichen Prozessen angehören. TCC überträgt eine Freigabe nicht allein anhand des Skriptpfads.

Sollte man die TCC-Datenbank zur Reparatur direkt löschen?

Nein. Stoppen Sie zuerst die betroffenen Jobs, setzen Sie als betroffener Benutzer nur den benötigten Dienst mit tccutil reset zurück und erteilen Sie die Freigabe anschließend in einer aktiven GUI-Sitzung neu.

OrbVPS Cloud-Mac

Builds auf exklusiven physischen Apple-Silicon-Knoten ausführen

Konfigurieren Sie den Knoten nach Modell, Region und Mietdauer. Maßgeblich ist der in der Konsole in Echtzeit angezeigte Verfügbarkeitsstatus.

Cloud-Mac jetzt mieten