Google Cloud Storage をロードバランサーのバックエンドにしつつ、直接はアクセスさせたくない場合
こんにちは、Google Cloud でプリセールスエンジニアをしている有賀です。
TL; DR
- 表題に関して、いい感じの方法はありません 😥
- 強いて言えば(VM なりサーバーレスサービスなりで作った)プロキシを使うことで実現できます
- ロードバランサーへのアクセス自体も制限してよければ VPC Service Controls も選択肢に入ります
目次
- 問題定義
- Compute Engine、Cloud Run などをプロキシにした IAM アカウントベースのアクセス制限
- Cloud Storage 閲覧用サービスアカウントの作成
- Cloud Storage バケットとオブジェクトの用意
- ファイアウォール ルールでロードバランサーからの VM へのアクセスを許可
- gcsfuse を使う方法
- NGINX をプロキシとして使う方法
- gcs-proxy-cloud-run を使う方法
- Envoy Proxy を使う方法
- VPC Service Controls による IP アドレスベースのアクセス制限
- まとめ
問題定義
以前から比較的頻繁にいただく質問が表題の「Google Cloud Storage を(一般公開してる)ロードバランサーのバックエンドにしつつ、でも直接 Cloud Storage へは直接アクセスして欲しくないんだけどどうすればいいの?」というご質問です。
ちなみに AWS ですと CDN サービスである CloudFront のオリジンとして S3 を指定し、S3 へのアクセスをオリジンアクセスアイデンティティで制限する方法がオフィシャルドキュメントにも載っています。
じゃあ、Google Cloud でも簡単にできるかというと…残念ながら現時点では簡単というかスッキリした方法はありません。
そこで本記事ではワークアラウンドを紹介してみたいと思っています。
と、その前に状況を整理します。
Google Cloud ではロードバランサーと CDN サービス(Cloud CDN)が統合されているので、ロードバランサーのバックエンドとして Cloud Storage を設定する形になります。
通常の VM 等をバックエンドにする場合は、上図の通り、Cloud Storageと並列するイメージになります。ちなみに図中の Cloud Armor は WAF/DDoS 対策のサービスです。
さてドキュメントを読むと、VM をバックエンドにする場合は、VM 側でロードバランサーからのアクセス(IP アドレスの範囲)をファイアウォールで許可する必要があります。
じゃあ、同じように Cloud Storage バケットでロードバランサー(の IP アドレス)からのアクセスのみを許可し、他からのアクセスを不許可にする設定ができればよさそうです。しかし、Cloud Storage は IAM 認証(ユーザーベース)や、署名付き URL ・クッキー等によるアクセスコントロールはできますが、残念ながら IP アドレスでアクセスをコントロールする方法は用意されていません。
(2022–03–30 追記:よく考えると、もしできたとしても、単にロードバランサーの IP アドレスからのアクセスのみに限定するんじゃダメですね。Google Cloud ではロードバランサー自体は全ユーザーで共用されてる(アドレス帯は同じになる)ので、IP アドレスでの制限だけだと、あるユーザーがロードバランサー作って、バックエンドに他人の Cloud Storage バケットを指定したらアクセスできることになっちゃいます。)
次に思いつく方法は、ロードバランサーに何らかの IAM アカウントを設定して、そのアカウントだけ Cloud Storage バケットにアクセスできるようにし、他からのアクセスを不許可にする設定をすればよさそうです。しかし、ロードバランサーには Cloud Storage へアクセスする際の IAM アカウントを設定する方法は無く、実際ドキュメントにもバックエンドとして設定する場合は「Cloud Storage バケットを一般公開する」よう書かれています。バケットが一般公開されていると、URL さえ分かれば誰でも(認証なしで)直接アクセスできてしまいます。
というわけで、やっぱり一筋縄ではいかないことが分かりました。 😣
ちなみに、一般公開された Cloud Storage オブジェクトの URL はバケット名+オブジェクト名から成る( https://storage.googleapis.com/[バケット名]/[オブジェクト名] )ので、バケット名を十分長くランダムにする(63文字まで使えます)ことでアクセスされにくくするというのも一つの方法と言えるかとも思います。
たとえば、ロードバランサー経由の場合は、 https://www.example.com/image/flower.jpg という URL だけど、Cloud Storage に直接アクセスする場合は、 https://storage.googleapis.com/iajzghueng388jendkjgisakikanzjmbv548/image/flower.jpg という URL を推測しないといけない、みたい感じです。
が、今回はさておきます。
では、次にワークアラウンドを見てみましょう。
考え方としてはいずれにせよ上に挙げた通り、IAM アカウントベースのアクセス制限か、IP アドレスベースのアクセス制限になりますが、Cloud Storage 単体では実現できないので、他のサービスを併用することにします。
- Compute Engine、Cloud Run などをプロキシとして使った、IAM アカウントベースのアクセス制限
- VPC Service Controls による IP アドレスベースのアクセス制限 (ただし、「一般公開してるロードバランサー」という要件を満たさないので、実はワークアラウンドとは言ません。)
なお、Cloud Storage をバックエンドにしたい理由の一つとしてスケーリングを気にしなくていい、という点が挙げられると思います。その点は前者の「プロキシを使う方法」ではプロキシ自体のスケーリングを考えなければいけなくなり、そもそもの目的(ie. スケーリングを気にしない)を達成できていないとも言えるかもしれません 😓 …が、一応ご紹介します。
Compute Engine、Cloud Run などをプロキシにした IAM アカウントベースのアクセス制限
先述の通り Cloud Storage は IAM 認証ベースのアクセスコントロールはできる(けどロードバランサーからそれを使う方法がない)ので、何らかの IAM アカウントが使えるものをプロキシとして使って、Cloud Storage 側ではそのアカウントからのアクセスだけ許可する、という方法が考えられます。
ここで「IAM アカウントが使える」の意味合いですが、以下の通りになります。
- Compute Engine VM や Cloud Run サービスなどに IAM アカウントとしてサービスアカウントを設定できる
- Compute Engine VM や Cloud Run サービス内から Cloud Storage へ API アクセスする際に、設定されたサービスアカウントの認証情報を追加できる
特に 2. に関しては Cloud SDK を使ってアクセスする場合は勝手に認証情報を追加してくれるので気にしないで大丈夫ですが、たとえば VM 内から curl でアクセスする場合や、NGINX 等をプロキシにする場合は、認証情報を追加する何らかの方法を考えないといけません。
また、Cloud Storage の場合、API の認証方法として 2 つあります。
- OAuth 2.0 のトークンを Authorization ヘッダで使う方法
- HMAC (Hash-based Message Authentication Code) キーを使う方法 (Amazon S3 の認証方法と互換)
前者はトークンに有効期限があるので更新されたトークンを使う方法を考える必要があり、後者はその必要が無いものの、鍵の管理をより厳しくおこなう必要があります。
というわけで、まとめると以下のような選択があります。
- プロキシを動かす場所:Compute Engine VM、Cloud Run サービス など
- 利用する認証情報:OAuth 2.0 トークン、HMAC キー
上記を前提とした上で、使えるソフトウェアをいくつか挙げてみます。
注意:本ブログ記事はあくまで技術的な選択肢をご紹介しているに過ぎず、その動作を保証するものではないことにご留意ください。
- Cloud Storage 用の FUSE (Filesystem in Userspace) の実装である https://github.com/GoogleCloudPlatform/gcsfuse を使って Cloud Storage のバケットをマウントし、ウェブサーバーで提供する方法。サービスアカウントの認証情報を透過的に使ってくれる。
- NGINX をリバースプロキシとして使い、NGINX から Cloud Storage にアクセスする際、手動でサービスアカウントから必要な認証情報を追加する設定をおこなう方法。
- 内部的に Cloud SDK を使った、Go 言語製の Cloud Storage へのアクセスプロキシ https://github.com/domZippilli/gcs-proxy-cloud-run を使う方法。サービスアカウントの認証情報を透過的に使ってくれる。
- Envoy の S3 へのアクセス機能を使って Cloud Storage へアクセスする。設定には s3-authn-proxy(実体は HTTP フィルタを使った Envoy)の設定を流用。S3 へのアクセスプロキシなので HMAC キーを認証に使う。( s3-authn-proxy は https://github.com/GoogleCloudPlatform/cdn-auth-proxy の一部)
絵にするとこんな感じです。
1 〜 4 のいずれも Compute Engine VM、Cloud Run サービスのどちらでも動作させることが可能です。本記事では分かりやすさを優先して、Compute Engine VM で動かしてみます。
ちなみに 2. の方法を考えてくださった shin5ok さんが Cloud Run で動かす方法を「Cloud Run で サーバーレス GCS Proxy」という記事で紹介してます。合わせてお読みください。
まず初めに、1 〜 3 の方法で必要となる Cloud Storage を閲覧するためのサービスアカウントを準備します。
Cloud Storage 閲覧用サービスアカウントの作成
まずは Cloud Console の ≡ メニューから「IAM と管理 > サービスアカウント」を選択します。
上部の「+ サービスアカウントを作成」をクリック。サービスアカウント名としてたとえば「cloud-storage-reader」を設定します。
ロールとして「Storage オブジェクト閲覧者」(Storage Object Viewer)を設定します。
必要に応じてこのサービスアカウント自体へのアクセス権限を設定します。(ここでは特に設定しません)
以上で Cloud Storage を閲覧できる権限(Storage Object Viewer)を持ったサービスアカウントができました。
Cloud Storage バケットとオブジェクトの用意
テストとして使う Cloud Storage バケットとオブジェクトも用意しておきます。
$ gsutil mb gs://agira-private-bucket
Creating gs://agira-private-bucket/...$ echo private | gsutil cp - gs://agira-private-bucket/private.txt
Copying from <STDIN>...
/ [1 files][ 0.0 B/ 0.0 B]
Operation completed over 1 objects.
agira-private-bucket
という Cloud Storage バケットに、”private” と書かれた private.txt
というオブジェクトを保存しました。
ファイアウォール ルールでロードバランサーからの VM へのアクセスを許可
Compute Engine VM でロードバランサーからのアクセスを受ける場合はファイアウォールルールで許可しておく必要があるので、ドキュメントにしたがって、130.211.0.0/22
と 35.191.0.0/16
からのアクセスを許可しておきます。
事前準備が整ったので、以下の順番で紹介します。
- gcsfuse を使う方法
- NGINX をプロキシとして使う方法
- gcs-proxy-cloud-run を(VMで)使う方法
- Envoy Proxy を使う方法
gcsfuse を使う方法
初めに gcsfuse を使う方法を紹介します。この方法は設定自体は一番簡単かもしれません。
ちなみに Cloud Run でも gcsfuse を使って同じことができます。
まず、VM の作成画面の「ID と API へのアクセス」の項目で、先に作ったサービスアカウント(cloud-storage-reader
)を選択して VM を起動します。
起動した VM にログインしたら、gcsfuse のインストール方法にしたがってソフトウェアをインストールします。本記事では Debian を例にします。(ついでに NGINX もインストールしておきました)
$ export GCSFUSE_REPO=gcsfuse-`lsb_release -c -s`$ echo "deb http://packages.cloud.google.com/apt $GCSFUSE_REPO main" | sudo tee /etc/apt/sources.list.d/gcsfuse.list
deb http://packages.cloud.google.com/apt gcsfuse-buster main$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
OK$ sudo apt-get update
$ sudo apt-get install -y gcsfuse nginx
これで準備が整ったので、実際に Cloud Storage バケットをマウントして、ウェブサーバー経由でのアクセスを試します。試した環境では NGINX のデフォルトの root ディレクトリが /var/www/html
なので、そこにマウントしてみます。
マウントの前に、NGINX プロセスが読み書きできるように、ディレクトリの所有者を NGINX が使ってるアカウントに変更し、元からある余計なファイルを削除しておきます。
$ sudo chown www-data:www-data /var/www/html
$ sudo rm /var/www/html/index.nginx-debian.html
先に作ったテスト用のバケット agira-private-bucket
をマウントしてみます。(NGINX が使ってるアカウント www-data
でマウントします)
$ sudo -u www-data gcsfuse agira-private-bucket /var/www/html
2022/02/21 05:20:03.396027 Start gcsfuse/0.40.0 (Go version go1.17.6) for app "" using mount point: /var/www/html
2022/02/21 05:20:03.411399 Opening GCS connection...
2022/02/21 05:20:03.771988 Mounting file system "agira-private-bucket"...
2022/02/21 05:20:03.787737 File system has been successfully mounted.
うまくマウントできたようなので、ローカルのウェブサーバー経由でアクセスしてみます。
$ curl http://127.0.0.1/private.txt
private
無事 Cloud Storage にあるオブジェクトにアクセスできました。
上の例で gcsfuse がマウントする際、Cloud Storage の認証情報を明示的には提供していないですが、サービスアカウントが提供する認証情報を内部的に使ってくれるので、ユーザーとしては意識的に設定する必要はありません。
なお、オブジェクトストレージを FUSE を使ってマウントする場合、パフォーマンスが気になりますが、gcsfuse のページにパフォーマンスに関するセクションがありますので参考にしてみてください。
あとは、この VM をロードバランサーのバックエンドサービスのバックエンドとして設定すれば、Cloud Storage バケット自体は非公開のままでコンテンツを公開することができます。( Managed Instance Group で使えるように Instance Template に設定する場合は、Startup script でソフトウェアのインストール等を実施します)
ロードバランサーの設定はたとえば「シンプルな外部 HTTP ロードバランサの設定」を参照してください。設定の確認画面の例はこんな感じです。
割り当てられたロードバランサーの IP アドレス経由でも念のためアクセスしてみます。
$ curl http://34.117.47.245/private.txt
private
当然ですがアクセスできました。
というわけで、比較的簡単な設定で非公開バケットをロードバランサー経由で公開することができました。(各 VM は状態を持たないので、スケールアウトさせるのも簡単です。)
NGINX をプロキシとして使う方法
次に NGINX をプロキシとして使う方法を紹介します。一つ目の gcsfuse を使った方法もある意味 NGINX をプロキシとして使ってるとも言えますが、こちらの方法では gcsfuse のような追加のソフトウェアを使わずに実現します。
まず、gcsfuse を使った方法と同じように、サービスアカウント(cloud-storage-reader
)を設定した VM を起動します。
できあがった VM から Cloud Storage にアクセスするにはサービスアカウントの認証情報を使う必要があります。初めに curl コマンドを使って試してみます。
ちなみに、Cloud Storage の API エンドポイントにはいくつかの形式がありますが、以下では XML API のパス形式を利用しています。
まず「 agira-private-bucket
という Cloud Storage バケットにおいたprivate.txt
というオブジェクト」は XML API のパス形式では https://storage.googleapis.com/agira-private-bucket/private.txt になります。この URL に VM から curl でアクセスしてみます。
$ curl https://storage.googleapis.com/agira-private-bucket/private.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>Anonymous caller does not have storage.objects.get access to the Google Cloud Storage object.</Details></Error>
何やらエラーになりました。Anonymous caller
に権限が無いというエラーのようです。VM には cloud-storage-reader
というサービスアカウントを設定してるのに、それが使われてなさそうです。
ちなみに Cloud SDK に含まれる gsutil コマンドでアクセスしたらどうでしょう?
$ gsutil cp gs://agira-private-bucket/private.txt -
private
こちらは普通にアクセスできました。違いはなんでしょう?
…ともったいぶった書き方をしましたが、そもそも上の方で「Cloud SDK を使ってアクセスする場合は勝手に認証情報を追加してくれるので気にしないで大丈夫ですが、たとえば VM 内から curl でアクセスする場合や、NGINX 等をプロキシにする場合は、認証情報を追加する何らかの方法を考えないといけません」と書いてました。
というわけで、curl コマンドを使う時に認証情報を追加する方法をドキュメントで確認すると、gcloud auth application-default print-access-token
が使えるとあるので、その通り試してみます。
$ curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" https://storage.googleapis.com/agira-private-bucket/private.txt
private
今度は無事アクセスできました。通常の HTTP リクエストの場合も、 Authorization ヘッダを追加し、値として Bearer トークンを設定すればよさそうです。
ということは、本題の「NGINX をプロキシとして使う方法」を実現するには、NGINX から Cloud Storage API にアクセスする際、Authorization ヘッダを追加する方法を考えればよさそうです。
いくつか方法はあると思いますが、Lua モジュールを使うのは簡単な方法の一つかなと思います。
そこで早速、NGIINX と Lua モジュールをインストールして設定します。
$ sudo apt install -y nginx libnginx-mod-http-lua
試してる環境(Debian)では NGINX の設定ファイルは/etc/nginx/sites-enabled/default
にあります。デフォルトでは以下のようになってる server { location {) }
を、
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
server_name _; location / {
try_files $uri $uri/ =404;
}
}
次のように書き換えます。
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
server_name _; location / {
proxy_pass https://agira-private-bucket.storage.googleapis.com;
access_by_lua_block {
local file = io.open("/tmp/token")
io.input(file)
local data = io.read()
io.close()
ngx.req.set_header("Authorization", "Bearer "..data);
}
}
}
上の
https://(バケット名).storage.googleapis.com/(オブジェクト名)
というのは XML API のバーチャルホスト形式です。
できたら設定を再読み込みします。
$ sudo systemctl reload nginx
上記 Lua スクリプトが読む /tmp/token
に Bearer トークンを保存します。
$ gcloud auth application-default print-access-token > /tmp/token
これで準備が整いました。実際にアクセスしてみましょう。
$ curl http://127.0.0.1/private.txt
private
無事読み込めました。(念のためロードバランサー経由でも試してみてください。問題なくアクセスできるはずです。)
ただ、このままだとトークン /tmp/token
の有効期限(デフォルトでは 1 時間)が切れたらアクセスできなくなっちゃうので、cron 等でトークンを更新する必要があります。(本記事では省略します)
ちなみに Bearer トークンをファイルに保存する代わりに、Compute Engine VM のメタデータサーバーから都度取得する方法もあります。サービスアカウントを設定した VM ではメタデータサーバーからトークンを取得できるので、それをそのまま利用できます。
一つの例として以下のような設定で試せます。(access_by_lua_block {}
を書き換えます)
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
server_name _; location / {
proxy_pass https://agira-private-bucket.storage.googleapis.com;
access_by_lua_block {
local json = require("json")
local ltn12 = require("ltn12")
local http = require("socket.http")
local res = {}
http.request {
url = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token",
headers = { ["Metadata-Flavor"] = "Google" },
sink = ltn12.sink.table(res)
}
body = table.concat(res)
parsed = json.decode(body)
ngx.req.set_header("Authorization", "Bearer "..parsed["access_token"])
}
}
}
設定を書き換えたら設定を再読み込みし、必要となる Lua モジュールを追加でインストールしてから、curl でアクセスしてみます。
$ sudo systemctl reload nginx$ sudo apt install -y lua-json lua-socket$ curl http://127.0.0.1/private.txt
private
無事読み込めました。
トークンをファイルに保存する必要が無いので楽ですが、一方、アクセスのたびにメタデータサーバーにアクセスしなければならず、ファイルに保存されたトークンを読み込むよりはオーバヘッドが高いと思います。
最後に VM をロードバランサーのバックエンドとして設定する方法は gcsfuse の場合と同じなので省略します。
以上が NGINX をプロキシとして使う方法になります。gcsfuse を使う方法と比べて追加の設定は必要になりますし、トークンの扱いを検討しないといけないですが、一方、依存するソフトウェアが NGINX だけで済む点は利点と言えるかもしれません。
gcs-proxy-cloud-run を使う方法
次に gcs-proxy-cloud-run ( https://github.com/domZippilli/gcs-proxy-cloud-run )を使った方法を紹介します。(gcs-proxy-cloud-run 自体はここで作者自ら紹介してます。)
こちらは Cloud Storage へのアクセスを書き換えたりと色々できますが、本記事では Cloud Storage へ IAM アカウントを付与してアクセスしてくれる単なるプロキシとして利用します。
なお名前に -cloud-run
と入っていますが、Go 言語で書かれたプロキシをコンテナ化して動かしてるだけなので、Compute Engine VM とかでも普通に動きます。というわけで、本記事では VM で動かしてみます。
また、コンテナの作成・実行に当たっては VM に Docker をインストールしてビルド、実行、というのでもいいのですが、せっかく Google Cloud を使っているので、Cloud Build でコンテナを作成し、Container-Optimized OS を使った VM でコンテナを実行してみようと思います。
まずは Cloud Build を実行する用に VM を実行します。本来は適切なロールを設定したサービスアカウントを使うべきですが、ここでは簡単にするために Compute Engine デフォルトのサービスアカウントで「すべての Cloud API に完全アクセス権を許可」を選んで実行します。
また、初めて Cloud Build を使う場合は API を有効にします。次に実行した VM にログインし、ビルドしていきます。
$ sudo apt install -y git$ git clone https://github.com/domZippilli/gcs-proxy-cloud-run$ cd gcs-proxy-cloud-run$ ./build.sh(...)
Container image built:
gcr.io/project-non-vpcsc/gcs-streaming-proxy
$
(ちなみに build.sh は中で gcloud builds submit
をしてるだけです。)
以上のステップでコンテナができて https://gcr.io/(プロジェクト名)/gcs-streaming-proxy に登録されました。( gcr.io は Container Registry サービスのドメインです。)
ではこのコンテナを Compute Engine VM で実行してみます。Compute Engine ではコンテナを実行する用の VM イメージ (Container-Optimized OS)が用意されており、VM の実行時に Cloud Console から直接実行するコンテナを選ぶこともできます。
VM 作成の画面にある「コンテナ」のセクションで、上で作ったコンテナイメージを選択します。
gcs-proxy-cloud-run は環境変数 BUCKET_NAME
でアクセスする先のバケット名を指定します。また、NGINX の例とポート番号(TCP:80)を合わせるため、環境変数 PORT
も指定しておきます。(= ロードバランサーがバックエンドにアクセスするのに使うポート)
- コンテナイメージ:
gcr.io/(プロジェクト名)/gcs-streaming-proxy
- BUCKET_NAME:
agira-private-bucket
- PORT:
80
VM が無事起動したらログインし、curl コマンドで確認してみます。
$ curl http://127.0.0.1/private.txt
private
無事読み込めました。後はこれまでと同じように、この VM をロードバランサーのバックエンドとして指定すればロードバランサー経由でアクセスできます。
コンテナが動いてる様子はおなじみの docker コマンドで確認できます。
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d34159932d95 gcr.io/project-non-vpcsc/gcs-streaming-proxy "/server" 3 minutes ago Up 3 minutes klt-instance-5-artn
なお先述の通り、この方法では VM に設定されているデフォルトのサービスアカウントの認証情報を gcs-proxy-cloud-run が Cloud SDK を使って透過的に取得・利用しています。
NGINX によるプロキシに比べ、トークンの扱いを気にしなくていい(Cloud SDK が裏でサービスアカウントから取得してくれる)ので楽ですが、NGINX のような慣れたソフトウェアではなく、普段は使っていないソフトウェアを使わなければならない点が難点と言えば難点かもしれません。
一方、紹介ページにあるように、コンテンツに対してフィルタをかけて変換をしたり、Go 言語による柔軟な拡張が可能なので、そういったことがした場合は非常に便利かと思います。
Envoy Proxy を使う方法
それでは最後に、Envoy Proxy を使う方法を紹介します。
Envoy Proxy の設定は結構複雑ですが、cdn-auth-proxy ( https://github.com/GoogleCloudPlatform/cdn-auth-proxy ) に含まれる s3-authn-proxy の設定を流用することで楽をします。
cdn-auth-proxy 自体を使ったソリューションはこちらのページにまとまっています。
具体的には、s3-authn-proxy が、もともと Envoy Proxy に用意されているAWS へのリクエストに署名をする HTTP フィルタを利用しているので、真似することにします。
s3-authn-proxy から Amazon S3 へアクセスするためには、アクセスキーを生成し、アクセスキー ID とシークレットアクセスキーを設定します。すると、上述の HTTP フィルタが Amazon S3 へのリクエストに適切に署名をしてくれて S3 にアクセスできるようになります。
これをどうやって Cloud Storage に適用するかですが、実は Amazon S3 へのリクエストにおこなわれる署名と、Cloud Storage へのリクエストにおこなわれる署名には互換性があります。
そこで、s3-authn-proxy からのアクセス先を Cloud Storage へ変えるだけで、そのまま認証ありのアクセスが可能になるのです。
ただし、リクエストへの署名に必要な認証情報は上の方の例で使われている Bearer トークン等ではなく、HMAC キー(Hash-based Message Authentication Code)になります。
つまり、Cloud Storage で使える HMAC キーを生成し、それを s3-authn-proxy にアクセスキーID、シークレットアクセスキー相当として渡して、Cloud Storage (のエンドポイント)へアクセスさせることで、認証ありの Cloud Storage バケットへアクセスできるようになります。
それではさっそく HMAC キーを生成してみます。HMAC キーの生成にはサービスアカウントを必要とするので、先に作ったサービスアカウント(cloud-storage-reader
)を使うことにします。
Cloud Console の ≡ メニューから「Cloud Storage > 設定」を選びます。
画面上部の「相互運用性」タブを選び「サービス アカウント HMAC」のセクションで「サービス アカウント用にキーを作成」を選びます。
目的のサービスアカウント cloud-storage-reader
を選んでキーを作成します。
次に生成されたアクセスキーとシークレットを安全に保管するために、Secret Manager を使います。
ちなみに、ここまでで紹介したサービスアカウントを使う方法では、Compute Engine VM から安全に認証情報にアクセスできましたが、HMAC キーを使う方法では、HMAC キー自体が認証情報なので、それを安全に保存する必要があります。
Cloud Console の ≡ メニューから「セキュリティ > Secret Manager」を選択し、「シークレットを作成」を選びます。
「アクセスキー」は「AWS_ACCESS_KEY_ID」として保存します。
「シークレット」は「AWS_SECRET_ACCESS_KEY」として保存します。
無事、アクセスキーもシークレットも安全に保存できました。
次に VM を立ち上げて Envoy Proxy を動かすのですが、その前に VM から Secret Manager にアクセスしてアクセスキーとシークレットを取得できるように、サービスアカウントを作成します。
secret-manager-accessor
という名前でサービスアカウントを作ります。
「Secret Manager のシークレット アクセサー」(Secret Manager Secret Accessor)というロールを付与します。
無事にサービスアカウントができたので、早速 VM を作成します。今作ったサービスアカウントを指定するのを忘れないようにします。
VM ができたらログインしてセットアップを進めます。
まず初めに、Secret Manager からアクセスキーとシークレットを取得して、Envoy Proxy へと環境変数経由で渡してくれる Berglas というオープンソースのソフトウェアをダウンロードしておきます。
$ curl -O https://storage.googleapis.com/berglas/main/linux_amd64/berglas
$ chmod 755 berglas
Berglas から Secret Manager に保存したアクセスキーにアクセスできるか確認しておきます。Berglas からは sm://(プロジェクト名)/(シークレット名)
という形式で Secret Manager へアクセスできます。
$ ./berglas access sm://$(gcloud config get-value project)/AWS_ACCESS_KEY_ID
GOOG1EXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
無事アクセスできたようです。
次に Envoy Proxy を公式のドキュメントにしたがってインストールします。
(...)
$ sudo apt update
$ sudo apt install getenvoy-envoy
最後に s3–authn-proxy が使ってる Envoy Proxy の設定テンプレートを流用します。
$ curl -O https://raw.githubusercontent.com/GoogleCloudPlatform/cdn-auth-proxy/main/s3-authn-proxy/envoy-template.yaml
設定のテンプレート内の変数を実際の値に置き換えて、設定ファイルを作ります。
- $ORIGIN_BUCKET_NAME:
agira-private-bucket
(アクセス先のバケット名) - $ORIGIN_STORAGE_ENDPOINT:
storage.googleapis.com
- $ORIGIN_BUCKET_REGION:
auto
(Cloud Storage の場合はauto
で大丈夫です)
$ export ORIGIN_BUCKET_NAME=agira-private-bucket
$ export ORIGIN_STORAGE_ENDPOINT=storage.googleapis.com
$ export ORIGIN_BUCKET_REGION=auto$ envsubst '$ORIGIN_BUCKET_NAME $ORIGIN_STORAGE_ENDPOINT $ORIGIN_BUCKET_REGION' < envoy-template.yaml > envoy.yaml(ついでに、ポート番号( "socket_address: { address: 0.0.0.0, port_value: 8080 }" )をデフォルトの 8080 から 80 へ変更しておきます。)
設定ファイルができたら、HMAC キーのアクセス先を Berglas 用に環境変数に保存した上で、Envoy Proxy を Berglas 経由で実行します。
$ export AWS_ACCESS_KEY_ID=sm://$(gcloud config get-value project)/AWS_ACCESS_KEY_ID
$ export AWS_SECRET_ACCESS_KEY=sm://$(gcloud config get-value project)/AWS_SECRET_ACCESS_KEY$ sudo -E ./berglas exec -- /usr/bin/envoy --config-path ./envoy.yaml
Envoy Proxy が起動したら別のターミナルからアクセスしてみます。
$ curl http://127.0.0.1/private.txt
private
無事読み込めました。
このように、標準の Envoy Proxy の設定するだけで、Cloud Storage のプロキシとして動かすことができることが分かっていただけたかと思います。
この後は Envoy Proxy が自動的に起動するように設定すればいいのですが、cdn-auth-proxy はコンテナとして動くように書かれており、せっかくなので gcs-proxy-cloud-run と同じように、コンテナとして動かしてみたいと思います。
実は上で手動でやった手順は s3-authn-proxy のビルド手順とまったく同じなので、先に Cloud Build 用に作成した VM にログインして以下の通りビルドしていきます。
$ git clone https://github.com/GoogleCloudPlatform/cdn-auth-proxy
$ cd cdn-auth-proxy/s3-authn-proxy/
$ sed -i.bak -e 's/port_value: 8080/port_value: 80/' envoy-template.yaml$ export PROJECT_ID=$(gcloud config get-value project)
$ export TAG="v1"$ ./build
(...)
gcr.io/project-non-vpcsc/authn-proxy:v1 SUCCESS
コンテナができて https://gcr.io/(プロジェクト名)/authn-proxy に登録されました。( gcs-proxy-cloud-run の時と同じように ./build はほぼ gcloud build submit
をしてるだけです。また、Berglas インストールとかは Dockerfile で指定されてます。)
早速このコンテナを動かします。上の gcs-proxy-cloud-run の例と同じように、ビルドしたコンテナを指定して、必要な環境変数を設定します。
- コンテナイメージ:
gcr.io/(プロジェクト名)/authn-proxy:v1
- ORIGIN_BUCKET_NAME:
agira-private-bucket
- ORIGIN_STORAGE_ENDPOINT:
storage.googleapis.com
- ORIGIN_BUCKET_REGION:
auto
- AWS_ACCESS_KEY_ID:
sm://(プロジェクト名)/AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY:
sm://(プロジェクト名)/AWS_SECRET_ACCESS_KEY
また、VM のサービスアカウントとして secret-manager-accessor
を指定するのですが、「Secret Manager のシークレット アクセサー」ロールだけだとコンテナのイメージをダウンロードできないので、IAM の設定に戻り「Storage オブジェクト 閲覧者」を追加しておきます。
ちなみに、gcr.io の実体が Cloud Storage なので「Storage オブジェクト 閲覧者」が必要になるのです。
逆に、Envoy Proxy から Cloud Storage へのアクセスにあたっては「Storage オブジェクト 閲覧者」は不要です。なぜなら Cloud Storage への認証には HMAC キーを使っており、IAM 認証ではないからです。
VM が無事起動したらログインし、curl コマンドで確認してみます。
$ curl http://127.0.0.1/private.txt
private
無事読み込めました。
何度も繰り返しになりますが、後はこれまでと同じように、この VM をロードバランサーのバックエンドとして指定すればオッケーです。
先に紹介してきたいくつかの方法に比べて、Envoy Proxy という広く使われているソフトウェア(と、Berglas という小粒のツール)だけで実現できるので安心感はあるかもしれません。
一方、認証情報として、他の方法で使ってるサービスアカウントではなく、Cloud Storage の HMAC キーを使っており、Secret Manager に保存してるとはいえ、生の認証情報を保存しておかなければならない点が気になる場合があるかと思います。
というわけで、色々なプロキシを経由して Cloud Storage のバケットにアクセスすることで、バケットを一般公開しないままで、ロードバランサーのバックエンドとして使う方法を紹介しました。
なお、ここまですべて Compute Engine VM での実行をベースにしましたが、初めに書いた通り、Cloud Run での実行も可能なので、VM の管理を避けたい、といった場合は、Cloud Run でサーバーレスで実現できます。
次にもう一つの方法として上げた、VPC Service Controls による IP アドレスベースのアクセス制限を紹介します。
VPC Service Controls による IP アドレスベースのアクセス制限
注意! 先に書いたとおり、こちらの方法ではロードバランサーを一般に公開できないので、そもそも前提条件を満たしておらず、ワークアラウンドとは言えません。
その点を念頭に置きつつ、興味がある方は読み進めてください。
Cloud Storage は API ベースのサービスとなりますが、API ベースのサービスに IP アドレスでのアクセス制限をかける方法として、VPC Service Controls (VPC SC)という機能があります。(ちなみに前提条件として組織(Organization)を使っている必要があります。)
VPC Service Controls を使うと VPC 内から、もしくは指定した IP アドレスからのアクセス以外は、たとえ有効な認証情報を持っていたとしてもアクセスをブロックすることができます。
一般に Cloud Storage や BigQuery などの API サービスへのアクセスは認証情報によって制御することになりますが、一旦、有効な認証情報が漏洩してしまうとどこからでもアクセスできてしまいます。
そこで VPC Service Controls を使うと、認証情報に加えて送信元の情報をアクセス制御に使うことができるようになり、より強固なアクセス制御が可能になります。
さらに外から API サービスへのアクセス制御に加えて、内側(VPC 内の VM)から外へのアクセス制御も可能で、情報の漏洩に対する対応も同時におこなえます。
詳細については下記ページが分かりやすいので、ぜひご参照ください。
- VPC Service Controlsを分かりやすく解説 (G-gen Tech Blog)
- VPC Service Controlsを使ってGoogle Cloud セキュリティ対策を強化しよう! (GCP | 株式会社トップゲート)
さて本題に戻ると、VPC Service Controls を使って Cloud Storage へのアクセス元をロードバランサーからの IP アドレス帯だけに制限できそうな気がします…が、そうは問屋が卸しません。
どう問屋が卸さないか、実際に設定してみてみましょう。設定の概要はこんな感じです。(Compute Engine VM も絵に入れちゃいましたが、実際には Cloud Storage バケットしか設定してません。)
まず初めにアクセスレベル(どこからのアクセスを許可するかの定義)を作成します。35.191.0.0/16 と 130.211.0.0/22 がロードバランサーの IP アドレス帯です。
次に VPC Service Controls のサービス境界を作成します。
以上で VPC Service Controls の設定は終わりです。
なんとなく Cloud Storage へのアクセスをロードバランサーからのアクセスだけに制限できたような気がします。というわけで、実際にアクセスしてみましょう。
新しく project-sandbox-public
というバケットを作り hello.txt
というファイルを置いた上で、一般公開しておきます。
ではまず Cloud Storage に直接アクセスしてみます。
$ curl https://storage.googleapis.com/project-sandbox-public/hello.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>SecurityPolicyViolated</Code><Message>Request violates VPC Service Controls.</Message><Details>Request is prohibited by organization's policy. vpcServiceControlsUniqueIdentifier: _wXl0pCgmFzFfZTLkwnGh2WGaDt9nI3iqLnihOal_q0-nceqXDDmRmA</Details></Error>
エラーになりました。Request is prohibited by organization's policy.
と出力されており、期待通り VPC Service Controls によってブロックされていることが分かります。
ではお待ちかねの、ロードバランサー経由のアクセスを試してみます。(34.111.148.78
というのがテスト用に作ったロードバランサーです。)
$ curl http://34.111.148.78/hello.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>SecurityPolicyViolated</Code><Message>Request violates VPC Service Controls.</Message></Error>
同じくエラーになってしまいました 😰 Request violates VPC Service Controls.
と表示されており、やはり VPC Service Controls によってブロックされたようです。アクセスレベルでロードバランサーの IP アドレスを許可したのに効いてないのでしょうか?
原因を探るためにログを見てみます。
一行目がロードバランサーへのアクセスが HTTP 403 で拒否されたログ。二行目が Cloud Storage API へのアクセスが VPC Service Controls によって拒否されたログとなります。
ここで実際のログを見てもいいのですがちょっと見づらいので、VPC Service Controls Troubleshooter によって見やすくなったものを確認します。(Troubleshooter に必要なエラーの ID は、上記二行目のログ中の vpcServiceControlsUniqueId
というフィールドにあります。)
Troubleshooter の出力を見ると “Violation reason” が “Service Perimeter does not have a matching Access Level” となっており、アクセス元がアクセスレベルで許可されてない(アクセス元に一致するアクセスレベルが無い)のが原因のようです。
ロードバランサーは許可してるはずなのにと思ってアクセス元の情報 callerIp
を見てみると、curl を実行したホストの IP アドレスになってます。実際、試しにこの IP アドレスを上のアクセスレベル allow_load_balancers
に追加するとアクセスできるようになります。(そして当然ではありますが、同時に、当該ホストから Cloud Storage に直接アクセスできるようになります。)
このことから、ロードバランサー経由で Cloud Storage にアクセスする際のアクセス元として VPC Service Controls が評価するのはロードバランサーの IP アドレスではなく、ロードバランサーにアクセスしているクライアントの IP アドレスであることが分かります。
つまり、一般に公開してるロードバランサー経由で Cloud Storage にアクセスできるようにするためには、すべてのクライアントを許可しなければならないですが、そうすると同時にすべてのクライアントは Cloud Storage へ直接アクセスできるようになってしまい、Cloud Storage へは直接アクセスさせない、という当初の目的が果たせません。
というわけで「そうは問屋が卸さない」のです。🤦♂️
なお前提条件を変えて、たとえば開発環境等において、オフィスなどの特定の IP アドレスからのみロードバランサーと Cloud Storage へのアクセスを許可したいという場合(つまり一般のクライアントからはいずれへのアクセスもブロックしたい場合)は、ここで紹介した VPC Service Controls で Cloud Storage を守る方法がぴったりです。
まとめ
以上、大変長くなってしまいましたが、「Google Cloud Storage をロードバランサーのバックエンドにしつつ、直接はアクセスさせたくない場合」に考えられる方法を紹介してみました。
結局のところ(要件を満たしてない VPC Service Controls の例を除いては)Cloud Storage を直接ロードバランサーのバックエンドとして設定できるわけではないので、「Cloud Storage をロードバランサーのバックエンドにしつつ」のところは満たせてない気もしますが、直接ロードバランサーのバックエンド(バケット)として設定するにはバケットを一般公開するしかない現状において、こういったワークアラウンドを活用できるか検討してもらえると嬉しいです。