Skip to content

Instantly share code, notes, and snippets.

@rkoster
Created May 6, 2026 09:24
Show Gist options
  • Select an option

  • Save rkoster/5b252b0edca606f10be2dbdcb81a796f to your computer and use it in GitHub Desktop.

Select an option

Save rkoster/5b252b0edca606f10be2dbdcb81a796f to your computer and use it in GitHub Desktop.
RFC0055: App-to-App mTLS Routing — Acceptance Testing Guide

RFC0055: App-to-App mTLS Routing — Acceptance Testing Guide

Tracking

Item Link
Tracking Issue cloudfoundry/community#1481
RFC cloudfoundry/community#1438
routing-release cloudfoundry/routing-release#535
cloud_controller_ng cloudfoundry/cloud_controller_ng#4910
capi-release cloudfoundry/capi-release#625
CLI cloudfoundry/cli#3758 (draft)

Prerequisites

  • A BOSH director (bosh-lite works)
  • cf-deployment cloned from main
  • Go toolchain (for running CLI from PR branch)

Step 1: Build & Upload Dev Releases

# routing-release
git clone --recurse-submodules https://github.com/cloudfoundry/routing-release.git
cd routing-release
git checkout feature/app-to-app-mtls-routing
bosh create-release --force --timestamp-version --name=routing
bosh upload-release
cd ..

# capi-release
git clone --recurse-submodules https://github.com/cloudfoundry/capi-release.git
cd capi-release
git checkout feature/app-to-app-mtls-routing
bosh create-release --force --timestamp-version --name=capi
bosh upload-release
cd ..

Step 2: Deploy CF

Deploy using stock cf-deployment with the following ops-file saved as ops-enable-mtls-app-routing.yml:

---
# Use dev routing release
- type: replace
  path: /releases/name=routing
  value:
    name: routing
    version: latest

# Use dev capi release
- type: replace
  path: /releases/name=capi
  value:
    name: capi
    version: latest

# BOSH DNS alias: *.apps.identity -> router instances
- type: replace
  path: /addons/name=bosh-dns-aliases/jobs/name=bosh-dns-aliases/properties/aliases/-
  value:
    domain: _.apps.identity
    targets:
    - deployment: cf
      domain: bosh
      instance_group: router
      network: default
      query: '*'

# Configure mTLS domain on gorouter
- type: replace
  path: /instance_groups/name=router/jobs/name=gorouter/properties/router/domains?
  value:
    - name: "*.apps.identity"
      ca_certs: ((diego_instance_identity_ca.certificate))
      forwarded_client_cert: sanitize_set
      xfcc_format: envoy

# Add *.apps.identity TLS cert to gorouter (served via SNI)
- type: replace
  path: /instance_groups/name=router/jobs/name=gorouter/properties/router/tls_pem/-
  value:
    cert_chain: ((apps_identity_router_tls.certificate))
    private_key: ((apps_identity_router_tls.private_key))

# Trust apps_identity_ca in app containers
- type: replace
  path: /instance_groups/name=compute/jobs/name=cflinuxfs4-rootfs-setup/properties/cflinuxfs4-rootfs/trusted_certs/-
  value: ((apps_identity_ca.certificate))

- type: replace
  path: /instance_groups/name=compute/jobs/name=rep/properties/containers/trusted_ca_certificates/-
  value: ((apps_identity_ca.certificate))

# BOSH variables
- type: replace
  path: /variables/-
  value:
    name: apps_identity_ca
    type: certificate
    options:
      common_name: apps-identity-ca
      is_ca: true

- type: replace
  path: /variables/-
  value:
    name: apps_identity_router_tls
    type: certificate
    options:
      ca: apps_identity_ca
      common_name: "*.apps.identity"
      alternative_names:
        - "*.apps.identity"
      extended_key_usage:
        - server_auth
bosh -d cf deploy cf-deployment/cf-deployment.yml \
  -o cf-deployment/operations/bosh-lite.yml \
  -o ops-enable-mtls-app-routing.yml \
  --non-interactive

Step 3: CF CLI from PR Branch

git clone https://github.com/cloudfoundry/cli.git
cd cli
git checkout feature/app-to-app-mtls-routing

# Use "go run" to invoke the CLI directly:
go run . login -a https://api.<system-domain> --skip-ssl-validation
go run . target -o <org> -s <space>

For convenience, create an alias:

alias cf='go run .'

Step 4: Setup Test Apps & Domain

# Push two simple apps (any app works)
cf push frontend
cf push backend

# Create the shared mTLS domain with access rule enforcement
cf create-shared-domain apps.identity --enforce-access-rules --scope any

# Map the backend to the mTLS domain
cf map-route backend apps.identity --hostname backend

Step 5: Acceptance Scenarios

Scenario 1: Default Deny

GIVEN backend has an mTLS route and no route policy exists
WHEN frontend calls backend using its instance identity cert
THEN 403 Forbidden

cf ssh frontend -c 'curl -s https://backend.apps.identity \
  --cert "${CF_INSTANCE_CERT}" --key "${CF_INSTANCE_KEY}" \
  --cacert <(cat /etc/cf-system-certificates/*)'
# Expected: 403 Forbidden

Scenario 2: Allow Specific App

GIVEN a route policy allows the frontend app
WHEN frontend calls backend
THEN 200 OK with X-Forwarded-Client-Cert header delivered to backend

cf add-route-policy apps.identity --source-app frontend --hostname backend

cf ssh frontend -c 'curl -s https://backend.apps.identity \
  --cert "${CF_INSTANCE_CERT}" --key "${CF_INSTANCE_KEY}" \
  --cacert <(cat /etc/cf-system-certificates/*)'
# Expected: 200 OK

Scenario 3: Allow by Space

GIVEN a route policy allows an entire space
WHEN any app in that space calls backend
THEN 200 OK

cf add-route-policy apps.identity \
  --source "cf:space:$(cf space <space-name> --guid)" \
  --hostname backend

cf ssh any-app-in-that-space -c 'curl -s https://backend.apps.identity \
  --cert "${CF_INSTANCE_CERT}" --key "${CF_INSTANCE_KEY}" \
  --cacert <(cat /etc/cf-system-certificates/*)'
# Expected: 200 OK

Scenario 4: Unauthorized App Denied

GIVEN a route policy only allows frontend
WHEN a different (unauthorized) app calls backend
THEN 403 Forbidden

cf ssh unauthorized-app -c 'curl -s https://backend.apps.identity \
  --cert "${CF_INSTANCE_CERT}" --key "${CF_INSTANCE_KEY}" \
  --cacert <(cat /etc/cf-system-certificates/*)'
# Expected: 403 Forbidden

Scenario 5: No Regression on Normal Routes

GIVEN the feature is deployed
WHEN normal HTTPS traffic flows to non-mTLS domains
THEN behavior is unchanged

curl -s https://frontend.<system-domain>
# Expected: 200 OK (normal routing unaffected)

Reset (for re-running tests)

cf remove-route-policy apps.identity --source-app frontend --hostname backend -f

Merge Ordering

All component PRs are independently safe to merge — the feature is dormant unless the operator deploys with the ops-file and creates the shared domain with --enforce-access-rules. No strict ordering required, but recommend merging around the same time to minimize partial-support windows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment