Skip to content

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:

SSL Mode: ENCRYPTED_ONLY

Connection strings use SSL by default. The connection secret format (stored in {env}-microservice-{name}-db-connection) includes the SSL CA certificate.

See Also