Security Review — checklist¶
Security review to systematyczne przejście przez każdą warstwę architektury pod kątem bezpieczeństwa. Robimy to po każdej zmianie Terraform, przed każdym apply.
Dlaczego security review jako kod?¶
Większość projektów ma security review jako slajdy PowerPoint, które nikt nie czyta. My mamy checklist jako /sec-review — uruchamiasz przed deployem, dostajesz raport. Zasady nie istnieją dopóki nie są testowalne.
Checklist — warstwa po warstwie¶
IAM¶
✅ SA nie mają roles/owner ani roles/editor
✅ gh-infra-worker: roles/iam.securityAdmin (nie projectIamAdmin)
✅ cloud-run-backend-sa: tylko logging.logWriter + datastore.user
✅ api-gateway-sa: tylko roles/run.invoker (na konkretnym service, nie na projekcie)
✅ Brak tworzenia SA keys — wszystko przez WIF lub ADC
Zasada: każde SA ma minimalny zestaw ról potrzebnych do działania. Nic więcej.
Dlaczego iam.securityAdmin zamiast resourcemanager.projectIamAdmin?
projectIamAdmin:
- może nadawać WSZYSTKIE role na projekcie
- jeśli gh-infra-worker zostanie skompromitowany → attacker ma god mode
iam.securityAdmin:
- może nadawać role, ale tylko te które sam posiada
- węższy blast radius przy kompromitacji
Cloud Run¶
✅ ingress = INGRESS_TRAFFIC_ALL (wymagane — API Gateway nie jest LB)
✅ IAM isolacja: tylko api-gateway-sa ma roles/run.invoker
✅ SA: cloud-run-backend-sa (nie default Compute SA)
⚠️ min_instance_count = 0 (cold start, ale nie security issue)
ℹ️ VPC Connector usunięty (ADR-008) — brak zasobów VPC-wewnętrznych; egress przez internet
Cloud Run z ingress=ALL jest technicznie dostępny z internetu. Ochrona:
1. Brak allUsers w IAM → 403 bez tokenu
2. Brak allAuthenticatedUsers → tylko api-gateway-sa może wywołać
# Test: wywołanie Cloud Run bez tokenu
curl -si https://backend-api-mngq3uouha-lm.a.run.app/api
# Oczekiwane: 403 Forbidden
Sieć¶
✅ VPC: auto_create_subnetworks = false
✅ Firewall: deny-all ingress (priority 65534) — eksplicytna dokumentacja intent
✅ private_ip_google_access = true — bez publicznych IP dla zasobów VPC
ℹ️ VPC Connector: usunięty (ADR-008) — $7/mies oszczędności, brak VPC-internal zasobów
Secrets Management¶
✅ OAuth credentials w Secret Manager (nie w plaintext TF zmiennych)
✅ TF zmienne sensitive=true dla credentiali
✅ GitHub Secrets — nie repozytoria kluczy SA
✅ Żadnych sekretów w kodzie (app.js apiKey jest publiczny z założenia)
✅ State bucket: public_access_prevention = enforced
# DOBRZE: Secret Manager
resource "google_secret_manager_secret_version" "oauth_secret" {
secret_data = var.google_oauth_client_secret # sensitive var
}
# ŹLE: plaintext w output
output "oauth_secret" {
value = var.google_oauth_client_secret # NIGDY NIE RÓB TEGO
}
API Gateway¶
✅ Wszystkie ścieżki mają security: - firebase: []
✅ OPTIONS (CORS preflight) ma security: [] (wymagane — preflight nie ma JWT)
✅ JWT walidowany: issuer, audience, podpis, expiry
✅ Backend SA: api-gateway-sa (nie default SA)
Storage¶
✅ State bucket: public_access_prevention = enforced (wymagane)
⚠️ Frontend buckets: public_access_prevention = unspecified + allUsers objectViewer
→ celowe — hosting publicznej SPA. Tylko czytanie obiektów, nie zarządzanie.
Różnica między state bucket a frontend bucket:
# State bucket — musi być prywatny
resource "google_storage_bucket" "tf_state" {
public_access_prevention = "enforced" # ✅
}
# Frontend bucket — publiczny SPA hosting
resource "google_storage_bucket" "frontend" {
public_access_prevention = "unspecified" # (1)
}
resource "google_storage_bucket_iam_member" "public_read" {
role = "roles/storage.objectViewer" # tylko czytanie plików
member = "allUsers"
}
unspecifiedboenforcedblokowałobyallUsersIAM binding. Nie używamyinherited(dziedziczyłoby z org policy).
Service Account Keys¶
✅ Brak tworzenia SA keys w całym TF (google_service_account_key)
✅ CI/CD: WIF (keyless) — tymczasowe tokeny, ważne 1h
✅ Lokalne: gcloud ADC (user credentials) lub impersonacja SA
Jeśli znajdziesz google_service_account_key w jakimkolwiek TF pliku — usuń natychmiast.
Security scanning pipeline¶
Oprócz ręcznego /sec-review, każdy PR i push uruchamia automatyczny pipeline w security.yml:
| Tool | Cel | Blokuje gdy |
|---|---|---|
pip-audit |
CVE w zależnościach Pythona | Znane podatności w requirements.txt |
trivy fs |
CVE w plikach projektu | CRITICAL lub HIGH |
checkov |
IaC misconfigurations w tf/ |
Finding poza .checkov.yaml |
Trivy container scan jest dodatkowo zintegrowany w deploy-backend.yml — skanuje zbudowany obraz Docker przed gcloud run deploy.
# Lokalnie — uruchom te same skany
pip-audit -r app/requirements.txt
checkov -d tf/ --config-file .checkov.yaml
Świadome kompromisy bezpieczeństwa¶
Nie wszystko jest idealne — to są świadome decyzje z udokumentowanym uzasadnieniem:
| Kompromis | Powód | Ścieżka upgrade |
|---|---|---|
ingress=ALL na Cloud Run |
API Gateway nie jest LB; izolacja przez IAM | Private Service Connect (złożone) |
| VPC Connector usunięty | Brak zasobów VPC-wewnętrznych; $7/mies oszczędności (ADR-008) | Ponownie dodać gdy Cloud SQL lub inny VPC zasób |
datastore.owner dla gh-infra-worker |
datastore.admin niedostępny na poziomie projektu |
Brak prostszej alternatywy w GCP |
| JWT decode bez weryfikacji w FastAPI | API Gateway już weryfikuje; backend chroniony IAM | Dodać python-jose jeśli defence-in-depth wymagane |
| Jeden SA dla Cloud Run prod+staging | Uproszczenie | Osobne SA per env |
Pułapki¶
State Terraform zawiera wrażliwe dane
terraform.tfstate może zawierać: endpointy Cloud Run, SA emails, czasem wartości zmiennych. State bucket musi mieć public_access_prevention = enforced i dostęp tylko dla gh-infra-worker i własnych adminów.
Terraform output sensitive=false
Nigdy nie dodawaj do outputs.tf wartości takich jak client_secret, klucze, tokeny bez sensitive = true. Outputs są widoczne w logach GitHub Actions.
Security review przed każdym PR
Uruchom /sec-review przed merge PR zawierającego zmiany w tf/. W tym projekcie jest to skill Claude Code — wykonuje automatyczną weryfikację wszystkich powyższych punktów na podstawie git diff.