Claude Media
Claude Codeセルフホスト環境の本番運用 — default-denyとgit認証

Claude Codeセルフホスト環境の本番運用 — default-denyとgit認証

Claude Codeのセルフホスト環境を本番投入する手順を、ハードニングのチェックリスト・ネットワーク遮断・git認証3方式・Kubernetes/Docker Compose構成まで確認します。

Claude Codeのセルフホスト環境は、最小構成ならクイックスタートの3ステップで動きます。ただし本番投入では話が変わります。Runnerは、その環境にセッションを送れる全員の代わりにモデルが指示したコードを自社インフラ上で実行します。組織の誰でも対象になり、Claude Tagのチャンネルセッションがルーティングされていれば、そのSlackワークスペースの参加者も含まれます。実運用に進む前に固めるハードニング・ネットワーク・git認証・オーケストレーションを、実際に使う設定値ごと確認します。

本番接続前に固める8項目

RunnerはBashを既定で承認済みツールに含むため、Auto modeを使っていなくてもシェル経由の外向き通信は確認なしで実行されます。本番システムに接続する前に、次を1つずつ潰します。

  • セッションごとの使い捨てコンテナ: Runnerプロセスをプロセス終了時に破棄される新規コンテナ・VMで動かし、--capacity 1と既定の--drain-grace-sec 0で1コンテナ1セッションに固定する。--capacityを上げるかドレイン猶予の値を正にすると、同じロックされたオーナーの複数セッションが1コンテナに同居する。Runner再起動をまたいでファイルシステムを再利用しない構成が原則で、意図的なpre-warmed checkout構成だけが例外だが、その場合もオーナーをまたぐ再利用は行わない
  • 広範な認証情報をイメージに含めない: 長期のSSHキー・クラウド認証情報・広い権限のPersonal Access Tokenをイメージに焼き込まない。push・APIトークンのようなセッション中の認証情報はwrapper scriptからセッションごとに発行する。wrapper実行前の初回クローンにはcheckoutライフサイクルフックか--use-anthropic-git-proxyを使う
  • environment secretをセッション実行ホストから遠ざける: environment secretはRunnerを登録でき、そのEnvironmentにキューされたどのセッションも拾える強い権限を持つ。固定Runner群ではどのホストにも常駐し、セッションのコードから読める状態になる。オンデマンドRunnerならsecretはユーザーコードを一切実行しないオーケストレーターホストだけに留まる
  • default-deny方式のアウトバウンド遮断: RunnerとセッションコンテナのアウトバウンドをEnvironmentごとに自社のネットワーク境界で制限する(詳細は次節)
  • ホストIAMを最小権限に: Runnerホストに付くインスタンスプロファイルやノードのサービスアカウントは、Runner自身が必要とする権限だけに絞る。セッションはホストの権限を継承しないため、認証情報はwrapper scriptから取得する構成にします。
  • クラウドメタデータエンドポイントの遮断: サブネット単位のegressポリシーはリンクローカルのメタデータ通信を素通りさせるため、コンテナ自体で遮断する。IMDSv2をホップ制限1に設定、GKE Workload Identityならメタデータ隠蔽を有効化、169.254.169.254への明示的な拒否ルールをセッションコンテナのネットワーク名前空間に追加、のいずれか
  • Runnerごとのファイルシステム分離: 他のプロセスから読み書きできない専用の作業ディレクトリを各Runnerプロセスに割り当てる。--hooks-dir・wrapper script・ホストの~/.claude/はセッションから読み取り専用にする
  • --confine-repo-settingsガードを有効化: 既定のwarnは違反をログに残しつつセッションを起動し、enforceはセッションを拒否し、offはスキャン自体を無効化する。スキャン対象は、セッションのワークスペース外を指すadditionalDirectoriesエントリ・permissions.allow内のEdit/Write/NotebookEditルール・sandbox.filesystem.allowWriteまたはallowReadエントリ、空でないenvブロック、sandbox.enabled: falseのように運用側の方針を上書きする設定

ディスパッチ自体にはEnvironment単位のアクセス制御が無い点も踏まえます。Anthropic組織のメンバーなら誰でも、どのEnvironmentにもセッションを送れます。OwnerがClaude TagチャンネルをそのEnvironmentにルーティングしていれば、Claude Tagのアクセス設定が許可する全員(既定では接続済みSlackワークスペースの全員、Claudeアカウントの有無を問わず)もチャンネルセッションを起動できます。--lock-to-accountはどのアカウントのセッションをそのホストが実行するかを絞るだけで、誰がEnvironmentへディスパッチできるかは絞りません。

ネットワークの外向き通信を制限する

RunnerとセッションのHTTPS接続先を、必要なホストだけに絞ります。

ホストポート用途
api.anthropic.comポート443(SCM connectorのみWSS)用途コントロールプレーン・セッションストリーミング・モデル推論・JWKS鍵取得・コミット署名
自社のgitホスト(github.com等)ポート443または22用途クローン・push。--use-anthropic-git-proxy使用時は不要
code.claude.com / claude.comポート443用途ドキュメント参照とWebFetch。遮断してもドキュメント参照にしか影響しない
registry.npmjs.orgポート443用途プラグインインストール・npx起動のMCPサーバー
*.frame.claudeusercontent.comポート443用途Artifactツール利用時のみ。CLAUDE_CODE_DISABLE_ARTIFACT=1で無効化可能

一部の企業向けegressプロキシは、接続ごとにProxy-Authorizationヘッダーを要求します。そのトークンはHTTPS_PROXYのURLに書き込むには回転が速すぎるため、HTTPS_PROXY/HTTP_PROXYは通常どおり設定した上で、--proxy-authorization-command--proxy-authorization-fileでヘッダー値の取得元を指定します(いずれもClaude Code v2.1.238以降)。

statsig.anthropic.com*.sentry.ioclaude.aiplatform.claude.comへの到達は不要です。この4つは古い企業向けチェックリストに残っていることがありますが、機能フラグの取得はapi.anthropic.com経由になり、Runnerはインタラクティブなwebサインインではなくenvironment secretで認証するためです。ただし2つのホスト側フローだけは例外です。インストーラーはclaude.aiからinstall.shを取得し、対話的なclaude auth loginclaude.aiclaude.complatform.claude.comを経由してサインインします。どちらもegressを許可した別のホストから実行します。

セッションのコードはモデルが指示した内容で任意のホストへ接続を試みる可能性があります。default-deny方式のアウトバウンド遮断はその接続がどこに着地するかを境界側で制限する対策であり、これは権限モードの設定に関係なく効きます(既定の承認済みツールにBashが含まれるため、Auto modeを使っていなくてもシェル経由の通信は確認なしで実行されます)。

git認証を設定する

Runnerはチェックアウト自体は管理しますが、git IDや認証情報は既定で設定しません。3つの選択肢があります。

方式必要なgitバージョン向く場面
Runnerに設定させる(--configure-git)必要なgitバージョン2.34以降(SSHコミット署名)向く場面Anthropicホスト環境と同じidentity・署名設定で揃えたい
イメージにgit設定を焼く(自前のbot identity)必要なgitバージョン制約なし(署名・proxy・push-outcome-on-releaseを使わなければ2.24で十分)向く場面自社のbot identityでコミットしたい
Anthropic git proxyを使う(--use-anthropic-git-proxy)必要なgitバージョン2.32以降向く場面イメージにgit認証情報を一切持たせたくない

Runnerに設定させる場合、user.name = Claudeuser.email = noreply@anthropic.comがAnthropicホストセッションと同じ形で書き込まれ、Anthropicの署名サービス経由でSSH形式のコミット署名も設定されます。push用の認証情報はこのフラグでは設定されないため、別途イメージ側で用意します。

イメージにgit設定を焼く場合、Dockerfileでシステム全体に設定します。

RUN git config --system user.name "Claude" && \
    git config --system user.email "noreply@anthropic.com"

長期間有効・広いスコープのpush用認証情報を共有イメージに焼き込まないことが重要です。イメージ内の認証情報は、そのイメージを実行する誰のセッションからも使えてしまいます。代わりに、セッション作成者のidentityをセッションJWTから復号し、wrapper scriptから短命でスコープの狭いトークンをセッションごとに発行します。--capacity 1のセッション単位使い捨てコンテナと組み合わせれば、認証情報がそのセッションより長生きしません。

Anthropic git proxyを使う場合、--use-anthropic-git-proxyを設定すると、セッション自身の短命トークンで認証しながらAnthropicのgit proxy経由でクローンします。通常のユーザーセッションではセッション作成者のGitHub OAuthトークンを、bot・agentセッションでは組織のGitHub Appインストールトークンを使います。Runnerイメージにはgit認証情報が一切不要になります(SSHキーもcredential helperも.netrcも無し)。ただしproxy URLはセッション単位のため--capacity 1が必須で、自社のgitホストがAnthropicのインフラから到達可能である必要があります(社内限定のgitホストにはcheckoutライフサイクルフックを使います)。

いずれの方式でも、Runnerの組み込みクローンはGIT_TERMINAL_PROMPT=0・SSHのBatchMode=yesGCM_INTERACTIVE=neverを設定し、プロンプトが出る認証方式は失敗します。認証情報を用意する際は、プロンプトなしで動く形にしておく必要があります。

Runnerイメージをビルドする

Anthropicは事前ビルド済みのRunnerイメージを配布していません。claudeバイナリを土台に、リポジトリが必要とする言語ランタイム・コンパイラ・パッケージマネージャーを積み上げます。

FROM debian:bookworm-slim
ARG CLAUDE_CODE_VERSION
RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates openssh-client \
 && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL "https://downloads.claude.ai/claude-code-releases/${CLAUDE_CODE_VERSION:?set with --build-arg CLAUDE_CODE_VERSION}/linux-x64/claude" \
      -o /usr/local/bin/claude && chmod +x /usr/local/bin/claude
RUN git config --system user.name "Claude" \
 && git config --system user.email "noreply@anthropic.com" \
 && git config --system --add safe.directory '*'
ENTRYPOINT ["claude"]

ARMノードならlinux-arm64、Alpineのようなmuslベースイメージならlinux-x64-muslまたはlinux-arm64-muslに差し替えます。Claude Code v2.1.224以降でビルドし、自社レジストリへpushします。

セッション1件あたりの目安は、メモリがrequest・limitとも4GiB(システム要件の最低ラインに合わせ、requestとlimitを揃えてスケジューラに全メモリを認識させる)、CPUはrequest 2・limit 4(ビルド時にバーストできるようにする)です。ビルドとテストが負荷の大半を占めるため、代表的なビルドを実行してピークを実測し、そこに余裕を乗せます。--capacityを1より大きくする場合は、1セッション分の値にcapacityを掛けた値へ引き上げます。

KubernetesとDocker Composeで動かす

Runnerはポート8080(既定、--health-portで変更可)でGET /healthzを返すため、追加設定なしでKubernetesのprobeが使えます。このエンドポイントはプロセスが生きていれば200を返すため、ポーリングが止まったまま生きているRunnerの検出には/metricslast_poll_age_seconds系列を別途監視します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: claude-runner
  namespace: claude-runners
spec:
  replicas: 3
  selector:
    matchLabels:
      app: claude-runner
  template:
    metadata:
      labels:
        app: claude-runner
    spec:
      terminationGracePeriodSeconds: 90
      containers:
        - name: runner
          image: <your-registry>/claude-runner:latest
          args:
            - self-hosted-runner
            - --environment-secret-file
            - /etc/claude/environment-secret
            - --capacity
            - "4"
          volumeMounts:
            - name: environment-secret
              mountPath: /etc/claude
              readOnly: true
          ports:
            - name: health
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 30
      volumes:
        - name: environment-secret
          secret:
            secretName: claude-runner-environment-secret

terminationGracePeriodSeconds: 90が要点です。Kubernetesの既定30秒はRunnerのドレイン処理より短く、ドレインが終わる前にpodが止められてしまいます。CPU・メモリのresourcesブロックは載せていないため、上のcapacityに応じて自分で追加します。environment secretはローカルファイルからKubernetes Secretを作って渡します。

kubectl create namespace claude-runners
kubectl create secret generic claude-runner-environment-secret \
  -n claude-runners --from-file=environment-secret=./environment-secret

Docker Composeではrestart: alwaysでクラッシュ・正常終了の両方から再起動しますが、書き込み可能レイヤーを維持したまま同じコンテナが復帰するため、評価用途に留め、本番ではコンテナを実行のたびに作り直す構成にします。

services:
  claude-runner:
    image: <your-registry>/claude-runner:latest
    command:
      - self-hosted-runner
      - --environment-secret-file
      - /run/secrets/environment-secret
      - --capacity
      - "4"
    secrets:
      - environment-secret
    restart: always
    stop_grace_period: 90s
 
secrets:
  environment-secret:
    file: ./environment-secret

シャットダウンのタイミングを設計する

SIGTERMを受けると、Runnerは新規受付を止め、--drain-wait-sec(既定0)まで進行中のターンを待ち、セッションのプロセスツリーを終了してpost-sessionフックを実行します。フルのドレイン経路には--session-stop-grace-sec + --drain-wait-sec + --post-session-hook-timeout-secに加え、固定オーバーヘッド15秒、--push-outcome-on-release設定時はさらに30秒がかかります。既定値では合計80秒で、Runnerは起動時にこの合計をログへ出力します。

ホストの停止方法に応じて、この合計以上の猶予を与えます。

  • SIGTERMの猶予期間で止める場合: KubernetesのterminationGracePeriodSecondsやDocker Composeのstop_grace_periodをこの合計以上に設定する
  • --retire-atで止める場合: retire時刻とホスト停止時刻の間に、通常のターンの長さ・バックグラウンドタスクの保持時間・このドレイン合計を足した余裕を持たせる
  • --defer-shutdown-max-minで止める場合: 設定した分数に加え、post-release grace(既定75秒)とドレイン合計を足す

--defer-shutdown-max-minは、再起動中のRunnerに保持中のセッションを最初のシグナルで即座に手放させず、指定した分数だけ通常通り動かし続けさせるオプションです(Claude Code v2.1.238以降)。2回目のシグナルはどの段階でも即座にドレインを強制します。

フリートのスケール方式

Runnerは1度に1人のオーナーしか担当しないため、最小レプリカ数は同時にアクティブになりうる利用者数とClaude Tag agentの数の合計です。--capacityはオーナー内の並列度を上げるだけで、オーナー間のスケールには寄与しません。

2つのスケール方式があります。

  • 固定フリート: 一定数のRunnerレプリカを動かし、Prometheusメトリクスでスケールする
  • オンデマンドRunner: claude self-hosted-runner orchestratorサブコマンドを動かし、Runnerのいないままキューされたセッションをポーリングしてspawn-runnerフックで1セッションごとに1台起動する。environment secretをオーケストレーターホストだけに留められる利点がある(前述のハードニング項目3)

Runnerが1人のオーナーに固定される仕組み

Runnerが最初に受け取ったセッションのオーナーにロックされ、以降は--capacityまで同じオーナーのセッションだけを実行する、というのがセルフホスト環境全体の設計の軸です。ユーザーが始めたセッションはそのユーザーのアカウントがオーナーに、Claude Tagチャンネルセッションはユーザーアカウントが紐付かないためClaude Tag agent自体がオーナーになります。

既知の制限

  • connectorトラフィックは自社ネットワークの外に出る: GitHub・Slack・Linear等のclaude.aiコネクタは、RunnerからではなくAnthropic自身のインフラから呼び出されるため、セルフホストセッションでコネクタを使うとそのトラフィックはapi.anthropic.com経由になり自社ネットワーク境界の中では完結しません。自社側に留めたい場合は、同等のツールをRunnerイメージ上のローカルMCPサーバーとして動かします。コネクタ自体を使わせたくない場合はallowedMcpServers/deniedMcpServersで他のMCPサーバーと同様にフィルタできます
  • 一部のセッションはアイドル判定されない: バックグラウンドタスクを保持したまま終わらないセッションや、実行中のツール呼び出しから承認待ちのセッションは--release-idle-session-minで解放されません。--kill-session-after-minを必ず併用してハードな上限にします
  • 再開したセッションは未pushの作業を失う: アイドルタイムアウトやRunner再起動でセッションが解放されユーザーが次のメッセージを送ると、新しいRunnerが開始ブランチから改めてクローンするため、pushしていなかった作業は失われます。--push-outcome-on-releaseを設定すると、解放前にRunnerがベストエフォートでoutcomeブランチをpushし、再開セッションがそこから始まります。有効化する前に、送信元リモートのclaude/*参照へのpush権限を絞っておく必要があります(誰がpushしたか検証せずに取得するため)
  • プライベートリポジトリはセッション途中で追加できない: セッション開始後に追加したリポジトリは認証情報付きでクローンされず失敗します。必要なリポジトリは開始時にすべて選びます

トラブルシューティング

ガイド付き診断には、Runnerホストでdoctorサブコマンドを実行します。claude auth loginでサインイン済みなら、EnvironmentとそのRunner・キュー済みセッションまで踏み込んで確認できます。

claude self-hosted-runner doctor

Runnerが管理画面に現れないときは、到達性・secretの有効性・時刻同期の3点を順に見ます。api.anthropic.comへのHTTPS到達性、environment secretの有効性、ホストの時刻同期(実時間との5分以上のズレは認証失敗の原因)の順に確認し、認証失敗時は[runner:fatal]ログに理由が出ます。

cannot create or write to base directoryで起動時に終了する場合、--base-dir(既定/workspace)への書き込み権限がありません。ディレクトリの所有権を直すか、書き込み可能なパスを指定します。v2.1.225より前はこのチェックが起動時になく、セッション拾得後に失敗していました。

キューが動かない原因は、オンラインのRunnerがすべて別のオーナーにロックされていることです。各Runnerのclaude_code_self_hosted_runner_locked_accountメトリクスか[runner:health]ログのlocked_accountフィールドで、誰にロックされているか確認します。レプリカを追加するか、既存Runnerのドレインを待ちます。

セッションが拾得直後に失敗する場合、claude.ai/codeでエラーを確認します。よくある原因はRunnerイメージのgit認証情報の不足とビルドツールの未インストールです。

podがドレイン途中で強制終了される場合、terminationGracePeriodSecondsをRunnerが起動時にログへ出す値以上に引き上げます。

まとめ

セルフホスト環境を本番投入するチェックリストは、使い捨てコンテナ・認証情報の非同梱・environment secretの隔離・default-deny egress・最小権限IAM・メタデータエンドポイント遮断・ファイルシステム分離・--confine-repo-settingsの8項目です。ディスパッチ自体にはEnvironment単位のアクセス制御が無いため、そのEnvironmentにセッションを送れる全員が読めて構わないデータだけをRunnerホストに置く、という前提で設計します。

git認証は--configure-git・イメージへの焼き込み・Anthropic git proxyの3方式から選び、いずれもプロンプトなしで動く形にします。KubernetesではterminationGracePeriodSecondsをドレイン合計以上に確保し、フリートは固定かオンデマンドRunnerで運用します。Runner・Environment・Sessionの用語や最小構成での動かし方は冒頭のクイックスタートに譲り、ここでは本番投入で追加になる設計だけをまとめました。ネットワーク遮断の設計はサンドボックス実行の考え方と地続きで、Claude Codeのサンドボックス設計Claude CodeをDevContainerで安全に動かす完全実装も同じ「モデルが指示したコードをどこまで信用するか」という論点を扱っています。ライフサイクルフックの書き方全般はClaude Code Hooks完全ガイドを参照してください。

よくある質問

environment secretはどのくらいの頻度でローテーションしますか

固定の推奨周期はありません。ローテーションが必要になるのは、セッション侵害が疑われたときです。固定フリートではsecretがすべてのRunnerホストに常駐しどのセッションのコードからも読める状態になるため、疑わしい事象があれば速やかに新しいsecretを発行し、古いものを無効化します。

--capacityを4にすると1コンテナで何が変わりますか

同じオーナーにロックされた状態で最大4セッションが並行実行されます。ハードニングの観点では、--capacity 1のセッション単位使い捨てコンテナに比べて分離が弱くなります。CPU・メモリはRunnerが自動で分割しないため、セッション間の上限はwrapper scriptから設定します。

Anthropic git proxyを使えばRunnerイメージの認証情報管理は不要になりますか

push・クローン用のgit認証情報という意味では不要になります。ただしproxy URLがセッション単位であるため--capacity 1が必須になり、自社のgitホストがAnthropicのインフラから到達可能である必要があります。社内限定のgitホストには使えないため、その場合はcheckoutライフサイクルフックで対応します。

オンデマンドRunnerと固定フリートはどちらを選ぶべきですか

本番投入のハードニングを優先するならオンデマンドRunnerが有利です。environment secretをユーザーコードの動かないオーケストレーターホストだけに留められ、Runner1台が1セッション専用の使い捨てとして起動するためです。固定フリートは構成がシンプルな分、secretがすべてのホストに常駐する前提でのローテーション運用が必要になります。

この記事を共有:XはてブLinkedIn