Add admin IP whitelist gate (ADMIN_SECRETS) to UI #9

Closed
opened 2026-04-19 21:17:50 +00:00 by mahmoud · 4 comments
Owner

Context

The UI currently accepts any connection on its UDS. Every Hero admin UI enforces an IP whitelist backed by the ADMIN_SECRETS secret in hero_proc (hero_ui_whitelists skill). The TCP gate runs before the HTTP router, is cached, refreshes periodically, and fails open if hero_proc is unreachable.

Goals

  • Add a whitelist module (per hero_ui_whitelists §Rust implementation).
  • Load the ADMIN_SECRETS secret via hero_proc_sdk on boot; cache the parsed CIDR list.
  • Reject non-whitelisted peers at the TCP accept layer with HTTP 403 (or connection close).
  • Background refresh every N seconds; fail-open on hero_proc unavailable with a warning log.
  • Expose PUT /admin/whitelist behind the same gate for write-through updates (optional but standard).

Related skills: hero_ui_whitelists, hero_proc_secrets, hero_proc_sdk.

## Context The UI currently accepts any connection on its UDS. Every Hero admin UI enforces an IP whitelist backed by the `ADMIN_SECRETS` secret in hero_proc (`hero_ui_whitelists` skill). The TCP gate runs *before* the HTTP router, is cached, refreshes periodically, and fails open if hero_proc is unreachable. ## Goals - Add a `whitelist` module (per `hero_ui_whitelists` §Rust implementation). - Load the `ADMIN_SECRETS` secret via `hero_proc_sdk` on boot; cache the parsed CIDR list. - Reject non-whitelisted peers at the TCP accept layer with HTTP 403 (or connection close). - Background refresh every N seconds; fail-open on hero_proc unavailable with a warning log. - Expose `PUT /admin/whitelist` behind the same gate for write-through updates (optional but standard). Related skills: `hero_ui_whitelists`, `hero_proc_secrets`, `hero_proc_sdk`.
Member

Implementation Spec for Issue #9

Objective

Add an IP-based whitelist gate to hero_livekit_ui that loads allowed CIDR ranges from the ADMIN_SECRETS secret in hero_proc, caches them, enforces them as axum middleware on every inbound HTTP request (reading the client IP from X-Forwarded-For / X-Real-Ip headers), refreshes periodically in the background, and fails open with a warning when hero_proc is unreachable.

Requirements

  • Create a new whitelist module (crates/hero_livekit_ui/src/whitelist.rs) containing all whitelist logic
  • On startup, connect to hero_proc via hero_proc_sdk and fetch the ADMIN_SECRETS secret (key = "ADMIN_SECRETS", context = "core")
  • Parse the secret value as a comma-separated list of CIDR ranges
  • Cache the parsed CIDR list in a shared Arc<RwLock<...>> for zero-allocation checks
  • Provide an axum middleware layer that extracts client IP from X-Real-Ip / X-Forwarded-For headers
  • If a whitelist is loaded and the IP is NOT in any allowed CIDR range, return HTTP 403 Forbidden
  • If no whitelist could be loaded (hero_proc unreachable), fail open and allow the request with a warning log
  • If the IP cannot be determined from headers (direct UDS access without proxy), allow the request
  • Spawn a background task that refreshes the CIDR list every 60 seconds
  • Optionally expose PUT /admin/whitelist behind the same gate for write-through updates
  • Add the ipnetwork crate as a dependency for CIDR parsing and matching

Files to Modify/Create

  • crates/hero_livekit_ui/src/whitelist.rs - New: WhitelistState, CIDR loading/parsing, axum middleware, background refresh task, optional PUT endpoint
  • crates/hero_livekit_ui/src/main.rs - Modify: add mod whitelist, initialize WhitelistState on boot, wire middleware into Router, spawn refresh task
  • crates/hero_livekit_ui/Cargo.toml - Modify: add ipnetwork = "0.20" dependency

Implementation Plan

Step 1: Add ipnetwork dependency

Files: crates/hero_livekit_ui/Cargo.toml

  • Add ipnetwork = "0.20" to the [dependencies] section
    Dependencies: none

Step 2: Create the whitelist.rs module

Files: crates/hero_livekit_ui/src/whitelist.rs

  • WhitelistState struct with Arc<RwLock> holding Vec and loaded flag
  • load_cidrs_from_hero_proc() async fn using hero_proc_sdk's hero_proc_factory() and secret_get()
  • WhitelistState::new() and refresh() methods
  • extract_client_ip() helper reading X-Real-Ip then X-Forwarded-For headers
  • whitelist_middleware() axum middleware function using Extension
  • spawn_refresh_task() background tokio task (60s interval)
  • update_whitelist_handler() PUT handler for write-through updates
    Dependencies: Step 1

Step 3: Wire the whitelist into main.rs

Files: crates/hero_livekit_ui/src/main.rs

  • Add mod whitelist; declaration
  • Initialize WhitelistState::new().await in main()
  • Spawn background refresh task
  • Add PUT /admin/whitelist route
  • Add Extension(whitelist) layer and whitelist_middleware as outermost layer on Router
    Dependencies: Step 2

Acceptance Criteria

  • New file crates/hero_livekit_ui/src/whitelist.rs exists with WhitelistState, middleware, refresh task, and IP extraction
  • Cargo.toml includes ipnetwork dependency
  • main.rs declares mod whitelist and initializes WhitelistState before building the router
  • Whitelist middleware is the outermost layer on the axum Router
  • When ADMIN_SECRETS contains valid CIDRs, requests from non-matching IPs receive HTTP 403
  • When hero_proc is unreachable on startup, the UI starts with a warning log and allows all requests (fail-open)
  • When hero_proc is unreachable during refresh, the last known CIDR list is retained (stale-on-error)
  • When no X-Forwarded-For / X-Real-Ip headers are present, requests are allowed through
  • Background task refreshes CIDR list every 60 seconds
  • PUT /admin/whitelist validates, writes through to hero_proc, and updates local cache
  • cargo build -p hero_livekit_ui succeeds

Notes

  • The UI listens on a Unix Domain Socket, not TCP. Client IP comes from HTTP headers set by the reverse proxy (hero_proxy), so the gate is at the HTTP middleware layer
  • Fail-open semantics: on fresh startup with no hero_proc, all traffic is allowed. Once a whitelist is loaded, it persists even if subsequent refreshes fail
  • ADMIN_SECRETS value is comma-separated CIDR notation (e.g., "10.0.0.0/8, 192.168.1.0/24, ::1/128"). Both IPv4 and IPv6 supported
  • ipnetwork 0.20 chosen for consistency with hero_proxy_server which uses the same crate
  • hero_proc_sdk is already a workspace dependency and already listed in the UI crate's Cargo.toml
## Implementation Spec for Issue #9 ### Objective Add an IP-based whitelist gate to hero_livekit_ui that loads allowed CIDR ranges from the ADMIN_SECRETS secret in hero_proc, caches them, enforces them as axum middleware on every inbound HTTP request (reading the client IP from X-Forwarded-For / X-Real-Ip headers), refreshes periodically in the background, and fails open with a warning when hero_proc is unreachable. ### Requirements - Create a new whitelist module (crates/hero_livekit_ui/src/whitelist.rs) containing all whitelist logic - On startup, connect to hero_proc via hero_proc_sdk and fetch the ADMIN_SECRETS secret (key = "ADMIN_SECRETS", context = "core") - Parse the secret value as a comma-separated list of CIDR ranges - Cache the parsed CIDR list in a shared Arc<RwLock<...>> for zero-allocation checks - Provide an axum middleware layer that extracts client IP from X-Real-Ip / X-Forwarded-For headers - If a whitelist is loaded and the IP is NOT in any allowed CIDR range, return HTTP 403 Forbidden - If no whitelist could be loaded (hero_proc unreachable), fail open and allow the request with a warning log - If the IP cannot be determined from headers (direct UDS access without proxy), allow the request - Spawn a background task that refreshes the CIDR list every 60 seconds - Optionally expose PUT /admin/whitelist behind the same gate for write-through updates - Add the ipnetwork crate as a dependency for CIDR parsing and matching ### Files to Modify/Create - `crates/hero_livekit_ui/src/whitelist.rs` - New: WhitelistState, CIDR loading/parsing, axum middleware, background refresh task, optional PUT endpoint - `crates/hero_livekit_ui/src/main.rs` - Modify: add mod whitelist, initialize WhitelistState on boot, wire middleware into Router, spawn refresh task - `crates/hero_livekit_ui/Cargo.toml` - Modify: add ipnetwork = "0.20" dependency ### Implementation Plan #### Step 1: Add ipnetwork dependency Files: `crates/hero_livekit_ui/Cargo.toml` - Add ipnetwork = "0.20" to the [dependencies] section Dependencies: none #### Step 2: Create the whitelist.rs module Files: `crates/hero_livekit_ui/src/whitelist.rs` - WhitelistState struct with Arc<RwLock<WhitelistInner>> holding Vec<IpNetwork> and loaded flag - load_cidrs_from_hero_proc() async fn using hero_proc_sdk's hero_proc_factory() and secret_get() - WhitelistState::new() and refresh() methods - extract_client_ip() helper reading X-Real-Ip then X-Forwarded-For headers - whitelist_middleware() axum middleware function using Extension<WhitelistState> - spawn_refresh_task() background tokio task (60s interval) - update_whitelist_handler() PUT handler for write-through updates Dependencies: Step 1 #### Step 3: Wire the whitelist into main.rs Files: `crates/hero_livekit_ui/src/main.rs` - Add mod whitelist; declaration - Initialize WhitelistState::new().await in main() - Spawn background refresh task - Add PUT /admin/whitelist route - Add Extension(whitelist) layer and whitelist_middleware as outermost layer on Router Dependencies: Step 2 ### Acceptance Criteria - [ ] New file crates/hero_livekit_ui/src/whitelist.rs exists with WhitelistState, middleware, refresh task, and IP extraction - [ ] Cargo.toml includes ipnetwork dependency - [ ] main.rs declares mod whitelist and initializes WhitelistState before building the router - [ ] Whitelist middleware is the outermost layer on the axum Router - [ ] When ADMIN_SECRETS contains valid CIDRs, requests from non-matching IPs receive HTTP 403 - [ ] When hero_proc is unreachable on startup, the UI starts with a warning log and allows all requests (fail-open) - [ ] When hero_proc is unreachable during refresh, the last known CIDR list is retained (stale-on-error) - [ ] When no X-Forwarded-For / X-Real-Ip headers are present, requests are allowed through - [ ] Background task refreshes CIDR list every 60 seconds - [ ] PUT /admin/whitelist validates, writes through to hero_proc, and updates local cache - [ ] cargo build -p hero_livekit_ui succeeds ### Notes - The UI listens on a Unix Domain Socket, not TCP. Client IP comes from HTTP headers set by the reverse proxy (hero_proxy), so the gate is at the HTTP middleware layer - Fail-open semantics: on fresh startup with no hero_proc, all traffic is allowed. Once a whitelist is loaded, it persists even if subsequent refreshes fail - ADMIN_SECRETS value is comma-separated CIDR notation (e.g., "10.0.0.0/8, 192.168.1.0/24, ::1/128"). Both IPv4 and IPv6 supported - ipnetwork 0.20 chosen for consistency with hero_proxy_server which uses the same crate - hero_proc_sdk is already a workspace dependency and already listed in the UI crate's Cargo.toml
Member

Test Results

  • Total: 34
  • Passed: 34
  • Failed: 0
  • Ignored: 4 (doc-tests)

All tests pass. cargo build -p hero_livekit_ui and cargo test both succeed with no errors or warnings.

## Test Results - Total: 34 - Passed: 34 - Failed: 0 - Ignored: 4 (doc-tests) All tests pass. cargo build -p hero_livekit_ui and cargo test both succeed with no errors or warnings.
Member

Implementation Summary

Changes Made

New file:

  • crates/hero_livekit_ui/src/whitelist.rs (289 lines) -- Complete IP whitelist module with:
    • WhitelistState: Arc cached CIDR list loaded from hero_proc ADMIN_SECRETS secret
    • load_cidrs_from_hero_proc(): connects via hero_proc_sdk, fetches and parses comma-separated CIDRs
    • extract_client_ip(): reads X-Real-Ip then X-Forwarded-For headers for client IP
    • whitelist_middleware(): axum middleware returning 403 for non-whitelisted IPs, fail-open when unloaded
    • spawn_refresh_task(): background tokio task refreshing every 60 seconds
    • update_whitelist_handler(): PUT /admin/whitelist endpoint for write-through updates via hero_proc

Modified files:

  • crates/hero_livekit_ui/Cargo.toml -- Added ipnetwork = "0.20" dependency for CIDR parsing
  • crates/hero_livekit_ui/src/main.rs -- Added mod whitelist declaration, WhitelistState initialization on startup, background refresh task spawn, PUT /admin/whitelist route, Extension and middleware layers on Router (whitelist_middleware as outermost layer)

Behavior

  • Green path: when ADMIN_SECRETS contains valid CIDRs, only matching IPs are allowed; others get HTTP 403
  • Fail-open: when hero_proc is unreachable on startup, all traffic is allowed with a warning log
  • Stale-on-error: when hero_proc becomes unreachable during refresh, last known CIDRs are retained
  • Direct UDS: when no X-Forwarded-For/X-Real-Ip headers present, requests pass through (local access)

Test Results

All 34 tests pass, 0 failures.

## Implementation Summary ### Changes Made **New file:** - `crates/hero_livekit_ui/src/whitelist.rs` (289 lines) -- Complete IP whitelist module with: - WhitelistState: Arc<RwLock> cached CIDR list loaded from hero_proc ADMIN_SECRETS secret - load_cidrs_from_hero_proc(): connects via hero_proc_sdk, fetches and parses comma-separated CIDRs - extract_client_ip(): reads X-Real-Ip then X-Forwarded-For headers for client IP - whitelist_middleware(): axum middleware returning 403 for non-whitelisted IPs, fail-open when unloaded - spawn_refresh_task(): background tokio task refreshing every 60 seconds - update_whitelist_handler(): PUT /admin/whitelist endpoint for write-through updates via hero_proc **Modified files:** - `crates/hero_livekit_ui/Cargo.toml` -- Added ipnetwork = "0.20" dependency for CIDR parsing - `crates/hero_livekit_ui/src/main.rs` -- Added mod whitelist declaration, WhitelistState initialization on startup, background refresh task spawn, PUT /admin/whitelist route, Extension and middleware layers on Router (whitelist_middleware as outermost layer) ### Behavior - Green path: when ADMIN_SECRETS contains valid CIDRs, only matching IPs are allowed; others get HTTP 403 - Fail-open: when hero_proc is unreachable on startup, all traffic is allowed with a warning log - Stale-on-error: when hero_proc becomes unreachable during refresh, last known CIDRs are retained - Direct UDS: when no X-Forwarded-For/X-Real-Ip headers present, requests pass through (local access) ### Test Results All 34 tests pass, 0 failures.
Member

Pull request opened: #20

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_livekit/pulls/20 This PR implements the changes discussed in this issue.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
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_livekit#9
No description provided.