PCI Architecture
This page is the technical reference for Gatelithix’s PCI DSS 4.0.1 architecture. It covers the CDE boundary definition, PAN data flow from ingress to destruction, network segmentation, encryption scheme, audit logging, and access control — with source evidence for each control.
CDE edit policy: The vault service (apps/vault/) is the CDE boundary.
Any code change inside this boundary requires explicit confirmation before
proceeding. Do not modify vault internals as a side effect of other work. See
CLAUDE.md for the full rule set.
CDE Boundary Definition
The Cardholder Data Environment (CDE) is strictly limited to the gatelithix-pci GCP project. No system outside this project ever stores, processes, or receives raw PAN data.
Systems In CDE (gatelithix-pci project)
| System | Role | Stores PAN? | Processes PAN? | Transmits PAN? |
|---|---|---|---|---|
Token Vault (Cloud Run, apps/vault/) | PAN encryption, tokenization, resolve | No (in-memory only) | Yes | Yes (to connectors, TLS) |
| PCI Cloud SQL (PostgreSQL 16) | Encrypted PAN storage | Yes (ciphertext only) | No | No |
| Cloud KMS / HSM | Key management, encrypt/decrypt API | No | Yes (envelope encryption) | No |
PCI VPC (10.1.0.0/20) | Network isolation boundary | No | No | Yes (transit only) |
| Cloud NAT (PCI) | Auditable egress routing | No | No | No |
Systems Connected to CDE (gatelithix-core project)
These systems communicate with the CDE but never handle raw PANs. They are in scope for PCI assessment with a reduced applicable control set.
| System | Role | Why Connected? |
|---|---|---|
| Gateway Public API | API gateway, routing, orchestration | Proxies tokenization to vault; resolves tokens |
| Stripe Connector | PSP integration | Receives decrypted PAN from vault for processor tokenization |
| NMI Connector | PSP integration | Same as above |
| FluidPay Connector | PSP integration | Same as above |
| TSYS Connector (planned) | PSP integration (Transnox REST API) | Same as above |
| Core Cloud SQL | Payment state, merchant config | Stores gateway tokens (tok_...) that reference vault records |
Systems Out of Scope
| System | Why Out of Scope? |
|---|---|
| Dashboard Portal | Communicates with Gateway API only; never touches vault or PAN |
| Docs Site | Static content; no API or data connectivity |
| Auth0 (IdP) | Authentication only; no cardholder data |
| Cloudflare (DNS) | DNS resolution only; does not proxy API traffic |
| Billing & Invoicing | Operates on aggregated transaction counts; no PAN or token access |
| Reporting / Exports | Reads from core DB (tokens, metadata); no vault access |
PAN Data Flow
Flow 1: Card Tokenization (PAN Ingress)
When a merchant tokenizes a card, the PAN follows this path:
Key guarantee: The raw PAN exists only in vault process memory for the duration of the encrypt call. The Go []byte slice is zeroed with defer before the handler returns. It is never written to disk, logged, or transmitted to any non-PCI system.
Flow 2: Payment Authorization (PAN Egress via Processor Token)
When a payment is authorized, the gateway resolves a processor token from the vault:
PAN Existence Matrix
Every location where a PAN exists, in any form:
| Location | Form | Max Duration | Protection | Destruction |
|---|---|---|---|---|
| TLS channel (merchant → LB) | Plaintext in TLS envelope | Milliseconds | TLS 1.2+ | Connection close |
| TLS channel (LB → gateway) | Plaintext in TLS envelope | Milliseconds | TLS 1.2+ (Cloud Run) | Connection close |
| TLS channel (gateway → vault) | Plaintext in TLS envelope | Milliseconds | TLS 1.2+ + identity token | Connection close |
| Vault process memory (tokenize) | Plaintext bytes, Go []byte | < 100ms | Process isolation (Cloud Run) | defer byte-zero |
| Cloud KMS HSM | Inside HSM boundary | Microseconds | FIPS 140-2 Level 3 HSM | HSM internal wipe |
| PCI Cloud SQL | AES-256-GCM ciphertext + wrapped DEK | Indefinite | Private IP, TLS, CMEK | Crypto-shredding via key rotation |
| Vault process memory (resolve) | Plaintext bytes, Go []byte | < 100ms | Process isolation (Cloud Run) | defer byte-zero |
| TLS channel (vault → connector) | Plaintext in TLS envelope | Milliseconds | TLS 1.2+ + identity token | Connection close |
| Connector process memory | Plaintext string | < 200ms | Process isolation (Cloud Run) | GC after request scope |
| TLS channel (connector → PSP) | Plaintext in TLS envelope | Milliseconds | TLS 1.2+ (PSP-enforced) | Connection close |
PAN is NEVER present in:
- Core Cloud SQL (gateway database)
- Redis (rate limiting / caching)
- Cloud Pub/Sub messages
- Application logs (PAN-filter slog handler)
- Error responses to merchants
- Audit log entries (log token ref, BIN, last4 — never PAN)
- Dashboard or reporting services
Code references:
| File | PAN Handling |
|---|---|
apps/vault/handlers/tokenize.go:104-110 | PAN received as []byte, defer byte-zero |
apps/vault/handlers/tokenize.go:194 | encryptor.EncryptPAN(ctx, pan) — KMS encryption |
apps/vault/handlers/resolve.go:142 | encryptor.DecryptPAN(ctx, card.ID, encryptedPAN) — KMS decryption |
apps/vault/handlers/resolve.go:153-157 | defer byte-zero of decrypted PAN |
apps/vault/handlers/resolve.go:160-166 | PAN sent to connector via TokenizeRequest |
apps/connectors/stripe/client.go | Test mode: PAN mapped to test token; Production: PAN sent to Stripe API |
Network Segmentation
VPC Architecture
Two separate GCP projects, each with its own VPC, connected via VPC peering:
| VPC | CIDR | Project | Purpose |
|---|---|---|---|
core-vpc | 10.0.0.0/20 | gatelithix-core | Gateway API, connectors, Core Cloud SQL, Redis |
pci-vpc | 10.1.0.0/20 | gatelithix-pci | Vault service, PCI Cloud SQL, Cloud KMS |
VPC peering is bidirectional (core-to-pci / pci-to-core) with custom route import/export disabled. Only private IP traffic crosses the peering link. The PCI VPC cannot reach arbitrary core resources.
Firewall Rules
Both VPCs inherit a deny-all baseline at priority 65534 (deny all ingress from 0.0.0.0/0, deny all egress to 0.0.0.0/0). Explicit allow rules at priority 900 permit only minimum required traffic:
| Rule | Direction | Source/Destination | Port | Purpose |
|---|---|---|---|---|
pci-vpc-allow-core-ingress | Ingress | 10.0.0.0/20 | TCP 443 | Gateway → vault calls |
pci-vpc-allow-psp-egress | Egress | 0.0.0.0/0 | TCP 443 | Vault → PSP APIs |
pci-vpc-allow-restricted-apis | Egress | 199.36.153.4/30 | TCP 443 | Vault → Cloud KMS (restricted APIs) |
core-vpc-allow-pci-egress | Egress | 10.1.0.0/20 | TCP 443 | Gateway → vault calls |
core-vpc-allow-psp-egress | Egress | 0.0.0.0/0 | TCP 443 | Connectors → PSP APIs |
Evidence: infra/terraform/pci/network.tf, infra/terraform/modules/vpc/main.tf
PCI Cloud SQL Isolation
ipv4_enabled = false— no public IP address; private IP onlyssl_mode = ENCRYPTED_ONLY— rejects non-TLS database connections- Application uses
pgxwithsslmode=require - Accessible only from the PCI VPC serverless connector (
10.1.32.0/28)
Evidence: infra/terraform/pci/database.tf
Vault Cloud Run Ingress
The vault Cloud Run service uses INGRESS_TRAFFIC_ALL (required because gateway calls arrive via VPC peering private IPs, not through Cloud Run’s public URL). Network isolation is enforced at the VPC firewall level: pci-vpc-allow-core-ingress restricts inbound traffic to source 10.0.0.0/20, TCP 443 only. There is no public internet path to the vault.
Evidence: infra/terraform/pci/cloudrun.tf
Encryption
Scheme Overview
Gatelithix uses envelope encryption via Google Cloud KMS backed by Cloud HSM:
- Data Encryption Key (DEK) — A per-PAN AES-256-GCM key generated for each card.
- Key Encryption Key (KEK) — The Cloud KMS master key (
pan-encryption-key) wraps the DEK. The KEK never leaves the HSM. - Storage — PCI Cloud SQL stores the encrypted PAN ciphertext and the wrapped DEK together. Neither is useful without the other, and the wrapped DEK is useless without the KMS key.
KMS Keys
| Key | Algorithm | Protection | Rotation | Purpose |
|---|---|---|---|---|
pan-encryption-key | AES-256-GCM | HSM (FIPS 140-2 Level 3) | 90-day auto-rotation | Encrypt/decrypt PAN ciphertext |
pan-fingerprint-key | HMAC-SHA256 | HSM | 90-day auto-rotation | PAN deduplication fingerprint |
Key ring: gatelithix-vault in gatelithix-pci project.
Evidence: infra/terraform/pci/kms.tf
IAM Restriction on KMS Keys
Only the vault service account (vault-sa@gatelithix-pci.iam.gserviceaccount.com) holds the following roles on PAN keys:
roles/cloudkms.cryptoKeyEncrypterDecrypteronpan-encryption-keyroles/cloudkms.signerVerifieronpan-fingerprint-key
No other service account, user, or service has access to these keys. Gateway, connector, and other service accounts have no KMS bindings.
Evidence: infra/terraform/pci/kms.tf IAM bindings section
Crypto-Shredding
Because the master key (pan-encryption-key) wraps all DEKs, destroying a key version makes all PANs encrypted with that version permanently unrecoverable. This enables crypto-shredding for GDPR right-to-erasure without physically deleting encrypted ciphertext rows.
Encryption in Transit
All paths carrying cardholder data use TLS 1.2 or higher:
| Path | TLS Enforcement |
|---|---|
| Merchant → Load Balancer | Google-managed TLS (Cloud Run Global LB) |
| Load Balancer → Gateway | Cloud Run enforces TLS |
| Gateway → Vault | TLS via Serverless VPC Connector, identity token auth |
| Vault → PCI Cloud SQL | ssl_mode=ENCRYPTED_ONLY + sslmode=require in pgx |
| Vault → Cloud KMS | HTTPS to restricted.googleapis.com |
| Vault → Connectors | TLS via Serverless VPC Connector, identity token auth |
| Connectors → PSP APIs | TLS 1.2+ enforced by PSP (Stripe, NMI, FluidPay) |
Evidence: infra/terraform/pci/cloudrun.tf, infra/terraform/pci/database.tf, apps/vault/db/db.go (sslmode=require)
Audit Logging
What Is Logged
Every access to the vault decrypt endpoint produces an audit entry. The vault never logs the PAN itself. Each audit record contains:
| Field | Purpose |
|---|---|
event_type | token.create, processor_token.resolve, token.lookup, token.delete |
gateway_token | The tok_... reference — identifies which card was accessed without revealing PAN |
merchant_id | Which merchant owns this token |
connector_id | Which connector/PSP the PAN was resolved for (on resolve events) |
bin | First 6 digits of card (not PAN; permitted metadata) |
last4 | Last 4 digits (not PAN; permitted metadata) |
timestamp | RFC 3339 UTC |
request_id | Trace correlation ID |
actor_sa | GCP service account that made the vault call |
Evidence: apps/vault/handlers/tokenize.go:263-278, apps/vault/handlers/resolve.go audit call
Log Storage and Retention
Audit logs flow through two sinks:
- PCI Audit Log Bucket — Cloud Logging bucket with
locked = true(tamper-resistant), 365-day retention. - PCI Audit Archive — Cloud Storage NEARLINE bucket with versioning and locked retention (365 days), for long-term archival.
Both sinks capture cloudaudit.googleapis.com log entries from the gatelithix-pci project.
Evidence: infra/terraform/pci/logging.tf
Tamper Protection
The audit log bucket uses locked = true on its retention policy. This prevents retention period reduction and log deletion, including by project owners. Once a log entry reaches the bucket, it cannot be modified or deleted before the retention period expires.
GCP Cloud Audit Logs also records all Admin Activity in the gatelithix-pci project (IAM changes, resource creation/deletion), providing an additional immutable audit trail.
Evidence: infra/terraform/pci/logging.tf — locked = true, retention_days = 365
Access Control
Service Account Model
Each service runs with a dedicated service account. No service account holds roles beyond what it needs.
| Service Account | Project | Roles |
|---|---|---|
gateway-sa | gatelithix-core | roles/run.invoker on vault; Cloud SQL user on core DB; Secret Manager accessor |
vault-sa | gatelithix-pci | cloudkms.cryptoKeyEncrypterDecrypter + cloudkms.signerVerifier on PAN keys; Cloud SQL user on PCI DB; Secret Manager accessor |
connector-sa | gatelithix-core | Cloud SQL user on core DB (read-only for connector config); no KMS access; no vault access |
No service account uses default credentials or has wildcard roles (roles/editor, roles/owner). All authentication uses Workload Identity — no service account key files are downloaded or stored.
Evidence: infra/terraform/pci/kms.tf, infra/terraform/core/iam.tf
Application RBAC
The gateway enforces role-based access on all API endpoints via pkg/auth/rbac.go:
| Role | Access Level |
|---|---|
platform_admin | Full platform access (internal only) |
platform_support | Read-only platform access (internal only) |
merchant_admin | Full access to own merchant’s resources |
merchant_user | Read-only access to own merchant’s resources |
All payment read queries filter by merchant_id from the auth context. Cross-tenant payment IDs return 404 (not 403) to prevent information disclosure.
Evidence: pkg/auth/rbac.go
MFA Enforcement
| System | MFA Policy |
|---|---|
| GCP Console | GCP IAM org policy requires MFA for all human users |
| Auth0 (merchant portal) | Guardian + TOTP MFA required for merchant_admin role |
| Stripe Dashboard | MFA required for all team members with access to live keys |
Evidence: GCP IAM org policy configuration; Auth0 tenant MFA policy
API Key Security
API keys are stored hashed with SHA-256 in Core Cloud SQL. The plaintext key is shown once at creation and never stored. Each API key has a unique per-key HMAC secret for webhook signature generation (resolved at request time by pkg/hmac/resolver.go). Keys can be revoked instantly from the dashboard.
Evidence: db/migrations/023_add_hmac_secret_to_api_keys.sql, pkg/hmac/resolver.go
Evidence Summary for QSA
| Control | Requirement | Evidence Location |
|---|---|---|
| Network segmentation | Req 1.2.1, 1.3.1, 1.3.2 | infra/terraform/pci/network.tf, infra/terraform/modules/vpc/main.tf |
| No public IP on PCI DB | Req 1.4.1 | infra/terraform/pci/database.tf — ipv4_enabled = false |
| PAN encrypted at rest | Req 3.5.1 | infra/terraform/pci/kms.tf, apps/vault/handlers/tokenize.go:194 |
| HMAC keyed fingerprint | Req 3.5.1.1 | infra/terraform/pci/kms.tf — pan_fingerprint_key |
| Key rotation policy | Req 3.6.1 | infra/terraform/pci/kms.tf — rotation_period = "7776000s" (90 days) |
| TLS 1.2+ everywhere | Req 4.2.1 | Cloud Run managed TLS; infra/terraform/pci/database.tf — ssl_mode = ENCRYPTED_ONLY |
| Container / dep scanning | Req 6.3.2 | .github/workflows/ci.yml — Trivy, gosec, govulncheck |
| KMS access restriction | Req 7.2.1 | infra/terraform/pci/kms.tf — IAM binding to vault_sa_email only |
| Separate service accounts | Req 7.2.2 | infra/terraform/core/iam.tf, infra/terraform/pci/iam.tf |
| Default-deny firewall | Req 7.2.3 | infra/terraform/modules/vpc/main.tf — deny-all rules |
| RBAC on API endpoints | Req 7.2.4 | pkg/auth/rbac.go |
| MFA for CDE access | Req 8.3.1, 8.4.2 | Auth0 MFA policy; GCP org IAM MFA policy |
| Audit logs enabled | Req 10.2.1 | infra/terraform/pci/logging.tf — pci_audit_sink |
| Tamper-proof audit logs | Req 10.3.3 | infra/terraform/pci/logging.tf — locked = true |
| 12-month log retention | Req 10.5.1 | infra/terraform/pci/logging.tf — retention_days = 365 |
| Archived audit logs | Req 10.6.3 | infra/terraform/pci/logging.tf — NEARLINE archive bucket |
| Internal vuln scans | Req 11.3.1 | .github/workflows/ci.yml — gosec on every PR |
| External vuln scans | Req 11.3.2 | .github/workflows/ci.yml — Trivy container scan |
| Byte-zero of PAN in memory | Req 3.3.x | apps/vault/handlers/tokenize.go:106-109, resolve.go:153-157 |
| No PAN in logs | Req 3.3.x | apps/vault/handlers/tokenize.go:263-278 (audit metadata) |
For deployed verification commands (gcloud, terraform) for each of these controls, see PCI Verification Evidence.