Skip to content

Commit 8966c9e

Browse files
committed
fix: Capture in-cluster TCP egress in enforce-redirect (keep DNS direct)
enforce-redirect blanket-RETURNed all CLUSTER_CIDRS (default 10.0.0.0/8) egress, so agent->in-cluster TCP (e.g. agent->tool) bypassed the egress pipeline entirely: outbound policy / token-exchange (OBO) never ran for those hops. Only the cooperative HTTP_PROXY path captured them, so any client that ignores HTTP_PROXY escaped enforcement for in-cluster calls. Narrow the in-cluster RETURN in the nat (TCP) chain to DNS-over-TCP (TCP/53) only: cluster name resolution stays direct while all other in-cluster TCP is REDIRECTed to the transparent listener and seen by the pipeline. The mangle (non-TCP) chain still RETURNs in-cluster CIDRs so DNS-over-UDP and other in-cluster UDP are untouched. Mesh-agnostic: the proxy's re-originated egress (PROXY_UID, exempt) still falls through to ISTIO_OUTPUT -> ztunnel for transport mTLS under Istio ambient, and goes out plain without a mesh. ztunnel re-captures it because its OUTPUT redirect is mark-gated (! 0x539), not UID-gated. Validation: structural test (test-enforce-redirect.sh) updated; full cluster validation (Istio ambient mTLS chaining + no-mesh) pending. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
1 parent f1c5e76 commit 8966c9e

2 files changed

Lines changed: 35 additions & 15 deletions

File tree

authbridge/proxy-init/init-iptables.sh

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,13 @@ SSH_PORT="${SSH_PORT:-22}"
178178
OUTBOUND_PORTS_EXCLUDE="${OUTBOUND_PORTS_EXCLUDE:-}"
179179
INBOUND_PORTS_EXCLUDE="${INBOUND_PORTS_EXCLUDE:-}"
180180

181-
# enforce-redirect mode: in-cluster destinations the agent may reach directly
182-
# (pods / services / DNS) — external TCP is REDIRECTed to the transparent
183-
# listener and external non-TCP is dropped. Defaults to the RFC1918 10/8 block
184-
# which covers typical Kind pod (10.244/16) and service (10.96/16) CIDRs;
185-
# override with the cluster's actual ranges.
181+
# enforce-redirect mode: in-cluster CIDRs used to keep cluster DNS resolution
182+
# working — in-cluster DNS-over-TCP (TCP/53) and in-cluster non-TCP (UDP, incl.
183+
# DNS) are left direct, while all OTHER in-cluster TCP is now captured by the
184+
# egress pipeline so agent->in-cluster calls (e.g. agent->tool) are seen.
185+
# Defaults to the RFC1918 10/8 block (typical Kind pod 10.244/16 + service
186+
# 10.96/16); override with the cluster's actual ranges (e.g. OpenShift
187+
# 172.30.0.0/16) so cluster DNS is not captured.
186188
CLUSTER_CIDRS="${CLUSTER_CIDRS:-10.0.0.0/8}"
187189
CLUSTER_CIDRS6="${CLUSTER_CIDRS6:-}" # IPv6 in-cluster CIDRs (dual-stack); empty = none
188190

@@ -226,9 +228,11 @@ fi
226228
# Rule order: RETURN ztunnel's own sockets (fwmark 0x539, no-op without ambient)
227229
# -> RETURN the proxy's own re-originated egress (PROXY_UID, avoids the loop) ->
228230
# RETURN loopback (app -> forward proxy via HTTP_PROXY, and any loopback) ->
229-
# RETURN in-cluster CIDRs (mesh/DNS, left direct) -> REDIRECT external TCP to
230-
# TRANSPARENT_PORT -> DROP all other external egress (UDP/QUIC, so HTTP/3 can't
231-
# bypass; well-behaved clients fall back to TCP and get captured).
231+
# RETURN in-cluster DNS-over-TCP (TCP/53 to CLUSTER_CIDRS, left direct) ->
232+
# REDIRECT all remaining TCP -- external AND in-cluster -- to TRANSPARENT_PORT
233+
# (so agent->in-cluster calls are captured too) -> DROP all other external
234+
# egress (UDP/QUIC, so HTTP/3 can't bypass; clients fall back to TCP). In-cluster
235+
# non-TCP (UDP, incl. DNS) is left direct by the mangle chain below.
232236
#
233237
# The nat REDIRECT chain has no conntrack ESTABLISHED rule: nat only evaluates
234238
# the first packet of a flow, so replies and established connections are not
@@ -248,7 +252,7 @@ setup_enforce_redirect() {
248252

249253
echo "enforce-redirect: installing fail-closed egress capture"
250254
echo "enforce-redirect: external TCP -> 127.0.0.1:${TRANSPARENT_PORT} (nat REDIRECT); external non-TCP -> DROP (mangle)"
251-
echo "enforce-redirect: exempt proxy UID=${PROXY_UID}; direct in-cluster CIDRs=${CLUSTER_CIDRS}"
255+
echo "enforce-redirect: exempt proxy UID=${PROXY_UID}; in-cluster TCP captured (DNS/53 + non-TCP left direct; CIDRs=${CLUSTER_CIDRS})"
252256

253257
# --- IPv4: nat REDIRECT for TCP ---
254258
${IPT} -t nat -N "${REDIR_CHAIN}" 2>/dev/null || true
@@ -261,11 +265,15 @@ setup_enforce_redirect() {
261265
# app -> forward proxy over loopback (HTTP_PROXY target), and any loopback.
262266
${IPT} -t nat -A "${REDIR_CHAIN}" -o lo -j RETURN
263267
${IPT} -t nat -A "${REDIR_CHAIN}" -d 127.0.0.0/8 -j RETURN
264-
# in-cluster traffic (pods / services / DNS) — left direct, carried by the mesh.
268+
# in-cluster DNS-over-TCP (TCP/53) — left direct so cluster name resolution is
269+
# not captured. All OTHER in-cluster TCP falls through to the REDIRECT below,
270+
# so the egress pipeline sees agent->in-cluster calls (e.g. agent->tool). The
271+
# proxy's re-originated egress (PROXY_UID, RETURNed above) is exempt, and in an
272+
# Istio ambient mesh it falls through to ISTIO_OUTPUT -> ztunnel for mTLS.
265273
for cidr in $(echo "${CLUSTER_CIDRS}" | tr ',' ' '); do
266-
[ -n "${cidr}" ] && ${IPT} -t nat -A "${REDIR_CHAIN}" -d "${cidr}" -j RETURN
274+
[ -n "${cidr}" ] && ${IPT} -t nat -A "${REDIR_CHAIN}" -p tcp --dport 53 -d "${cidr}" -j RETURN
267275
done
268-
# external TCP that bypassed the forward proxy — capture it transparently.
276+
# all remaining TCP (external + in-cluster, minus in-cluster DNS) — capture it transparently.
269277
${IPT} -t nat -A "${REDIR_CHAIN}" -p tcp -j REDIRECT --to-port "${TRANSPARENT_PORT}"
270278
if ! ${IPT} -t nat -C OUTPUT -j "${REDIR_CHAIN}" 2>/dev/null; then
271279
${IPT} -t nat -I OUTPUT 1 -j "${REDIR_CHAIN}"
@@ -280,6 +288,9 @@ setup_enforce_redirect() {
280288
${IPT} -t mangle -A "${NOTCP_CHAIN}" -m owner --uid-owner "${PROXY_UID}" -j RETURN
281289
${IPT} -t mangle -A "${NOTCP_CHAIN}" -o lo -j RETURN
282290
${IPT} -t mangle -A "${NOTCP_CHAIN}" -d 127.0.0.0/8 -j RETURN
291+
# in-cluster non-TCP (UDP, incl. DNS) — left direct. In-cluster TCP is now
292+
# captured by the nat chain above; only in-cluster UDP relies on this RETURN
293+
# (notably DNS-over-UDP, which the terminal DROP below would otherwise kill).
283294
for cidr in $(echo "${CLUSTER_CIDRS}" | tr ',' ' '); do
284295
[ -n "${cidr}" ] && ${IPT} -t mangle -A "${NOTCP_CHAIN}" -d "${cidr}" -j RETURN
285296
done
@@ -305,8 +316,10 @@ setup_enforce_redirect() {
305316
${IP6T} -t nat -A "${REDIR_CHAIN}" -d ::1/128 -j RETURN
306317
${IP6T} -t nat -A "${REDIR_CHAIN}" -d fe80::/10 -j RETURN
307318
${IP6T} -t nat -A "${REDIR_CHAIN}" -d ff02::/16 -j RETURN
319+
# in-cluster DNS-over-TCP (TCP/53) only — mirror of IPv4; all other in-cluster
320+
# v6 TCP is captured below.
308321
for cidr in $(echo "${CLUSTER_CIDRS6}" | tr ',' ' '); do
309-
[ -n "${cidr}" ] && ${IP6T} -t nat -A "${REDIR_CHAIN}" -d "${cidr}" -j RETURN
322+
[ -n "${cidr}" ] && ${IP6T} -t nat -A "${REDIR_CHAIN}" -p tcp --dport 53 -d "${cidr}" -j RETURN
310323
done
311324
${IP6T} -t nat -A "${REDIR_CHAIN}" -p tcp -j REDIRECT --to-port "${TRANSPARENT_PORT}"
312325
if ! ${IP6T} -t nat -C OUTPUT -j "${REDIR_CHAIN}" 2>/dev/null; then

authbridge/proxy-init/test-enforce-redirect.sh

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
# position 1 with the expected RETURN exemptions and a `-p tcp` REDIRECT to
99
# TRANSPARENT_PORT (no DROP — the nat table forbids it); and the AB_NOTCP
1010
# chain is hooked from mangle OUTPUT with `-p tcp RETURN` then a terminal
11-
# DROP for external non-TCP egress.
11+
# DROP for external non-TCP egress. In-cluster DNS-over-TCP (TCP/53) is left
12+
# direct; all OTHER in-cluster TCP is captured (no blanket in-cluster RETURN).
1213
# 2. CAPTURE (not drop) + AMBIENT ROBUSTNESS — external TCP egress is
1314
# REDIRECTed to TRANSPARENT_PORT, preempting a simulated Istio ambient
1415
# "nat OUTPUT REDIRECT" appended after our chain. Proven via packet
@@ -65,7 +66,13 @@ assert "nat ztunnel mark RETURN" 'AB_REDIRECT .*mark.*0x539.*-j RETUR
6566
assert "nat proxy UID RETURN" 'AB_REDIRECT .*--uid-owner 1337 -j RETURN' "${natdump}"
6667
assert "nat loopback iface RETURN" 'AB_REDIRECT -o lo -j RETURN' "${natdump}"
6768
assert "nat loopback cidr RETURN" 'AB_REDIRECT -d 127.0.0.0/8 -j RETURN' "${natdump}"
68-
assert "nat cluster cidr RETURN" 'AB_REDIRECT -d 10.0.0.0/8 -j RETURN' "${natdump}"
69+
# in-cluster DNS-over-TCP (TCP/53) is left direct so cluster name resolution is
70+
# not captured; all OTHER in-cluster TCP now falls through to the REDIRECT.
71+
assert "nat in-cluster DNS-over-TCP RETURN" 'AB_REDIRECT.*10\.0\.0\.0/8.*dport 53.*RETURN' "${natdump}"
72+
# the blanket in-cluster RETURN must be gone — in-cluster TCP is now captured.
73+
if echo "${natdump}" | grep -qE '^-A AB_REDIRECT -d 10\.0\.0\.0/8 -j RETURN$'; then
74+
echo "FAIL: nat AB_REDIRECT still blanket-RETURNs in-cluster (in-cluster TCP not captured)"; fail=1
75+
else echo "PASS: no blanket in-cluster RETURN — in-cluster TCP captured, only DNS/53 left direct"; fi
6976
assert "nat tcp REDIRECT to transparent" "AB_REDIRECT -p tcp -j REDIRECT --to-ports ${TPORT}" "${natdump}"
7077
if echo "${natdump}" | grep -qE 'AB_REDIRECT -j DROP'; then
7178
echo "FAIL: nat AB_REDIRECT must not contain DROP (nat table forbids it)"; fail=1

0 commit comments

Comments
 (0)