この記事でやること
Cloud Run のサービスを「公開(未認証アクセス可)」にする方法は、大きく2つに分けられます。
方法①:「そもそも入口で身分証を確認しない」
サービス自体の設定で「認証チェックをしない」と指定するやり方です。誰かに許可を出したわけではないので、IAM ポリシーには何も書かれません。
方法②:「全員に入場許可を出す」
allUsers(=インターネット上の誰でも)に対して、呼び出し権限を与えるやり方です。これまで一般的だった方法で、IAM ポリシーに「allUsers に許可」という記録が残ります。
結果はどちらも「誰でもアクセスできる」で同じです。しかし設定が記録される場所が違います。
自分は allUsers バインディングを検索して公開サービスを洗い出そうとしたのですが、「公開されているはずなのに見つからない」という事態にハマりました、、
原因は方法①でした。IAM ポリシーには何も書かれていないので、いくら allUsers を探しても見つからないわけです。
そこでこの記事では、3つの Cloud Run サービス(公開2種類+非公開1種類)を実際に立てて、それぞれの検出方法で何が拾えて、何が見落とされるのかを検証してみます。
最後に、公開サービスを漏れなく棚卸しするためのコマンドも紹介します。
対象読者
- Cloud Run を運用していて、公開リソースの棚卸しをしている人
-
gcloud assetや IAM ポリシー検索でセキュリティチェックを自動化している人
結論
先に結果を出しておきます。3つのサービスを立てて、各検出手段を試した結果です。
| ① Invoker IAMチェック無効 | ② allUsers に付与 | 非公開 | |
|---|---|---|---|
| 実際のアクセス(無認証) | 200 | 200 | 403 |
| サービスの IAM ポリシー | 空 |
allUsers あり |
空 |
policy:allUsers 検索 |
❌ 検出できない | ✅ 検出できる | — |
| Asset Inventory の検索API | ❌ クエリ不可 | ✅ 検出できる | — |
gcloud asset list + フィルタ |
✅ 検出できる | ✅ 検出できる | — |
| コンソールのセキュリティタブ | ✅ 表示される | ✅ 警告が出る | — |
ポイントは3つです。
- ①と非公開は、IAM ポリシー上まったく同じに見えます(どちらも空)。それでも一方は誰でもアクセスできます
- ①は Google が「推奨」としている方式です。裏技でも設定ミスでもありません
- コンソールを開けば分かります。しかしスクリプトで棚卸ししているなら①は引っかかりません
対策は記事の最後にまとめます。組織ポリシーで禁止することもできます(デフォルトでは有効になっていないはずです)。
Cloud Run の「公開」には2種類ある
まず前提の整理です。公式ドキュメントには、公開サービスを作る方法が2つ挙げられています。
| 方法 | 実体 | |
|---|---|---|
| ① | Invoker IAM チェックを無効化(推奨と明記) | サービス側の設定(invokerIamDisabled) |
| ② |
allUsers に Cloud Run Invoker ロールを付与 |
IAM ポリシーのバインディング |
①はサービス自体の設定で「invoker の IAM チェックをそもそも行わない」ようにするものです。IAM ポリシーには何も追加されません。
そして公式ドキュメントは、こちらを推奨としています。理由も書かれていて、ドメイン制限共有(DRS)の組織ポリシーが適用されているプロジェクトでは、allUsers へのIAM付与自体が失敗するためです。つまり②が使えない環境では①を使うことになります。
①を有効にする方法はいくつかあります。
gcloud run deploy SERVICE_NAME --no-invoker-iam-check
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
annotations:
run.googleapis.com/invoker-iam-disabled: 'true'
Terraform では invoker_iam_disabled 引数として指定できます。
検証環境を作る
3つのサービスを立てます。中身はどれも同じ hello world です。
gcloud run deploy pub-noiam \
--source . \
--region asia-northeast1 \
--no-invoker-iam-check \
--quiet
gcloud run deploy pub-allusers \
--source . \
--region asia-northeast1 \
--allow-unauthenticated \
--quiet
gcloud run deploy private-svc \
--source . \
--region asia-northeast1 \
--no-allow-unauthenticated \
--quiet
デプロイ時の出力に既に差が出ています。②だけ ✓ Setting IAM Policy... というステップがあり、①にはありません。①がIAMを触っていないことが、ここでも分かります。
まず実態を確認する
3つのサービスについて、サービス設定・IAMポリシー・実際のHTTPアクセスを並べて見ます。
for s in pub-noiam pub-allusers private-svc; do
echo "===== $s ====="
curl -sS -H "Authorization: Bearer $(gcloud auth print-access-token)" \
"https://run.googleapis.com/v2/projects/${PROJECT_ID}/locations/asia-northeast1/services/${s}" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('invokerIamDisabled:', d.get('invokerIamDisabled', False))"
echo "-- IAM policy --"
gcloud run services get-iam-policy "$s" --region asia-northeast1 --format="value(bindings.members)" 2>&1 | head -3
url=$(gcloud run services describe "$s" --region asia-northeast1 --format="value(status.url)")
echo "-- HTTP (no auth): $(curl -s -o /dev/null -w '%{http_code}' $url)"
done
===== pub-noiam =====
invokerIamDisabled: True
-- IAM policy --
-- HTTP (no auth): 200
===== pub-allusers =====
invokerIamDisabled: False
-- IAM policy --
['allUsers']
-- HTTP (no auth): 200
===== private-svc =====
invokerIamDisabled: False
-- IAM policy --
-- HTTP (no auth): 403
ここが出発点です。pub-noiam と private-svc は、IAM ポリシーの見え方が完全に同一(どちらも空)。それでも一方は 200、もう一方は 403 です。
IAM ポリシーだけを見ている限り、この2つは区別できません。
検出手段を1つずつ試す
① IAMポリシー検索(policy:allUsers)
公開リソースの棚卸しで、一般的な手法だと思います。
gcloud asset search-all-iam-policies \
--scope="projects/${PROJECT_ID}" \
--query="policy:allUsers" \
--format="table(resource)"
RESOURCE
//run.googleapis.com/projects/PROJECT_ID/locations/asia-northeast1/services/pub-allusers
pub-allusers しか出てきません。 pub-noiam は同じく誰でもアクセスできる状態なのに、1件も引っかかりません。
そもそもIAMポリシーに何も書かれていないので、当然といえば当然です。しかしこの方法を棚卸しに使っている場合、公開サービスの見落としが起きます。
② Asset Inventory のリソース検索
では、リソース側の構成を見ればどうか。
gcloud asset search-all-resources \
--scope="projects/${PROJECT_ID}" \
--asset-types="run.googleapis.com/Service" \
--read-mask="name,versionedResources" \
--format="json" | python3 -c "
import json,sys
for r in json.load(sys.stdin):
s = json.dumps(r.get('versionedResources',''))
print(r.get('name','').split('/')[-1], '->', 'あり' if 'invokerIamDisabled' in s else 'なし')
"
private-svc -> なし
pub-allusers -> なし
pub-noiam -> なし
出てきません。ならばクエリで直接指定できないか、と試すとこうなります。
gcloud asset search-all-resources \
--scope="projects/${PROJECT_ID}" \
--asset-types="run.googleapis.com/Service" \
--query="run.googleapis.com/invoker-iam-disabled:true"
ERROR: (gcloud.asset.search-all-resources) INVALID_ARGUMENT: Unsupported field:
'run.googleapis.com/invoker-iam-disabled'. Supported fields include:
'name, displayName, description, location, networkTags, project, folders,
organization, ... labels, kmsKey, createTime, updateTime, state, tagKeys, ...'
サポート外のフィールドとして明示的に拒否されます。 labels や tagKeys は検索できるのに、この値は検索できません。
つまり Asset Inventory の検索APIでは、この条件で絞り込むことができないわけです。
③ gcloud asset list なら痕跡がある
一方、リソースの完全な構成を取得する gcloud asset list では話が変わります。
gcloud asset list \
--project="$PROJECT_ID" \
--asset-types="run.googleapis.com/Service" \
--content-type=resource \
--format="json" | python3 -c "
import json,sys
for a in json.load(sys.stdin):
d = a.get('resource',{}).get('data',{})
ann = d.get('metadata',{}).get('annotations',{})
print(a['name'].split('/')[-1], '| annotation:',
ann.get('run.googleapis.com/invoker-iam-disabled','(なし)'))
"
private-svc | annotation: (なし)
pub-allusers | annotation: (なし)
pub-noiam | annotation: true
pub-noiam だけ true が入っています。
検索APIでは引けませんが、全件取得してクライアント側でフィルタすれば検出できます。
④ コンソールは正しく表示される
念のためコンソールも確認しました。結論から言うと、コンソールは両方式とも正しく警告してくれます。
まず pub-noiam。「パブリック アクセスを許可」が選択された状態で、「認証チェックは実行されません」と説明されています。
比較用に、非公開の private-svc はこうなります。「認証が必要」が選択され、IAM にチェックが入っています。
そして pub-allusers。ラジオボタンとしては private-svc と同じ「認証が必要」が選択されています(invokerIamDisabled が false なので)。ただし黄色の警告バナーが出て、allUsers に権限が付与されているため一般公開されている旨が明示され、修正用のボタンまで用意されています。
つまりコンソールを1つずつ開けば、どちらの方式でも気づけます。
問題は、サービスが数十個ある環境でそれを毎回やるのかという話です。棚卸しをスクリプト化しているなら、①の検出は前述のとおり工夫が必要になります。
漏れなく棚卸しするには
2つの方式を両方カバーする必要があります。
① Invoker IAMチェック無効のサービスを洗い出す
gcloud asset list \
--project="$PROJECT_ID" \
--asset-types="run.googleapis.com/Service" \
--content-type=resource \
--format="json" | python3 -c "
import json,sys
hits=[]
for a in json.load(sys.stdin):
d=a.get('resource',{}).get('data',{})
ann=d.get('metadata',{}).get('annotations',{})
if ann.get('run.googleapis.com/invoker-iam-disabled')=='true':
hits.append(a['name'].replace('//run.googleapis.com/',''))
print('=== Invoker IAMチェックが無効なサービス ===')
[print(' -',h) for h in hits] if hits else print('(なし)')
"
=== Invoker IAMチェックが無効なサービス ===
- projects/PROJECT_ID/locations/asia-northeast1/services/pub-noiam
組織全体を対象にするなら --project を --organization=ORGANIZATION_ID に変えます。
② allUsers で公開されているサービスを洗い出す
こちらは従来どおりです。
gcloud asset search-all-iam-policies \
--scope="projects/${PROJECT_ID}" \
--query="policy:allUsers" \
--format="table(resource)"
この2つを両方回して、はじめて公開サービスの全体像が掴めます。
組織ポリシーで禁止することもできる
そもそも①を使わせない、という選択肢もあります。公式ドキュメントによれば、constraints/run.managed.requireInvokerIam というマネージド制約で、Invoker IAM チェックの無効化を制限できます。
ただしこの制約はデフォルトでは適用されていません。明示的に有効化する必要があります。
お片付け
検証で作ったサービスを消しておきます。
for s in pub-noiam pub-allusers private-svc; do
gcloud run services delete "$s" --region asia-northeast1 --quiet
done
まとめ
- Cloud Run の公開方式は2つあり、Invoker IAM チェックの無効化(Google推奨)はIAMポリシーを変更しない
- そのため
policy:allUsersを検索する棚卸しでは検出できない。Asset Inventory の検索APIでもクエリできない -
gcloud asset listで全件取得し、run.googleapis.com/invoker-iam-disabledアノテーションを見れば検出できる - コンソールは両方式とも正しく警告してくれる。盲点になるのはスクリプトによる棚卸し
- 予防するなら
constraints/run.managed.requireInvokerIam(デフォルト無効)
「公開リソースを洗い出す」処理を書いている場合は、allUsers の検索だけで済ませていないか確認してみてください。


