Skip to main content

Internal-Only Services

Peak Gateway uses a two-tier security model. Two services — processing and merchant-onboarding — are internal-only. They must never be reachable from the public internet.

  • processing — core payment engine (sale, auth, capture, void, refund, subscriptions). Talks to TransIT. Called via Cloud Run IAM by management and online-txn.
  • merchant-onboarding — CDE-scoped. Manages TransIT processor credentials and NexGO XTMS device binding. Called via Cloud Run IAM by management.

Public-facing services (auth, management, online-txn, status) reach these two over IAM-authenticated service-to-service calls (InternalServiceClient).

Defense-in-depth layers

  1. Cloud Run ingress on the service resource is set to INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER. This is enforced in infra/terraform/modules/cloud-run/main.tf for every service, and means requests from the public internet are dropped before ever reaching the container.
  2. External load balancer has no URL map route to either service. Neither processing nor merchant-onboarding appears in the externally_routed_services map in infra/terraform/main.tf, nor in the gateway_routes or pay_routes lists in infra/terraform/modules/load-balancer/main.tf.
  3. Cloud Run IAM — only the specific caller service accounts (see internal_service_invokers in infra/terraform/modules/cloud-run/main.tf) hold roles/run.invoker on these services.

All three layers must hold independently — removing any one should not open the service to the public.

Verification checklist

Run this check any time infra/terraform/main.tf or the cloud-run / load-balancer modules are modified.

1. Terraform audit (source of truth)

# externally_routed_services must not contain processing or merchant-onboarding
grep -A 6 'externally_routed_services' infra/terraform/main.tf

Expected output: the map contains only auth, management, online-txn, status.

# URL maps must not reference either service
grep -E 'processing|merchant-onboarding' infra/terraform/modules/load-balancer/main.tf

Expected output: no matches.

# Ingress must be INTERNAL_LOAD_BALANCER on every service
grep 'ingress' infra/terraform/modules/cloud-run/main.tf

Expected output: every ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER".

2. Live verification (runtime)

# Confirm the live Cloud Run service has internal-only ingress
gcloud run services describe gateway-merchant-onboarding \
--region us-central1 --project peak-gateway-prod \
--format='value(status.conditions.ingress,spec.ingress,ingress)'

gcloud run services describe gateway-processing \
--region us-central1 --project peak-gateway-prod \
--format='value(ingress)'

Expected value: INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER for both.

3. External reachability smoke test

From a machine outside the VPC (a laptop on the public internet), hit the service's *.run.app URL. The request must be rejected at the edge — not receive a 200, 401, or 403 response body from the application itself. The expected failure modes are a TLS-level reject, a 404 from the LB, or a connection timeout.

curl -sS -o /dev/null -w '%{http_code}\n' \
https://gateway-merchant-onboarding-<PROJECT_NUMBER>.us-central1.run.app/actuator/health

Expected: 000 (connection refused / DNS not resolvable from outside VPC), 403, or 404. Never a 200 with a health payload.

History

  • 2026-04-17: Audit (INF-05, #414) verified ingress config and discovered merchant-onboarding was present in externally_routed_services in infra/terraform/main.tf. Although no URL map actually routed to it (and Cloud Run ingress was already internal-only, so traffic could not reach it), the map entry was creating unused NEGs and backend services and contradicted the architecture. Fix: removed merchant-onboarding from externally_routed_services in the same PR. See commit chore(infra): verify merchant-onboarding ingress remains internal-only.