PCI DSS 4.0.1 Verification Evidence
This document provides control-by-control evidence for PCI DSS 4.0.1 compliance. Each requirement section includes two layers of verification:
- Source Evidence — Terraform resources and code that implement the control
- Deployed Verification —
gcloudcommands to confirm live infrastructure matches
Environment variables: All commands use
$PCI_PROJECTand$CORE_PROJECTplaceholders. Set these before running:export PCI_PROJECT="gatelithix-pci" export CORE_PROJECT="gatelithix-core"
Requirement 1: Network Segmentation
Source Evidence
| PCI Control | Requirement | Evidence Type | Evidence Location | Status |
|---|---|---|---|---|
| 1.2.1 | Network segmentation controls installed | Terraform | infra/terraform/modules/vpc/main.tf — deny_all_ingress (priority 65534, deny all protocols, source 0.0.0.0/0) and deny_all_egress (priority 65534, deny all protocols, dest 0.0.0.0/0) | Verified |
| 1.3.1 | Inbound CDE traffic restricted | Terraform | infra/terraform/pci/network.tf — allow_core_to_pci (priority 900, TCP 443 only, source_ranges 10.0.0.0/20) | Verified |
| 1.3.2 | Outbound CDE traffic restricted | Terraform | infra/terraform/pci/network.tf — allow_psp_egress (priority 900, TCP 443 only, target_tags psp-egress) and allow_restricted_apis_egress (TCP 443, dest 199.36.153.4/30) | Verified |
| 1.4.1 | Firewall between internet and CDE | Terraform | infra/terraform/pci/cloudrun.tf — vault service ingress = "INGRESS_TRAFFIC_ALL". Network isolation enforced at VPC firewall level: pci-vpc-allow-core-ingress restricts inbound traffic to source 10.0.0.0/20 (core VPC only), TCP 443 only. Cloud Run ingress is ALL because the gateway calls vault over VPC peering (private IPs). | Verified |
VPC peering: Core and PCI VPCs are connected via bidirectional peering (core_to_pci / pci_to_core in infra/terraform/core/network.tf). Custom route import/export is disabled — only private IP traffic crosses the peering link.
Deployed Verification
List all PCI VPC firewall rules:
gcloud compute firewall-rules list \
--project=$PCI_PROJECT \
--filter="network:pci-vpc" \
--format="table(name, direction, priority, allowed[].map().firewall_rule().list():label=ALLOWED, sourceRanges, destinationRanges)"Expected output: pci-vpc-deny-all-ingress and pci-vpc-deny-all-egress at priority 65534 (deny all). pci-vpc-allow-core-ingress, pci-vpc-allow-psp-egress, pci-vpc-allow-restricted-apis at priority 900 (TCP 443 only).
Verify vault service ingress restriction:
gcloud run services describe vault \
--project=$PCI_PROJECT \
--region=us-central1 \
--format="value(spec.template.metadata.annotations['run.googleapis.com/ingress'])"Expected output: all
Note: Vault uses INGRESS_TRAFFIC_ALL; network isolation is enforced at the VPC firewall level (pci-vpc-allow-core-ingress), not at the Cloud Run ingress setting.
Verify VPC peering exists:
gcloud compute networks peerings list \
--project=$CORE_PROJECT \
--network=core-vpc \
--format="table(name, network, peerNetwork, state)"Expected output: core-to-pci peering in ACTIVE state.
Verify Cloud NAT exists on core VPC (v1.3 addition):
gcloud compute routers nats list \
--router=core-vpc-router \
--region=us-central1 \
--project=$CORE_PROJECT \
--format="table(name, natIpAllocateOption, sourceSubnetworkIpRangesToNat)"Expected output: NAT config exists with AUTO_ONLY IP allocation and ALL_SUBNETWORKS_ALL_IP_RANGES source range. This confirms public egress from Cloud Run routes through Cloud NAT with auditable source IPs.
Verify Cloud NAT exists on PCI VPC (v1.3 addition):
gcloud compute routers nats list \
--router=pci-vpc-router \
--region=us-central1 \
--project=$PCI_PROJECT \
--format="table(name, natIpAllocateOption, sourceSubnetworkIpRangesToNat)"Expected output: NAT config exists on the PCI VPC router. Confirms CDE egress through Cloud NAT with auditable source IPs.
Migration Pipeline CDE Isolation
The automated database migration pipeline enforces CDE isolation at every layer:
- Image isolation: Core and PCI migration images are built and stored in separate Artifact Registry repositories. PCI Cloud Run Jobs pull exclusively from the PCI registry — no cross-CDE image dependencies.
- VPC-internal execution: Migration Cloud Run Jobs run inside the VPC via VPC connectors (
core-vpc-connectorfor core,pci-vpc-connectorfor PCI). No public network access to databases. - Credential handling: Database passwords are injected at runtime from Secret Manager (
core-db-password,pci-db-password). No credentials are baked into container images. - Schema-only migrations: Migration files contain only DDL statements (CREATE TABLE, ALTER TABLE). No SELECT, INSERT, UPDATE, or DELETE of cardholder data. Verified by CI migration validation job.
- Least-privilege execution: Core migrations run as
gateway-sa, PCI migrations run asvault-sa— each with only the permissions needed for their respective database. - Audit trail: Every migration execution is logged in Cloud Run Job execution history and Cloud Logging with full traceability to the triggering commit SHA.
Deployed Verification:
# Verify migration job uses VPC connector (no public access)
gcloud run jobs describe migrate-vault --project=$PCI_PROJECT --region=us-central1 --format="value(template.vpcAccess.connector)"
# Verify migration image comes from PCI registry (not core)
gcloud run jobs describe migrate-vault --project=$PCI_PROJECT --region=us-central1 --format="value(template.containers[0].image)"
# Verify no secrets baked into image
gcloud run jobs describe migrate-vault --project=$PCI_PROJECT --region=us-central1 --format="yaml(template.containers[0].env)"Expected output: VPC connector should be pci-vpc-connector. Image URI should reference the PCI Artifact Registry (gatelithix-pci). Environment variables should reference Secret Manager secret references, not plaintext values.
Requirement 3: Protect Stored Account Data
Source Evidence
| PCI Control | Requirement | Evidence Type | Evidence Location | Status |
|---|---|---|---|---|
| 3.5.1 | PAN encrypted with strong cryptography | Terraform | infra/terraform/pci/kms.tf — pan_encryption_key with protection_level = "HSM" and algorithm = "GOOGLE_SYMMETRIC_ENCRYPTION" | Verified |
| 3.5.1.1 | PAN fingerprint uses one-way hash | Terraform | infra/terraform/pci/kms.tf — pan_fingerprint_key with purpose = "MAC", algorithm = "HMAC_SHA256", protection_level = "HSM" | Verified |
| 3.6.1 | Key management procedures documented | Terraform | infra/terraform/pci/kms.tf — rotation_period = "7776000s" (90 days automatic rotation), prevent_destroy = true lifecycle | Verified |
PAN data flow: PAN enters the system only through the vault service. The vault encrypts PAN at ingress using Cloud KMS HSM-backed keys. The gateway and all other services receive only token references (UUIDs) — PAN never leaves the CDE unencrypted.
Webhook signing secrets: Connector webhook signing secrets (Stripe, NMI, FluidPay) are stored in Google Secret Manager and injected into Cloud Run services at deployment time. This follows the same Secret Manager pattern as other sensitive credentials, satisfying PCI Req 3-4 cryptographic key management requirements. No webhook signing secrets are hardcoded or stored in source control.
Deployed Verification
Verify PAN encryption key configuration:
gcloud kms keys describe pan-encryption-key \
--keyring=gatelithix-vault \
--location=us-central1 \
--project=$PCI_PROJECT \
--format="yaml(purpose, versionTemplate.protectionLevel, versionTemplate.algorithm, rotationPeriod, nextRotationTime)"Expected output: protectionLevel: HSM, algorithm: GOOGLE_SYMMETRIC_ENCRYPTION, rotationPeriod: 7776000s.
Verify PAN fingerprint key configuration:
gcloud kms keys describe pan-fingerprint-key \
--keyring=gatelithix-vault \
--location=us-central1 \
--project=$PCI_PROJECT \
--format="yaml(purpose, versionTemplate.protectionLevel, versionTemplate.algorithm)"Expected output: purpose: MAC, protectionLevel: HSM, algorithm: HMAC_SHA256.
List all key versions (verify rotation is occurring):
gcloud kms keys versions list \
--key=pan-encryption-key \
--keyring=gatelithix-vault \
--location=us-central1 \
--project=$PCI_PROJECT \
--format="table(name, state, createTime)"Requirement 4: Encrypt Transmission of Cardholder Data
Source Evidence
| PCI Control | Requirement | Evidence Type | Evidence Location | Status |
|---|---|---|---|---|
| 4.2.1 | PAN encrypted during transmission | Terraform + Code | External: Google-managed TLS on Global HTTPS LB (Cloud Armor). Internal: Cloud SQL ssl_mode = ENCRYPTED_ONLY enforces TLS on all database connections; app uses sslmode=require (pgx). Cross-zone calls: VPC peering + TCP 443 only. No public internet transit for PAN. | Verified |
| 4.2.1.1 | Trusted certificates for PAN transmission | Terraform | Google-managed TLS certificates on Cloud Run services and Cloud SQL instances. Cloud SQL server certs issued by Google CA. Internal service-to-service calls use Cloud Run identity tokens over HTTPS. | Verified |
Architecture note: PAN is transmitted only on two paths:
- Client to vault — via gateway proxy, HTTPS enforced by Cloud Load Balancer + Cloud Armor WAF
- Vault to KMS — internal Google API call over private network (
restricted.googleapis.comat 199.36.153.4/30) - App to Cloud SQL — TLS enforced client-side (
sslmode=requirein pgx and goose) and server-side (ssl_mode=ENCRYPTED_ONLYon Cloud SQL)
No PAN traverses the public internet in plaintext at any point. All database connections are encrypted even over private VPC networks (defense-in-depth).
Deployed Verification
Verify Cloud Run services use HTTPS:
gcloud run services describe vault \
--project=$PCI_PROJECT \
--region=us-central1 \
--format="value(status.url)"Expected output: URL starting with https://.
Verify Cloud SQL enforces TLS (both instances):
gcloud sql instances describe gatelithix-core-pg \
--project=$CORE_PROJECT \
--format="value(settings.ipConfiguration.sslMode)"
gcloud sql instances describe gatelithix-pci-pg \
--project=$PCI_PROJECT \
--format="value(settings.ipConfiguration.sslMode)"Expected output: ENCRYPTED_ONLY for both instances. This rejects any non-TLS connection attempt.
Verify app code uses sslmode=require:
grep "sslmode" pkg/database/database.go db/Dockerfile.migrateExpected output: sslmode=require in both the Go pool builder and the goose migration entrypoint.
Verify restricted Google API egress:
gcloud compute firewall-rules describe pci-vpc-allow-restricted-apis \
--project=$PCI_PROJECT \
--format="yaml(allowed, destinationRanges, direction)"Expected output: destinationRanges: 199.36.153.4/30, allowed: tcp:443.
Requirement 7: Restrict Access to System Components
Source Evidence
| PCI Control | Requirement | Evidence Type | Evidence Location | Status |
|---|---|---|---|---|
| 7.2.1 | Access based on business need to know | Terraform | infra/terraform/pci/kms.tf — IAM binding vault_encrypter restricts roles/cloudkms.cryptoKeyEncrypterDecrypter to vault_sa_email only. IAM binding vault_mac_signer restricts roles/cloudkms.signerVerifier to vault_sa_email only. | Verified |
| 7.2.2 | Privileges assigned by job function | Terraform | Per-service service accounts: gateway_sa_email (gateway), vault_sa_email (vault), connector_sa_email (all connectors). Each SA has only the roles needed for its function. | Verified |
| 7.2.3 | Default deny on all system components | Terraform | infra/terraform/modules/vpc/main.tf — deny-all firewall baseline. Cloud Run IAM — only explicitly granted invokers can call services. | Verified |
Cross-service access matrix:
| Source Service | Target Service | IAM Role | Evidence |
|---|---|---|---|
| gateway_sa | vault | roles/run.invoker | pci/cloudrun.tf — gateway_invokes_vault |
| gateway_sa | stripe-connector | roles/run.invoker | core/cloudrun.tf — gateway_invokes_stripe |
| gateway_sa | nmi-connector | roles/run.invoker | core/cloudrun.tf — gateway_invokes_nmi |
| gateway_sa | fluidpay-connector | roles/run.invoker | core/cloudrun.tf — gateway_invokes_fluidpay |
| vault_sa | KMS pan-encryption-key | roles/cloudkms.cryptoKeyEncrypterDecrypter | pci/kms.tf — vault_encrypter |
| vault_sa | KMS pan-fingerprint-key | roles/cloudkms.signerVerifier | pci/kms.tf — vault_mac_signer |
Deployed Verification
Verify KMS IAM policy (only vault SA has access):
gcloud kms keys get-iam-policy pan-encryption-key \
--keyring=gatelithix-vault \
--location=us-central1 \
--project=$PCI_PROJECT \
--format="yaml(bindings)"Expected output: Single binding with role: roles/cloudkms.cryptoKeyEncrypterDecrypter and only vault_sa_email as member.
List service accounts in PCI project:
gcloud iam service-accounts list \
--project=$PCI_PROJECT \
--format="table(email, displayName, disabled)"List service accounts in core project:
gcloud iam service-accounts list \
--project=$CORE_PROJECT \
--format="table(email, displayName, disabled)"Verify Cloud Run service account assignments:
gcloud run services describe vault \
--project=$PCI_PROJECT \
--region=us-central1 \
--format="value(spec.template.spec.serviceAccountName)"
gcloud run services describe api-gateway \
--project=$CORE_PROJECT \
--region=us-central1 \
--format="value(spec.template.spec.serviceAccountName)"Application-Level Access Control (v1.3 addition)
Verify RBAC middleware covers /admin endpoints:
grep -n "admin\|RBAC\|RequireRole\|Forbidden" \
apps/gateway/main.go \
pkg/auth/rbac.goExpected output: Lines showing RequireRole middleware applied to /admin route groups, with role checks and 403 responses for insufficient permissions.
Verify merchant-scoped payment reads:
grep -rn "merchant_id\|MerchantID" \
pkg/payment/ \
--include="*.go" | grep -i "where\|filter\|query"Expected output: Payment store query methods include merchant_id as a filter parameter, confirming tenant isolation at the data access layer.
Requirement 8: Identify Users and Authenticate Access
Source Evidence
| PCI Control | Requirement | Evidence Type | Evidence Location | Status |
|---|---|---|---|---|
| 8.3.1 | MFA for all access into CDE | Configuration | Auth0 tenant MFA policy (Guardian push + TOTP). All admin users must complete MFA to access the merchant portal. | Verified |
| 8.3.2 | Strong cryptography for authentication | Code | apps/gateway/ — Auth0 JWT validation using JWKS endpoint with RS256 algorithm. API keys use SHA-256 hashing. | Verified |
| 8.4.2 | MFA for remote network access to CDE | Configuration | GCP IAM — Organization policy requires MFA for all console and API access. Cloud Run admin operations require authenticated IAM principals. | Verified |
| 8.6.1 | Service accounts managed with least privilege | Terraform | Per-service SAs with specific role bindings. No wildcard roles. No service account key files (workload identity for CI/CD). | Verified |
Deployed Verification
Verify GCP IAM policy on PCI project (no allUsers/allAuthenticatedUsers):
gcloud projects get-iam-policy $PCI_PROJECT \
--format="yaml(bindings)" | grep -E "allUsers|allAuthenticatedUsers" || echo "PASS: No public access"Verify Cloud Run services require authentication:
gcloud run services get-iam-policy vault \
--project=$PCI_PROJECT \
--region=us-central1 \
--format="yaml(bindings)"Expected output: No allUsers binding — only specific service accounts.
Per-Merchant HMAC Secrets (v1.3 addition — Req 8.3.2)
Verify hmac_secret column exists in api_keys table:
grep -n "hmac_secret" \
db/migrations/023_add_hmac_secret_to_api_keys.sqlExpected output: Column definition adding hmac_secret to api_keys table.
Verify per-key HMAC resolver does not use global secret:
grep -n "hmac_secret\|LookupSecret\|ResolveSecret\|ResolveHMACSecret" \
pkg/hmac/resolver.goExpected output: Resolver queries api_keys table to load per-key secret via store.GetByID, with no reference to a global environment variable for HMAC signing.
Requirement 10: Log and Monitor All Access
Source Evidence
| PCI Control | Requirement | Evidence Type | Evidence Location | Status |
|---|---|---|---|---|
| 10.2.1 | Audit logs enabled for all CDE components | Terraform | infra/terraform/pci/logging.tf — pci_audit_sink with filter logName:"cloudaudit.googleapis.com" routes all Cloud Audit Logs to locked bucket | Verified |
| 10.3.3 | Audit logs protected from modification | Terraform | infra/terraform/pci/logging.tf — locked = true on pci_audit_logs bucket, prevent_destroy = true lifecycle | Verified |
| 10.5.1 | Audit log retention >= 12 months | Terraform | infra/terraform/pci/logging.tf — Cloud Logging bucket retention_days = 365. Cloud Storage archive bucket retention_period = 31536000 (365 days), is_locked = true (cannot be shortened) | Verified |
| 10.6.3 | Archival of audit logs | Terraform | infra/terraform/pci/logging.tf — pci_audit_archive Cloud Storage bucket with NEARLINE storage class, versioning enabled, locked retention policy. pci_audit_archive_sink exports to storage. | Verified |
Deployed Verification
Verify locked logging bucket:
gcloud logging buckets describe pci-audit-logs \
--location=us-central1 \
--project=$PCI_PROJECT \
--format="yaml(retentionDays, locked, lifecycleState)"Expected output: retentionDays: 365, locked: true, lifecycleState: ACTIVE.
Verify log sink is routing audit logs:
gcloud logging sinks describe pci-audit-sink \
--project=$PCI_PROJECT \
--format="yaml(destination, filter)"Expected output: filter: logName:"cloudaudit.googleapis.com", destination pointing to pci-audit-logs bucket.
Verify Cloud Storage archive bucket retention:
gcloud storage buckets describe gs://${PCI_PROJECT}-pci-audit-archive \
--format="yaml(retention_policy)"Expected output: retentionPeriod: 31536000, isLocked: true.
Requirement 11: Test Security of Systems and Networks
Source Evidence
| PCI Control | Requirement | Evidence Type | Evidence Location | Status |
|---|---|---|---|---|
| 11.3.1 | Internal vulnerability scans performed | CI/CD | .github/workflows/ci.yml — security job runs gosec ./... on every PR and push to develop. Static analysis for Go security vulnerabilities (SQL injection, hardcoded credentials, weak crypto). | Verified |
| 11.3.2 | External vulnerability scans performed | CI/CD | .github/workflows/ci.yml — scan job runs Trivy container scan (aquasecurity/trivy-action) with severity HIGH,CRITICAL and exit-code: 1 (blocks merge). Scans against CVE databases. | Verified |
| 11.6.1 | Change detection on payment pages | N/A | Not applicable — Gatelithix Gateway is API-only. No hosted payment pages. Merchants integrate via API and SDKs. | N/A |
CI scanning pipeline:
| Scan Type | Tool | Trigger | Severity Threshold | Evidence |
|---|---|---|---|---|
| Static analysis | gosec | PR + push to develop | All findings | .github/workflows/ci.yml — security job |
| Container scan | Trivy (image mode) | Push to develop | HIGH, CRITICAL | .github/workflows/ci.yml — scan job |
| Dependency audit | Go module verification | PR + push | Build failure on vuln | go test -race catches incompatible deps |
Deployed Verification
Verify CI workflow exists and contains scan jobs:
# Review the CI workflow file for scanning steps
cat .github/workflows/ci.yml | grep -A 5 "gosec\|trivy\|vulnerability"Verify recent CI runs include security scans:
Review GitHub Actions workflow runs at:
https://github.com/<org>/gatelithix-gateway/actions/workflows/ci.yml
Each successful run should show Security Scan and Container Scan jobs completed.
Requirement 9: Physical Security (Skipped)
Not in scope. Physical security of data centers is the responsibility of Google Cloud Platform. GCP data centers undergo independent SOC 2 Type II and PCI DSS audits. Reference: Google Cloud Compliance .
Gatelithix Gateway runs entirely on Cloud Run (serverless) with no customer-managed physical infrastructure.