GCP VPC Service Controls - request zablokowany przez perymetr
Diagnoza i naprawa zablokowanych request API przez VPC Service Controls: weryfikacja access level, konfiguracja perymetru, ingress/egress rules, audit logi VPC-SC.
PERMISSION_DENIED lub Request is prohibited by organization's policy mimo poprawnych uprawnień IAM. VPC Service Controls perymetr aktywnie blokuje wywołanie API (request) - tożsamość lub projekt wywołujący nie jest autoryzowany do przekroczenia granicy perymetru. Serwisy wewnątrz perymetru są niedostępne z zewnątrz, a próba dostępu z nieautoryzowanego kontekstu generuje odmowę (denial) na poziomie organizacji.
Ten runbook opisuje diagnozę i naprawę sytuacji, w której VPC Service Controls blokuje poprawne wywołania API. Szczegółowe omówienie architektury bezpieczeństwa GCP z VPC-SC w kontekście PCI DSS znajdziesz w artykule Infrastruktura GCP dla fintechu - PCI DSS, SOC2 i Google Cloud.
Objaw
Wywołania API do serwisów chronionych perymetrem VPC-SC zwracają błędy autoryzacji mimo poprawnych uprawnień IAM:
# BigQuery query zwraca PERMISSION_DENIED
bq query --project_id=protected-project \
'SELECT * FROM dataset.table LIMIT 10'
# Error: Access Denied: BigQuery BigQuery: Request is prohibited by
# organization's policy. vpcServiceControlsUniqueIdentifier: abcd1234
# Cloud Storage zwraca błąd
gsutil ls gs://protected-bucket/
# AccessDeniedException: 403 Request is prohibited by organization's policy.
# VPC Service Controls: Request is prohibited by organization's policy.
# vpcServiceControlsUniqueIdentifier: efgh5678
# KMS zwraca odmowę
gcloud kms keys list \
--location=europe-west1 \
--keyring=my-keyring \
--project=protected-project
# ERROR: (gcloud.kms.keys.list) PERMISSION_DENIED: Request is prohibited
# by organization's policy.
# Terraform plan/apply kończy się błędem
terraform plan
# Error: Error when reading or editing BigQuery Dataset:
# googleapi: Error 403: Request is prohibited by organization's policy.
# VPC Service Controls: Request is prohibited by organization's policy.
# Service account w Cloud Function nie może odczytać z GCS
# Logi Cloud Function:
# google.api_core.exceptions.Forbidden: 403 Request is prohibited by
# organization's policy. vpcServiceControlsUniqueIdentifier: ijkl9012
Kluczowy identyfikator to vpcServiceControlsUniqueIdentifier - pozwala on odnaleźć konkretną odmowę w Audit Logs i zdiagnozować przyczynę.
Przyczyna
VPC Service Controls blokuje wywołanie z pięciu głównych powodów:
-
Service account lub użytkownik nie jest w poziomie dostępu (access level). Perymetr VPC-SC wymaga, aby wywołujący spełniał warunki zdefiniowane w poziomie dostępu (np. konkretna sieć korporacyjna, urządzenie zarządzane, tożsamość w dozwolonej liście). Jeśli zapytanie pochodzi z IP poza dozwoloną siecią lub od tożsamości bez poziomu dostępu - jest blokowane.
-
Projekt nie jest uwzględniony w zasobach perymetru. Projekt, z którego pochodzi wywołanie (lub projekt docelowy), nie jest dodany do listy
resourcesperymetru. VPC-SC traktuje taki projekt jako zewnętrzny i blokuje komunikację. -
API nie jest na liście restricted_services powodując implicit deny. Jeśli serwis API (np.
bigquery.googleapis.com) nie jest dodany dorestricted_servicesperymetru, zapytanie do tego serwisu z chronionego projektu może być traktowane inaczej niż oczekiwano. Konfiguracja bywa nieintuicyjna - niektóre serwisy wymagają jawnego dodania. -
Reguły ingress/egress są źle skonfigurowane. Ingress rules kontrolują kto może wejść do perymetru, egress rules - co może wyjść. Częsty błąd: brak ingress rule dla CI/CD pipeline, Terraform runner lub Cloud Function w innym projekcie.
-
Użycie gcloud/gsutil z lokalizacji poza perymetrem bez poziomu dostępu. Developer pracujący z laptopa (poza siecią korporacyjną) próbuje wywołać API chronione perymetrem. Bez skonfigurowanego poziomu dostępu bazującego na tożsamości (a nie tylko IP) - wywołanie jest blokowane.
Rozwiązanie
Krok 1: Znajdź odmowę w VPC-SC Audit Logs
# Wyszukaj VPC-SC denial po unique identifier z komunikatu błędu
gcloud logging read \
'protoPayload.metadata.@type="type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata" AND
protoPayload.metadata.vpcServiceControlsUniqueId="abcd1234"' \
--project=protected-project \
--format="json" \
--limit=5
# Alternatywnie - szukaj wszystkich denial w ostatniej godzinie
gcloud logging read \
'protoPayload.metadata.@type="type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata" AND
protoPayload.metadata.violationReason!=""' \
--project=protected-project \
--freshness=1h \
--format="json(
timestamp,
protoPayload.authenticationInfo.principalEmail,
protoPayload.methodName,
protoPayload.metadata.violationReason,
protoPayload.metadata.resourceNames,
protoPayload.metadata.accessLevels,
protoPayload.metadata.vpcServiceControlsUniqueId
)"
# Przykładowy output:
# {
# "timestamp": "2026-07-31T08:15:23Z",
# "protoPayload": {
# "authenticationInfo": {"principalEmail": "terraform@ci-project.iam.gserviceaccount.com"},
# "methodName": "google.storage.objects.list",
# "metadata": {
# "violationReason": "NO_MATCHING_ACCESS_LEVEL",
# "resourceNames": ["projects/_/buckets/protected-bucket"],
# "accessLevels": []
# }
# }
# }
Krok 2: Sprawdź konfigurację perymetru
# Wylistuj perymetry w organizacji
gcloud access-context-manager perimeters list \
--policy=POLICY_ID \
--format="table(name, title, status.resources, status.restrictedServices)"
# Szczegóły konkretnego perymetru
gcloud access-context-manager perimeters describe my-perimeter \
--policy=POLICY_ID \
--format="yaml(name, title, status)"
# Sprawdź access levels powiązane z perymetrem
gcloud access-context-manager levels list \
--policy=POLICY_ID \
--format="table(name, title, basic.conditions)"
# Sprawdź szczegóły access level
gcloud access-context-manager levels describe my-access-level \
--policy=POLICY_ID \
--format="yaml(basic.conditions)"
Krok 3: Dodaj tożsamość do poziomu dostępu (dla użytkowników/service accounts)
# Utwórz lub zaktualizuj access level z listą dozwolonych tożsamości
gcloud access-context-manager levels update my-access-level \
--policy=POLICY_ID \
--title="CI/CD and admin access" \
--basic-level-spec=access-level-spec.yaml
# Plik access-level-spec.yaml:
cat << 'EOF' > access-level-spec.yaml
- members:
- serviceAccount:terraform@ci-project.iam.gserviceaccount.com
- serviceAccount:cloud-function@protected-project.iam.gserviceaccount.com
- user:admin@company.com
# Opcjonalnie: ogranicz do konkretnych IP
ipSubnetworks:
- 203.0.113.0/24
- 198.51.100.0/24
EOF
# Zastosuj zaktualizowany access level
gcloud access-context-manager levels update my-access-level \
--policy=POLICY_ID \
--basic-level-spec=access-level-spec.yaml
Krok 4: Skonfiguruj ingress rules dla zewnętrznych wywołań
# Ingress rules pozwalają na precyzyjną kontrolę kto może wejść do perymetru
# i do jakich serwisów/zasobów ma dostęp
# Zaktualizuj perymetr z ingress rule
gcloud access-context-manager perimeters update my-perimeter \
--policy=POLICY_ID \
--set-ingress-policies=ingress-policy.yaml
# Plik ingress-policy.yaml:
cat << 'EOF' > ingress-policy.yaml
- ingressFrom:
identityType: ANY_IDENTITY
sources:
- accessLevel: accessPolicies/POLICY_ID/accessLevels/my-access-level
ingressTo:
operations:
- serviceName: storage.googleapis.com
methodSelectors:
- method: "*"
- serviceName: bigquery.googleapis.com
methodSelectors:
- method: "*"
resources:
- projects/123456789
- ingressFrom:
identities:
- serviceAccount:terraform@ci-project.iam.gserviceaccount.com
sources:
- resource: projects/987654321
ingressTo:
operations:
- serviceName: storage.googleapis.com
methodSelectors:
- method: google.storage.objects.get
- method: google.storage.objects.list
- serviceName: cloudkms.googleapis.com
methodSelectors:
- method: "*"
resources:
- projects/123456789
EOF
Krok 5: Dodaj projekt do zasobów perymetru (jeśli brakuje)
# Dodaj projekt do perymetru
gcloud access-context-manager perimeters update my-perimeter \
--policy=POLICY_ID \
--add-resources="projects/MISSING_PROJECT_NUMBER"
# Sprawdź wynik
gcloud access-context-manager perimeters describe my-perimeter \
--policy=POLICY_ID \
--format="yaml(status.resources)"
Krok 6: Terraform - ingress rule jako kod (rekomendowane podejście)
# Terraform konfiguracja ingress rule dla VPC Service Controls
resource "google_access_context_manager_service_perimeter" "main" {
parent = "accessPolicies/${var.access_policy_id}"
name = "accessPolicies/${var.access_policy_id}/servicePerimeters/main_perimeter"
title = "Main Production Perimeter"
status {
resources = [
"projects/${var.protected_project_number}",
]
restricted_services = [
"bigquery.googleapis.com",
"storage.googleapis.com",
"cloudkms.googleapis.com",
]
access_levels = [
google_access_context_manager_access_level.ci_cd.name,
]
ingress_policies {
ingress_from {
identity_type = "ANY_IDENTITY"
sources {
access_level = google_access_context_manager_access_level.ci_cd.name
}
}
ingress_to {
resources = ["projects/${var.protected_project_number}"]
operations {
service_name = "bigquery.googleapis.com"
method_selectors {
method = "*"
}
}
operations {
service_name = "storage.googleapis.com"
method_selectors {
method = "*"
}
}
}
}
ingress_policies {
ingress_from {
sources {
resource = "projects/${var.ci_cd_project_number}"
}
identities = [
"serviceAccount:terraform@${var.ci_cd_project_id}.iam.gserviceaccount.com",
]
}
ingress_to {
resources = ["projects/${var.protected_project_number}"]
operations {
service_name = "storage.googleapis.com"
method_selectors {
method = "google.storage.objects.get"
}
method_selectors {
method = "google.storage.objects.list"
}
method_selectors {
method = "google.storage.objects.create"
}
}
operations {
service_name = "cloudkms.googleapis.com"
method_selectors {
method = "*"
}
}
}
}
}
}
resource "google_access_context_manager_access_level" "ci_cd" {
parent = "accessPolicies/${var.access_policy_id}"
name = "accessPolicies/${var.access_policy_id}/accessLevels/ci_cd_access"
title = "CI/CD Pipeline Access"
basic {
conditions {
members = [
"serviceAccount:terraform@${var.ci_cd_project_id}.iam.gserviceaccount.com",
"serviceAccount:deployer@${var.ci_cd_project_id}.iam.gserviceaccount.com",
]
ip_subnetworks = var.allowed_cidr_ranges
}
}
}
Krok 7: Dry-run mode dla testowania zmian (bez blokowania produkcji)
# Użyj dry-run mode do testowania nowej konfiguracji perymetru
# Dry-run loguje violation ale nie blokuje request
gcloud access-context-manager perimeters dry-run create my-perimeter-test \
--policy=POLICY_ID \
--resources="projects/123456789" \
--restricted-services="bigquery.googleapis.com,storage.googleapis.com" \
--ingress-policies=ingress-policy.yaml
# Sprawdź dry-run violations w logach
gcloud logging read \
'protoPayload.metadata.@type="type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata" AND
protoPayload.metadata.dryRun=true' \
--project=protected-project \
--freshness=1h \
--format="json(timestamp, protoPayload.metadata.violationReason)"
# Po potwierdzeniu braku false-positive - zastosuj konfigurację na produkcji
gcloud access-context-manager perimeters dry-run enforce my-perimeter \
--policy=POLICY_ID
Walidacja
Po zastosowaniu zmian w konfiguracji perymetru zweryfikuj, że poprawne wywołania przechodzą:
# 1. Powtórz request, który wcześniej był blokowany
gsutil ls gs://protected-bucket/
# Expected: lista plików (brak błędu 403)
bq query --project_id=protected-project \
'SELECT 1 AS test'
# Expected: wynik zapytania
# 2. Sprawdź brak nowych VPC-SC denial w logach
gcloud logging read \
'protoPayload.metadata.@type="type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata" AND
protoPayload.metadata.violationReason!="" AND
protoPayload.authenticationInfo.principalEmail="terraform@ci-project.iam.gserviceaccount.com"' \
--project=protected-project \
--freshness=30m
# Expected: brak wyników
# 3. Zweryfikuj że perymetr nadal chroni przed nieautoryzowanym dostępem
# (test negatywny - request z niepowiązanego service account powinien być blokowany)
gcloud auth activate-service-account unauthorized@other-project.iam.gserviceaccount.com \
--key-file=unauthorized-key.json
gsutil ls gs://protected-bucket/
# Expected: AccessDeniedException 403 (perymetr nadal chroni)
# 4. Sprawdź pełny status perymetru
gcloud access-context-manager perimeters describe my-perimeter \
--policy=POLICY_ID \
--format="yaml(status)"
# Potwierdź że resources, restrictedServices, ingressPolicies są poprawne
# 5. Uruchom Terraform plan - potwierdź brak zmian (idempotent)
terraform plan
# Expected: No changes. Your infrastructure matches the configuration.
VPC Service Controls jest fundamentem bezpieczeństwa danych w GCP dla środowisk regulowanych (PCI DSS, SOC2, HIPAA). Odmowa oznacza, że ochrona działa - problem jest w konfiguracji dostępu, nie w mechanizmie zabezpieczającym. Nigdy nie usuwaj perymetru ani nie poszerzaj go nadmiernie, aby naprawić jedną odmowę. Zamiast tego twórz precyzyjne ingress rules z minimalnym zakresem uprawnień (least privilege). Każda zmiana perymetru powinna przejść przez dry-run mode przed zastosowaniem na produkcji, aby uniknąć zablokowania poprawnego ruchu.
VPC Service Controls blokuje API w GCP?
Umów bezpłatną 30-minutową rozmowę. Zaprojektujemy konfigurację perymetru z precyzyjnymi ingress rules, poziomami dostępu i zgodnością z PCI DSS bez blokowania pipeline.