Claude Media
MCP tunnelsをHelmでデプロイする手順 — 通信制限とOIDCトークン供給

MCP tunnelsをHelmでデプロイする手順 — 通信制限とOIDCトークン供給

HelmチャートでMCP tunnelsをKubernetesにデプロイし、NetworkPolicyでの通信制限とOIDCトークンの差し替え、症状別の切り分けまで、チャート2.0.2の設定キー単位でまとめます。

HelmチャートはKubernetes上に何を作るのか

Anthropicが配布するHelmチャートは、MCP tunnelsのトンネルスタック(cloudflaredとプロキシの2コンテナ)を単一のDeploymentとしてKubernetesクラスタにインストールします。クラスタ自体をClaudeから直接操作したい場合はKubernetes MCPサーバーの追加手順が別記事にあります。接続先は、チャートのsetupフックが作る新規トンネルか、Consoleで先に作った既存トンネルのどちらかです。作成手順はMCP tunnelsをConsoleで作成・管理する手順で扱っています。

チャートのバージョンは2.0.2です。バージョンは変わり得るので、実行前にhelm show valuesで最新のデフォルト値を取得してから進めます。既存の配布チャートではなく汎用的なHelmチャートを自分でゼロから書く手順はClaude CodeでHelmチャートを作成する手順にまとめています。

最初の分かれ道は、認証情報をどう用意するかです。この選択で、以降の手順とトークン・証明書の運用が変わります。

くらべる

認証情報の用意の仕方で運用が変わる

既定・推奨

プログラム的アクセス

setup.enabled: true(既定)。setupコンポーネントがWorkload Identity Federationで認証し、トンネルトークンの取得、CAとサーバー証明書の生成、CAの登録までを行います。federation ruleの用意が必要で、証明書更新はCronJobが担います。

WIFを組みたくない場合

手動

setup.enabled: false。チャートはAPI呼び出しをせず、トークンの取得、CAと証明書の作成、Secretの更新はすべて利用者の作業です。CronJobもありません。

単一ホストでの検証ならMCP tunnelsをDocker Composeでデプロイする手順もあります。Composeでも、mcp-tunnel/ディレクトリを各ホストにコピーすれば、同じトンネルトークンと証明書で複数台に広げられます。Helmとの違いは動かす場所と、証明書更新をdocker compose run --rm setup renew-cert --output=dir:/dataで自分で実行する点です。

事前に必要なもの

  • トンネル。プログラム的アクセスではチャートのsetupフックが未指定なら新規作成し、指定すればConsoleで作った既存トンネルに接続します。手動フローでは必ずConsoleで先に作成し、トンネルトークンとドメインを控えます
  • Tunnels APIへの認証手段。プログラム的アクセスならworkspace:manage_tunnelsスコープのfederation rule、手動ならトンネルトークンとCA証明書
  • helmとkubectlが使えるKubernetesクラスタ。手動フローではopenssl(1.1.1以降)も使います
  • アウトバウンド通信。api.anthropic.comへの443/TCPと、トンネルエッジ(198.41.192.0/19、2606:4700:a0::/44)への7844/TCP・UDP。前者はsetupコンポーネントが使う通信です
  • 到達可能なMCPサーバーが1つ以上。無ければ次節のサンプルDeploymentで代用できます
  • Zero Data RetentionやHIPAA BAAの対象範囲。MCP tunnelsは研究プレビューの機能で、対象かどうかはAPIとデータ保持のページにある機能別の一覧で見ます

手元にMCPサーバーが無いときのテスト用サンプル

到達可能なMCPサーバーをまだ持っていない場合、helloツールが1つあるだけのFastMCPサンプルをDeploymentとして立てられます。サーバーのソースはConfigMapに入れ、Podの/appにマウントする構成です。ConfigMapを省くと、起動コマンドが指す/app/hello_server.pyが存在せず、Podは起動に失敗します。

kubectl create namespace mcp-tunnel --dry-run=client -o yaml | kubectl apply -f -
kubectl -n mcp-tunnel apply -f - <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: hello-mcp-src
data:
  hello_server.py: |
    from mcp.server.fastmcp import FastMCP
 
    mcp = FastMCP("hello-server", host="0.0.0.0", port=9000)
 
 
    @mcp.tool()
    def hello(name: str = "world") -> str:
        """Say hello to someone."""
        return f"Hello, {name}!"
 
 
    if __name__ == "__main__":
        mcp.run(transport="streamable-http")
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-mcp
spec:
  replicas: 1
  selector:
    matchLabels: { app: hello-mcp }
  template:
    metadata:
      labels: { app: hello-mcp }
    spec:
      containers:
        - name: hello-mcp
          image: python:3.13-slim
          command: ["sh", "-c", "pip install --quiet mcp && python /app/hello_server.py"]
          volumeMounts:
            - { name: src, mountPath: /app }
          ports:
            - { containerPort: 9000 }
      volumes:
        - name: src
          configMap: { name: hello-mcp-src }
---
apiVersion: v1
kind: Service
metadata:
  name: hello-mcp
spec:
  selector: { app: hello-mcp }
  ports:
    - { port: 9000, targetPort: 9000 }
EOF

このサンプルを使う場合、gateway.config.routesはecho: http://hello-mcp:9000にします。以降の検証ではこのechoルートを使います。

プログラム的アクセスでインストールする

まずクラスタのOIDC発行者を登録し、federation ruleを作ります。setupコンポーネントはリリース名から導出される専用のServiceAccountで動くため、リリース名がmcp-tunnel以外なら次のコマンドでServiceAccount名を確認してからruleを作ります。

helm template <リリース名> \
  oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \
  --version 2.0.2 -n mcp-tunnel | grep -A2 'kind: ServiceAccount'
項目値
Subject値system:serviceaccount:mcp-tunnel:mcp-tunnel-setup
Audience値api.anthropic.com(スキームなし)
Scope値workspace:manage_tunnels

トンネルが組織の既定以外のワークスペースにある場合は、ruleのサービスアカウントをそのワークスペースのメンバーにも加えます。Tunnels APIは、サービスアカウントのワークスペース所属で認可するためです。毎日動く証明書更新のCronJobは別のServiceAccountで動きますが、Tunnels APIを呼ばないのでruleの対象に含める必要はありません。

デフォルト値を取得し、federation ruleのIDとルート先を設定します。

helm show values \
  oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \
  --version 2.0.2 > values.yaml
values.yaml(プログラム的アクセスで編集する主要キー)
api:
  wif:
    federationRuleId: "fdrl_..."
    organizationId: "00000000-0000-0000-0000-000000000000"
    # workspaceId: "wrkspc_..."   # 非デフォルトワークスペースのときだけ設定
 
tunnel:
  id: ""              # 空なら新規作成。既存トンネルに繋ぐならtnl_...を指定
  tokenVersion: "1"   # ローテーション時にこの値を上げる
 
gateway:
  config:
    routes:
      docs: http://docs-mcp.internal:8080
      search: http://search-mcp.internal:8080

routesのキーはサブドメインになり、この例ならClaudeからはdocs.<トンネルのドメイン>とsearch.<トンネルのドメイン>で届きます。値はscheme://host:portの形で、ポートは必須です。パスを含めると、設定の読み込みでinvalid upstream (must be scheme://host:port)として拒否されます。リクエストのパスとクエリは、変更されずにそのままMCPサーバーへ転送されます。

レンダリング結果を確認してからインストールします。

helm template mcp-tunnel \
  oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \
  --version 2.0.2 -n mcp-tunnel -f values.yaml > rendered.yaml
 
helm install mcp-tunnel \
  oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \
  --version 2.0.2 \
  --namespace mcp-tunnel --create-namespace \
  -f values.yaml

setupコンポーネントはHelmのpre-installフックJobとして動くため、helm installはこのJobが完了するまでブロックします。成功するとHelmがJobを自動で削除します。tunnel.idを空にした場合、トンネルはfederation ruleが対象とするワークスペースに作られます(api.wif.workspaceIdを設定しなければ組織の既定ワークスペースです)。そのIDとドメインはmcp-tunnelという名前のSecretに保存され、次のコマンドで読み出せます。ConsoleのManage > MCP tunnelsにあるトンネルの詳細ページでも、同じドメインを確認できます。

kubectl -n mcp-tunnel get secret mcp-tunnel \
  -o jsonpath='{.data.tunnel-domain}' | base64 -d

アップグレードやトークンローテーションでsetupコンポーネントを再実行しても、このSecretに保存されたトンネルIDを再利用するだけで、2本目のトンネルは作られません。

api.wif.*の値は識別子であって秘密情報ではないため、Helmのリリース履歴Secretに残ってもリスクにはなりません。実際に機微なのは、setupコンポーネントが作るmcp-tunnel Secret(トンネルトークンとTLS秘密鍵を保持)のほうです。

手動フローでインストールする

setup.enabled: falseにすると、チャートはAPI呼び出しを一切行わず、setupコンポーネントも証明書更新のCronJobも動きません。Workload Identity Federationを組みたくない場合はこちらを使います。

まずConsoleでトンネルを作り、トンネルトークンを控えます。詳細ページに表示されるトンネルのドメインは、後でgateway.config.tunnel_domainに設定します。次に、CAとサーバー証明書を自分で作ります。サーバー証明書のSANには*.<トンネルのドメイン>を含める必要があります。

export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE
mkdir -p mcp-tunnel/data && cd mcp-tunnel
 
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout data/ca.key -out data/ca.crt \
  -days 3650 -subj "/CN=mcp-tunnel-ca" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash"
 
cat > data/tls.ext <<EOF
subjectAltName = DNS:${TUNNEL_DOMAIN},DNS:*.${TUNNEL_DOMAIN}
authorityKeyIdentifier = keyid,issuer
extendedKeyUsage = serverAuth
EOF
 
openssl req -newkey rsa:2048 -nodes \
  -keyout data/tls.key -out /tmp/server.csr \
  -subj "/CN=${TUNNEL_DOMAIN}"
openssl x509 -req -in /tmp/server.csr \
  -CA data/ca.crt -CAkey data/ca.key -CAcreateserial \
  -out data/tls.crt -days 90 -extfile data/tls.ext

拡張を-copy_extensionsでなく-extfileで渡しているのは、OpenSSL 3.0以降でしか使えない前者を避け、1.1.xでも動かすためです。上のコマンドで作った証明書を読み出すと、確認すべき点は次のように出ます(ドメインは例です)。

openssl x509 -in data/tls.crt -noout -text \
  | grep -A1 -E "Alternative|Authority Key|Extended Key"
X509v3 Subject Alternative Name:
    DNS:abc123.tunnel.example.com, DNS:*.abc123.tunnel.example.com
X509v3 Authority Key Identifier:
    AF:E3:20:3E:D5:18:9A:EC:EE:23:71:FC:A2:02:44:E1:8A:9B:95:C0
X509v3 Extended Key Usage:
    TLS Web Server Authentication

Authority Key Identifierは、CA側のSubject Key Identifier(openssl x509 -in data/ca.crt -noout -textで見えます)と一致している必要があります。サーバー証明書は、登録済みのCAが中間CAを挟まずに直接署名したものでなければならないからです。CAのdata/ca.crtはConsoleに登録し、data/ca.keyは更新時の署名に使うので安全な場所に残します。

そのうえで、チャートが読む2つのSecretを作ります。Secret名は変更できますが、中のキー名は固定です。

kubectl create namespace mcp-tunnel --dry-run=client -o yaml | kubectl apply -f -
kubectl -n mcp-tunnel create secret generic mcp-tunnel-token \
  --from-literal=tunnel-token='eyJ...'
kubectl -n mcp-tunnel create secret generic mcp-tunnel-cert \
  --from-file=tls.crt=data/tls.crt --from-file=tls.key=data/tls.key

values.yamlでは以下を設定します。

  • setup.enabled: false
  • external.tunnelTokenSecretName(中身のキーはtunnel-token)
  • external.serverCertSecretName(中身のキーはtls.crtとtls.key)
  • gateway.config.tunnel_domain(setup有効時はSecretから自動注入されるが、無効時は明示が必須)

この先のhelm templateとhelm installはプログラム的アクセスと同じコマンドです。

インストール後に疎通を確かめる

プロキシはプレーンなWebSocketで待ち受け、TLSはそのストリームの内側で行います。このため、プロキシのアドレスにcurl https://...を打つとwrong version numberで失敗しますが、これは正常です。確認はAnthropic側から行います。Managed AgentsのセッションかMessages APIのリクエストで、https://<route>.<トンネルのドメイン>/<path>を指定します。<route>はgateway.config.routesのキー、<path>はMCPサーバーが公開しているパスです。サンプルサーバーならhttps://echo.<トンネルのドメイン>/mcpになります。

失敗したときは、2つのコンテナのログを見ます。

kubectl -n mcp-tunnel logs deploy/mcp-tunnel -c mcp-proxy
kubectl -n mcp-tunnel logs deploy/mcp-tunnel -c cloudflared

NetworkPolicyで外向き通信(egress)を絞る(任意設定)

プロキシPodへの受信通信は既定で遮断されます(networkPolicy.ingress.enabledの既定値がtrue)。さらにPodからの送信も絞るなら、networkPolicy.egress.enabled: trueにし、networkPolicy.egress.mcpServersにPodラベルセレクタかCIDRを列挙して、アップストリームのMCPサーバー以外への通信を遮断します。cloudflaredからトンネルエッジへの送信は別枠のnetworkPolicy.egress.cloudflaredEgressCIDRsで許可されるので、この2つは独立して設定します。

NetworkPolicyはネットワーク経路を絞るだけで、トンネル自体はアップストリームのMCPサーバーを認証しません。トンネルが運ぶのは暗号化された通信で、MCPサーバーが認証を要求するなら、ほかのMCPサーバーと同じ方法で個別に渡します。各MCPサーバー側にOAuthかbearer認証を設定する必要があり、仕組みはリモートMCPのOAuth認証で解説しています。

プロキシの設定を調整する

gateway.config.*の項目は、プロキシの設定ファイルにそのまま渡されます。よく触るのはupstream.allowed_ips、log_level、upstream.tlsです。listen_addr、tls.cert_file、tls.key_fileはチャートが常に設定するので、gateway.configに書いても効きません。

アップストリームがhttps://のルートなら、upstream.tls.ca_fileかupstream.tls.include_system_casのどちらかを設定します。どちらも無いと、プロキシにはアップストリームの証明書を検証する信頼の起点がありません。

自前のOIDCトークンを供給する(任意設定)

チャートは既定でKubernetesのServiceAccountトークンをsetupコンポーネントに投影します。SPIFFEやVault、クラウドSDKのサイドカーなど別のIDプロバイダのトークンを使いたい場合は、setup.extraVolumesとsetup.extraVolumeMountsでトークンファイルをマウントし、api.wif.tokenFileにそのマウントパスを指定します。チャートはANTHROPIC_IDENTITY_TOKEN_FILE環境変数をそのパスに設定し、setupコンポーネントはそこからトークンを読みます。

設定を変える・トークンをローテーションする

ルートやレプリカ数、NetworkPolicyなどの通常の変更は、values.yamlを直してhelm upgradeします。helm upgradeには必ず--versionを付けます。省くと、意図せず新しいチャートを引いてしまいます。

トークンのローテーションは、プログラム的アクセスならvalues.yamlのtunnel.tokenVersionを上げてから、--set setup.force=trueを付けてアップグレードします。setupコンポーネントは、フォースしない限りアップグレード時には再実行されません。

helm upgrade mcp-tunnel \
  oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \
  --version 2.0.2 -n mcp-tunnel \
  -f values.yaml --set setup.force=true

この経路はWorkload Identity Federationで認証するので、失効させるAPIトークンはありません。

手動フローでは、Consoleのトンネル詳細ページでRotate tokenを押した後、mcp-tunnel-token Secretを更新してDeploymentをロールアウトし直します。

kubectl -n mcp-tunnel create secret generic mcp-tunnel-token \
  --from-literal=tunnel-token='eyJ...' --dry-run=client -o yaml | kubectl apply -f -
kubectl -n mcp-tunnel rollout restart deploy/mcp-tunnel

証明書を更新する

寿命まわりの数字は次のとおりです。setupコンポーネントが自動生成する証明書の値で、更新の判定に使うrenewBeforeもあわせて載せています。

数字

自動生成される証明書の寿命と更新の周期

  • サーバー証明書の有効期間

    90日

    RSA 4096ビット、ワイルドカードSAN

  • 更新を始める残り日数

    30日

    serverCert.renewBeforeの既定値

  • CAの有効期間

    5年

    ECDSA P-256

  • 登録できる有効なCA

    2つ

    無停止のローテーションに使う

プログラム的アクセスでは、リリース名にちなんで命名されたCronJob(サフィックス-cert-renew)が、serverCert.cronScheduleの既定値(毎日UTC 0時)でsetup renew-certを実行します。有効期限までserverCert.renewBeforeを切っていない限りno-opです。更新はローカルで完結し、Secretに保存済みのCAで新しい証明書に署名するだけなのでAPI呼び出しは発生せず、プロキシはSecretのマウントを自動でホットリロードします。CronJobが動いていても、期限の監視と更新の完了確認は利用者の仕事として残ります。

手動フローにはCronJobが無いため、インストール後も残したmcp-tunnel/ディレクトリの中で、既存のCAを使って署名し直します。CAは作り直しません。

openssl req -new -key data/tls.key -out /tmp/server.csr \
  -subj "/CN=${TUNNEL_DOMAIN}"
openssl x509 -req -in /tmp/server.csr \
  -CA data/ca.crt -CAkey data/ca.key -CAcreateserial \
  -out data/tls.crt -days 90 -extfile data/tls.ext
 
kubectl -n mcp-tunnel create secret generic mcp-tunnel-cert \
  --from-file=tls.crt=data/tls.crt --from-file=tls.key=data/tls.key \
  --dry-run=client -o yaml | kubectl apply -f -

Secretを更新すれば、プロキシがマウントを自動でホットリロードするので、Deploymentの再起動は要りません。

チャート1.xからアップグレードする

チャート2.0.0で、トンネルIDの置き場所がapi.wif.tunnelIdからtunnel.idに変わりました。アップグレード前にvalues.yamlを編集し、tnl_...の値をtunnel.idへ移してapi.wif.tunnelIdを削除します。tunnel.idを空のままにしても、setupコンポーネントがmcp-tunnel Secretに保存済みのIDを再利用するので動きますが、values.yamlの記述が実態とずれます。federation ruleのスコープも、Console側でorg:manage_tunnelsからworkspace:manage_tunnelsへ更新が必要です。

詰まったときの切り分け

リクエストは3つの層のどこかで失敗します。外側から順に、トンネルエッジへの外向き接続、AnthropicからプロキシへのインナーTLS、プロキシからアップストリームへのルーティングとIP検証です。症状がどの層かを決めてから、下の表で原因を探します。

手順

切り分けの順番

  1. 1

    トンネルエッジに繋がっているか

    cloudflaredのログを見ます。トンネルトークンの欠落・期限切れ・コピーミスか、7844番ポートのTCP/UDPをファイアウォールが塞いでいるのが典型です。UDP受信バッファのサイズに関する警告はQUICの調整のヒントで、エラーではありません。

  2. 2

    インナーTLSが通っているか

    プロキシがtls handshake failedを出すなら、サーバー証明書の期限、SANの*.<トンネルのドメイン>、署名したCAがこのトンネルに登録されているか、の3点を順に見ます。

  3. 3

    ルートとアップストリームに届いているか

    プロキシのログにno route for hostやIP validation failedが出ていれば、この層です。routesのキー、tunnel_domain、許可IPの順に確認します。

症状原因直し方
helm installがフックのエラーで失敗し、setup Jobが残る原因federationの交換が失敗している直し方kubectl logs job/mcp-tunnel-setup -n mcp-tunnelで読み、helm uninstall後にJobを手で消してから再試行
Tunnels APIから403原因ruleのスコープにworkspace:manage_tunnelsが無い、またはサービスアカウントがトンネルのワークスペースのメンバーでない直し方スコープを設定し、メンバーに加える
エージェントの+ MCP Serverにトンネルが出ない原因一覧に出るのは、セッションのワークスペースにあり、有効な証明書が1つ以上あるトンネルだけ直し方CA証明書を登録するか、トンネルを作ったワークスペースでセッションを開く
no route for host原因tunnel_domainが割り当てドメインと違うか、設定を直したのにプロキシが再起動していない直し方詳細ページのドメインを設定し、helm upgradeで反映する
cannot unmarshal !!seq into map[string]stringでプロキシが終了原因routesがYAMLのリストになっている直し方routes: { 名前: http://host:port }のマップにする

認証の失敗は、federationの交換そのものの問題と、MCP tunnels固有の原因に分かれます。前者は件名・Audience・発行者・JWKS・有効期限の不一致で、Workload Identity Federationのリファレンスにある切り分けと同じです。後者の代表がAudienceのスキーム違いで、チャートの既定はapi.anthropic.com、Consoleの提案はhttps://api.anthropic.comでした。

IP validation failed: <ip> is not a private addressは、プロキシのSSRF対策に当たっています。プロキシが接続するのは、既定ではRFC1918の私設アドレス(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)で、IPv4のみです。マネージドKubernetesの一部は、ServiceのCIDRをこの範囲の外に割り当てます。次のコマンドの結果が私設範囲に入っていなければ、そのクラスタのCIDRを足す必要があります。

kubectl get svc kubernetes -n default -o jsonpath='{.spec.clusterIP}'

足すときは、必要最小の範囲をgateway.config.upstream.allowed_ipsに書きます。落とし穴になるのは、allowed_ipsが既定のRFC1918を置き換えることです。拡張ではないので、ほかのMCPサーバーが私設アドレスにあるなら、その範囲も一緒に列挙します。

gateway:
  config:
    upstream:
      allowed_ips:
        - 10.0.0.0/8
        - 172.16.0.0/12
        - 192.168.0.0/16
        - 203.0.113.0/24   # 例: 自クラスタのService CIDRに置き換える

0.0.0.0/0にするとSSRF対策が丸ごと無効になるため、ローカルでの試験以外では使いません。

まとめ

Helmで詰まるのは、Audienceの文字列、allowed_ipsが既定を置き換える点、手動フローの証明書更新の3か所です。運用の手間を減らしたいなら、トークンはtokenVersionとforceアップグレード、証明書は日次のCronJobで回せるプログラム的アクセスを選ぶ、という分かれ道になります。

この記事を共有:XはてブLinkedIn
MCP をもっと見る →