エンジニアリング記事

クラウドMac CIのmacOS TCC権限診断と安全なリセット

クラウドMac CIのmacOS TCC権限診断と安全なリセット

同じUI自動化コマンドがSSH経由の手動実行では正常に動作するのに、CI runnerに組み込むとスクリーンショットの取得、ウィンドウのクリック、別アプリケーションへのイベント送信に失敗することがあります。ジョブを再実行しても通常は解決しません。原因はスクリプトのロジックではなく、macOSの「透明性・同意・制御」、つまりTCCにあるためです。TCCは「どのユーザーが実行したか」だけでなく、コマンドがどのグラフィカルセッションに属しているか、どのプロセスが責任を負うか、そのプロセスのコードIDが変わっていないかも判定します。

まずTCC障害かどうかを見極める

TCCの問題は、ドライバーの不具合、ウィンドウの起動失敗、コマンドのタイムアウトと誤認されがちです。まずジョブを、純粋なコマンドライン処理とグラフィカル権限を必要とする処理の2つに分けます。ビルド、リポジトリの読み取り、通常のネットワークリクエストは成功する一方で、スクリーンショット、アクセシビリティ制御、オートメーションイベントだけが失敗する場合に、TCCを調査する価値があります。

最初に実行コンテキストを記録します。

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

/dev/consoleのユーザーがrunnerユーザーと異なる場合や、gui/<uid>自体が存在しない場合、そのジョブから利用できるグラフィカルログインドメインはありません。この状態で権限を何度リセットしても意味はありません。まずジョブを起動する場所を修正してください。

「SSHで実行できる」という事実が示すのは、シェル、パス、ファイル権限を利用できることだけです。そのプロセスに画面収録、アクセシビリティ、アプリケーション自動化の権限があることまでは証明できません。

システムログで拒否元を確認する

失敗を再現した直後に短い時間範囲のログを照会し、無関係な記録に埋もれないようにします。

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

要求されたサービス、クライアントのパス、責任プロセス、拒否結果を重点的に確認します。スクリプト名だけを検索してはいけません。シェルスクリプトは、ターミナル、runner、osascript、テストホストなどから起動されることが多く、実際に権限を必要としているのはスクリプトをホストする上流プロセスかもしれません。

4つの識別情報のベースラインを作成する

各クラウドMacには、秘密情報を含まない権限ベースラインを保存しておく必要があります。少なくとも、実行ユーザー、起動ドメイン、実行ファイルの実体パス、コード署名IDを記録します。runnerのアップグレードやバイナリの交換後は、このベースラインと比較してください。

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"

パスが同じでも、IDまで同じとは限りません。ファイルの上書き、未署名ツールの一時ダウンロード、異なるバージョンによる同一シンボリックリンクの共有は、いずれもTCCから見えるクライアントIDを変える可能性があります。より確実なのは、各バージョンを個別のディレクトリに配置し、管理された切り替えによってエントリーポイントを更新する方法です。切り替え後は、署名チェックを再実行します。

誰が「責任プロセス」なのかも明確にする必要があります。たとえばrunnerがシェルを起動し、そのシェルがosascriptを起動してグラフィカルアプリケーションを制御する場合、権限の対象はリポジトリ内のスクリプトではない可能性があります。調査時はPPIDを上流へたどり、無関係な複数のツールにまとめて権限を付与しないでください。

ジョブを正しいグラフィカルセッションで実行する

デスクトップ操作を必要とするジョブをLaunchDaemonとして実行してはいけません。LaunchDaemonはシステムドメインに属します。指定ユーザーで起動しても、そのユーザーのAquaセッションに入ったことにはなりません。より適切なのは、runnerを対象ユーザーのLaunchAgentとして登録し、グラフィカルログイン後に読み込む方法です。

<?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>

まずplutil -lintでファイルを検証し、その後、対象ユーザーとして読み込みます。

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"

bootstrapがドメインを見つけられないというエラーを返す場合は、対象ユーザーにアクティブなグラフィカルセッションがあるかを先に確認します。ジョブを別ユーザーのセッションへ無理に入れたり、一時的なsudo呼び出しで所有関係の誤りを隠したりしてはいけません。

本当に必要な権限だけをリセットする

ユーザー、セッション、プロセスIDがすべて正しいことを確認してから、リセットを検討します。リセット中にrunnerが要求を繰り返して混乱を招かないよう、最初にrunnerを停止します。その後、影響を受けるユーザーとして、対象サービスに限定したリセットを実行します。

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

tccutil reset Accessibility
tccutil reset ScreenCapture
tccutil reset AppleEvents

この3つのコマンドを固定セットとして扱わないでください。画面収録だけが必要なジョブではScreenCaptureだけを処理します。UI操作が必要な場合にAccessibilityを処理し、特定のアプリケーションへオートメーションイベントを送る場合にのみAppleEventsを処理します。リセット後は、有効なグラフィカルセッション内で最小限のテストを1回実行し、必要な許可を完了してからrunnerを再起動します。

画面収録権限を変更しても、既存プロセスが以前の権限状態を保持していることがあります。リポジトリ内のスクリプトを再実行するだけでなく、責任プロセスを完全に終了して起動し直してください。また、オートメーション権限は対象アプリケーションごとに記録される場合があります。そのため、「アプリケーションAを制御できる」ことから「アプリケーションBも制御できる」とは判断できません。

ユーザーディレクトリ内のTCCデータベースを直接削除したり、SQLiteレコードを書き換えたりする方法は、信頼できる修復手段ではありません。他のアプリケーションに対する決定まで消去され、復旧プロセスの監査も難しくなります。OrbVPSのクラウドMacは専有物理ノードですが、専有環境であってもmacOSの権限モデルは変わりません。最小権限は、実際のジョブと責任プロセスに対して適用する必要があります。

復旧手順を受け入れ確認項目にする

復旧後、すぐに完全なパイプラインへ戻してはいけません。まず、スクリーンショットを1回取得するだけ、またはオートメーションイベントを1回送信するだけのプローブを実行し、その後でテストジョブを段階的に復旧します。ノードの受け入れ記録には、次の結果を残すことを推奨します。

  1. runnerユーザーとコンソールユーザーが一致している。
  2. gui/<uid>ドメインが存在し、LaunchAgentの状態がrunningである。
  3. runnerの実体パスが想定バージョンと一致している。
  4. codesignの出力がベースラインと一致している。
  5. TCCログに対象サービスの拒否が出力されなくなった。
  6. runnerの再起動後も最小プローブが成功する。
  7. 完全なジョブの終了後に、テストホストや自動化プロセスが残っていない。

最後に、ノード再起動後の復旧も一度シミュレーションします。グラフィカルセッションがまだ確立されていない場合、runnerはジョブを受け付けない状態を維持するべきです。ジョブを受け付けた後、スクリーンショットやウィンドウ制御の段階で停止する設計にしてはいけません。ジョブのエントリーポイントにセッションチェックを追加し、失敗時には明確な診断を出力して終了できます。

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

TCC障害への対応で最も難しいのは、許可ボタンを操作することではなく、その権限が誰に属するべきかを特定することです。実行ユーザー、LaunchAgentの起動ドメイン、実行ファイルのパス、署名IDを固定すれば、権限問題を突発的な障害ではなく、検証可能でロールバック可能な実行条件として管理できるようになります。

よくある質問

SSHでは動く処理がCIサービスではTCCに拒否されるのはなぜですか?

実行ユーザーが同じでも、ログインセッション、launchdドメイン、責任プロセスが異なるためです。TCCはスクリプト名だけでなく、実際に処理を担うホストプロセスの識別情報も評価します。

TCCデータベースを直接削除してもよいですか?

推奨しません。対象ジョブを停止し、影響を受けるユーザーでtccutil resetを使って必要なサービスだけをリセットし、有効なGUIセッション内で再承認してください。

OrbVPS クラウドMac

専用のApple Silicon物理ノードでビルドを実行

機種、リージョン、レンタル期間を選んでノードを設定できます。実際の利用可否は、管理コンソールにリアルタイムで表示される情報をご確認ください。

クラウドMacを今すぐレンタル