GCP VPC Service Controls security PCI DSS access denied

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.

Jerzy Kopaczewski ·
Wywołania API do BigQuery, Cloud Storage lub KMS kończą się błędem 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:

  1. 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.

  2. Projekt nie jest uwzględniony w zasobach perymetru. Projekt, z którego pochodzi wywołanie (lub projekt docelowy), nie jest dodany do listy resources perymetru. VPC-SC traktuje taki projekt jako zewnętrzny i blokuje komunikację.

  3. API nie jest na liście restricted_services powodując implicit deny. Jeśli serwis API (np. bigquery.googleapis.com) nie jest dodany do restricted_services perymetru, zapytanie do tego serwisu z chronionego projektu może być traktowane inaczej niż oczekiwano. Konfiguracja bywa nieintuicyjna - niektóre serwisy wymagają jawnego dodania.

  4. 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.

  5. 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.

 

Jerzy Kopaczewski

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.