Skip to content

feat(proxy-init): add enforce-drop mode for proxy-sidecar egress enforcement#484

Merged
huang195 merged 4 commits into
rossoctl:mainfrom
huang195:feat/proxy-init-enforce-drop
Jun 5, 2026
Merged

feat(proxy-init): add enforce-drop mode for proxy-sidecar egress enforcement#484
huang195 merged 4 commits into
rossoctl:mainfrom
huang195:feat/proxy-init-enforce-drop

Conversation

@huang195

@huang195 huang195 commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

proxy-sidecar mode enforces egress purely via the HTTP_PROXY env var, which is cooperative — a workload that ignores HTTP_PROXY or sets NO_PROXY egresses directly and bypasses AuthBridge. This adds a fail-closed egress guard (MODE=enforce-drop) to the shared proxy-init image so all external traffic is forced through the forward proxy regardless of whether the app cooperates.

This is PR 1 of 3 in the proxy-sidecar egress-enforcement series:

  1. (this PR) proxy-init enforce-drop mode — lands first, no behavior change until something sets MODE=enforce-drop
  2. kagenti-operator: fix the proxy sidecar RunAsUser to the dedicated 1337 (currently hardcoded to the app's UID) + annotation-gated proxy-init injection in proxy-sidecar mode
  3. docs + flag surfacing

What changed (authbridge/proxy-init/)

  • init-iptables.sh — adds a MODE switch. Default redirect preserves the existing envoy-sidecar behavior byte-for-byte. MODE=enforce-drop builds an AB_EGRESS chain hooked from mangle OUTPUT at position 1: RETURN ztunnel sockets (0x539), the proxy --uid-owner, loopback, and CLUSTER_CIDRS; then DROP everything else (incl. UDP/QUIC). Includes an IPv6 mirror. POD_IP is now required only in redirect mode.
  • test-enforce-drop.sh (new) — unshare --net harness that asserts the rule structure and proves (via packet counters) the mangle DROP preempts a simulated Istio ambient nat OUTPUT REDIRECT.
  • README.md — two-mode model, env-var table (MODE, CLUSTER_CIDRS, CLUSTER_CIDRS6, IP6TABLES_CMD), the mangle-vs-filter rationale, testing/injection notes.

Why mangle, not filter

When Istio ambient is active it installs an in-pod nat OUTPUT REDIRECT (ISTIO_OUTPUT → ztunnel :15001). The netfilter OUTPUT hook order is raw → mangle → nat → filter, so a DROP in mangle evaluates the original destination and fires before ambient rewrites it; a filter DROP runs after nat and would be defeated. This keeps the guard robust with no ambient, in-pod ambient, or node-level ambient.

Testing

sudo ./authbridge/proxy-init/test-enforce-drop.sh

All assertions pass, including the ambient-robustness gate:

PASS: AB_EGRESS at OUTPUT position 1
PASS: no nat-table rules created
mangle AB_EGRESS DROP pkts=2 | nat REDIRECT pkts=0
PASS: mangle DROP preempted nat REDIRECT (ambient-robust)
ALL TESTS PASSED

Compatibility / scope

  • Backward compatible: MODE defaults to redirect; envoy-sidecar is unchanged. Nothing sets MODE=enforce-drop until the follow-up operator PR.
  • No Dockerfile.init change needed — Alpine's iptables/iptables-legacy packages already provide the ip6tables variants (the v6 path is command -v-guarded and degrades cleanly).
  • The operator wiring (MODE=enforce-drop, cluster CIDRs, pinned image tag) and CI test integration land in the follow-up kagenti-operator PR.

Assisted-By: Claude Code

Summary by CodeRabbit

  • New Features

    • Added an enforce-drop egress mode alongside redirect: a fail-closed outbound guard that exempts proxy, loopback, and in-cluster traffic, then drops other external egress. IPv6 handling is supported when available.
  • Documentation

    • Expanded mode-aware environment reference, clarified behavioral differences (e.g., redirect requires pod IP), and updated injection/testing guidance.
  • Tests

    • Added a Linux test harness to validate enforce-drop wiring, rule ordering, and preemption behavior.

…rcement

proxy-sidecar mode routes outbound traffic through AuthBridge via the
HTTP_PROXY env var, which is purely cooperative: a workload that ignores
HTTP_PROXY (or sets NO_PROXY) egresses directly and bypasses AuthBridge
entirely. Add a fail-closed egress guard so all external traffic is
forced through the forward proxy regardless of whether the workload
honors HTTP_PROXY.

init-iptables.sh gains a MODE switch. Default `redirect` preserves the
existing envoy-sidecar behavior byte-for-byte. MODE=enforce-drop builds
an AB_EGRESS chain hooked from mangle OUTPUT at position 1:

  - RETURN ztunnel sockets (fwmark 0x539), the proxy UID, loopback, and
    the in-cluster CIDRs (pods/services/DNS)
  - DROP everything else (direct external egress, including UDP/QUIC)

plus an IPv6 mirror that drops external v6 egress.

Placement is the mangle table, not filter: when Istio ambient is active
it installs an in-pod `nat OUTPUT` REDIRECT (ISTIO_OUTPUT -> ztunnel
:15001). The netfilter OUTPUT hook order is raw -> mangle -> nat ->
filter, so a DROP in mangle evaluates the original destination and fires
before ambient's nat redirect can rewrite it; a DROP in filter would run
after nat and be defeated. -I 1 also keeps us ahead of Istio's appended
mangle chain. This makes the guard robust with no ambient, in-pod
ambient, or node-level ambient.

test-enforce-drop.sh validates the rule structure and proves the mangle
DROP preempts a simulated ISTIO_OUTPUT nat REDIRECT via packet counters,
in an unshare --net namespace.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds MODE selection (redirect|enforce-drop) to proxy-init; implements enforce-drop by creating an AB_EGRESS mangle OUTPUT chain that RETURNs exempt traffic and DROPs other egress (IPv6 mirrored), updates README and env docs, and adds a namespace test validating chain wiring and preemption.

Changes

Enforce-drop egress enforcement feature

Layer / File(s) Summary
Core enforce-drop implementation
authbridge/proxy-init/init-iptables.sh
Introduces MODE (redirect
Mode documentation and environment variable reference
authbridge/proxy-init/README.md
Documents redirect vs enforce-drop modes, details AB_EGRESS design and IPv6 behavior, expands env var table (MODE, PROXY_UID, CLUSTER_CIDRS, CLUSTER_CIDRS6, IPTABLES_CMD, IP6TABLES_CMD), clarifies POD_IP applies only to redirect, and adds Testing and injection notes.
Enforce-drop test harness
authbridge/proxy-init/test-enforce-drop.sh
Adds isolated-network test that runs init-iptables.sh with MODE=enforce-drop, configures a namespace, asserts AB_EGRESS chain structure and position, verifies no nat REDIRECT/PROXY_ rules, and confirms mangle DROP preempts a simulated nat REDIRECT via packet counters.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • pdettori

Poem

A rabbit at the iptables gate, 🐇
Counts marks and UIDs, decides each fate,
"Return for friends, drop for the rest,"
IPv6 twins stand by the test,
In namespaces safe the rules hold fast—egress kept straight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main change: adding an enforce-drop mode to proxy-init for egress enforcement, which aligns with the primary objective of introducing a fail-closed egress guard via the new MODE switch.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@authbridge/proxy-init/test-enforce-drop.sh`:
- Line 19: The script currently enables only "set -u" which leaves command
failures unhandled; update the top-level shell options in
authbridge/proxy-init/test-enforce-drop.sh by replacing the existing set -u with
set -euo pipefail so the test harness exits on errors, treats unset variables as
errors, and makes pipelines fail if any stage fails; ensure this change is
applied where the current "set -u" appears so the new strict mode governs the
whole script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98032e74-8a0b-4903-b59c-1151191ebff3

📥 Commits

Reviewing files that changed from the base of the PR and between c856452 and 91708b7.

📒 Files selected for processing (3)
  • authbridge/proxy-init/README.md
  • authbridge/proxy-init/init-iptables.sh
  • authbridge/proxy-init/test-enforce-drop.sh

Comment thread authbridge/proxy-init/test-enforce-drop.sh Outdated
Address review on rossoctl#484: the AB_EGRESS chain filtered purely on
dest/owner/mark, so reply packets for inbound connections (which also
traverse mangle OUTPUT) to an off-cluster peer would hit the terminal
DROP — e.g. kubelet health-probe responses to the Kind node IP
(172.18.0.0/16, outside the default CLUSTER_CIDRS=10.0.0.0/8), causing
probe failures and pod restarts once enforcement is enabled.

- Add `-m conntrack --ctstate ESTABLISHED,RELATED -j RETURN` as the
  first rule (IPv4 + IPv6). Only NEW app-initiated flows are gated;
  a reply is never a bypass.
- IPv6: also allow link-local multicast (ff02::/16) so NDP
  neighbor/router solicitations work, and correct the comment that
  overstated NDP preservation.
- README: clarify the enforce-drop injection lands in the follow-up
  operator PR rather than reading as already-available.
- test: assert the established/related RETURN exists and is the first
  rule in AB_EGRESS.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@huang195

huang195 commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review — all three addressed in 17c05ec:

1 (ESTABLISHED,RELATED — the real one): Spot on. Replies to inbound connections traverse mangle OUTPUT too, and an off-cluster peer (Kind node 172.18.0.0/16, outside default CLUSTER_CIDRS) would hit the terminal DROP → health-probe failures. Added -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN as the first rule in AB_EGRESS (v4 + v6). NEW app-initiated bypass flows still drop (verified: the harness's bypass SYN is ctstate NEW → DROP pkts=2 | REDIRECT pkts=0 unchanged). Test now asserts the rule exists and is first.

2 (IPv6 NDP): Correct — only fe80::/10 unicast was allowed; NS/RS go to ff02:: multicast. Added -d ff02::/16 -j RETURN and fixed the comment.

3 (README honesty): Qualified the enforce-drop injection bullet — it now states the operator wiring lands in the follow-up PR; this PR only adds the mode to the image.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@mrsabath mrsabath left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Genuinely nice piece of work, Hai. The mangle OUTPUT placement is the right call: hooking before nat evaluates the original destination and survives an in-pod ambient ISTIO_OUTPUT REDIRECT — and the test proves it via packet counters rather than asserting on rule text alone. 滴水不漏 (dī shuǐ bù lòu — "not letting a single drop through"); apt for a fail-closed egress guard. The PR-1-of-3 framing is also helpful — keeping this commit a no-op until the operator wires MODE=enforce-drop is the right way to land it.

Areas reviewed: Shell, Docs
Commits: 1, signed-off + Assisted-By
CI status: all green; CodeRabbit's strict-mode nit on the test harness is left inline below — agree it's worth promoting.

Left three non-blocking suggestions on docs and the test harness. None should hold this up.

# bash, the dummy kernel module. Runs on Linux / CI (e.g. ubuntu-latest); not
# on macOS. Uses `unshare --net` (not named `ip netns`) so it also works inside
# nested containers. Exit code 0 = all pass.
set -u

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (CodeRabbit's point, agree): promoting set -u to set -euo pipefail would catch silent failures in the harness — there are several awk-piped extractions and ${IPT} -t ... invocations whose non-zero exit should fail the test, not just be observed. Especially because the test runs in a fresh netns inside unshare, so a malformed pipeline would otherwise look like a clean run that just "didn't trigger."

-set -u
+set -euo pipefail

Bash shebang on line 1 supports it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already landed in 4f66a6b — switched to set -euo pipefail, verified all 13 assertions still run and the harness exits 0. Thanks (and to CodeRabbit) for promoting it.

| `OUTBOUND_PORTS_EXCLUDE` | (empty) | redirect | Comma-separated outbound port list to skip (e.g. `8080`) |
| `INBOUND_PORTS_EXCLUDE` | (empty) | redirect | Comma-separated inbound port list to skip |
| `POD_IP` | (required in `redirect`) | redirect | Set via Downward API; DNAT target for ambient-mesh inbound. Not used by `enforce-drop`. |
| `CLUSTER_CIDRS` | `10.0.0.0/8` | enforce-drop | Comma-separated in-cluster CIDRs allowed direct (pods/services/DNS) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: the 10.0.0.0/8 default covers Kind (pods 10.244/16 + services 10.96/16) cleanly, but the same operator dropped onto a stock OpenShift cluster will see services in 172.30.0.0/16 and pods in 10.128.0.0/14. Both are outside 10/8 for services. Could be worth a one-line callout in the README that the default is Kind-shaped and OCP/EKS users should override CLUSTER_CIDRS (or that init-iptables.sh could log the resolved value at startup — which it already does, just confirming that's the intended diagnostic path).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on OCP — 172.30.0.0/16 services are outside 10/8 and would be dropped. Added a README callout (f3101d7): the default is Kind-shaped and OCP/EKS must override CLUSTER_CIDRS, with the startup log as the diagnostic path. The follow-up operator PR will set CLUSTER_CIDRS from the cluster's real pod+service ranges, so the default only matters for standalone/Kind testing.

| `PROXY_UID` | `1337` | both | UID of the AuthBridge sidecar process; exempted from redirect / drop |
| `PROXY_PORT` | `15123` | redirect | AuthBridge outbound listener port |
| `INBOUND_PROXY_PORT` | `15124` | redirect | AuthBridge inbound listener port |
| `OUTBOUND_PORTS_EXCLUDE` | (empty) | redirect | Comma-separated outbound port list to skip (e.g. `8080`) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clarifying question: the OUTBOUND_PORTS_EXCLUDE / INBOUND_PORTS_EXCLUDE rows being redirect-only matches the implementation. I want to confirm the design intent: in enforce-drop, an operator who currently uses OUTBOUND_PORTS_EXCLUDE=11434 (Ollama) for redirect would lose direct egress to that port — they'd be forced through the forward proxy, which is exactly what enforce-drop exists to do. Just flagging because the docs/operator wiring (PR 2) might want to surface this as "if you've been relying on OUTBOUND_PORTS_EXCLUDE to bypass the proxy in proxy-sidecar mode, enforce-drop will close that off — that's the point."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — that's the intended design. enforce-drop deliberately does not honor OUTBOUND_PORTS_EXCLUDE: a destination previously bypassed via it (e.g. Ollama at host.docker.internal:11434) is now dropped unless it goes through the forward proxy or is in CLUSTER_CIDRS. Added a README note saying exactly that ("enforce-drop closes direct-egress holes"). Agreed the operator PR should call this out in its rollout notes.

Address CodeRabbit on rossoctl#484: switch the test harness from set -u to
set -euo pipefail (the repo's shell convention) so command/pipe
failures in the setup phase surface instead of being masked. The
run-all-assertions design is unaffected — asserts return 0 via
if/else and expected-nonzero commands are guarded with || true;
verified all 13 assertions still run and the harness exits 0.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
authbridge/proxy-init/test-enforce-drop.sh (1)

46-76: ⚡ Quick win

Add IPv6 rule validation.

The test sets IP6TABLES_CMD=ip6tables-nft at line 48, enabling IPv6 rule creation in init-iptables.sh, but never validates that IPv6 rules were installed. Since IPv6 support is documented (README mentions "An IPv6 mirror") and part of the feature, the test should verify the IPv6 AB_EGRESS chain exists and has the expected structure.

Consider adding after line 76:

# Validate IPv6 mirror (if ip6tables-nft is available)
if command -v ip6tables-nft >/dev/null 2>&1; then
  dump6=$(ip6tables-nft -t mangle -S)
  echo "--- mangle IPv6 ruleset ---"; echo "${dump6}"
  
  assert6() { if echo "${dump6}" | grep -qE "$2"; then echo "PASS: [IPv6] $1"; else echo "FAIL: [IPv6] $1"; fail=1; fi; }
  assert6 "AB_EGRESS hooked from OUTPUT" '^-A OUTPUT -j AB_EGRESS'
  assert6 "established/related RETURN" 'AB_EGRESS -m conntrack --ctstate (ESTABLISHED,RELATED|RELATED,ESTABLISHED) -j RETURN'
  assert6 "terminal DROP" 'AB_EGRESS -j DROP'
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/proxy-init/test-enforce-drop.sh` around lines 46 - 76, Add IPv6
validation similar to the IPv4 checks: when IP6TABLES_CMD (or ip6tables-nft) is
available, capture the IPv6 mangle rules into a variable (e.g., dump6), print
it, and run parallel assertions (assert6) against the AB_EGRESS chain to verify
the hook from OUTPUT, the established/related conntrack RETURN, and the terminal
DROP; place this block after the IPv4 mangle assertions/natcount check so it
executes as part of the same test flow and use the same pattern of setting
fail=1 on failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@authbridge/proxy-init/test-enforce-drop.sh`:
- Around line 46-76: Add IPv6 validation similar to the IPv4 checks: when
IP6TABLES_CMD (or ip6tables-nft) is available, capture the IPv6 mangle rules
into a variable (e.g., dump6), print it, and run parallel assertions (assert6)
against the AB_EGRESS chain to verify the hook from OUTPUT, the
established/related conntrack RETURN, and the terminal DROP; place this block
after the IPv4 mangle assertions/natcount check so it executes as part of the
same test flow and use the same pattern of setting fail=1 on failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0eba212d-1bd3-4c32-b881-7fa62a8255df

📥 Commits

Reviewing files that changed from the base of the PR and between 17c05ec and 4f66a6b.

📒 Files selected for processing (1)
  • authbridge/proxy-init/test-enforce-drop.sh

…semantics

Address mrsabath review on rossoctl#484 (non-blocking):
- CLUSTER_CIDRS=10.0.0.0/8 default is Kind-shaped; OCP (services
  172.30.0.0/16, pods 10.128.0.0/14) and EKS users must override it
  or in-cluster service traffic is dropped. Operator wiring (follow-up
  PR) sets it from the cluster's real CIDRs.
- enforce-drop intentionally ignores OUTBOUND_PORTS_EXCLUDE
  (redirect-only); a destination previously bypassed that way is now
  dropped unless routed through the proxy or in CLUSTER_CIDRS — by design.

The third suggestion (set -euo pipefail) already landed in 4f66a6b.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants