Security Model¶
Before You Read¶
This page covers security architecture. For hands-on operations see Secrets Management and Access & Permissions.
Security Layers¶
graph TB
L1["Layer 1: Network\nFirewall rules · VPC · Private IPs"]
L2["Layer 2: Identity\nGCP IAM · Workload Identity · Service Accounts"]
L3["Layer 3: Transport\nIstio mTLS · TLS 1.3 at ingress"]
L4["Layer 4: Secrets\nGCP Secret Manager · External Secrets Operator"]
L5["Layer 5: Application\nAPI keys · JWT · OAuth2 · KMS encryption"]
L1 --> L2 --> L3 --> L4 --> L5
Network Security¶
Dev: Zero-Trust Firewall¶
The development cluster enforces zero-trust at the GCP firewall level. Two complementary rules are defined via the modules/network module:
# Priority 700 — allow known IPs
orofi-dev-cloud-dev-zerotrust-allow:
source_ranges: ["35.226.57.140/32", "10.0.0.0/8", "11.0.0.0/16"]
allow: ALL protocols
# Priority 800 — deny everything else
orofi-dev-cloud-dev-zerotrust-deny:
source_ranges: ["0.0.0.0/0"]
deny: ALL protocols
The 35.226.57.140/32 entry is the Bitbucket Pipelines runner IP, allowing CI/CD access. The RFC-1918 ranges cover internal cluster and cross-environment traffic.
Staging: Istio-Layer Control¶
Staging does not enforce zero-trust at the firewall level (zero_trust = false). Access control happens at the Istio layer via:
- PeerAuthentication requiring mTLS for all service-to-service calls
- VirtualServices limiting which hostnames are exposed
- The GKE control plane is still restricted to 35.226.57.140/32, 10.0.0.0/8, 11.0.0.0/16
Private Networking for Data Services¶
All data services (Cloud SQL, Redis) are private-only: - No public IP is assigned to Cloud SQL instances - Redis connects via Private Service Access - Database FQDNs resolve to private VPC IPs - Services connect to databases using DNS names that resolve within the VPC
Istio mTLS (Service-to-Service)¶
Every microservice namespace has a PeerAuthentication resource configured in STRICT mode. This means:
- All inter-service HTTP traffic inside the mesh is automatically upgraded to mTLS
- A service without a valid SPIFFE identity cannot connect to another service
- SPIFFE identities are issued by Istio based on GKE Workload Identity
The shared Helm chart at infrastructure-configuration/projects/templates/microservice/helm/templates/peerauthentication.yaml applies this to all application namespaces.
TLS at Ingress¶
External HTTPS termination happens at the Istio IngressGateway (oro-gateway):
- Certificate: istio-tls-cert (in istio-system namespace)
- Issued by: Let's Encrypt production via cert-manager
- Mode: SIMPLE (one-way TLS from client to gateway)
- After termination, traffic flows as mTLS inside the cluster
IAM & Workload Identity¶
Principle of Least Privilege¶
Every workload has a dedicated GCP service account with only the permissions it needs. No two workloads share a service account.
graph LR
subgraph K8s["Kubernetes"]
KSA["K8s ServiceAccount\n(namespace-scoped)"]
Pod["Pod"]
end
subgraph GCP["GCP"]
GSA["GCP Service Account\n(project-scoped)"]
SecMgr["Secret Manager\n(specific secrets only)"]
CloudSQL["Cloud SQL\n(specific instance only)"]
Storage["Cloud Storage\n(specific bucket only)"]
end
Pod --> KSA
KSA -->|Workload Identity binding| GSA
GSA --> SecMgr
GSA --> CloudSQL
GSA --> Storage
Service Account Permissions Matrix¶
Microservice Service Accounts (all have roles/iam.workloadIdentityUser):
| Service Account | Extra Permissions | Secrets Accessible |
|---|---|---|
api-gateway-public-sa |
— | stage-api-gateway-public-secret, shared secrets, redis auth |
api-gateway-account-sa |
— | stage-api-gateway-account-secret, shared secrets, redis auth |
api-gateway-oro-sa |
— | stage-api-gateway-oro-secret, shared secrets, redis auth |
api-gateway-admin-dashboard-sa |
— | stage-api-gateway-admin-dashboard-secret, shared secrets, redis auth |
microservice-communication-sa |
roles/storage.admin |
stage-microservice-communication-secret, Firebase, shared secrets |
microservice-identity-sa |
roles/storage.admin |
stage-microservice-identity-secret, Firebase, shared secrets |
microservice-monolith-sa |
roles/storage.admin |
stage-microservice-monolith-secret, shared secrets |
microservice-analytics-sa |
roles/storage.admin |
stage-microservice-analytics-secret, shared secrets |
Platform Service Accounts:
| Service Account | Permissions | Purpose |
|---|---|---|
bitbucket |
roles/artifactregistry.writer, roles/storage.objectCreator/Viewer, roles/secretmanager.viewer/secretAccessor, roles/iam.serviceAccountTokenCreator |
Bitbucket Pipelines CI/CD |
k8s-scaler-cross |
roles/container.admin, roles/container.clusterAdmin |
Cross-project cluster scaling (staging → dev) |
flyway-staging-admin |
DB access via secret | Database migrations |
firebase-admin-sdk |
roles/firebase.sdkAdminServiceAgent |
Firebase Admin operations |
{env}-ext-secrets-manager |
roles/secretmanager.secretAccessor |
External Secrets Operator |
terraform-mnl@orofi-stage-cloud |
Broad infra permissions | Terraform automation (staging) |
terraform-mnl@orofi-dev-cloud |
Broad infra permissions | Terraform automation (dev) |
Workload Identity Binding¶
For each microservice, a Workload Identity binding is created via modules/service-accounts:
# Kubernetes ServiceAccount → GCP ServiceAccount binding
resource "google_service_account_iam_binding" "workload_identity" {
service_account_id = google_service_account.sa.name
role = "roles/iam.workloadIdentityUser"
members = [
"serviceAccount:{project}.svc.id.goog[{namespace}/{k8s-sa-name}]"
]
}
This allows pods running as the Kubernetes service account to impersonate the GCP service account without any static credentials.
Secrets Management¶
Architecture: GCP Secret Manager → Kubernetes¶
Secrets follow a two-stage pipeline:
flowchart LR
GCPSec["GCP Secret Manager\n(source of truth)"]
ESO["External Secrets Operator\n(external-secrets namespace)"]
K8SSec["Kubernetes Secret\n(namespace-scoped)"]
Pod["Pod\n(reads as env var or volume)"]
GCPSec -->|sync via ESO| K8SSec
ESO -->|reconciles every N minutes| K8SSec
K8SSec --> Pod
The External Secrets Operator service account ({env}-ext-secrets-manager) has roles/secretmanager.secretAccessor on all secrets. It runs in the external-secrets namespace and creates Kubernetes Secrets in each application namespace.
Critically: Kubernetes Secret manifests are never committed to Git. Only the ExternalSecret resource (referencing a secret name in GCP) is in Git.
Secrets Inventory¶
See Secrets Management Guide for the full inventory and how to add/rotate secrets.
KMS Encryption (Identity Service)¶
The microservice-identity service uses GCP KMS for data-level encryption:
| Key Ring | Location | Keys |
|---|---|---|
identity-microservice-{env} |
us-central1 |
data-hmac-search-key-v2, data-encryption-key-v2 |
These keys are used for searchable encryption and HMAC hashing of PII fields. They are provisioned via modules/kms and the service account microservice-identity-sa is granted roles/cloudkms.cryptoKeyEncrypterDecrypter.
JWT & API Keys¶
The identity microservice owns all authentication primitives:
- microservice-identity-jwt-private-key-secret — RSA private key for signing JWTs
- microservice-identity-apikey-private-key-secret — Private key for API key signing
- microservice-identity-encryption-search-hash-pepper-key-secret — HMAC pepper for search hashing
- admin-dashboard-gateway-apikey, oro-gateway-apikey, public-gateway-apikey, account-gateway-apikey — Static API keys used by gateways to authenticate with downstream services
Internal Tool Authentication¶
Developer-facing tools (Kafka UI, Mongo Express, Grafana) are protected by OAuth2 Proxy enforcing Google OAuth2:
# oauth2-proxy configuration (from tools/kafka-ui/values-dev.yaml)
clientID: 707248768728-el7g0trka40ekp0184q9c1fqa45no5go.apps.googleusercontent.com
emailDomain: orofi.xyz # Only @orofi.xyz Google accounts allowed
This means any user with a valid @orofi.xyz Google account can access internal tools after OAuth2 authentication. No separate password is needed.
AppStore Credentials¶
Mobile app release credentials are stored in Secret Manager:
- {env}-appstore-api-key — App Store Connect API key
- {env}-appstore-cert — Signing certificate
- {env}-appstore-profile — Provisioning profile
Cloud SQL SSL¶
All database connections require encryption:
Connection strings use SSL by default. The connection secret format (stored in {env}-microservice-{name}-db-connection) includes the SSL CA certificate.
See Also¶
- Secrets Management Guide — How to add, update, rotate secrets
- Access & Permissions — What access to request
- Compliance — Audit posture and access matrix
- Security Incident Runbook — Incident response