Configure ADMIN_SECRETS IPv6 whitelist for dashboard access control #11

Closed
opened 2026-04-26 12:19:53 +00:00 by mahmoud · 2 comments
Owner

Parent: #8

Context

hero_codescalers TCP connections are gated by an IPv6 whitelist stored in hero_proc secrets under the key ADMIN_SECRETS. This needs to be configured so only trusted admin users (e.g. over Mycelium) can reach the dashboard. Unix-socket calls are always trusted and bypass this.

What to do

  • Store admin Mycelium IPv6 addresses in hero_proc secrets under ADMIN_SECRETS key
  • Verify hero_codescalers_ui reads and enforces this whitelist on TCP/HTTP connections (Mycelium IPv6 range 400::/7)
  • Unix socket path remains always trusted (no change needed there)
  • Test: non-whitelisted IPv6 → blocked; whitelisted → allowed; Unix socket → always allowed

Relevant paths

  • hero_codescalers/crates/hero_codescalers_ui/ — Axum web dashboard, where the IPv6 whitelist check should be enforced
  • hero_proc/crates/hero_proc_server/ — secrets storage
  • Socket: $HERO_SOCKET_DIR/hero_codescalers_server<N>/

Acceptance Criteria

  • ADMIN_SECRETS populated in hero_proc with admin IPv6 addresses
  • TCP connections from non-whitelisted IPs are rejected
  • Whitelisted Mycelium IPv6 addresses can reach dashboard
  • Unix socket access remains unrestricted
Parent: #8 ## Context hero_codescalers TCP connections are gated by an IPv6 whitelist stored in hero_proc secrets under the key `ADMIN_SECRETS`. This needs to be configured so only trusted admin users (e.g. over Mycelium) can reach the dashboard. Unix-socket calls are always trusted and bypass this. ## What to do - Store admin Mycelium IPv6 addresses in hero_proc secrets under `ADMIN_SECRETS` key - Verify hero_codescalers_ui reads and enforces this whitelist on TCP/HTTP connections (Mycelium IPv6 range `400::/7`) - Unix socket path remains always trusted (no change needed there) - Test: non-whitelisted IPv6 → blocked; whitelisted → allowed; Unix socket → always allowed ## Relevant paths - `hero_codescalers/crates/hero_codescalers_ui/` — Axum web dashboard, where the IPv6 whitelist check should be enforced - `hero_proc/crates/hero_proc_server/` — secrets storage - Socket: `$HERO_SOCKET_DIR/hero_codescalers_server<N>/` ## Acceptance Criteria - [ ] ADMIN_SECRETS populated in hero_proc with admin IPv6 addresses - [ ] TCP connections from non-whitelisted IPs are rejected - [ ] Whitelisted Mycelium IPv6 addresses can reach dashboard - [ ] Unix socket access remains unrestricted
Author
Owner

ADMIN_SECRETS whitelist verified — design works as documented

Setup applied on kristof4 (root context)

hero_proc secret set --root --context core ADMIN_SECRETS \
  "<kristof1-host-mycelium>,<kristof3-host-mycelium>,<kristof4-host-mycelium>,<kristof6-host-mycelium>"

The four entries are each kristof box's mycelium TUN IP:

Box Host TUN IPv6
kristof1 543:66c5:6430:8f31:5293:1ad9:694a:70f3
kristof3 58b:c3be:cf70:4826:765a:2c6d:6c8d:a5e2
kristof4 4a0:6976:8fa7:efc:9abc:c9c0:a338:f22
kristof6 4c0:b29a:2940:5d2:42e4:4a63:7002:1ac0

Verification matrix

Path Source Result Expected
UDS to /root/hero/var/sockets/hero_router/ui.sock n/a (local socket) HTTP 200 bypass — pass
TCP to [kristof4-host]:9988/health from kristof4 mahmoud local lo, src=host TUN (whitelisted) HTTP 200 pass
TCP to [kristof4-host]:9988/health from kristof3 mahmoud mycelium underlay, src=kristof3 host TUN (whitelisted) HTTP 200 pass
TCP to [kristof4-host]:9988/hero_codescalers_server/ui/ from kristof3 same HTTP 308 (redirect → UI) pass
Same TCP before kristof3 host was in whitelist src=kristof3 host TUN (NOT in list) HTTP 000, curl rc=56 silently dropped — pass
Earlier with mahmoud-bridge-IPs in list src=any host TUN (NOT bridge) dropped shows the gate IS enforcing

Important architectural finding

The whitelist enforces against the TCP source IP at the accept loop. For cross-host mycelium connections the source IP that hero_router sees is the ORIGIN HOST's mycelium TUN address — not the calling user's bridge IP. This is because outgoing connections from a host go through that host's mycelium daemon, and the kernel's source-address selection picks the host TUN address.

For the on-host case (a kristof4 user reaching kristof4's own mycelium IP), Linux routes via lo with source = host TUN IP. So all on-host users share the same source IP from the gate's perspective.

Practical consequence — ADMIN_SECRETS is a list of admin hosts, not admin users. Per-user gating on the same host must use UDS (filesystem perms), not the TCP whitelist.

Pre-conditions that needed to be met (worth flagging)

  1. Root's hero_router has to be on a real mycelium IP, not 127.0.0.1. The default service_router start --root auto-detects via ~/hero/cfg/hero_cfg.toml [mycelium].ipv6_address first, then ip -6 addr for /7 mycelium-range global. On kristof4 the cfg file was empty, so the ip-scan fallback found the mycelium TUN address.
  2. Root's hero_router binary must have the access.rs module (the per-host gate). Older binaries don't enforce — verify with git log on the source clone or by testing.
  3. Background refresh is 60 s — when ADMIN_SECRETS changes, the gate picks it up after up to 60 s. To force immediate, restart hero_router (service_router start --root --reset).

Coverage on the issue checklist

  • ADMIN_SECRETS populated in hero_proc (4 admin host TUN IPs).
  • TCP from non-whitelisted IP is rejected (silent drop / curl rc=56).
  • Whitelisted Mycelium IPv6 reaches dashboard (kristof3 mahmoud → kristof4 codescalers UI = 308 redirect through router).
  • Unix socket access remains unrestricted (UDS health = 200).

Notes / follow-ups

  • No code changes needed for the gate itself — implementation is already canonical (hero_router/src/access.rs + accept loop in main.rs:453). Same module is mirrored in hero_codescalers_ui (admin_secrets.rs) for the alternative path of codescalers exposing its own TCP listener.
  • An optional one-line tightening in admin_secrets::save to reject non-400::/7 addresses was suggested in the analysis. Not done yet — easy follow-up if/when desired.
  • The 60 s refresh delay is fine for ops but jarring during testing. A secret_refresh SIGUSR1 / RPC call would help if we want immediate propagation; out of scope for this issue.
## ADMIN_SECRETS whitelist verified — design works as documented ### Setup applied on kristof4 (root context) ``` hero_proc secret set --root --context core ADMIN_SECRETS \ "<kristof1-host-mycelium>,<kristof3-host-mycelium>,<kristof4-host-mycelium>,<kristof6-host-mycelium>" ``` The four entries are each kristof box's mycelium TUN IP: | Box | Host TUN IPv6 | |---|---| | kristof1 | `543:66c5:6430:8f31:5293:1ad9:694a:70f3` | | kristof3 | `58b:c3be:cf70:4826:765a:2c6d:6c8d:a5e2` | | kristof4 | `4a0:6976:8fa7:efc:9abc:c9c0:a338:f22` | | kristof6 | `4c0:b29a:2940:5d2:42e4:4a63:7002:1ac0` | ### Verification matrix | Path | Source | Result | Expected | |---|---|---|---| | UDS to `/root/hero/var/sockets/hero_router/ui.sock` | n/a (local socket) | HTTP 200 | bypass — pass | | TCP to `[kristof4-host]:9988/health` from kristof4 mahmoud | local lo, src=host TUN (whitelisted) | HTTP 200 | pass | | TCP to `[kristof4-host]:9988/health` from kristof3 mahmoud | mycelium underlay, src=kristof3 host TUN (whitelisted) | HTTP 200 | pass | | TCP to `[kristof4-host]:9988/hero_codescalers_server/ui/` from kristof3 | same | HTTP 308 (redirect → UI) | pass | | Same TCP **before** kristof3 host was in whitelist | src=kristof3 host TUN (NOT in list) | HTTP 000, curl rc=56 | silently dropped — pass | | Earlier with mahmoud-bridge-IPs in list | src=any host TUN (NOT bridge) | dropped | shows the gate IS enforcing | ### Important architectural finding The whitelist enforces against the **TCP source IP** at the accept loop. For cross-host mycelium connections the source IP that hero_router sees is the ORIGIN HOST's mycelium TUN address — **not** the calling user's bridge IP. This is because outgoing connections from a host go through that host's mycelium daemon, and the kernel's source-address selection picks the host TUN address. For the on-host case (a kristof4 user reaching kristof4's own mycelium IP), Linux routes via `lo` with source = host TUN IP. So all on-host users share the same source IP from the gate's perspective. **Practical consequence — ADMIN_SECRETS is a list of admin _hosts_, not admin _users_.** Per-user gating on the same host must use UDS (filesystem perms), not the TCP whitelist. ### Pre-conditions that needed to be met (worth flagging) 1. **Root's `hero_router` has to be on a real mycelium IP**, not `127.0.0.1`. The default `service_router start --root` auto-detects via `~/hero/cfg/hero_cfg.toml [mycelium].ipv6_address` first, then `ip -6 addr` for `/7 mycelium-range global`. On kristof4 the cfg file was empty, so the ip-scan fallback found the mycelium TUN address. 2. **Root's `hero_router` binary must have the `access.rs` module** (the per-host gate). Older binaries don't enforce — verify with `git log` on the source clone or by testing. 3. **Background refresh is 60 s** — when ADMIN_SECRETS changes, the gate picks it up after up to 60 s. To force immediate, restart hero_router (`service_router start --root --reset`). ### Coverage on the issue checklist - [x] **ADMIN_SECRETS populated in hero_proc** (4 admin host TUN IPs). - [x] **TCP from non-whitelisted IP is rejected** (silent drop / curl rc=56). - [x] **Whitelisted Mycelium IPv6 reaches dashboard** (kristof3 mahmoud → kristof4 codescalers UI = 308 redirect through router). - [x] **Unix socket access remains unrestricted** (UDS health = 200). ### Notes / follow-ups - No code changes needed for the gate itself — implementation is already canonical (`hero_router/src/access.rs` + `accept` loop in `main.rs:453`). Same module is mirrored in hero_codescalers_ui (`admin_secrets.rs`) for the alternative path of codescalers exposing its own TCP listener. - An optional one-line tightening in `admin_secrets::save` to reject non-`400::/7` addresses was suggested in the analysis. Not done yet — easy follow-up if/when desired. - The 60 s refresh delay is fine for ops but jarring during testing. A `secret_refresh` SIGUSR1 / RPC call would help if we want immediate propagation; out of scope for this issue.
Author
Owner

Closed — verified, no code change required

ADMIN_SECRETS is set on kristof4 root's hero_proc secret store with the four kristof host TUN IPs. The gate at hero_router/src/access.rs enforces against the TCP source IP at the accept loop (main.rs:453); UDS paths bypass it entirely.

Path Source Result Expected
UDS to hero_router/ui.sock n/a (local socket) HTTP 200 bypass — pass
TCP to [kristof4-host]:9988/health from kristof4 mahmoud local lo, src=host TUN (whitelisted) HTTP 200 pass
TCP from kristof3 mahmoud mycelium underlay, src=kristof3 host TUN (whitelisted) HTTP 200 pass
Same TCP before kristof3 host was in whitelist src=kristof3 host TUN (NOT in list) curl rc=56 (silent drop) pass

Important architectural finding: the whitelist enforces against the TCP source IP at accept time. For cross-host mycelium connections the source is the origin host TUN, not the calling user's bridge IP — so ADMIN_SECRETS is a list of admin hosts, not admin users. Per-user gating on the same host needs UDS (filesystem perms), not the TCP whitelist.

Pre-conditions worth flagging for ops:

  1. Root's hero_router must be on a real mycelium IP, not 127.0.0.1. The default service_router start --root auto-detects via ~/hero/cfg/hero_cfg.toml [mycelium].ipv6_address first, then ip -6 addr for /7 mycelium-range global.
  2. Root's hero_router binary must include the access.rs module (the per-host gate). Older binaries don't enforce — verify with git log or by testing.
  3. Background secret refresh is 60 s — restart hero_router with --reset to force immediate propagation of ADMIN_SECRETS changes.

Optional follow-up: tighten admin_secrets::save to reject non-400::/7 addresses. Not done — easy to add later if desired.

## Closed — verified, no code change required `ADMIN_SECRETS` is set on kristof4 root's hero_proc secret store with the four kristof host TUN IPs. The gate at `hero_router/src/access.rs` enforces against the **TCP source IP** at the accept loop (`main.rs:453`); UDS paths bypass it entirely. | Path | Source | Result | Expected | |---|---|---|---| | UDS to `hero_router/ui.sock` | n/a (local socket) | HTTP 200 | bypass — pass | | TCP to `[kristof4-host]:9988/health` from kristof4 mahmoud | local lo, src=host TUN (whitelisted) | HTTP 200 | pass | | TCP from kristof3 mahmoud | mycelium underlay, src=kristof3 host TUN (whitelisted) | HTTP 200 | pass | | Same TCP **before** kristof3 host was in whitelist | src=kristof3 host TUN (NOT in list) | curl rc=56 (silent drop) | pass | **Important architectural finding:** the whitelist enforces against the TCP source IP at accept time. For cross-host mycelium connections the source is the **origin host TUN**, not the calling user's bridge IP — so `ADMIN_SECRETS` is a list of admin **hosts**, not admin **users**. Per-user gating on the same host needs UDS (filesystem perms), not the TCP whitelist. **Pre-conditions worth flagging for ops:** 1. Root's `hero_router` must be on a real mycelium IP, not `127.0.0.1`. The default `service_router start --root` auto-detects via `~/hero/cfg/hero_cfg.toml [mycelium].ipv6_address` first, then `ip -6 addr` for `/7` mycelium-range global. 2. Root's `hero_router` binary must include the `access.rs` module (the per-host gate). Older binaries don't enforce — verify with `git log` or by testing. 3. Background secret refresh is 60 s — restart hero_router with `--reset` to force immediate propagation of `ADMIN_SECRETS` changes. **Optional follow-up:** tighten `admin_secrets::save` to reject non-`400::/7` addresses. Not done — easy to add later if desired.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_codescalers#11
No description provided.