#!/usr/bin/env bash # T02 (re-run 2 / r2) — outboundTrafficPolicy REGISTRY_ONLY via namespace-scoped Sidecar CR # blocks unregistered destinations differently per protocol (HTTP 502 vs TLS reset), # and ServiceEntry registration restores access. # # CORRECTION vs attempt1: attempt1 used the in-cluster mock service # (mock.istio-verify-ext) as the "unregistered destination". That is WRONG — any # k8s Service in the mesh is automatically part of Istio's service registry # (registered as ..svc.cluster.local, IP-based EDS cluster), so it was # never actually "unregistered", regardless of what hostname/DNS-domain was used # to reach it. Under REGISTRY_ONLY, TLS/TCP traffic on port 443 matched that # pre-existing EDS cluster by destination IP and sailed through with 200, which # falsely looked like "TLS bypasses REGISTRY_ONLY". # # SECOND CORRECTION (discovered live during this r2 run): the harness guidance's # suggested stand-in, httpbin.org, is ALSO not a valid "unregistered destination" # on THIS cluster — the pre-existing `mesh-test` namespace already has a # ServiceEntry `httpbin-ext` (hosts: httpbin.org, no exportTo => default "*" => # mesh-wide visible to every sidecar, including a brand-new namespace's), plus a # mesh-wide DestinationRule `httpbin-ext-outlier` (outlier detection + 2s connect # timeout on host httpbin.org) and a VirtualService `egress-httpbin` bound to the # reserved `mesh` gateway for host httpbin.org (applies to every sidecar's # outbound listener mesh-wide). Confirmed empirically: even BEFORE applying our # own REGISTRY_ONLY Sidecar, a baseline HTTPS call to httpbin.org:443 already # failed with curl exit 35 (SSL connect error) because of this pre-existing # fixture, not because of anything under test. edition.cnn.com is in the same # situation (SE `cnn-ext`, no exportTo => mesh-wide). example.com / example.org / # www.wikipedia.org are also SE-registered in mesh-test but with exportTo # restricted to ["."]/["egress-pt"]/["egress-mtls"] respectively, so they are NOT # visible to our namespace — usable in principle, but chosen against to avoid any # ambiguity. Verified clean via `kubectl get serviceentry,virtualservice, # destinationrule -A` (zero hits for our chosen host) before use: # postman-echo.com — a real external host with ZERO Istio config anywhere in # this cluster, reachable over both HTTP and HTTPS (precheck from an uninjected # pod returned 200/200). This run uses postman-echo.com as the genuinely # unregistered destination. # # Adaptation rule applied (harness-notes.md CORRECTION 2026-07-05): # - curl to k8s Services -> short name (n/a here, no in-mesh Service target) # - Istio resource hosts for k8s Services -> ..svc.cluster.local (n/a here) # - ServiceEntry hosts -> literal hostname the client actually uses (postman-echo.com) # - "unregistered destination" -> real external host with NO pre-existing Istio # registration anywhere in the cluster (postman-echo.com), not an in-cluster # mock Service, and not httpbin.org/edition.cnn.com which turned out to # already be mesh-wide registered by an unrelated pre-existing fixture. # # Re-runnable: creates its own namespace, applies manifests in stages exactly as # the spec commands do (destination substituted per the corrections above), and # cleans up the namespace on exit (success or failure). set -euo pipefail NS="istio-vt-t02-r2" HOST="postman-echo.com" DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cleanup() { echo "=== [CLEANUP] deleting namespace $NS ===" kubectl delete namespace "$NS" --wait=false --ignore-not-found } trap cleanup EXIT echo "=== [PRECHECK] confirm $HOST has zero pre-existing Istio config cluster-wide ===" kubectl get serviceentry,virtualservice,destinationrule -A -o json | grep -i "postman" && echo "WARNING: found existing config!" || echo "clean: no SE/VS/DR references $HOST anywhere" echo "=== [SETUP] create namespace $NS (istio-injection=enabled) ===" kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl label --local -f - istio-injection=enabled -o yaml --dry-run=client | kubectl apply -f - echo "=== [SETUP] apply client-echo.yaml (Pod client + Deployment echo + Service echo) ===" kubectl apply -f "$DIR/client-echo.yaml" echo "=== [WAIT] client pod Ready ===" kubectl -n "$NS" wait --for=condition=Ready pod/client --timeout=90s echo "=== [WAIT] echo rollout ===" kubectl -n "$NS" rollout status deployment/echo --timeout=90s echo "=== [CHECK] pod status (expect 2/2 sidecar injected) ===" kubectl -n "$NS" get pods -o wide echo "" echo "=== [DIAG-A0] proxy-config clusters client (baseline, before anything, filter=$HOST) ===" istioctl proxy-config clusters "client.$NS" --fqdn "$HOST" 2>/dev/null || true echo "(expect: no dedicated $HOST cluster yet -> ALLOW_ANY traffic to it rides PassthroughCluster)" echo "" echo "=== CMD1: kubectl apply -f client-echo.yaml && wait Ready ===" kubectl apply -f "$DIR/client-echo.yaml" && kubectl -n "$NS" wait --for=condition=Ready pod/client --timeout=90s echo "" echo "=== CMD2: allow_any_http check (real external host $HOST) ===" kubectl -n "$NS" exec client -c curl -- curl -s -o /dev/null -w 'allow_any_http=%{http_code}\n' "http://$HOST/get" echo "" echo "=== CMD3: allow_any_https check (real external host $HOST) ===" kubectl -n "$NS" exec client -c curl -- curl -sk -o /dev/null -w 'allow_any_https=%{http_code}\n' --max-time 5 "https://$HOST:443/get" echo "" echo "=== [DIAG-A1] proxy-config clusters client (ALLOW_ANY, path taken for $HOST:80/443) ===" istioctl proxy-config clusters "client.$NS" 2>/dev/null | grep -Ei 'passthrough|postman|blackhole' || true echo "" echo "=== CMD4: kubectl apply -f registry-only-sidecar.yaml ===" kubectl apply -f "$DIR/registry-only-sidecar.yaml" echo "" echo "=== CMD5: sleep 5 ===" sleep 5 echo "" echo "=== CMD6: istioctl proxy-config cluster client.$NS | grep -i blackhole ===" istioctl proxy-config cluster "client.$NS" 2>/dev/null | grep -i blackhole || true echo "" echo "=== [DIAG-B1] proxy-config clusters client (REGISTRY_ONLY, before SE, filter=$HOST) ===" istioctl proxy-config clusters "client.$NS" --fqdn "$HOST" 2>/dev/null || true echo "(expect: still no dedicated $HOST cluster -> no vhost/no IP match -> falls to BlackHoleCluster on both protocols)" echo "" echo "=== CMD7: registry_only_http check ===" kubectl -n "$NS" exec client -c curl -- curl -s -o /dev/null -w 'registry_only_http=%{http_code}\n' "http://$HOST/get" echo "" echo "=== CMD8: registry_only_https check ===" set +e kubectl -n "$NS" exec client -c curl -- curl -sk -o /dev/null -w 'registry_only_https_code=%{http_code}\n' --max-time 5 "https://$HOST:443/get" ; echo "curl_exit=$?" set -e echo "" echo "=== [DIAG-B2] proxy-config listener client on 0.0.0.0_443 (virtualOutbound default filter chain path, REGISTRY_ONLY) ===" istioctl proxy-config listener "client.$NS" --port 15001 -o json 2>/dev/null | grep -Eio '"name": "[^"]*(blackhole|passthrough)[^"]*"' | sort -u || true echo "" echo "=== CMD9: kubectl apply -f postman-ext-se.yaml (ServiceEntry hosts: $HOST) ===" kubectl apply -f "$DIR/postman-ext-se.yaml" echo "" echo "=== CMD10: sleep 5 ===" sleep 5 echo "" echo "=== [DIAG-C1] proxy-config clusters client (after SE, filter=$HOST) ===" istioctl proxy-config clusters "client.$NS" --fqdn "$HOST" 2>/dev/null || true echo "(expect: outbound|80||$HOST and outbound|443||$HOST now exist, STRICT_DNS)" echo "" echo "=== CMD11: after_se_registered_http check ===" kubectl -n "$NS" exec client -c curl -- curl -s -o /dev/null -w 'after_se_registered_http=%{http_code}\n' "http://$HOST/get" echo "" echo "=== [DIAG-C2] after_se_registered_https check (extra, not in spec commands but useful corroboration) ===" kubectl -n "$NS" exec client -c curl -- curl -sk -o /dev/null -w 'after_se_registered_https=%{http_code}\n' --max-time 5 "https://$HOST:443/get" || true echo "" echo "=== DONE (namespace $NS will be deleted by trap on exit) ==="