Skip to content

feat(egress): host hardening survey — follow-up to #30179#35187

Open
Bartok9 wants to merge 1 commit into
NousResearch:feat/iron-proxyfrom
Bartok9:bartok9/iron-proxy-harden-survey
Open

feat(egress): host hardening survey — follow-up to #30179#35187
Bartok9 wants to merge 1 commit into
NousResearch:feat/iron-proxyfrom
Bartok9:bartok9/iron-proxy-harden-survey

Conversation

@Bartok9

@Bartok9 Bartok9 commented May 30, 2026

Copy link
Copy Markdown
Contributor

Why this complements #30179

#30179 introduced the iron-proxy sandbox-egress layer (swap opaque proxy tokens for real upstream creds at the network boundary, so a prompt-injected agent never sees real keys). That stops credential exfil from inside the sandbox.

This PR adds the host-perimeter side of the picture. hermes egress harden is a read-only survey that probes the host's firewall (UFW / firewalld / nftables), Tailscale, fail2ban, SSH config, and Docker seccomp — alongside two iron-proxy runtime signals via get_status() — so an operator sees the whole defense-in-depth stack in one table.

The two layers solve different threats; neither substitutes for the other:

  • A firewall does nothing against a prompt-injected agent that already runs inside a sandbox and POSTs OPENAI_API_KEY to an attacker. iron-proxy stops that.
  • The egress proxy does nothing about an open SSH port with password auth. Perimeter hardening stops that.

A companion CA-rotation PR (hermes egress rotate-ca) is open separately against the same base.

What lands

  • New module agent/proxy_sources/host_hardening.py — stdlib-only, no side effects, every probe graceful (missing binary / non-Linux host → skip, never fail). The only touch to the existing iron-proxy code is a from .iron_proxy import get_status for the two runtime signals.
  • New CLI subcommand hermes egress harden (one sub-parser entry + one handler in hermes_cli/proxy_cli.py):
    • --baseline {minimal,catalin,paranoid} (default minimal)
    • --json{"signals":[{name,status,detail,fix}],"baseline":str,"satisfied":bool,"missing":[str]}
    • --all → show passing signals too (default: gaps-only)
    • Always exits 0 — informational, never gates anything.
  • 27 hermetic tests in tests/test_iron_proxy_harden.py (mirrors test_iron_proxy_doctor.py style if/when doctor lands).
  • New docs page website/docs/user-guide/egress/hardening-baselines.md + a "Host hardening" section in iron-proxy.md and a harden block in cli-commands.md.

The 10 signals

# Signal Probe
1 tailscale tailscale status --jsonBackendState=Running
2 ufw ufw status verbose → active + default-deny incoming
3 firewalld firewall-cmd --stateexact match "running" (see Bugbot fix below)
4 nftables nft list ruleset → non-empty
5 fail2ban fail2ban-client status → ≥1 jail
6 ssh-password-auth ^PasswordAuthentication no in sshd_config
7 ssh-root-login `^PermitRootLogin (no
8 iron-proxy-enabled reuse get_status()
9 iron-proxy-running reuse get_status() (pid alive + listening)
10 docker-seccomp docker info SecurityOptions includes seccomp

Baselines

Color the summary line only — never force a fail:

  • minimal — any one firewall (ufw / firewalld / nftables / tailscale) + ssh-password-auth + iron-proxy-enabled
  • catalin — tailscale + ufw + fail2ban + ssh-password-auth + iron-proxy-enabled (named for @catalinmpit's public Hermes Hetzner deployment posture)
  • paranoid — all 10 signals pass

Failure modes considered

  • Hung firewall / VPN binary — every probe runs with a 4 s timeout via a shared _run() helper; a hang → skip, never a wedged survey.
  • Non-Linux hostufw / firewalld / nftables / fail2ban skip cleanly on macOS; Tailscale / SSH-config / iron-proxy / Docker still run.
  • Missing /etc/ssh/sshd_config — both SSH signals skip (not fail); many minimal containers don't run sshd.
  • get_status() raising — wrapped; the iron-proxy signals degrade to skip, never crash the survey.
  • Non-JSON / partial command outputtailscale non-JSON → warn; nft / docker non-zero exit → warn with an actionable hint.
  • Last-directive-wins — sshd_config parsing scans all matches and takes the last effective directive.

Bugbot review fixes baked in

Two issues Cursor Bugbot caught on the original cross-fork iteration, fixed inline here (no separate cleanup PR needed):

🔴 High severity — firewalld substring match false-positives a stopped daemon

"running" in out.strip().lower() evaluates True for "not running" — meaning the probe would report PASS for a stopped firewalld, the worst-possible failure mode for a hardening probe. Fix: exact-equality match on the stripped output. Regression test added: test_firewalld_stopped_is_fail_not_pass.

🟡 Medium severity — table column 3 showed redundant status text

The third column rendered s.status ("fail"/"warn"/"skip") which is already encoded in the glyph in column 1, while the useful s.detail ("ufw installed but inactive", "BackendState=Running") was never shown in non-JSON mode. Fix: column renamed to "Detail" and now renders s.detail. Regression test added: test_cmd_harden_table_shows_detail_not_status.

Validation

$ pytest tests/test_iron_proxy*.py -q
109 passed, 1 skipped in 3.88s

The 1 skip is the existing E2E test gated behind HERMES_RUN_E2E=1 (unchanged).

Metric Before (this PR's base = feat/iron-proxy) After
iron-proxy suite 82 passed, 1 skipped 109 passed, 1 skipped
New tests 27 (tests/test_iron_proxy_harden.py)

hermes egress harden --json runs clean (exit 0) on a macOS dev host with Linux-only signals degrading to skip — confirms graceful cross-platform behavior.

Coverage gaps

  • The iron-proxy enabled signal treats a generated CA + proxy.yaml as "enabled" because get_status() doesn't read config.yaml's proxy.enabled. An explicit enabled=True also passes. Documented inline.
  • nft list ruleset without root returns a non-zero exit → warn (we can't distinguish "no ruleset" from "needs root"); the hint says to re-run with sudo.

Ambiguity flags

  1. "iron-proxy enabled" definitionget_status() exposes .enabled (dataclass default False, not populated from config) and .configured (CA + proxy.yaml present). I treated enabled OR configured as PASS so the signal is useful without a full config load. If you want strict config.yaml: proxy.enabled semantics, that's a one-line change.
  2. harden as its own command vs. a doctor check — kept as a separate command tree (hermes egress harden). They're complementary: doctor checks the proxy is healthy; harden checks the host is locked down.
  3. Baseline OR-groups — "any firewall" is modeled as an OR-group; when none pass, the missing list reports all acceptable members so the operator sees every valid fix. Open to reporting just a representative one if that's noisy.

Cut from scope (known follow-ups)

Documented in hardening-baselines.md under "Future enhancements":

  • Hetzner-specific cloud-firewall detection
  • Cloudflare edge / CIDR DNS caching (not host-detectable from inside the box)
  • legacy iptables -L baseline
  • nftables-vs-iptables differentiation
  • Docker user-namespace remapping (userns-remap)

Attribution

Opened by Bartok9 (Daniel Pike) at Daniel's request, in response to Teknium's invitation on X to @catalinmpit for a security review of #30179. Teknium's prompt was essentially "can we get a security review of how Hermes' egress proxy holds up in a real deployment?" — and Catalin's public deployment described the shape this PR encodes as the catalin baseline:

"I've deployed my Hermes agent on a Hetzner VPS — locked behind Tailscale, UFW default-deny, Cloudflare in front, and fail2ban watching SSH. The only thing exposed is what I explicitly allow." — @catalinmpit (paraphrased from the X thread Teknium linked)

This PR turns that ad-hoc perimeter into a first-class, surveyable baseline that composes with the existing sandbox-egress isolation.

Read-only survey of host perimeter controls — firewall (UFW / firewalld /
nftables), Tailscale, fail2ban, SSH config, Docker seccomp — alongside two
iron-proxy runtime signals (via the existing `get_status()`). Shows the
whole defense-in-depth stack in one table.

`hermes egress harden` is informational; it always exits 0 and never gates
a deploy. The `--baseline` flag (`minimal` / `catalin` / `paranoid`)
colors the summary line only; the `--all` flag includes passing signals
in the table; `--json` emits a stable schema for SIEM/dashboard ingest.

The 10 signals: tailscale, ufw, firewalld, nftables, fail2ban,
ssh-password-auth, ssh-root-login, iron-proxy-enabled, iron-proxy-running,
docker-seccomp. Each probe is best-effort with a 4 s timeout — missing
binary / non-Linux host → skip (not fail), so the survey never wedges.

Stdlib only, no new dependencies. Reuses `get_status()` from the
iron-proxy core (the only touch outside the new module).

Inspired by @catalinmpit's Hetzner+Tailscale+UFW+Cloudflare+fail2ban
deployment that prompted Teknium's "secure hermes" question on X — the
`catalin` baseline encodes that posture as a first-class target.

Bugbot review fixes baked in (caught on the original fork PR, retargeted
to upstream here as one clean commit):

  * firewalld substring match -> exact-equality match
    "running" in "not running".lower() was True, falsely reporting PASS
    when firewalld is stopped (high severity). Regression test added:
    test_firewalld_stopped_is_fail_not_pass.

  * cmd_harden table column 3 now shows s.detail (e.g. "ufw installed
    but inactive") instead of s.status (which is already rendered as a
    glyph in column 1). Mirrors cmd_doctor. Regression test added:
    test_cmd_harden_table_shows_detail_not_status.

Validation:
  27 passed in 1.23 s (tests/test_iron_proxy_harden.py)

Author: Bartok9 (Daniel Pike), opened in response to Teknium's invitation
to @catalinmpit for a security review of NousResearch#30179. Complements NousResearch#30179
(sandbox-egress isolation) by surveying the perimeter side of the stack.
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard area/docker Docker image, Compose, packaging labels May 30, 2026
@Bartok9

Bartok9 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Polish — improved description bullets

hermes egress harden — read-only perimeter survey across UFW/firewalld/nftables, Tailscale, fail2ban, SSH config, Docker seccomp, and iron-proxy runtime signals
• Three baselines (minimal / catalin / paranoid) + --all / --json output for SIEM/dashboard ingestion
• Bugbot fixes shipped with regression tests: exact-match firewalld detection + detail column rendering
• Directly addresses Teknium’s “secure hermes” question on X; catalin baseline encodes the exact Hetzner+Tailscale+UFW+fail2ban posture
• 27 tests, stdlib only, best-effort probes (never wedges on missing tools or non-Linux hosts)

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

Labels

area/docker Docker image, Compose, packaging comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants