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 bymanagementandonline-txn.merchant-onboarding— CDE-scoped. Manages TransIT processor credentials and NexGO XTMS device binding. Called via Cloud Run IAM bymanagement.
Public-facing services (auth, management, online-txn, status) reach
these two over IAM-authenticated service-to-service calls (InternalServiceClient).
Defense-in-depth layers
- Cloud Run ingress on the service resource is set to
INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER. This is enforced ininfra/terraform/modules/cloud-run/main.tffor every service, and means requests from the public internet are dropped before ever reaching the container. - External load balancer has no URL map route to either service.
Neither
processingnormerchant-onboardingappears in theexternally_routed_servicesmap ininfra/terraform/main.tf, nor in thegateway_routesorpay_routeslists ininfra/terraform/modules/load-balancer/main.tf. - Cloud Run IAM — only the specific caller service accounts (see
internal_service_invokersininfra/terraform/modules/cloud-run/main.tf) holdroles/run.invokeron 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-onboardingwas present inexternally_routed_servicesininfra/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 fromexternally_routed_servicesin the same PR. See commitchore(infra): verify merchant-onboarding ingress remains internal-only.