|
#!/bin/bash |
|
# experiments/shared-route-tags.sh |
|
# |
|
# Experiment: How do GoRouter route tags behave with shared routes? |
|
# |
|
# Context: Beyhan proposed simplifying mTLS authorization by using the route |
|
# destination's tags (organization_id, space_id) to enforce "same org/space" |
|
# checks instead of explicit GUID lists in BOSH manifests. This experiment |
|
# verifies that the tags carry the right information for shared routes. |
|
# |
|
# Prerequisites: |
|
# - CF environment with admin access |
|
# - BOSH director configured (source bosh.env) |
|
# - GoRouter status endpoint credentials (from gorouter.yml) |
|
# |
|
# Run: ./experiments/shared-route-tags.sh |
|
# |
|
# Date: 2026-03-20 |
|
# Result: Tags reflect the destination APP's org/space, not the route owner's. |
|
# Each endpoint carries its own org_id/space_id. Shared routes produce |
|
# endpoints with different space_ids in the same pool. This makes |
|
# "scope: org" and "scope: space" checks viable. |
|
|
|
set -euo pipefail |
|
|
|
YELLOW='\033[1;33m' |
|
GREEN='\033[0;32m' |
|
CYAN='\033[0;36m' |
|
NC='\033[0m' |
|
|
|
step() { echo -e "\n${CYAN}=== $1 ===${NC}"; } |
|
info() { echo -e "${GREEN}[INFO]${NC} $1"; } |
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } |
|
|
|
# -------------------------------------------------------------------------- |
|
# Configuration - adjust for your environment |
|
# -------------------------------------------------------------------------- |
|
ORG="my-org" |
|
SPACE_A="my-space" |
|
SPACE_B="test-space" |
|
APP_A="cf-env" # existing app in SPACE_A |
|
APP_B="test-app" # will be created in SPACE_B |
|
DOMAIN="10.246.0.21.sslip.io" |
|
|
|
# GoRouter status endpoint (from /var/vcap/jobs/gorouter/config/gorouter.yml) |
|
ROUTER_STATUS_USER="router-status" |
|
ROUTER_STATUS_PASS="<ROUTER_STATUS_PASSWORD>" |
|
ROUTER_STATUS_PORT="8082" |
|
|
|
# -------------------------------------------------------------------------- |
|
# Setup |
|
# -------------------------------------------------------------------------- |
|
step "1. Capture initial state" |
|
|
|
cf target -o "$ORG" -s "$SPACE_A" |
|
|
|
APP_A_GUID=$(cf app "$APP_A" --guid) |
|
ORG_GUID=$(cf org "$ORG" --guid) |
|
SPACE_A_GUID=$(cf space "$SPACE_A" --guid) |
|
ROUTE_HOST="$APP_A" |
|
ROUTE_URL="${ROUTE_HOST}.${DOMAIN}" |
|
|
|
info "App A: $APP_A (guid: $APP_A_GUID)" |
|
info "Org: $ORG (guid: $ORG_GUID)" |
|
info "Space A: $SPACE_A (guid: $SPACE_A_GUID)" |
|
info "Route: $ROUTE_URL" |
|
|
|
# Find route GUID |
|
ROUTE_GUID=$(cf curl "/v3/routes?hosts=${ROUTE_HOST}" | python3 -c " |
|
import sys, json |
|
data = json.loads(sys.stdin.read()) |
|
print(data['resources'][0]['guid']) |
|
") |
|
info "Route GUID: $ROUTE_GUID" |
|
|
|
step "2. Query GoRouter route table BEFORE sharing" |
|
|
|
query_router() { |
|
local label="$1" |
|
source "$(dirname "$0")/../bosh.env" |
|
bosh -d cf ssh router/0 -c \ |
|
"curl -s http://${ROUTER_STATUS_USER}:${ROUTER_STATUS_PASS}@localhost:${ROUTER_STATUS_PORT}/routes" \ |
|
2>/dev/null | python3 -c " |
|
import sys, json |
|
raw = sys.stdin.read() |
|
for line in raw.split('\n'): |
|
line = line.strip() |
|
if 'stdout |' in line: |
|
line = line.split('stdout |', 1)[1].strip() |
|
if line.startswith('{'): |
|
data = json.loads(line) |
|
for route_key, endpoints in data.items(): |
|
if '${ROUTE_URL}' == route_key: |
|
print('--- ${label} ---') |
|
print(f'Route: {route_key}') |
|
print(f'Endpoints: {len(endpoints)}') |
|
for i, ep in enumerate(endpoints): |
|
tags = ep.get('tags', {}) or {} |
|
print(f' [{i}] app_id={tags.get(\"app_id\",\"N/A\")} ' |
|
f'app_name={tags.get(\"app_name\",\"N/A\")} ' |
|
f'space_id={tags.get(\"space_id\",\"N/A\")} ' |
|
f'space_name={tags.get(\"space_name\",\"N/A\")} ' |
|
f'organization_id={tags.get(\"organization_id\",\"N/A\")} ' |
|
f'organization_name={tags.get(\"organization_name\",\"N/A\")}') |
|
break |
|
" |
|
} |
|
|
|
query_router "Before sharing" |
|
|
|
# -------------------------------------------------------------------------- |
|
# Enable route sharing and create second space |
|
# -------------------------------------------------------------------------- |
|
step "3. Enable route sharing feature flag" |
|
cf enable-feature-flag route_sharing |
|
|
|
step "4. Create second space and push app" |
|
cf create-space "$SPACE_B" -o "$ORG" 2>/dev/null || true |
|
cf target -s "$SPACE_B" |
|
SPACE_B_GUID=$(cf space "$SPACE_B" --guid) |
|
info "Space B: $SPACE_B (guid: $SPACE_B_GUID)" |
|
|
|
# Push a minimal app |
|
cf push "$APP_B" --no-route -b binary_buildpack -m 64M \ |
|
-c 'while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK" | nc -l -p 8080 -w1; done' |
|
|
|
APP_B_GUID=$(cf app "$APP_B" --guid) |
|
info "App B: $APP_B (guid: $APP_B_GUID)" |
|
|
|
# -------------------------------------------------------------------------- |
|
# Share route and map destination |
|
# -------------------------------------------------------------------------- |
|
step "5. Share route with Space B" |
|
cf curl -X POST "/v3/routes/${ROUTE_GUID}/relationships/shared_spaces" \ |
|
-d "{\"data\": [{\"guid\": \"${SPACE_B_GUID}\"}]}" |
|
|
|
step "6. Map shared route to App B" |
|
cf curl -X POST "/v3/routes/${ROUTE_GUID}/destinations" \ |
|
-d "{\"destinations\": [{\"app\": {\"guid\": \"${APP_B_GUID}\"}}]}" |
|
|
|
info "Waiting 5s for route-emitter to register..." |
|
sleep 5 |
|
|
|
step "7. Query GoRouter route table AFTER sharing" |
|
query_router "After sharing (both apps mapped)" |
|
|
|
# -------------------------------------------------------------------------- |
|
# Simulate app migration: remove App A from route |
|
# -------------------------------------------------------------------------- |
|
step "8. Simulate migration: unmap App A from route" |
|
|
|
# Get App A's destination GUID |
|
DEST_A_GUID=$(cf curl "/v3/routes/${ROUTE_GUID}/destinations" | python3 -c " |
|
import sys, json |
|
data = json.loads(sys.stdin.read()) |
|
for d in data['destinations']: |
|
if d['app']['guid'] == '${APP_A_GUID}': |
|
print(d['guid']) |
|
break |
|
") |
|
|
|
if [ -n "$DEST_A_GUID" ]; then |
|
cf curl -X DELETE "/v3/routes/${ROUTE_GUID}/destinations/${DEST_A_GUID}" |
|
info "Unmapped App A from route" |
|
info "Waiting 5s for route-emitter to deregister..." |
|
sleep 5 |
|
fi |
|
|
|
step "9. Query GoRouter route table AFTER unmapping App A" |
|
query_router "After migration (only App B)" |
|
|
|
# -------------------------------------------------------------------------- |
|
# Cleanup |
|
# -------------------------------------------------------------------------- |
|
step "10. Cleanup" |
|
|
|
# Re-map App A (restore original state) |
|
cf curl -X POST "/v3/routes/${ROUTE_GUID}/destinations" \ |
|
-d "{\"destinations\": [{\"app\": {\"guid\": \"${APP_A_GUID}\"}}]}" |
|
|
|
# Remove App B destination |
|
DEST_B_GUID=$(cf curl "/v3/routes/${ROUTE_GUID}/destinations" | python3 -c " |
|
import sys, json |
|
data = json.loads(sys.stdin.read()) |
|
for d in data['destinations']: |
|
if d['app']['guid'] == '${APP_B_GUID}': |
|
print(d['guid']) |
|
break |
|
") |
|
if [ -n "$DEST_B_GUID" ]; then |
|
cf curl -X DELETE "/v3/routes/${ROUTE_GUID}/destinations/${DEST_B_GUID}" |
|
fi |
|
|
|
# Unshare route |
|
cf curl -X DELETE "/v3/routes/${ROUTE_GUID}/relationships/shared_spaces/${SPACE_B_GUID}" |
|
|
|
# Delete test app and space |
|
cf target -s "$SPACE_B" |
|
cf delete "$APP_B" -f -r |
|
cf target -s "$SPACE_A" |
|
cf delete-space "$SPACE_B" -f |
|
|
|
info "Cleanup complete" |
|
|
|
# -------------------------------------------------------------------------- |
|
# Summary |
|
# -------------------------------------------------------------------------- |
|
step "Summary" |
|
cat <<'EOF' |
|
Findings: |
|
1. Route tags are PER-ENDPOINT, set by the route-emitter |
|
2. Tags reflect the DESTINATION APP's org/space, not the route owner's |
|
3. When a route is shared and mapped to an app in another space: |
|
- Both endpoints appear in the same GoRouter pool |
|
- Each endpoint carries its own space_id/organization_id |
|
4. After "migrating" (unmapping old app, keeping new app): |
|
- Only the new app's endpoint remains |
|
- Tags correctly reflect the new app's space |
|
|
|
Implication for scope-based authorization: |
|
- scope: org → Compare caller cert org GUID against endpoint tags organization_id |
|
Works because all endpoints in a shared route are in the same org |
|
(route sharing requires same org unless cross-org feature enabled) |
|
- scope: space → Compare caller cert space GUID against ANY endpoint's space_id |
|
Naturally handles shared routes: callers from any space that has |
|
a mapped app are allowed |
|
- scope: any → Any authenticated caller passes domain-level check |
|
EOF |