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/tf/gcp/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/tf/gcp/main.tf, nor in thegateway_routesorpay_routeslists ininfra/tf/gcp/modules/load-balancer/main.tf. - Cloud Run IAM — only the specific caller service accounts (see
internal_service_invokersininfra/tf/gcp/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/tf/gcp/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/tf/gcp/main.tf
Expected output: the map contains only auth, management, online-txn, card-present, status.
# URL maps must not reference either service
grep -E 'processing|merchant-onboarding' infra/tf/gcp/modules/load-balancer/main.tf
Expected output: no matches.
# Ingress must be INTERNAL_LOAD_BALANCER on every service
grep 'ingress' infra/tf/gcp/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-east1 --project peak-gateway-prod \
--format='value(status.conditions.ingress,spec.ingress,ingress)'
gcloud run services describe gateway-processing \
--region us-east1 --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-east1.run.app/health
Expected: 000 (connection refused / DNS not resolvable from outside VPC),
403, or 404. Never a 200 with a health payload.