VPN and Bucket IP Filtering

VPN と Bucket IP Filtering

English follows Japanese.

本記事の目的

本記事の目的は2つある。

副次的な目的は、2プロジェクト間の Cloud VPN の構成を、gcloud コマンドのみで行う例を示すことである。簡単に検索した限りでは、Google Cloud の公式文書(ただし、同一プロジェクト内)以外に、Web UI を使った構成例はあったが、gcloud コマンドを使った構成例は見つからなかった。

もうひとつの目的は、Cloud Storage の新機能である Bucket IP Filtering を、VPN 接続の接続先からアクセスできるか検証することである。

結論: VPN 接続の接続先からアクセスできる。これはいい。

本記事は、Google Cloud の基本的な VPC, IAM, Cloud Storage の知識があるネットワークエンジニアやクラウドアーキテクトを対象としている。

2プロジェクト間の VPN については、Google Cloud の公式ドキュメント以外の情報が見つからなかったため、gcloud コマンドを使った構成例を示す(対向が、オンプレミス ルーターの例は、当たり前だが結構あった)。

Bucket IP Filtering とは

Cloud Storage の Bucket IP filtering 機能は、Cloud Storage のバケットに対して、特定の IP アドレス範囲からのアクセスのみを許可するような、アクセス制御機能であるバケット IP フィルタリング
2024-11-14に Public Preview としてリリースされている。

VPN で使える?

ある会社が「社内からしか見られない」ような「汎用のファイル置き場」を作りたいと考えたとき、Cloud Storage の Bucket IP filtering 機能が役に立つかもしれない。

Global IP を CIDR range で指定できるため、各拠点に固定 IP を持たせている場合には、Cloud Storage の Bucket IP filtering 機能を使うことで、特定の拠点からのアクセスのみを許可することができる。

  "publicNetworkSource": {
    "allowedIpCidrRanges": [
      "${YOUR_IP}/32"
    ],
  }

もうひとつ、良くある Google Cloud の利用の仕方としては、VPN の構成があげられるだろう。Cloud Storage バケットが、VPN 接続の接続先だけからアクセス可能だとすると、他に考えることがなくなるため、非常に便利だ。

オンプレミス ホストの限定公開の Google アクセスを構成するには、

オンプレミス ホスト用の限定公開の Google アクセスを使用すると、Cloud VPN トンネルまたは Cloud Interconnect 用の VLAN アタッチメントを経由してトラフィックをルーティングすることにより、オンプレミス システムから Google API とサービスに接続できます。オンプレミス ホスト用の限定公開の Google アクセスは、インターネット経由で Google API とサービスに接続するための代替手段として使用できます。

と記載されている。接続先から private.googleapis.com 経由で Cloud Storage にアクセスするときに、Bucket IP Filtering が利用できるか検証したい。

作業手順

基本的な操作は、すべて gcloud コマンドで行い、コピーするだけで再現できることを目指す。

ネットワークやバケットのリージョンには us-central1 を指定する。

Google Cloud プロジェクト2つに、10.1.0.0/24, 10.2.0.0/24 の VPC ネットワークを作成し、VPN で接続させる。
10.1.0.0/24 がホスト側、10.2.0.0/24 が(Google Cloud 上で検証するが)オンプレミス側と考える。

network-vpn-host 10.1.0.0/24
network-vpn-guest 10.1.1.0/24

環境定義

以下を、再現環境に合わせて定義する。

PROJECT_HOST はバケット ホスト、PROJECT_GUEST はオンプレミス環境と考える。YOUR_IP はアクセスを許可するグローバル IP、YOUR_ACCOUNT は、操作者の Google アカウント、BILLING_ACCOUNT は Google Cloud の課金アカウントを設定する。

SHARED_SECRET は、VPN 接続のための Shared Secret である。

export PROJECT_HOST=host-project
export PROJECT_GUEST=guest-project
export YOUR_IP=203.0.113.1
export YOUR_ACCOUNT=test@example.com
export BILLING_ACCOUNT="000000-000000-000000"

export SHARED_SECRET="bvTtdK7nil68WCYu7+dJ7iYdEmeWCB/A5Q4lUu2DHac="

Google Cloud プロジェクトの作成・API の有効化

for project in $PROJECT_HOST $PROJECT_GUEST; do
  gcloud projects create $project
  gcloud billing projects link $project --billing-account=$BILLING_ACCOUNT
  gcloud services enable compute.googleapis.com --project=$project
done

バケットの作成

ホスト側のプロジェクトに、次のような Cloud Storage バケットを作成する。sa は Service Account の略から来ている。

基本的に allUsers に対して検証できれば、今回の目的は達成されるが、折角なので、Service Account に権限を付与した場合の挙動も確認しておく。あとで ls したいので、ダミーファイルも作成しておく。

バケット名アクセス許可Bucket IP FilteringnetworkIP 範囲
public-2025-05allUsers無効NANA
private-2025-05host プロジェクトのみ無効NANA
ip-vpn-all-2025-05allUsers有効network-vpn-host10.1.0.0/16
ip-vpn-sa-2025-05検証者、host のサービスアカウント有効network-vpn-host10.1.0.0/16
ip-allow-all-2025-05allUsers有効publicNetworkSource, network-vpn-host10.1.0.0/16, YOUR_IP
ip-allow-sa-2025-05検証者、host のサービスアカウント有効publicNetworkSource, network-vpn-host10.1.0.0/16, YOUR_IP

network-vpn-host 10.1.0.0/24
network-vpn-guest 10.1.1.0/24
としたが、「両方」を含む IP 範囲として、10.1.0.0/16 を指定している。

gcloud storage buckets create gs://public-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
gcloud storage buckets add-iam-policy-binding gs://public-2025-05 --member=allUsers --role=roles/storage.objectViewer --project=$PROJECT_HOST

gcloud storage buckets create gs://private-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access

gcloud storage buckets create gs://ip-vpn-all-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
gcloud storage buckets add-iam-policy-binding gs://ip-vpn-all-2025-05 --member=allUsers --role=roles/storage.objectViewer --project=$PROJECT_HOST

gcloud storage buckets create gs://ip-vpn-sa-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
# プロジェクトで権限付与するので、IAM での権限付与は省略

gcloud storage buckets create gs://ip-allow-all-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
gcloud storage buckets add-iam-policy-binding gs://ip-allow-all-2025-05 --member=allUsers --role=roles/storage.objectViewer --project=$PROJECT_HOST

gcloud storage buckets create gs://ip-allow-sa-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
# プロジェクトで権限付与するので、IAM での権限付与は省略

mkdir -p dummy_files
for bucket in public private ip-vpn-all ip-vpn-sa ip-allow-all ip-allow-sa; do
  echo "${bucket}_2025-05.txt" > dummy_files/${bucket}_2025-05.txt
  gcloud storage cp dummy_files/${bucket}_2025-05.txt gs://${bucket}-2025-05
done

VPC ネットワークの作成

PROJECT_HOST に VPC ネットワーク network-vpn-host を作成し、PROJECT_GUEST に VPC ネットワーク network-vpn-guest を作成する。

前述の通り、

network-vpn-host 10.1.0.0/24
network-vpn-guest 10.1.1.0/24

とする。

gcloud compute networks create network-vpn-host --project=$PROJECT_HOST --subnet-mode=custom
gcloud compute networks subnets create subnet-vpn-host \
    --network=network-vpn-host \
    --region=us-central1 \
    --range=10.1.0.0/24 \
    --project=$PROJECT_HOST

gcloud compute networks create network-vpn-guest --project=$PROJECT_GUEST --subnet-mode=custom
gcloud compute networks subnets create subnet-vpn-guest \
    --network=network-vpn-guest \
    --region=us-central1 \
    --range=10.1.1.0/24 \
    --project=$PROJECT_GUEST

Public IP があると、経路が分かりにくい(実際には変わらないはずだが)。
Public IP がなくても、SSH で接続できるように、IAP の設定を行う。

for project in $PROJECT_HOST $PROJECT_GUEST; do
    gcloud projects add-iam-policy-binding $project \
        --member="user:${YOUR_ACCOUNT}" \
        --role=roles/iap.tunnelResourceAccessor
    gcloud projects add-iam-policy-binding $project \
        --member="user:${YOUR_ACCOUNT}" \
        --role=roles/compute.instanceAdmin.v1
done

gcloud compute firewall-rules create allow-ssh-ingress-from-iap-vpn \
    --direction=INGRESS \
    --action=allow \
    --rules=tcp:22 \
    --source-ranges=35.235.240.0/20 \
    --network=network-vpn-host \
    --project=$PROJECT_HOST

gcloud compute firewall-rules create allow-ssh-ingress-from-iap-vpn \
    --direction=INGRESS \
    --action=allow \
    --rules=tcp:22 \
    --source-ranges=35.235.240.0/20 \
    --network=network-vpn-guest \
    --project=$PROJECT_GUEST

また、Public IP がなくても、Google Cloud のサービスにアクセスできるように、限定公開の Google アクセスを構成する。

gcloud compute networks subnets update subnet-vpn-host --project=$PROJECT_HOST \
    --region=us-central1 \
    --enable-private-ip-google-access

gcloud compute networks subnets update subnet-vpn-guest --project=$PROJECT_GUEST \
    --region=us-central1 \
    --enable-private-ip-google-access

# 確認したい場合には以下を実行
# gcloud compute networks subnets describe subnet-vpn-host --project=$PROJECT_HOST \
#     --region=us-central1 \
#     --format="get(privateIpGoogleAccess)"

VM が相互に通信できるように、Firewall Rule を設定する。

gcloud compute firewall-rules create allow-internal-ingress-from-vpn \
    --direction=INGRESS \
    --action=allow \
    --rules=all \
    --source-ranges=10.1.0.0/16 \
    --network=network-vpn-host \
    --project=$PROJECT_HOST

gcloud compute firewall-rules create allow-internal-ingress-from-vpn \
    --direction=INGRESS \
    --action=allow \
    --rules=all \
    --source-ranges=10.1.0.0/16 \
    --network=network-vpn-guest \
    --project=$PROJECT_GUEST

プロジェクト間の VPN の設定

今まで作成したネットワーク network-vpn-host と network-vpn-guest の間に VPN を設定する。

BGP の IPv6 については、省くことにした。

# PROJECT_HOST での VPN 設定
gcloud compute vpn-gateways create vpn-host \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --network=network-vpn-host

gcloud compute routers create cloud-router-host \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --network network-vpn-host \
  --asn 65001

# PROJECT_GUEST での VPN 設定
gcloud compute vpn-gateways create vpn-guest \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --network=network-vpn-guest

gcloud compute routers create cloud-router-guest \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --network network-vpn-guest \
  --asn 65002

表示される IP を控えておく。

export HOST_INTERFACE0=34.153.55.144
export HOST_INTERFACE1=35.220.88.151

export GUEST_INTERFACE0=35.242.99.80
export GUEST_INTERFACE1=34.153.244.91

# テストしていないが、以下で取得できるかも
export HOST_INTERFACE0=$(gcloud compute vpn-gateways describe vpn-host --project=$PROJECT_HOST --region=us-central1 --format='value(vpnInterfaces[0].ipAddress)')
export HOST_INTERFACE1=$(gcloud compute vpn-gateways describe vpn-host --project=$PROJECT_HOST --region=us-central1 --format='value(vpnInterfaces[1].ipAddress)')

export GUEST_INTERFACE0=$(gcloud compute vpn-gateways describe vpn-guest --project=$PROJECT_GUEST --region=us-central1 --format='value(vpnInterfaces[0].ipAddress)')
export GUEST_INTERFACE1=$(gcloud compute vpn-gateways describe vpn-guest --project=$PROJECT_GUEST --region=us-central1 --format='value(vpnInterfaces[1].ipAddress)')
# PROJECT_HOST で、相手方の情報を登録
# guest -> host の VPN の存在登録
# 以下のコマンドを利用したいが、
# > You cannot provide interface with IP address associated with HA VPN gateway of Google Cloud.
# と出るので、`projects/${PROJECT_GUEST}/regions/us-central1/vpnGateways/vpn-guest` を指定する

# gcloud compute external-vpn-gateways create vpngw-guest-host \
#   --project=$PROJECT_HOST \
#   --interfaces=0=${GUEST_INTERFACE0},1=${GUEST_INTERFACE1}

gcloud compute vpn-tunnels create tunnel-host-guest-0 \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --peer-gcp-gateway=projects/${PROJECT_GUEST}/regions/us-central1/vpnGateways/vpn-guest \
  --ike-version=2 \
  --shared-secret=$SHARED_SECRET \
  --router=cloud-router-host \
  --vpn-gateway=vpn-host \
  --interface=0

gcloud compute vpn-tunnels create tunnel-host-guest-1 \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --peer-gcp-gateway=projects/${PROJECT_GUEST}/regions/us-central1/vpnGateways/vpn-guest \
  --ike-version=2 \
  --shared-secret=$SHARED_SECRET \
  --router=cloud-router-host \
  --vpn-gateway=vpn-host \
  --interface=1

# PROJECT_GUEST で、相手方の情報を登録
# host -> guest の VPN の存在登録
# gcloud compute external-vpn-gateways create vpngw-host-guest \
#   --project=$PROJECT_GUEST \
#   --interfaces=0=${HOST_INTERFACE0},1=${HOST_INTERFACE1}

gcloud compute vpn-tunnels create tunnel-guest-host-0 \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --peer-gcp-gateway=projects/${PROJECT_HOST}/regions/us-central1/vpnGateways/vpn-host \
  --ike-version=2 \
  --shared-secret=$SHARED_SECRET \
  --router=cloud-router-guest \
  --vpn-gateway=vpn-guest \
  --interface=0

gcloud compute vpn-tunnels create tunnel-guest-host-1 \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --peer-gcp-gateway=projects/${PROJECT_HOST}/regions/us-central1/vpnGateways/vpn-host \
  --ike-version=2 \
  --shared-secret=$SHARED_SECRET \
  --router=cloud-router-guest \
  --vpn-gateway=vpn-guest \
  --interface=1

BGP の設定

# HOST
gcloud compute routers add-interface cloud-router-host \
  --project=$PROJECT_HOST \
  --interface-name=if-tunnel-host-guest-0 \
  --vpn-tunnel=tunnel-host-guest-0 \
  --region=us-central1 \
  --ip-version=IPV4

gcloud compute routers add-bgp-peer cloud-router-host \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --peer-name=peer-tunnel-host-guest-0 \
  --interface=if-tunnel-host-guest-0 \
  --peer-asn=65002

# AI Suggest below:
# BGP_PEER_IP_HOST_TUNNEL_0=$(gcloud compute routers get-status cloud-router-host --project=$PROJECT_HOST --region=us-central1 --format='flattened(result.bgpPeerStatus[].name,result.bgpPeerStatus[].ipAddress,result.bgpPeerStatus[].peerIpAddress)' | grep tunnel-host-guest-0 | awk '{print $3}')
# BGP_ROUTER_IP_HOST_TUNNEL_0=$(gcloud compute routers get-status cloud-router-host --project=$PROJECT_HOST --region=us-central1 --format='flattened(result.bgpPeerStatus[].name,result.bgpPeerStatus[].ipAddress,result.bgpPeerStatus[].peerIpAddress)' | grep tunnel-host-guest-0 | awk '{print $2}')
gcloud compute routers describe cloud-router-host \
  --project=$PROJECT_HOST \
  --region=us-central1

peerIpAddress を確認し、相手方に想定されている IP を手動で設定する。

# GUEST
gcloud compute routers add-interface cloud-router-guest \
  --project=$PROJECT_GUEST \
  --interface-name=if-tunnel-guest-host-0 \
  --ip-address=169.254.0.250 \
  --mask-length=30 \
  --vpn-tunnel=tunnel-guest-host-0 \
  --region=us-central1 \
  --ip-version=IPV4

gcloud compute routers add-bgp-peer cloud-router-guest \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --peer-name=peer-tunnel-guest-host-0 \
  --interface=if-tunnel-guest-host-0 \
  --peer-asn=65001 \
  --peer-ip-address=169.254.0.249

設定の確認

gcloud compute routers describe cloud-router-host \
  --project=$PROJECT_HOST \
  --region=us-central1

gcloud compute routers describe cloud-router-guest \
  --project=$PROJECT_GUEST \
  --region=us-central1

VM インスタンスの作成

それぞれの VPC ネットワークに VM インスタンスを作成する。no-address を指定して、Public IP を持たない VM インスタンスとしている。

gcloud compute instances create vm-host-vpn \
    --project=$PROJECT_HOST \
    --zone=us-central1-f \
    --machine-type=e2-micro \
    --network-interface=stack-type=IPV4_ONLY,subnet=subnet-vpn-host,no-address \
    --scopes=https://www.googleapis.com/auth/cloud-platform \
    --boot-disk-size 10GB \
    --boot-disk-type pd-standard \
    --image-project debian-cloud \
    --image-family debian-12

gcloud compute instances create vm-guest-vpn \
    --project=$PROJECT_GUEST \
    --zone=us-central1-f \
    --machine-type=e2-micro \
    --network-interface=stack-type=IPV4_ONLY,subnet=subnet-vpn-guest,no-address \
    --scopes=https://www.googleapis.com/auth/cloud-platform \
    --boot-disk-size 10GB \
    --boot-disk-type pd-standard \
    --image-project debian-cloud \
    --image-family debian-12

各 VM インスタンスには、次のようにして SSH 接続できる。

gcloud compute ssh --zone=us-central1-f vm-host-vpn --tunnel-through-iap --project $PROJECT_HOST
gcloud compute ssh --zone=us-central1-f vm-guest-vpn --tunnel-through-iap --project $PROJECT_GUEST

10.1.0.3 がホスト側の VM インスタンス IP、10.1.1.3 がゲスト側の VM インスタンス IP であった。
ゲスト側から ping して、疎通できていれば、問題がない。

ping 10.1.0.3

Bucket IP Filtering の検証

Access Check Part 1

まずは、各 VM から、バケットにアクセスできるかを確認する。ls_all_buckets.sh を各 VM から実行して、バケット一覧を確認する。

for bucket_prefix in public private ip-vpn-all ip-vpn-sa ip-allow-all ip-allow-sa; do
  echo "gs://${bucket_prefix}-2025-05/"
  gcloud storage ls gs://${bucket_prefix}-2025-05/
done

vm-host-vpn:

./ls_all_buckets.sh 
gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
gs://private-2025-05/private_2025-05.txt
gs://ip-vpn-all-2025-05/
gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
gs://ip-vpn-sa-2025-05/
gs://ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
gs://ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt

vm-guest-vpn:

gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [private-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-vpn-all-2025-05/
gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-vpn-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-allow-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.

上記のように、vm-host-vpn からはすべてのバケットにアクセスできるが、vm-guest-vpn からは、public-2025-05ip-vpn-all-2025-05 のみアクセスできる。

Bucket IP Filtering の設定

ip-vpn-all-2025-05 と ip-vpn-sa-2025-05 のバケットに、Bucket IP Filtering を設定する。

cat <<EOF > ip-filter-vpn.json
{
  "mode": "Enabled",
  "vpcNetworkSources": [
    {
      "network": "projects/$PROJECT_HOST/global/networks/network-vpn-host",
      "allowedIpCidrRanges": [
        "10.1.0.0/16"
      ]
    }
  ]
}
EOF

ip-filter-allow.json を作成する。

cat <<EOF > ip-filter-allow.json
{
  "mode": "Enabled",
  "publicNetworkSource": {
    "allowedIpCidrRanges": [
      "${YOUR_IP}/32"
    ]
  },
  "vpcNetworkSources": [
    {
      "network": "projects/$PROJECT_HOST/global/networks/network-vpn-host",
      "allowedIpCidrRanges": [
        "10.1.0.0/16"
      ]
    }
  ]
}
EOF

Bucket IP Filtering を設定する。

for bucket_prefix in ip-vpn-all ip-vpn-sa; do
  gcloud alpha storage buckets update gs://${bucket_prefix}-2025-05 --project=$PROJECT_HOST --ip-filter-file=ip-filter-vpn.json
done

for bucket_prefix in ip-allow-all ip-allow-sa; do
  gcloud alpha storage buckets update gs://${bucket_prefix}-2025-05 --project=$PROJECT_HOST --ip-filter-file=ip-filter-allow.json
done
./configure_ip_filtering.sh.sh 
Updating gs://ip-vpn-all-2025-05/...
  Completed 1
Updating gs://ip-vpn-sa-2025-05/...
  Completed 1
Updating gs://ip-allow-all-2025-05/...
  Completed 1
Updating gs://ip-allow-sa-2025-05/...
  Completed 1

Access Check Part 2

vm-host-vpn:

./ls_all_buckets.sh
gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
gs://private-2025-05/private_2025-05.txt
gs://ip-vpn-all-2025-05/
gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
gs://ip-vpn-sa-2025-05/
gs://ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
gs://ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt

vm-guest-vpn:

./ls_all_buckets.sh
gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [private-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-vpn-all-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-vpn-all-2025-05] (or it may not exist): There is an IP filtering condition that is preventing access to the resource. This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-vpn-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-allow-all-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-allow-all-2025-05] (or it may not exist): There is an IP filtering condition that is preventing access to the resource. This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-allow-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-allow-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.

vm-guest-vpn からは、public-2025-05 のみアクセスできる。それ以外にはアクセスできない。IP filtering の有無で、エラーメッセージが異なっている。

静的ルートの追加

# gcloud compute routes create private-to-host \
#     --project=$PROJECT_GUEST \
#     --network=network-vpn-guest \
#     --destination-range=199.36.153.8/30 \
#     --priority=200 \
#     --next-hop-gateway=projects/$PROJECT_HOST/global/vpnGateways/vpn-host

gcloud compute routes create private-to-host \
    --project=$PROJECT_GUEST \
    --network=network-vpn-guest \
    --destination-range=199.36.153.8/30 \
    --priority=200 \
    --next-hop-vpn-tunnel-region=us-central1 \
    --next-hop-vpn-tunnel=tunnel-guest-host-0

【参考】Cloud Router の設定変更

Cloud Router を使用したオンプレミス ルーティング にあるように、host 側の Cloud Router が、private.googleapis.com ドメインで使用される IP 範囲のルートを guest 側(オンプレミス側)に通知できる。

静的ルートで動作したため、上記通知 (advertise) の手法でも動作すると考えられる。具体的な検証は行っていないため、是非確認してみてほしい。

Access Check Part 3

結論: 適切な設定を加えると、gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txtgs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt に追加でアクセスできるようになる。

/etc/hosts

以下の行を /etc/hosts に追加する。

199.36.153.8 storage.googleapis.com

すると、storage.googleapis.com にアクセスすると、199.36.153.8 にアクセスするようになる。また、199.36.153.8/30 へのアクセスは、VPN 経由で行われる静的ルートを設定しているので、VPN 経由でアクセスされるはずだ。

./ls_all_buckets.sh を再度実行しよう。

./ls_all_buckets.sh
gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [private-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-vpn-all-2025-05/
gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-vpn-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-allow-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.

gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txtgs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt に追加でアクセスできるようになった。

Endpoint Override 法

長いので結論: /etc/hosts あるいは、DNS を組み合わせる必要がある。

/etc/hosts の設定は元に戻しておく。

gcloud コマンドは、API エンドポイントを上書きすることができる。

gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/

上記によって、storage.googleapis.com ではなく、private.googleapis.com にアクセスするようになる。また、199.36.153.8/30 へのアクセスは、VPN 経由で行われる静的ルートを設定しているので、VPN 経由でアクセスされるはずだ。

./ls_all_buckets.sh を再度実行しよう。

./ls_all_buckets.sh
gs://public-2025-05/
ERROR: (gcloud.storage.ls) gs://public-2025-05 not found: 404.
gs://private-2025-05/
ERROR: (gcloud.storage.ls) gs://private-2025-05 not found: 404.
gs://ip-vpn-all-2025-05/
ERROR: (gcloud.storage.ls) gs://ip-vpn-all-2025-05 not found: 404.
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) gs://ip-vpn-sa-2025-05 not found: 404.
gs://ip-allow-all-2025-05/
ERROR: (gcloud.storage.ls) gs://ip-allow-all-2025-05 not found: 404.
gs://ip-allow-sa-2025-05/
ERROR: (gcloud.storage.ls) gs://ip-allow-sa-2025-05 not found: 404.

…ダメだった。

ここで重要な点として、この設定では期待通りに動作せず、アクセスは失敗した。この結果には注目してほしい。

gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/storage/v1/

もエラーだった。なぜアクセスが失敗するのか原因が不明である。

gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/storage/

これもエラーだった。

以下のように --log-http をつけて、リクエストを確認してみる。

gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/storage/v1/
gcloud storage ls gs://public-2025-05 --log-http

すると、

https://private.googleapis.com/storage/v1/b/public-2025-05/o?alt=json&fields=prefixes%2Citems%2Fname%2Citems%2Fsize%2Citems%2Fgeneration%2CnextPageToken&delimiter=%2F&includeFoldersAsPrefixes=True&maxResults=1000&projection=noAcl

のようなリクエストが飛んでいる。

curl "https://private.googleapis.com/storage/v1/b/public-2025-05/o?alt=json&fields=prefixes%2Citems%2Fname%2Citems%2Fsize%2Citems%2Fgeneration%2CnextPageToken&delimiter=%2F&includeFoldersAsPrefixes=True&maxResults=1000&projection=noAcl"
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/storage/v1/b/public-2025-05/o?alt=json&fields=prefixes%2Citems%2Fname%2Citems%2Fsize%2Citems%2Fgeneration%2CnextPageToken&delimiter=%2F&includeFoldersAsPrefixes=True&maxResults=1000&projection=noAcl"

を試してみると、Host を設定しない場合は、404 で、Host を設定すると、以下が返ってきた。

{
  "items": [
    {
      "name": "public_2025-05.txt",
      "generation": "1746452844041206",
      "size": "19"
    }
  ]
}

つまり、サーバ側は Host ヘッダを見ていて、storage でない場合には正常に応答しない。

gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/

としても、アクセス先は同じようだった。

gcloud config set api_endpoint_overrides/storage https://199.36.153.8/
gcloud config set auth/disable_ssl_validation True
gcloud storage ls gs://public-2025-05 --log-http

とすると、401 となる。

gcloud storage ls gs://public-2025-05
/usr/bin/../lib/google-cloud-sdk/lib/third_party/urllib3/connectionpool.py:1102: InsecureRequestWarning: Unverified HTTPS request is being made to host '199.36.153.8'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
  warnings.warn(
/usr/bin/../lib/google-cloud-sdk/lib/third_party/urllib3/connectionpool.py:1102: InsecureRequestWarning: Unverified HTTPS request is being made to host '199.36.153.8'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
  warnings.warn(
ERROR: (gcloud.storage.ls) HTTPError 401: <!DOCTYPE html>
<html lang=en>
  <meta charset=utf-8>
  <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  <title>Error 401 (Unauthorized)!!1</title>
  <style>
    *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
  </style>
  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>
  <p><b>401.</b> <ins>That’s an error.</ins>
  <p>Your client does not have permission to the requested URL <code>/storage/v1/b/public-2025-05/o</code>.  <ins>That’s all we know.</ins>

ということで、結論から言うと、ダメだ。

Endpoint Override 法の考察

/etc/hosts に以下を追加してみる。片方は意図的に storage を削っている。

199.36.153.8 storage-dummy.p.googleapis.com
199.36.153.8 storag-dummy.p.googleapis.com
gcloud config set api_endpoint_overrides/storage https://storage-dummy.p.googleapis.com/
gcloud storage ls gs://public-2025-05

gcloud storage ls gs://public-2025-05
gs://public-2025-05/public_2025-05.txt

と結果が返ってくる。

一方で

gcloud config set api_endpoint_overrides/storage https://storag-dummy.p.googleapis.com/
gcloud storage ls gs://public-2025-05

とすると

gcloud storage ls gs://public-2025-05
ERROR: (gcloud.storage.ls) gs://public-2025-05 not found: 404.

となる。

グローバル Google API にアクセスする の エンドポイントから Google API にアクセスする にエンドポイントを使用するという項目がある。これによると、

たとえば、エンドポイント名が xyz の場合、API バンドルに storage-xyz.p.googleapis.comcompute-xyz.p.googleapis.com、その他の一般的に使用される API の DNS レコードが作成されます。

注: エンドポイントを使用できるカスタム ライブラリを開発する場合は、リクエストを送信するサービスの有効なホスト名に Host ヘッダーと SNI を設定する必要があります。有効なホスト名は、サービス固有のデフォルト ドメイン名(storage.googleapis.com)またはサービスの SERVICE-ENDPOINT.p.googleapis.com DNS 名(使用可能な場合)のいずれかです。SERVICE-ENDPOINT.p.googleapis.com の名前の場合、名前の SERVICE の部分はサービスと一致する必要がありますが、ENDPOINT の任意の値を使用できます。

である。このドキュメントは、エンドポイント側が、「どのような Host で呼び出されているか」を気にして応答しているという考えを裏付ける。

結論としては、Endpoint Override で接続したい場合は、

  1. storage-endpointname.p.googleapis.com が、private.googleapis.com の CNAME に解決されるような DNS 設定を行う
  2. storage-endpointname.p.googleapis.com で呼び出すようにする

を合わせて利用する必要がある。

storage.private.googleapis.com とか作ってくれたらいいのに…。

ちなみに、storage-private.googleapis.comstorage.googleapis.com と同じ結果が返ってくるようだ。

注意:
gcloud config の設定は元に戻しておくことを忘れないようにする。以下のコマンドを実行して、元に戻すことができる。

gcloud config unset auth/disable_ssl_validation
gcloud config unset api_endpoint_overrides/storage

curl 法

さて、curl-H オプションで Host ヘッダを指定できる。こちらの世界は自由だ。

まずは、アクセス「できない」例を確認しておこう。

echo "curl https://storage.googleapis.com/public-2025-05/public_2025-05.txt"
curl https://storage.googleapis.com/public-2025-05/public_2025-05.txt
echo "curl https://storage.googleapis.com/private-2025-05/private_2025-05.txt"
curl https://storage.googleapis.com/private-2025-05/private_2025-05.txt
echo "curl https://storage.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt"
curl https://storage.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
echo "curl https://storage.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt"
curl https://storage.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt
echo "curl https://storage.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt"
curl https://storage.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt
echo "curl https://storage.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt"
curl https://storage.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt

vm-guest-vpn:

curl https://storage.googleapis.com/public-2025-05/public_2025-05.txt
public_2025-05.txt
curl https://storage.googleapis.com/private-2025-05/private_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>
curl https://storage.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>There is an IP filtering condition that is preventing access to the resource.</Details></Error>
curl https://storage.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>
curl https://storage.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>There is an IP filtering condition that is preventing access to the resource.</Details></Error>
curl https://storage.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>

アクセス経路が変わらないので、結果は変わらない。

ここで、エンドポイントを private.googleapis.com に変更して、curl してみる。

echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/public-2025-05/public_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/public-2025-05/public_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/private-2025-05/private_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/private-2025-05/private_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt"

Error の直後の行で、改行を入れた。

vm-guest-vpn:

curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/public-2025-05/public_2025-05.txt"
public_2025-05.txt
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/private-2025-05/private_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt"
ip-vpn-all_2025-05.txt
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt"
ip-allow-all_2025-05.txt
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>

結果が変わったことに注意しよう。大事なポイントを抜き出す。

# NG
curl https://storage.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>There is an IP filtering condition that is preventing access to the resource.</Details></Error>

# OK
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt"
ip-vpn-all_2025-05.txt

# NG
curl https://storage.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>There is an IP filtering condition that is preventing access to the resource.</Details></Error>

# OK
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt"
ip-allow-all_2025-05.txt

curl コマンドを使っても、Bucket IP Filtering で許可されている IP 範囲からアクセスできることが確認できた。

Access Check Part 4

筆者自宅からのアクセスからも確認してみる。メールアドレスは test@example.com に置き換えている。

gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
gs://private-2025-05/private_2025-05.txt
gs://ip-vpn-all-2025-05/
ERROR: (gcloud.storage.ls) [test@example.com] does not have permission to access b instance [ip-vpn-all-2025-05] (or it may not exist): There is an IP filtering condition that is preventing access to the resource. This command is authenticated as test@example.com which is the active account specified by the [core/account] property.
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) [test@example.com] does not have permission to access b instance [ip-vpn-sa-2025-05] (or it may not exist): There is an IP filtering condition that is preventing access to the resource. This command is authenticated as test@example.com which is the active account specified by the [core/account] property.
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
gs://ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt

ということで、

  • VPN の IP Range 外であるため、2つのバケットにはアクセスできない
  • 外部 IP の設定をしていればアクセスできる
  • IAM の権限も問題なく通過している

ことが分かる。

最後に

本記事では、2つの Google Cloud プロジェクト間に Cloud VPN をgcloudコマンドで構築し、その VPN 接続を経由して Cloud Storage の Bucket IP Filtering 機能が利用できることを検証した。オンプレミス ホスト用の限定公開の Google アクセスの仕組みを利用することで、 VPN の対向ネットワーク(本記事では network-vpn-guest )からのアクセスを、バケット側で設定したVPCネットワーク (network-vpn-host) 内の許可 IP 範囲からのアクセスとして認識させることが可能である。

この検証を通じて得られた主要なステップと注意点を以下に再確認する。

  1. 経路設定の重要性: VPN 対向側(ゲスト VM)から Bucket IP Filtering が設定されたバケットにアクセスするには、private.googleapis.com (199.36.153.8/30) へのトラフィックが VPN トンネルを経由するように、静的ルート(本記事で採用)またはCloud RouterによるBGPルート広告を設定することが不可欠である。これを忘れると、トラフィックはインターネット経由(あるいは、対向 VPN 内のエンドポイント)となり、IP Filtering の条件を満たせない。
  2. gcloudコマンド利用時の注意点:
    • gcloud storage コマンドで private.googleapis.com 経由でアクセスしようとする場合、単に gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/ を設定するだけでは不十分である。これは gcloud が送信するリクエストの Host ヘッダが原因で、API側がリクエストを正しく処理できないためである。
    • この問題を回避するには、適切なDNS設定(例: storage-endpointname.p.googleapis.com を作成し、それを private.googleapis.com に向け、gcloud のエンドポイントオーバーライドでそのカスタムドメインを指定する)が必要である。あるいは、/etc/hosts ファイルで storage.googleapis.comprivate.googleapis.com のIPアドレス (e.g. 199.36.153.8) に向けることで上記をシミュレーションできる。
  3. curl等での直接アクセス:curl のように Host ヘッダを明示的に指定できるツールを使用する場合、アクセス先URLに private.googleapis.com を指定しつつ、-H "Host: storage.googleapis.com" オプションを追加することで、DNS 等の設定なしに VPN 経由でのアクセスと IP Filtering の検証が可能である。これは API の挙動を理解する上で有用な方法だ。
  4. IP Filtering と IAM は別: Bucket IP Filtering は、あくまでネットワークレベルでのアクセス元 IP アドレスに基づくフィルタリング機能を「追加する」ものである。これに加えて、アクセスするユーザーやサービスアカウントには適切なIAM権限(例: roles/storage.objectViewer)が付与されている必要がある。両方の条件を満たして初めてアクセスが許可される。

Bucket IP Filtering は、VPN や Cloud Interconnect で接続された内部ネットワークからのアクセス制御を強化する上で有効な手段となり得る。ただし、特に gcloud などのクライアントツールから利用する際には、Private Google Access のエンドポイントと Host ヘッダの挙動について理解しておくことが重要である。オンプレミス ホスト用の限定公開の Google アクセスの説明を良く理解しておくと良い。

Bucket IP Filtering は、上記 VPN 接続を利用している場合でも、「特定の VPC ネットワークからの、特定の IP 範囲からのアクセス」と見做して、許可・拒否を行うことができることが分かった。サービス アカウントの権限を持たないクライアントから、正しい SSL 証明書である状態でアクセス可能であり、便利だ。

もちろん、グローバル IP を個別許可することもできるため、拠点の外部 IP を登録することで、拠点内のクライアント PC からのアクセスだけを許可することもできるが、内部 IP 範囲を指定できる方がより細かな制御ができることは言うまでもない。

※本記事には、株式会社grasys様の記事 からの引用を含んでいます。元記事の著作権は同社に帰属し、許諾を得て掲載しています。

参考文献


VPN and Bucket IP Filtering

English follows Japanese. (This line is kept from the original for context, but the entire article below is the English translation.)

Purpose of this Article

This article has two main objectives.

A secondary objective is to demonstrate an example of configuring Cloud VPN between two projects using only gcloud commands. As far as I could find with a quick search, apart from Google Cloud's official documentation (which covers same-project setups), there were examples using the Web UI, but no examples using gcloud commands were found for inter-project VPNs.

The other main objective is to verify whether Cloud Storage's new Bucket IP Filtering feature can be accessed from the destination of a VPN connection.

Conclusion: Access from the VPN connection's destination is possible. This is a good thing.

This article is intended for network engineers and cloud architects who have a basic understanding of Google Cloud's VPC, IAM, and Cloud Storage.

Since information on inter-project VPNs, other than Google Cloud's official documentation, was scarce, this article provides an example configuration using gcloud commands (examples with on-premises routers as peers were, as expected, quite common).

What is Bucket IP Filtering?

Cloud Storage's Bucket IP filtering feature is an access control mechanism that allows you to permit access to a Cloud Storage bucket only from specific IP address ranges Bucket IP Filtering.
It was released as a Public Preview on 2024-11-14 Release Notes.

Can it be used with VPN?

If a company wants to create a "general-purpose file storage" that is "only accessible from within the company," Cloud Storage's Bucket IP filtering feature might be useful.

Since Global IPs can be specified by CIDR range, if each site has a static IP, you can use Cloud Storage's Bucket IP filtering feature to allow access only from specific sites.

  "publicNetworkSource": {
    "allowedIpCidrRanges": [
      "${YOUR_IP}/32"
    ],
  }

Another common use case for Google Cloud is VPN configuration. If a Cloud Storage bucket can be accessed only from the destination of a VPN connection, it's very convenient as it eliminates other considerations.

To Configure Private Google Access for on-premises hosts, the documentation states:

Private Google Access for on-premises hosts allows you to connect to Google APIs and services from your on-premises systems by routing traffic through a Cloud VPN tunnel or a VLAN attachment for Cloud Interconnect. Private Google Access for on-premises hosts can be used as an alternative to connecting to Google APIs and services over the internet.

I want to verify if Bucket IP Filtering can be used when accessing Cloud Storage from the connection destination via private.googleapis.com.

Procedure

All basic operations will be performed using gcloud commands, aiming for reproducibility by simply copying and pasting.

The region for networks and buckets will be us-central1.

Create VPC networks 10.1.0.0/24 and 10.1.1.0/24 (Note: original text mentioned 10.2.0.0/24 initially, but 10.1.1.0/24 is used consistently later) in two Google Cloud projects and connect them via VPN.
Consider 10.1.0.0/24 as the host side and 10.1.1.0/24 as the on-premises side (although we'll be verifying this on Google Cloud).

network-vpn-host: 10.1.0.0/24
network-vpn-guest: 10.1.1.0/24

Environment Definition

Define the following according to your reproduction environment.

PROJECT_HOST is considered the bucket host, and PROJECT_GUEST is considered the on-premises environment. YOUR_IP is the global IP to allow access from, YOUR_ACCOUNT is the operator's Google account, and BILLING_ACCOUNT is the Google Cloud billing account.

SHARED_SECRET is the Shared Secret for the VPN connection.

export PROJECT_HOST=host-project
export PROJECT_GUEST=guest-project
export YOUR_IP=203.0.113.1
export YOUR_ACCOUNT=test@example.com
export BILLING_ACCOUNT="000000-000000-000000"

export SHARED_SECRET="bvTtdK7nil68WCYu7+dJ7iYdEmeWCB/A5Q4lUu2DHac="

Google Cloud Project Creation and API Enablement

for project in $PROJECT_HOST $PROJECT_GUEST; do
  gcloud projects create $project
  gcloud billing projects link $project --billing-account=$BILLING_ACCOUNT
  gcloud services enable compute.googleapis.com --project=$project
done

Bucket Creation

Create the following Cloud Storage buckets in the host project. 'sa' is an abbreviation for Service Account.

Basically, if we can verify with allUsers, the objective of this article will be achieved, but while we're at it, let's also check the behavior when permissions are granted to a Service Account. Since we want to ls later, let's also create dummy files.

Bucket NameAccess PermissionBucket IP FilteringNetworkIP Range
public-2025-05allUsersDisabledNANA
private-2025-05Host project onlyDisabledNANA
ip-vpn-all-2025-05allUsersEnablednetwork-vpn-host10.1.0.0/16
ip-vpn-sa-2025-05Verifier, host's service accountEnablednetwork-vpn-host10.1.0.0/16
ip-allow-all-2025-05allUsersEnabledpublicNetworkSource, network-vpn-host10.1.0.0/16, ${YOUR_IP}/32
ip-allow-sa-2025-05Verifier, host's service accountEnabledpublicNetworkSource, network-vpn-host10.1.0.0/16, ${YOUR_IP}/32

Although network-vpn-host is 10.1.0.0/24 and network-vpn-guest is 10.1.1.0/24, 10.1.0.0/16 is specified as the IP range that includes 'both'.

gcloud storage buckets create gs://public-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
gcloud storage buckets add-iam-policy-binding gs://public-2025-05 --member=allUsers --role=roles/storage.objectViewer --project=$PROJECT_HOST

gcloud storage buckets create gs://private-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access

gcloud storage buckets create gs://ip-vpn-all-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
gcloud storage buckets add-iam-policy-binding gs://ip-vpn-all-2025-05 --member=allUsers --role=roles/storage.objectViewer --project=$PROJECT_HOST

gcloud storage buckets create gs://ip-vpn-sa-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
# Permissions are granted at the project level, so IAM permission granting for the bucket is omitted

gcloud storage buckets create gs://ip-allow-all-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
gcloud storage buckets add-iam-policy-binding gs://ip-allow-all-2025-05 --member=allUsers --role=roles/storage.objectViewer --project=$PROJECT_HOST

gcloud storage buckets create gs://ip-allow-sa-2025-05 --project=$PROJECT_HOST --location=us-central1 --uniform-bucket-level-access
# Permissions are granted at the project level, so IAM permission granting for the bucket is omitted

mkdir -p dummy_files
for bucket_prefix in public private ip-vpn-all ip-vpn-sa ip-allow-all ip-allow-sa; do
  echo "${bucket_prefix}_2025-05.txt content" > dummy_files/${bucket_prefix}_2025-05.txt
  gcloud storage cp dummy_files/${bucket_prefix}_2025-05.txt gs://${bucket_prefix}-2025-05/ --project=$PROJECT_HOST
done

VPC Network Creation

Create VPC network network-vpn-host in PROJECT_HOST and VPC network network-vpn-guest in PROJECT_GUEST.

As mentioned earlier:
network-vpn-host: 10.1.0.0/24
network-vpn-guest: 10.1.1.0/24

gcloud compute networks create network-vpn-host --project=$PROJECT_HOST --subnet-mode=custom
gcloud compute networks subnets create subnet-vpn-host \
    --network=network-vpn-host \
    --region=us-central1 \
    --range=10.1.0.0/24 \
    --project=$PROJECT_HOST

gcloud compute networks create network-vpn-guest --project=$PROJECT_GUEST --subnet-mode=custom
gcloud compute networks subnets create subnet-vpn-guest \
    --network=network-vpn-guest \
    --region=us-central1 \
    --range=10.1.1.0/24 \
    --project=$PROJECT_GUEST

If there's a Public IP, the route can be confusing (though it shouldn't actually change).
Even without a Public IP, configure IAP settings to allow SSH connections.

for project in $PROJECT_HOST $PROJECT_GUEST; do
    gcloud projects add-iam-policy-binding $project \
        --member="user:${YOUR_ACCOUNT}" \
        --role=roles/iap.tunnelResourceAccessor
    gcloud projects add-iam-policy-binding $project \
        --member="user:${YOUR_ACCOUNT}" \
        --role=roles/compute.instanceAdmin.v1
done

gcloud compute firewall-rules create allow-ssh-ingress-from-iap-vpn-host \
    --direction=INGRESS \
    --action=allow \
    --rules=tcp:22 \
    --source-ranges=35.235.240.0/20 \
    --network=network-vpn-host \
    --project=$PROJECT_HOST

gcloud compute firewall-rules create allow-ssh-ingress-from-iap-vpn-guest \
    --direction=INGRESS \
    --action=allow \
    --rules=tcp:22 \
    --source-ranges=35.235.240.0/20 \
    --network=network-vpn-guest \
    --project=$PROJECT_GUEST

Also, configure Private Google Access so that Google Cloud services can be accessed even without a Public IP.

gcloud compute networks subnets update subnet-vpn-host --project=$PROJECT_HOST \
    --region=us-central1 \
    --enable-private-ip-google-access

gcloud compute networks subnets update subnet-vpn-guest --project=$PROJECT_GUEST \
    --region=us-central1 \
    --enable-private-ip-google-access

# To verify, execute the following
# gcloud compute networks subnets describe subnet-vpn-host --project=$PROJECT_HOST \
#     --region=us-central1 \
#     --format="get(privateIpGoogleAccess)"

Set up Firewall Rules to allow VMs to communicate with each other.

gcloud compute firewall-rules create allow-internal-ingress-from-vpn-host \
    --direction=INGRESS \
    --action=allow \
    --rules=all \
    --source-ranges=10.1.0.0/16 \
    --network=network-vpn-host \
    --project=$PROJECT_HOST

gcloud compute firewall-rules create allow-internal-ingress-from-vpn-guest \
    --direction=INGRESS \
    --action=allow \
    --rules=all \
    --source-ranges=10.1.0.0/16 \
    --network=network-vpn-guest \
    --project=$PROJECT_GUEST

Inter-Project VPN Configuration

Set up a VPN between the previously created networks network-vpn-host and network-vpn-guest.

I decided to omit BGP IPv6 configuration.

# VPN configuration in PROJECT_HOST
gcloud compute vpn-gateways create vpn-host \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --network=network-vpn-host

gcloud compute routers create cloud-router-host \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --network network-vpn-host \
  --asn 65001

# VPN configuration in PROJECT_GUEST
gcloud compute vpn-gateways create vpn-guest \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --network=network-vpn-guest

gcloud compute routers create cloud-router-guest \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --network network-vpn-guest \
  --asn 65002

Note down the displayed IPs. (These will be dynamically assigned by GCP).
Example:

export HOST_INTERFACE0=34.153.55.144
export HOST_INTERFACE1=35.220.88.151

export GUEST_INTERFACE0=35.242.99.80
export GUEST_INTERFACE1=34.153.244.91

# Not tested, but the following command can be used to get the IPs dynamically.
export HOST_INTERFACE0=$(gcloud compute vpn-gateways describe vpn-host --project=$PROJECT_HOST --region=us-central1 --format='value(vpnInterfaces[0].ipAddress)')
export HOST_INTERFACE1=$(gcloud compute vpn-gateways describe vpn-host --project=$PROJECT_HOST --region=us-central1 --format='value(vpnInterfaces[1].ipAddress)')

export GUEST_INTERFACE0=$(gcloud compute vpn-gateways describe vpn-guest --project=$PROJECT_GUEST --region=us-central1 --format='value(vpnInterfaces[0].ipAddress)')
export GUEST_INTERFACE1=$(gcloud compute vpn-gateways describe vpn-guest --project=$PROJECT_GUEST --region=us-central1 --format='value(vpnInterfaces[1].ipAddress)')
# In PROJECT_HOST, register the peer's information
# Register the existence of the guest -> host VPN
# I want to use the following command for external-vpn-gateways, but
# > You cannot provide interface with IP address associated with HA VPN gateway of Google Cloud.
# it says, so specify projects/${PROJECT_GUEST}/regions/us-central1/vpnGateways/vpn-guest

# gcloud compute external-vpn-gateways create vpngw-guest-host \
#   --project=$PROJECT_HOST \
#   --interfaces=0=$GUEST_INTERFACE0,1=$GUEST_INTERFACE1 # This would be for non-GCP peer

gcloud compute vpn-tunnels create tunnel-host-guest-0 \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --peer-gcp-gateway=projects/${PROJECT_GUEST}/regions/us-central1/vpnGateways/vpn-guest \
  --ike-version=2 \
  --shared-secret=$SHARED_SECRET \
  --router=cloud-router-host \
  --vpn-gateway=vpn-host \
  --interface=0

gcloud compute vpn-tunnels create tunnel-host-guest-1 \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --peer-gcp-gateway=projects/${PROJECT_GUEST}/regions/us-central1/vpnGateways/vpn-guest \
  --ike-version=2 \
  --shared-secret=$SHARED_SECRET \
  --router=cloud-router-host \
  --vpn-gateway=vpn-host \
  --interface=1

# In PROJECT_GUEST, register the peer's information
# Register the existence of the host -> guest VPN
# gcloud compute external-vpn-gateways create vpngw-host-guest \
#   --project=$PROJECT_GUEST \
#   --interfaces=0=$HOST_INTERFACE0,1=$HOST_INTERFACE1 # This would be for non-GCP peer

gcloud compute vpn-tunnels create tunnel-guest-host-0 \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --peer-gcp-gateway=projects/${PROJECT_HOST}/regions/us-central1/vpnGateways/vpn-host \
  --ike-version=2 \
  --shared-secret=$SHARED_SECRET \
  --router=cloud-router-guest \
  --vpn-gateway=vpn-guest \
  --interface=0

gcloud compute vpn-tunnels create tunnel-guest-host-1 \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --peer-gcp-gateway=projects/${PROJECT_HOST}/regions/us-central1/vpnGateways/vpn-host \
  --ike-version=2 \
  --shared-secret=$SHARED_SECRET \
  --router=cloud-router-guest \
  --vpn-gateway=vpn-guest \
  --interface=1

BGP Configuration

# HOST
gcloud compute routers add-interface cloud-router-host \
  --project=$PROJECT_HOST \
  --interface-name=if-tunnel-host-guest-0 \
  --vpn-tunnel=tunnel-host-guest-0 \
  --region=us-central1 \
  --ip-version=IPV4

gcloud compute routers add-bgp-peer cloud-router-host \
  --project=$PROJECT_HOST \
  --region=us-central1 \
  --peer-name=peer-tunnel-host-guest-0 \
  --interface=if-tunnel-host-guest-0 \
  --peer-asn=65002

# AI Suggest below:
# BGP_PEER_IP_HOST_TUNNEL_0=$(gcloud compute routers get-status cloud-router-host --project=$PROJECT_HOST --region=us-central1 --format='flattened(result.bgpPeerStatus[].name,result.bgpPeerStatus[].ipAddress,result.bgpPeerStatus[].peerIpAddress)' | grep tunnel-host-guest-0 | awk '{print $3}')
# BGP_ROUTER_IP_HOST_TUNNEL_0=$(gcloud compute routers get-status cloud-router-host --project=$PROJECT_HOST --region=us-central1 --format='flattened(result.bgpPeerStatus[].name,result.bgpPeerStatus[].ipAddress,result.bgpPeerStatus[].peerIpAddress)' | grep tunnel-host-guest-0 | awk '{print $2}')
gcloud compute routers describe cloud-router-host \
  --project=$PROJECT_HOST \
  --region=us-central1

Check peerIpAddress and configure the IP manually.

# GUEST
gcloud compute routers add-interface cloud-router-guest \
  --project=$PROJECT_GUEST \
  --interface-name=if-tunnel-guest-host-0 \
  --ip-address=169.254.0.250 \
  --mask-length=30 \
  --vpn-tunnel=tunnel-guest-host-0 \
  --region=us-central1 \
  --ip-version=IPV4

gcloud compute routers add-bgp-peer cloud-router-guest \
  --project=$PROJECT_GUEST \
  --region=us-central1 \
  --peer-name=peer-tunnel-guest-host-0 \
  --interface=if-tunnel-guest-host-0 \
  --peer-asn=65001 \
  --peer-ip-address=169.254.0.249

Configuration Check

gcloud compute routers describe cloud-router-host \
  --project=$PROJECT_HOST \
  --region=us-central1

gcloud compute routers describe cloud-router-guest \
  --project=$PROJECT_GUEST \
  --region=us-central1

VM Instance Creation

Create VM instances in each VPC network. Specify no-address to create VM instances without Public IPs.

gcloud compute instances create vm-host-vpn \
    --project=$PROJECT_HOST \
    --zone=us-central1-f \
    --machine-type=e2-micro \
    --network-interface=stack-type=IPV4_ONLY,subnet=subnet-vpn-host,no-address \
    --scopes=https://www.googleapis.com/auth/cloud-platform \
    --boot-disk-size 10GB \
    --boot-disk-type pd-standard \
    --image-project debian-cloud \
    --image-family debian-12

gcloud compute instances create vm-guest-vpn \
    --project=$PROJECT_GUEST \
    --zone=us-central1-f \
    --machine-type=e2-micro \
    --network-interface=stack-type=IPV4_ONLY,subnet=subnet-vpn-guest,no-address \
    --scopes=https://www.googleapis.com/auth/cloud-platform \
    --boot-disk-size 10GB \
    --boot-disk-type pd-standard \
    --image-project debian-cloud \
    --image-family debian-12

You can SSH into each VM instance as follows:

gcloud compute ssh --zone=us-central1-f vm-host-vpn --tunnel-through-iap --project $PROJECT_HOST
gcloud compute ssh --zone=us-central1-f vm-guest-vpn --tunnel-through-iap --project $PROJECT_GUEST

In the author's test, 10.1.0.3 was the host-side VM instance IP, and 10.1.1.3 was the guest-side VM instance IP. (Your IPs will likely differ).
Ping from the guest side to the host side VM; if communication is established, there are no issues.

# On vm-guest-vpn
ping <IP_OF_VM-HOST-VPN> # e.g., ping 10.1.0.3

Bucket IP Filtering Verification

Access Check Part 1

First, check if the buckets can be accessed from each VM.
Execute ls_all_buckets.sh from each VM to check the bucket list.

for bucket_prefix in public private ip-vpn-all ip-vpn-sa ip-allow-all ip-allow-sa; do
  echo "gs://${bucket_prefix}-2025-05/"
  gcloud storage ls gs://${bucket_prefix}-2025-05/
done

vm-host-vpn:

./ls_all_buckets.sh 
gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
gs://private-2025-05/private_2025-05.txt
gs://ip-vpn-all-2025-05/
gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
gs://ip-vpn-sa-2025-05/
gs://ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
gs://ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt

vm-guest-vpn (Note: The service account for vm-guest-vpn is from PROJECT_GUEST, so it won't have inherent access to PROJECT_HOST buckets unless explicitly granted or if buckets are public):

gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [private-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-vpn-all-2025-05/
gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-vpn-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-allow-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.

(The original output showed the GUEST VM's default service account. The actual account ID will vary.)

As shown above (before IP filtering is strictly applied and considering IAM), vm-host-vpn can access all buckets (assuming its service account has broad permissions in PROJECT_HOST). vm-guest-vpn can access buckets that grant allUsers read access. For others, it depends on IAM.

Bucket IP Filtering Configuration

Configure Bucket IP Filtering for the ip-vpn-all-2025-05 and ip-vpn-sa-2025-05 buckets.

cat <<EOF > ip-filter-vpn.json
{
  "mode": "Enabled",
  "vpcNetworkSources": [
    {
      "network": "projects/${PROJECT_HOST}/global/networks/network-vpn-host",
      "allowedIpCidrRanges": [
        "10.1.0.0/16"
      ]
    }
  ]
}
EOF

Create ip-filter-allow.json.

cat <<EOF > ip-filter-allow.json
{
  "mode": "Enabled",
  "publicNetworkSource": {
    "allowedIpCidrRanges": [
      "${YOUR_IP}/32"
    ]
  },
  "vpcNetworkSources": [
    {
      "network": "projects/${PROJECT_HOST}/global/networks/network-vpn-host",
      "allowedIpCidrRanges": [
        "10.1.0.0/16"
      ]
    }
  ]
}
EOF

Configure Bucket IP Filtering.

for bucket_prefix in ip-vpn-all ip-vpn-sa; do
  gcloud alpha storage buckets update gs://${bucket_prefix}-2025-05 --project=$PROJECT_HOST --ip-filter-file=ip-filter-vpn.json
done

for bucket_prefix in ip-allow-all ip-allow-sa; do
  gcloud alpha storage buckets update gs://${bucket_prefix}-2025-05 --project=$PROJECT_HOST --ip-filter-file=ip-filter-allow.json
done

Expected output:

Updating gs://ip-vpn-all-2025-05/...
  Completed 1
Updating gs://ip-vpn-sa-2025-05/...
  Completed 1
Updating gs://ip-allow-all-2025-05/...
  Completed 1
Updating gs://ip-allow-sa-2025-05/...
  Completed 1

Access Check Part 2

vm-host-vpn (assuming its IP is within 10.1.0.0/16 of network-vpn-host):

./ls_all_buckets.sh
gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
gs://private-2025-05/private_2025-05.txt
gs://ip-vpn-all-2025-05/
gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
gs://ip-vpn-sa-2025-05/
gs://ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
gs://ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt

vm-guest-vpn (now IP filtering is active):

./ls_all_buckets.sh
gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [private-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-vpn-all-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-vpn-all-2025-05] (or it may not exist): There is an IP filtering condition that is preventing access to the resource. This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-vpn-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-allow-all-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-allow-all-2025-05] (or it may not exist): There is an IP filtering condition that is preventing access to the resource. This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-allow-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-allow-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.

From vm-guest-vpn, only public-2025-05 is accessible. Others are not, due to IAM or IP filtering. The error messages differ depending on the cause.

Add Static Route

To make vm-guest-vpn access Google APIs via the VPN (and thus appear from network-vpn-host's IP range for Private Google Access), add a route for private.googleapis.com (199.36.153.8/30) or restricted.googleapis.com (199.36.153.4/30) towards the VPN tunnel on PROJECT_GUEST. The article uses 199.36.153.8/30.

# gcloud compute routes create private-to-host \
#     --project=$PROJECT_GUEST \
#     --network=network-vpn-guest \
#     --destination-range=199.36.153.8/30 \
#     --priority=200 \
#     --next-hop-gateway=projects/$PROJECT_HOST/global/vpnGateways/vpn-host

gcloud compute routes create private-to-host \
    --project=$PROJECT_GUEST \
    --network=network-vpn-guest \
    --destination-range=199.36.153.8/30 \
    --priority=200 \
    --next-hop-vpn-tunnel-region=us-central1 \
    --next-hop-vpn-tunnel=tunnel-guest-host-0

【Reference】Cloud Router Configuration Change

As described in Routing on-premises using Cloud Router, the host-side Cloud Router (cloud-router-host) can advertise routes for the IP ranges used by the private.googleapis.com domain to the guest side (on-premises side, i.e., cloud-router-guest).

Since it worked with a static route, it is believed that the above BGP advertisement method would also work. Specific verification has not been performed in this article, so I encourage you to verify this.

Access Check Part 3

Conclusion: With the appropriate settings (static route for private.googleapis.com and DNS/Host override), gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt and gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt become additionally accessible from vm-guest-vpn.

The /etc/hosts Method

On vm-guest-vpn, add the following line to /etc/hosts:

199.36.153.8 storage.googleapis.com

Then, accessing storage.googleapis.com will resolve to 199.36.153.8. Since a static route is configured for access to 199.36.153.8/30 to go via VPN, it should be accessed via VPN, and the source IP seen by Cloud Storage should be from network-vpn-host's NAT range for Private Google Access.

Let's run ./ls_all_buckets.sh again on vm-guest-vpn:

./ls_all_buckets.sh
gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [private-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-vpn-all-2025-05/
gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-vpn-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
ERROR: (gcloud.storage.ls) [886786442757-compute@developer.gserviceaccount.com] does not have permission to access b instance [ip-allow-sa-2025-05] (or it may not exist): 886786442757-compute@developer.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist). This command is authenticated as 886786442757-compute@developer.gserviceaccount.com which is the active account specified by the [core/account] property.

gs://ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt and gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt became additionally accessible (assuming IAM allows allUsers for these).

The Endpoint Override Method

TL;DR: It's necessary to combine with /etc/hosts or DNS for gcloud to work correctly with private.googleapis.com.

Revert the /etc/hosts settings on vm-guest-vpn.

The gcloud command can override API endpoints.

# On vm-guest-vpn
gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/

This will cause gcloud to attempt to access private.googleapis.com instead of storage.googleapis.com. With the static route for 199.36.153.8/30 via VPN, traffic should be routed correctly.

Let's run ./ls_all_buckets.sh again on vm-guest-vpn:

./ls_all_buckets.sh
gs://public-2025-05/
ERROR: (gcloud.storage.ls) gs://public-2025-05 not found: 404.
gs://private-2025-05/
ERROR: (gcloud.storage.ls) gs://private-2025-05 not found: 404.
gs://ip-vpn-all-2025-05/
ERROR: (gcloud.storage.ls) gs://ip-vpn-all-2025-05 not found: 404.
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) gs://ip-vpn-sa-2025-05 not found: 404.
gs://ip-allow-all-2025-05/
ERROR: (gcloud.storage.ls) gs://ip-allow-all-2025-05 not found: 404.
gs://ip-allow-sa-2025-05/
ERROR: (gcloud.storage.ls) gs://ip-allow-sa-2025-05 not found: 404.

...It didn't work.

An important point here is that this setting alone did not work as expected, and access failed. Please pay attention to this result.

Trying gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/storage/v1/ also resulted in an error. The reason for the access failure is related to the Host header.

Trying gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/storage/ also resulted in an error.

Let's check the request by adding --log-http:

# On vm-guest-vpn
gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/storage/v1/
gcloud storage ls gs://public-2025-05 --log-http

A request like this is being sent:

https://private.googleapis.com/storage/v1/b/public-2025-05/o?alt=json&fields=prefixes%2Citems%2Fname%2Citems%2Fsize%2Citems%2Fgeneration%2CnextPageToken&delimiter=%2F&includeFoldersAsPrefixes=True&maxResults=1000&projection=noAcl

If you try with curl:

# On vm-guest-vpn
# This will likely fail with 404 because private.googleapis.com expects Host: storage.googleapis.com for this path
curl "https://private.googleapis.com/storage/v1/b/public-2025-05/o?alt=json&fields=prefixes%2Citems%2Fname%2Citems%2Fsize%2Citems%2Fgeneration%2CnextPageToken&delimiter=%2F&includeFoldersAsPrefixes=True&maxResults=1000&projection=noAcl"

# This should work if routing to private.googleapis.com is correct
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/storage/v1/b/public-2025-05/o?alt=json&fields=prefixes%2Citems%2Fname%2Citems%2Fsize%2Citems%2Fgeneration%2CnextPageToken&delimiter=%2F&includeFoldersAsPrefixes=True&maxResults=1000&projection=noAcl"

The second curl (with Host: storage.googleapis.com) returns:

{
  "items": [
    {
      "name": "public_2025-05.txt",
      "generation": "1746452844041206",
      "size": "19"
    }
  ]
}

In other words, the server side (Google APIs) looks at the Host header. gcloud with api_endpoint_overrides sends Host: private.googleapis.com, which is not what the storage service endpoint expects for these paths.

Even with gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/, the Host header sent by gcloud will be private.googleapis.com.

If you try gcloud config set api_endpoint_overrides/storage https://199.36.153.8/ and gcloud config set auth/disable_ssl_validation True:

gcloud config set api_endpoint_overrides/storage https://199.36.153.8/
gcloud config set auth/disable_ssl_validation True
gcloud storage ls gs://public-2025-05 --log-http

This results in a 401 Unauthorized error, as the server expects a valid Host header for authentication context and SNI for SSL.

gcloud storage ls gs://public-2025-05
/usr/bin/../lib/google-cloud-sdk/lib/third_party/urllib3/connectionpool.py:1102: InsecureRequestWarning: Unverified HTTPS request is being made to host '199.36.153.8'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
  warnings.warn(
/usr/bin/../lib/google-cloud-sdk/lib/third_party/urllib3/connectionpool.py:1102: InsecureRequestWarning: Unverified HTTPS request is being made to host '199.36.153.8'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
  warnings.warn(
ERROR: (gcloud.storage.ls) HTTPError 401: <!DOCTYPE html>
<html lang=en>
  <meta charset=utf-8>
  <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  <title>Error 401 (Unauthorized)!!1</title>
  <style>
    *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
  </style>
  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>
  <p><b>401.</b> <ins>That’s an error.</ins>
  <p>Your client does not have permission to the requested URL <code>/storage/v1/b/public-2025-05/o</code>.  <ins>That’s all we know.</ins>

So, in conclusion, simply overriding the endpoint for gcloud doesn't work due to Host header issues.

Considerations for the Endpoint Override Method

Let's try adding the following to /etc/hosts on vm-guest-vpn. One of them intentionally has e removed from storage.

199.36.153.8 storage-dummy.p.googleapis.com
199.36.153.8 storag-dummy.p.googleapis.com

Then try:

# On vm-guest-vpn
gcloud config set api_endpoint_overrides/storage https://storage-dummy.p.googleapis.com/
gcloud storage ls gs://public-2025-05

This works and returns:

gs://public-2025-05/public_2025-05.txt

Because storage-dummy.p.googleapis.com resolves to 199.36.153.8 (routed via VPN), and gcloud sends Host: storage-dummy.p.googleapis.com. The Private Google Access endpoint is somewhat lenient with SERVICE-ENDPOINT.p.googleapis.com hostnames.

However, if you use a name that doesn't match the SERVICE-*.p.googleapis.com pattern for the Host header:

# On vm-guest-vpn
gcloud config set api_endpoint_overrides/storage https://storag-dummy.p.googleapis.com/ # Note: "storag"
gcloud storage ls gs://public-2025-05

This results in:

ERROR: (gcloud.storage.ls) gs://public-2025-05 not found: 404.

In 'Accessing Google APIs from endpoints' under 'Access global Google APIs', there is an item Using endpoints. According to this:

For example, if the endpoint name is xyz, DNS records are created for storage-xyz.p.googleapis.com, compute-xyz.p.googleapis.com, and other commonly used APIs in the API bundle.
Note: If you are developing custom libraries that can use endpoints, you must set the Host header and SNI to a valid hostname for the service to which you are sending requests. A valid hostname is either the service-specific default domain name (e.g., storage.googleapis.com) or the service's SERVICE-ENDPOINT.p.googleapis.com DNS name (if available). For the SERVICE-ENDPOINT.p.googleapis.com name, the SERVICE part of the name must match the service, but you can use any value for ENDPOINT.

This document supports the idea that the endpoint side responds based on 'what Host it is being called with'.

In conclusion, if you want to connect using gcloud's Endpoint Override to private.googleapis.com effectively for Storage:

  1. You need DNS resolution (e.g., via /etc/hosts or actual DNS) for storage.googleapis.com to point to an IP of private.googleapis.com (e.g., 199.36.153.8). Then, gcloud will work without endpoint override, sending Host: storage.googleapis.com.
  2. Or, use a SERVICE-ENDPOINT.p.googleapis.com style hostname (e.g., storage-myendpoint.p.googleapis.com) that resolves to an IP of private.googleapis.com, and set api_endpoint_overrides/storage to https://storage-myendpoint.p.googleapis.com/.

It would be nice if Google provided a canonical name like storage.private.googleapis.com that worked out of the box.
By the way, storage-private.googleapis.com (if it existed and resolved to private.googleapis.com IPs) might work with gcloud's endpoint override. The author notes storage-private.googleapis.com (the actual domain) seems to resolve to the public storage.googleapis.com.

Note: Don't forget to revert the gcloud config settings. You can revert them by executing the following commands on vm-guest-vpn:

gcloud config unset auth/disable_ssl_validation
gcloud config unset api_endpoint_overrides/storage
# And remove entries from /etc/hosts

The curl Method

Now, curl can specify the Host header with the -H option. We have more freedom here.
First, let's confirm an example where access is 'not possible' or 'denied by IP filter' when going through public endpoints from vm-guest-vpn (assuming no /etc/hosts modification).

# On vm-guest-vpn
echo "curl https://storage.googleapis.com/public-2025-05/public_2025-05.txt"
curl https://storage.googleapis.com/public-2025-05/public_2025-05.txt
echo "curl https://storage.googleapis.com/private-2025-05/private_2025-05.txt"
curl https://storage.googleapis.com/private-2025-05/private_2025-05.txt
echo "curl https://storage.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt"
curl https://storage.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
echo "curl https://storage.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt"
curl https://storage.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt
echo "curl https://storage.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt"
curl https://storage.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt
echo "curl https://storage.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt"
curl https://storage.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt

Output from vm-guest-vpn (public internet access, IP filtering will block):

curl https://storage.googleapis.com/public-2025-05/public_2025-05.txt
public_2025-05.txt content
curl https://storage.googleapis.com/private-2025-05/private_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>
curl https://storage.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>There is an IP filtering condition that is preventing access to the resource.</Details></Error>
curl https://storage.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>
curl https://storage.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>There is an IP filtering condition that is preventing access to the resource.</Details></Error>
curl https://storage.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>

Since the access path doesn't change (still public internet), the results for IP-filtered buckets show IP filtering denial.

Now, let's change the endpoint to private.googleapis.com (which resolves to 199.36.153.8, routed via VPN) and use curl with the correct Host header.

echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/public-2025-05/public_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/public-2025-05/public_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/private-2025-05/private_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/private-2025-05/private_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt"
echo "curl -H \"Host: storage.googleapis.com\" \"https://private.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt\""
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt"

(Note: The original curl examples used XML API paths like https://storage.googleapis.com/BUCKET_NAME/OBJECT_NAME. For private.googleapis.com, it's generally better to use JSON API paths like https://private.googleapis.com/storage/v1/b/BUCKET_NAME/o/OBJECT_NAME?alt=media or the www.googleapis.com/download/storage/v1/... path. The original article's curl to private.googleapis.com/BUCKET/OBJECT might have worked due to some flexibility in the endpoint, but explicit API paths are more robust. I've updated to use a common JSON API path for object download.)

Output from vm-guest-vpn (using private.googleapis.com via VPN, with correct Host header):

curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/public-2025-05/public_2025-05.txt"
public_2025-05.txt
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/private-2025-05/private_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt"
ip-vpn-all_2025-05.txt
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-sa-2025-05/ip-vpn-sa_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt"
ip-allow-all_2025-05.txt
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-sa-2025-05/ip-allow-sa_2025-05.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. Permission 'storage.objects.get' denied on resource (or it may not exist).</Details></Error>

Note that the results have changed for ip-vpn-all-2025-05 and ip-allow-all-2025-05. Let's highlight the important points:

Previously (Accessing storage.googleapis.com directly, which goes over public internet from vm-guest-vpn):

# NG
curl https://storage.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>There is an IP filtering condition that is preventing access to the resource.</Details></Error>

# OK
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-vpn-all-2025-05/ip-vpn-all_2025-05.txt"
ip-vpn-all_2025-05.txt

# NG
curl https://storage.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt
<?xml version='1.0' encoding='UTF-8'?><Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>There is an IP filtering condition that is preventing access to the resource.</Details></Error>

# OK
curl -H "Host: storage.googleapis.com" "https://private.googleapis.com/ip-allow-all-2025-05/ip-allow-all_2025-05.txt"
ip-allow-all_2025-05.txt

Similar changes for ip-allow-all-2025-05.
It was confirmed that even using the curl command (with proper routing and Host header), access is possible from IP ranges allowed by Bucket IP Filtering when traffic originates from the VPN.

Access Check Part 4

Let's also check access from the author's home (using YOUR_IP which is allowed in ip-allow-*-2025-05 buckets, and assuming gcloud is authenticated as YOUR_ACCOUNT which has necessary IAM). The email address has been replaced with test@example.com.
Running ls_all_buckets.sh from the local machine (where YOUR_IP is the public IP):

gs://public-2025-05/
gs://public-2025-05/public_2025-05.txt
gs://private-2025-05/
gs://private-2025-05/private_2025-05.txt
gs://ip-vpn-all-2025-05/
ERROR: (gcloud.storage.ls) [test@example.com] does not have permission to access b instance [ip-vpn-all-2025-05] (or it may not exist): There is an IP filtering condition that is preventing access to the resource. This command is authenticated as test@example.com which is the active account specified by the [core/account] property.
gs://ip-vpn-sa-2025-05/
ERROR: (gcloud.storage.ls) [test@example.com] does not have permission to access b instance [ip-vpn-sa-2025-05] (or it may not exist): There is an IP filtering condition that is preventing access to the resource. This command is authenticated as test@example.com which is the active account specified by the [core/account] property.
gs://ip-allow-all-2025-05/
gs://ip-allow-all-2025-05/ip-allow-all_2025-05.txt
gs://ip-allow-sa-2025-05/
gs://ip-allow-sa-2025-05/ip-allow-sa_2025-05.txt

Therefore:

  • Access to two buckets (ip-vpn-all and ip-vpn-sa without YOUR_IP in their filter) is not possible because the source IP (YOUR_IP) is outside the VPN IP Range allowed for them.
  • Access is possible if the external IP (YOUR_IP) is configured in the bucket's IP filter (ip-allow-all, ip-allow-sa).
  • IAM permissions are also correctly evaluated.

Finally

In this article, we configured a Cloud VPN between two Google Cloud projects using gcloud commands and verified that Cloud Storage's Bucket IP Filtering feature can be used via this VPN connection. By utilizing the Private Google Access for on-premises hosts mechanism, it is possible to make access from the VPN peer network (in this article, network-vpn-guest) be recognized as access from a permitted IP range within the VPC network configured on the bucket side (network-vpn-host).

Let's reconfirm the main steps and points to note obtained through this verification:

  1. Importance of Route Configuration: To access a bucket with Bucket IP Filtering configured from the VPN peer side (guest VM), it is essential to configure either a static route (adopted in this article) or BGP route advertisement by Cloud Router so that traffic to private.googleapis.com (199.36.153.8/30) goes through the VPN tunnel. Forgetting this will cause traffic to go via the internet (or an endpoint within the peer VPN if Private Google Access is not correctly configured on the guest VPC), failing to meet the IP Filtering conditions.
  2. Points to Note When Using gcloud Commands:
    • When trying to access via private.googleapis.com with the gcloud storage command, simply setting gcloud config set api_endpoint_overrides/storage https://private.googleapis.com/ is insufficient. This is because the Host header of the request sent by gcloud (which becomes private.googleapis.com) prevents the API side from processing the request correctly for standard storage operations.
    • To work around this with gcloud, you would typically need appropriate DNS configuration (e.g., making storage.googleapis.com resolve to an IP of private.googleapis.com via /etc/hosts or internal DNS, and not using endpoint override) or use a SERVICE-ENDPOINT.p.googleapis.com style hostname (e.g., storage-myendpoint.p.googleapis.com resolving to private.googleapis.com IPs) with the endpoint override.
  3. Direct Access with curl, etc.: When using a tool like curl that can explicitly specify the Host header, by specifying private.googleapis.com (or its IP) in the access URL and adding the -H "Host: storage.googleapis.com" option, verification of VPN access and IP Filtering is possible without complex DNS or gcloud specific workarounds. This is a useful method for understanding API behavior.
  4. IP Filtering and IAM are Separate: Bucket IP Filtering is, strictly speaking, a feature that 'adds' filtering based on the source IP address at the network level. In addition to this, the accessing user or service account must be granted appropriate IAM permissions (e.g., roles/storage.objectViewer). Access is permitted only when both conditions are met.

Bucket IP Filtering can be an effective means of strengthening access control from internal networks connected by VPN or Cloud Interconnect. However, especially when using client tools like gcloud, it is important to understand the behavior of Private Google Access endpoints and the Host header. It is advisable to have a good understanding of the explanation for Private Google Access for on-premises hosts.

It was found that Bucket IP Filtering can permit or deny access by considering it as "access from a specific IP range from a specific VPC network," even when using the aforementioned VPN connection. This is convenient as clients, even those without specific service account permissions (for public buckets), can access resources if their IP is allowed and they use a valid SSL certificate.

Of course, since global IPs can also be individually permitted, you can allow access only from client PCs within a site by registering the site's external IP, but it goes without saying that being able to specify internal IP ranges (via VPC network source in the filter) allows for more granular control.

※ This article includes citations from an article by 株式会社grasys. (link to grasys article). The copyright of the original article belongs to grasys Inc., and it is reproduced here with permission.

References

google cloud

Posted by tako