[Phase 1] Auth gateway: OSIS migration, secp256k1 signature auth, OAuth enforcement, identity injection #8

Open
opened 2026-03-26 11:24:56 +00:00 by timur · 5 comments
Owner

Summary

The OAuth2 infrastructure (providers, sessions, domain route auth policies) is fully implemented in the DB layer and management API, but not yet enforced in the proxy request path. Requests to OAuth-protected domain routes are forwarded directly without checking auth_mode or validating session cookies.

Current State

What exists and works:

  • SQLite schema for domain_routes (with auth_mode: "none"|"bearer"|"oauth", oauth_provider, allowed_emails), oauth_providers, oauth_sessions
  • OAuth provider CRUD via OpenRPC management API (oauth.set_provider, oauth.list_providers, oauth.remove_provider)
  • Domain route CRUD with auth policy fields (domain_route.add/update/remove)
  • OAuth2 helpers in oauth.rs: make_auth_url(), exchange_code_for_email(), session cookie utilities, CSRF state store
  • Google + GitHub provider presets
  • Bearer token auth middleware on all proxy routes (auth.rs)
  • Admin UI with OAuth provider and domain route management tabs

What is NOT wired up:

  1. dispatch_domain_route() in proxy.rs:58 forwards requests directly without checking route.auth_mode. No code validates a session cookie, redirects to OAuth, or blocks unauthenticated requests on OAuth-protected routes.

  2. No /oauth/callback HTTP endpointmake_auth_url() generates redirect URLs to {base}/oauth/callback but build_proxy_router() in lib.rs has no route to handle the callback.

  3. No identity header injectionforward_axum() in weblib/src/proxy/forward.rs only strips the Host header and adds X-Forwarded-Prefix. After OAuth validation, it should inject identity headers so backend services know WHO the user is.

Implementation Plan

1. OAuth enforcement middleware in dispatch_domain_route()

Before forwarding, check route.auth_mode:

  • "none" → forward directly (current behavior)
  • "bearer" → validate Authorization: Bearer <token> (existing auth.rs logic)
  • "oauth" → check for valid hero_proxy_session cookie → if missing/expired, redirect to OAuth provider → if valid, continue to forwarding

2. /oauth/callback endpoint

Add to build_proxy_router():

GET /oauth/callback → exchange auth code for email → validate against allowed_emails → create session in DB → set cookie → redirect to original URL

The plumbing already exists in oauth.rs — just needs an Axum handler that calls exchange_code_for_email(), checks route.allowed_emails, creates an oauth_sessions row, and sets the cookie.

3. Identity header injection in forward_axum()

After session validation, inject headers into the forwarded request:

X-Proxy-User-Email: user@example.com
X-Proxy-Auth-Method: oauth | bearer
X-Proxy-Session-Id: <session_id>

This enables the two-layer auth model where hero_proxy handles web/session auth at the edge, and backend services (e.g., znzfreezone_backend) handle API-level auth (secp256k1, API keys, per-RPC-method enforcement) while knowing the authenticated user's identity.

Context

  • References architect's intent in issue #6 (proxy server with OAuth support)
  • Related to znzfreezone_code/znzfreezone_backend issues #28 (backend auth consolidation) and #29 (cross-context auth architecture)
  • The weblib crate's own auth system (auth/middleware.rs, auth/session.rs) is for protecting hero_proxy_ui admin panel, NOT for proxy domain route enforcement — these are separate concerns

Files to modify

  • crates/hero_proxy_server/src/proxy.rs — add auth_mode checking in dispatch_domain_route()
  • crates/hero_proxy_server/src/lib.rs — add /oauth/callback route to build_proxy_router()
  • crates/weblib/src/proxy/forward.rs — inject identity headers in forward_axum() and forward_hyper()
  • crates/hero_proxy_server/src/oauth.rs — add callback handler function
## Summary The OAuth2 infrastructure (providers, sessions, domain route auth policies) is fully implemented in the DB layer and management API, but **not yet enforced** in the proxy request path. Requests to OAuth-protected domain routes are forwarded directly without checking `auth_mode` or validating session cookies. ## Current State ### What exists and works: - SQLite schema for `domain_routes` (with `auth_mode: "none"|"bearer"|"oauth"`, `oauth_provider`, `allowed_emails`), `oauth_providers`, `oauth_sessions` - OAuth provider CRUD via OpenRPC management API (`oauth.set_provider`, `oauth.list_providers`, `oauth.remove_provider`) - Domain route CRUD with auth policy fields (`domain_route.add/update/remove`) - OAuth2 helpers in `oauth.rs`: `make_auth_url()`, `exchange_code_for_email()`, session cookie utilities, CSRF state store - Google + GitHub provider presets - Bearer token auth middleware on all proxy routes (`auth.rs`) - Admin UI with OAuth provider and domain route management tabs ### What is NOT wired up: 1. **`dispatch_domain_route()` in `proxy.rs:58`** forwards requests directly without checking `route.auth_mode`. No code validates a session cookie, redirects to OAuth, or blocks unauthenticated requests on OAuth-protected routes. 2. **No `/oauth/callback` HTTP endpoint** — `make_auth_url()` generates redirect URLs to `{base}/oauth/callback` but `build_proxy_router()` in `lib.rs` has no route to handle the callback. 3. **No identity header injection** — `forward_axum()` in `weblib/src/proxy/forward.rs` only strips the `Host` header and adds `X-Forwarded-Prefix`. After OAuth validation, it should inject identity headers so backend services know WHO the user is. ## Implementation Plan ### 1. OAuth enforcement middleware in `dispatch_domain_route()` Before forwarding, check `route.auth_mode`: - `"none"` → forward directly (current behavior) - `"bearer"` → validate `Authorization: Bearer <token>` (existing `auth.rs` logic) - `"oauth"` → check for valid `hero_proxy_session` cookie → if missing/expired, redirect to OAuth provider → if valid, continue to forwarding ### 2. `/oauth/callback` endpoint Add to `build_proxy_router()`: ``` GET /oauth/callback → exchange auth code for email → validate against allowed_emails → create session in DB → set cookie → redirect to original URL ``` The plumbing already exists in `oauth.rs` — just needs an Axum handler that calls `exchange_code_for_email()`, checks `route.allowed_emails`, creates an `oauth_sessions` row, and sets the cookie. ### 3. Identity header injection in `forward_axum()` After session validation, inject headers into the forwarded request: ``` X-Proxy-User-Email: user@example.com X-Proxy-Auth-Method: oauth | bearer X-Proxy-Session-Id: <session_id> ``` This enables the two-layer auth model where hero_proxy handles web/session auth at the edge, and backend services (e.g., znzfreezone_backend) handle API-level auth (secp256k1, API keys, per-RPC-method enforcement) while knowing the authenticated user's identity. ## Context - References architect's intent in issue #6 (proxy server with OAuth support) - Related to `znzfreezone_code/znzfreezone_backend` issues #28 (backend auth consolidation) and #29 (cross-context auth architecture) - The `weblib` crate's own auth system (`auth/middleware.rs`, `auth/session.rs`) is for protecting `hero_proxy_ui` admin panel, NOT for proxy domain route enforcement — these are separate concerns ## Files to modify - `crates/hero_proxy_server/src/proxy.rs` — add auth_mode checking in `dispatch_domain_route()` - `crates/hero_proxy_server/src/lib.rs` — add `/oauth/callback` route to `build_proxy_router()` - `crates/weblib/src/proxy/forward.rs` — inject identity headers in `forward_axum()` and `forward_hyper()` - `crates/hero_proxy_server/src/oauth.rs` — add callback handler function
Author
Owner

for 3. Identity header injection in forward_axum()

i'm not sure its a good idea to have then the api services also handle their own auth, instead it would be cleaner if authenticity of secp256k1, API keys etc were also checked in proxy. authorization given such identities can always be handled in the proxied services themselves. as such maybe we need to add functionality to proxy to also verify keypairs and endpoints which they can do this with. Also maybe even payload signature verification would be helpful to verify the integrity of payloads.

for 3. Identity header injection in forward_axum() i'm not sure its a good idea to have then the api services also handle their own auth, instead it would be cleaner if authenticity of secp256k1, API keys etc were also checked in proxy. authorization given such identities can always be handled in the proxied services themselves. as such maybe we need to add functionality to proxy to also verify keypairs and endpoints which they can do this with. Also maybe even payload signature verification would be helpful to verify the integrity of payloads.
Author
Owner

Reply: Moving All Authentication to hero_proxy

Agreed — the cleaner model is:

  • hero_proxy = authentication (verifying WHO you are)
  • backend services = authorization (deciding what you're allowed to DO)

This is a significant but clean shift. Here's what it looks like concretely.

Current State in znzfreezone_backend

The backend currently does both authentication AND authorization in two middleware layers:

  1. auth_middleware.rs — extracts auth from HTTP headers:

    • X-Public-Key + X-Signature + X-Signature-Timestamp → secp256k1 (admin ops)
    • X-Api-Key → API key (reseller ops)
    • Authorization: Bearer → JWT (user ops)
    • Uses k256 crate for ECDSA verification, sha2 for hashing
  2. rpc_auth.rs — enforces per-method + per-context rules:

    • AdminRegistrationService.* → must be signed by admin pubkey
    • RegistrationRequestService.* → must have valid API key or signature
    • Context isolation: reseller can only access reseller_{their_id} context
    • Verifies SHA256(timestamp + body) signature with 5-minute replay window

The key insight: authentication (crypto verification) and authorization (business rules) are mixed together in rpc_auth.rs. They should be separated.

Proposed: hero_proxy Handles All Authentication

New auth_mode values for DomainRoute

Extend the current "none" | "bearer" | "oauth" enum:

auth_mode: "none" | "bearer" | "oauth" | "apikey" | "signature" | "apikey+signature"
  • "apikey" — validate X-Api-Key header against proxy's key store
  • "signature" — verify secp256k1 signature (X-Public-Key + X-Signature + X-Signature-Timestamp) including payload integrity
  • "apikey+signature" — accept either (current znzfreezone reseller behavior)

New api_keys table in proxy SQLite

CREATE TABLE IF NOT EXISTS api_keys (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    key_hash        TEXT    NOT NULL UNIQUE,    -- SHA256 of the actual key
    key_prefix      TEXT    NOT NULL,           -- first 8 chars for identification
    owner_id        TEXT    NOT NULL,           -- e.g. reseller pubkey or user ID
    route_id        INTEGER,                    -- NULL = valid for all routes
    scopes          TEXT    NOT NULL DEFAULT '*',
    is_active       INTEGER NOT NULL DEFAULT 1,
    created_at      TEXT    NOT NULL DEFAULT (datetime('now')),
    last_used_at    TEXT,
    FOREIGN KEY (route_id) REFERENCES domain_routes(id)
);

New allowed_pubkeys on DomainRoute

Similar to how OAuthProvider has allowed_emails, domain routes need:

ALTER TABLE domain_routes ADD COLUMN allowed_pubkeys TEXT;  -- comma-separated, empty = any valid sig

Payload signature verification in dispatch_domain_route()

This is the most involved piece. For auth_mode: "signature", before forwarding:

  1. Extract X-Public-Key, X-Signature, X-Signature-Timestamp headers
  2. Buffer the request body
  3. Verify secp256k1_verify(pubkey, SHA256(timestamp + body), signature)
  4. Check timestamp freshness (5-minute window + optional nonce)
  5. Check pubkey against allowed_pubkeys if set
  6. On success: inject identity headers and forward
  7. On failure: return 401

This requires adding k256 (or secp256k1) + sha2 as dependencies to hero_proxy.

Identity headers injected after authentication

Regardless of auth method, on success hero_proxy injects:

X-Proxy-Auth-Method: oauth | bearer | apikey | signature
X-Proxy-User-Email: user@example.com          (if OAuth)
X-Proxy-User-Pubkey: 02abc...def              (if signature)
X-Proxy-Api-Key-Owner: reseller_abc123         (if API key)
X-Proxy-Signature-Verified: true               (if payload sig checked)
X-Proxy-Auth-Timestamp: 1711400000             (original timestamp)

Backend services then TRUST these headers (since they come from the proxy on the internal network / Unix socket) and only do authorization.

API key management via OpenRPC

New management methods:

  • apikey.create { owner_id, route_id?, scopes? } → returns the raw key (only shown once)
  • apikey.list { owner_id?, route_id? } → list keys (prefix + metadata only)
  • apikey.revoke { key_id } → deactivate a key
  • apikey.rotate { key_id } → revoke old + create new

What Changes in znzfreezone_backend

auth_middleware.rs simplifies dramatically:

// BEFORE: Extracts AND verifies secp256k1, API keys, JWT from raw headers
// AFTER: Reads pre-verified identity from proxy headers

fn extract_auth_context(headers: &HeaderMap) -> AuthContext {
    // Trust proxy-verified identity
    if let Some(method) = headers.get("X-Proxy-Auth-Method") {
        match method.to_str().unwrap_or("") {
            "signature" => AuthContext::Signed {
                public_key: headers.get("X-Proxy-User-Pubkey").unwrap().to_str().unwrap().to_string(),
                verified: true,  // proxy already verified
            },
            "apikey" => AuthContext::ApiKey {
                reseller_id: headers.get("X-Proxy-Api-Key-Owner").unwrap().to_str().unwrap().to_string(),
                api_key_id: String::new(),
                scopes: vec!["*".to_string()],
            },
            "oauth" => AuthContext::Jwt {
                user_id: String::new(),
                email: headers.get("X-Proxy-User-Email").unwrap().to_str().unwrap().to_string(),
                token_type: "oauth".to_string(),
            },
            _ => AuthContext::Anonymous,
        }
    } else {
        AuthContext::Anonymous
    }
}

rpc_auth.rs keeps ONLY the authorization logic:

  • Method-level rules (which methods need admin vs reseller vs public)
  • Context isolation (reseller can only access own context)
  • Business logic permissions

The k256, sha2, hex dependencies move OUT of znzfreezone_backend and INTO hero_proxy.

Migration Concern: API Key Storage

Currently API keys are stored in znzfreezone_backend's OSIS ApiKey model (application data). Moving to proxy means:

  • API key CRUD moves to hero_proxy management API
  • znzfreezone_backend stops managing keys — it trusts X-Proxy-Api-Key-Owner
  • Migration: existing keys need to be exported from OSIS and imported into proxy's api_keys table
  • Key creation flow: reseller onboarding calls apikey.create on hero_proxy instead of creating an OSIS record

This is cleaner long-term because API keys become a proxy-level concern shared across ALL backend services, not just znzfreezone.

Security Consideration: Trust Boundary

The backend trusts X-Proxy-* headers because:

  • hero_proxy connects to backends via Unix sockets (not TCP) — no network interception possible
  • The proxy STRIPS any incoming X-Proxy-* headers from external requests before injecting its own (important!)
  • For dev/testing without proxy, the backend can fall back to direct header verification (feature flag or env var)

Updated Issue Scope

This issue should now cover:

  1. OAuth enforcement in dispatch (original scope — still needed)
  2. /oauth/callback endpoint (original scope — still needed)
  3. API key auth mode — new api_keys table, validation in dispatch, management API
  4. secp256k1 signature auth modek256 dependency, payload verification, allowed_pubkeys
  5. Identity header injection — all auth methods inject X-Proxy-* headers
  6. Strip incoming X-Proxy-* headers — security: never trust externally-supplied proxy headers
  7. OpenRPC spec update — new auth_mode enum values, apikey.* methods

Implementation Order

Phase 1: Infrastructure
  - Add api_keys table + allowed_pubkeys column
  - Add k256, sha2 dependencies
  - Strip incoming X-Proxy-* headers in proxy_handler()

Phase 2: Auth enforcement in dispatch
  - Wire auth_mode checking in dispatch_domain_route()
  - "none" → pass through (existing)
  - "bearer" → check Authorization header (existing logic)
  - "oauth" → session cookie validation + /oauth/callback
  - "apikey" → validate X-Api-Key against api_keys table
  - "signature" → verify secp256k1(X-Public-Key, X-Signature, body)

Phase 3: Identity injection
  - After auth passes, inject X-Proxy-* headers
  - Forward to backend with verified identity

Phase 4: Management API
  - apikey.create/list/revoke/rotate methods
  - Update OpenRPC spec with new auth_mode values
## Reply: Moving All Authentication to hero_proxy Agreed — the cleaner model is: - **hero_proxy** = **authentication** (verifying WHO you are) - **backend services** = **authorization** (deciding what you're allowed to DO) This is a significant but clean shift. Here's what it looks like concretely. ### Current State in znzfreezone_backend The backend currently does both authentication AND authorization in two middleware layers: 1. **`auth_middleware.rs`** — extracts auth from HTTP headers: - `X-Public-Key` + `X-Signature` + `X-Signature-Timestamp` → secp256k1 (admin ops) - `X-Api-Key` → API key (reseller ops) - `Authorization: Bearer` → JWT (user ops) - Uses `k256` crate for ECDSA verification, `sha2` for hashing 2. **`rpc_auth.rs`** — enforces per-method + per-context rules: - `AdminRegistrationService.*` → must be signed by admin pubkey - `RegistrationRequestService.*` → must have valid API key or signature - Context isolation: reseller can only access `reseller_{their_id}` context - Verifies `SHA256(timestamp + body)` signature with 5-minute replay window The key insight: authentication (crypto verification) and authorization (business rules) are mixed together in `rpc_auth.rs`. They should be separated. ### Proposed: hero_proxy Handles All Authentication #### New `auth_mode` values for DomainRoute Extend the current `"none" | "bearer" | "oauth"` enum: ``` auth_mode: "none" | "bearer" | "oauth" | "apikey" | "signature" | "apikey+signature" ``` - **`"apikey"`** — validate `X-Api-Key` header against proxy's key store - **`"signature"`** — verify secp256k1 signature (`X-Public-Key` + `X-Signature` + `X-Signature-Timestamp`) including payload integrity - **`"apikey+signature"`** — accept either (current znzfreezone reseller behavior) #### New `api_keys` table in proxy SQLite ```sql CREATE TABLE IF NOT EXISTS api_keys ( id INTEGER PRIMARY KEY AUTOINCREMENT, key_hash TEXT NOT NULL UNIQUE, -- SHA256 of the actual key key_prefix TEXT NOT NULL, -- first 8 chars for identification owner_id TEXT NOT NULL, -- e.g. reseller pubkey or user ID route_id INTEGER, -- NULL = valid for all routes scopes TEXT NOT NULL DEFAULT '*', is_active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')), last_used_at TEXT, FOREIGN KEY (route_id) REFERENCES domain_routes(id) ); ``` #### New `allowed_pubkeys` on DomainRoute Similar to how `OAuthProvider` has `allowed_emails`, domain routes need: ```sql ALTER TABLE domain_routes ADD COLUMN allowed_pubkeys TEXT; -- comma-separated, empty = any valid sig ``` #### Payload signature verification in `dispatch_domain_route()` This is the most involved piece. For `auth_mode: "signature"`, before forwarding: 1. Extract `X-Public-Key`, `X-Signature`, `X-Signature-Timestamp` headers 2. Buffer the request body 3. Verify `secp256k1_verify(pubkey, SHA256(timestamp + body), signature)` 4. Check timestamp freshness (5-minute window + optional nonce) 5. Check pubkey against `allowed_pubkeys` if set 6. On success: inject identity headers and forward 7. On failure: return 401 This requires adding `k256` (or `secp256k1`) + `sha2` as dependencies to hero_proxy. #### Identity headers injected after authentication Regardless of auth method, on success hero_proxy injects: ``` X-Proxy-Auth-Method: oauth | bearer | apikey | signature X-Proxy-User-Email: user@example.com (if OAuth) X-Proxy-User-Pubkey: 02abc...def (if signature) X-Proxy-Api-Key-Owner: reseller_abc123 (if API key) X-Proxy-Signature-Verified: true (if payload sig checked) X-Proxy-Auth-Timestamp: 1711400000 (original timestamp) ``` Backend services then TRUST these headers (since they come from the proxy on the internal network / Unix socket) and only do authorization. #### API key management via OpenRPC New management methods: - `apikey.create { owner_id, route_id?, scopes? }` → returns the raw key (only shown once) - `apikey.list { owner_id?, route_id? }` → list keys (prefix + metadata only) - `apikey.revoke { key_id }` → deactivate a key - `apikey.rotate { key_id }` → revoke old + create new ### What Changes in znzfreezone_backend `auth_middleware.rs` simplifies dramatically: ```rust // BEFORE: Extracts AND verifies secp256k1, API keys, JWT from raw headers // AFTER: Reads pre-verified identity from proxy headers fn extract_auth_context(headers: &HeaderMap) -> AuthContext { // Trust proxy-verified identity if let Some(method) = headers.get("X-Proxy-Auth-Method") { match method.to_str().unwrap_or("") { "signature" => AuthContext::Signed { public_key: headers.get("X-Proxy-User-Pubkey").unwrap().to_str().unwrap().to_string(), verified: true, // proxy already verified }, "apikey" => AuthContext::ApiKey { reseller_id: headers.get("X-Proxy-Api-Key-Owner").unwrap().to_str().unwrap().to_string(), api_key_id: String::new(), scopes: vec!["*".to_string()], }, "oauth" => AuthContext::Jwt { user_id: String::new(), email: headers.get("X-Proxy-User-Email").unwrap().to_str().unwrap().to_string(), token_type: "oauth".to_string(), }, _ => AuthContext::Anonymous, } } else { AuthContext::Anonymous } } ``` `rpc_auth.rs` keeps ONLY the authorization logic: - Method-level rules (which methods need admin vs reseller vs public) - Context isolation (reseller can only access own context) - Business logic permissions The `k256`, `sha2`, `hex` dependencies move OUT of znzfreezone_backend and INTO hero_proxy. ### Migration Concern: API Key Storage Currently API keys are stored in znzfreezone_backend's OSIS `ApiKey` model (application data). Moving to proxy means: - **API key CRUD moves to hero_proxy** management API - **znzfreezone_backend stops managing keys** — it trusts `X-Proxy-Api-Key-Owner` - **Migration**: existing keys need to be exported from OSIS and imported into proxy's `api_keys` table - **Key creation flow**: reseller onboarding calls `apikey.create` on hero_proxy instead of creating an OSIS record This is cleaner long-term because API keys become a proxy-level concern shared across ALL backend services, not just znzfreezone. ### Security Consideration: Trust Boundary The backend trusts `X-Proxy-*` headers because: - hero_proxy connects to backends via **Unix sockets** (not TCP) — no network interception possible - The proxy STRIPS any incoming `X-Proxy-*` headers from external requests before injecting its own (important!) - For dev/testing without proxy, the backend can fall back to direct header verification (feature flag or env var) ### Updated Issue Scope This issue should now cover: 1. ~~OAuth enforcement in dispatch~~ (original scope — still needed) 2. ~~`/oauth/callback` endpoint~~ (original scope — still needed) 3. **API key auth mode** — new `api_keys` table, validation in dispatch, management API 4. **secp256k1 signature auth mode** — `k256` dependency, payload verification, `allowed_pubkeys` 5. **Identity header injection** — all auth methods inject `X-Proxy-*` headers 6. **Strip incoming `X-Proxy-*` headers** — security: never trust externally-supplied proxy headers 7. **OpenRPC spec update** — new auth_mode enum values, apikey.* methods ### Implementation Order ``` Phase 1: Infrastructure - Add api_keys table + allowed_pubkeys column - Add k256, sha2 dependencies - Strip incoming X-Proxy-* headers in proxy_handler() Phase 2: Auth enforcement in dispatch - Wire auth_mode checking in dispatch_domain_route() - "none" → pass through (existing) - "bearer" → check Authorization header (existing logic) - "oauth" → session cookie validation + /oauth/callback - "apikey" → validate X-Api-Key against api_keys table - "signature" → verify secp256k1(X-Public-Key, X-Signature, body) Phase 3: Identity injection - After auth passes, inject X-Proxy-* headers - Forward to backend with verified identity Phase 4: Management API - apikey.create/list/revoke/rotate methods - Update OpenRPC spec with new auth_mode values ```
Author
Owner

Reply: OSIS as Data Layer + Keypair-Only Auth

1. OSIS Instead of SQLite for hero_proxy

Agreed this is the right direction. znzfreezone_backend already proves the pattern — it embeds OSIS directly via AxumRpcServer with a 107KB .oschema file, full codegen, and SmartID-based storage.

hero_proxy currently uses SQLite with 7 tables:

  • domain_routes — routing rules with auth_mode
  • tls_domains — Let's Encrypt / selfsigned config
  • oauth_providers — Google, GitHub, custom OAuth
  • oauth_sessions — cookie-based sessions
  • listeners — TCP listener configs
  • ssh_tunnels — remote port forwarding
  • settings — key/value store

All of these map cleanly to OSIS types. The migration path:

Step 1: Define proxy.oschema

DomainRoute = {
    sid: sid
    domain: str                    [index]
    target_type: str               # "socket" | "http" | "https"
    target: str                    # socket path or upstream URL
    strip_prefix: str?
    https_redirect: bool
    auth_mode: str                 # "none" | "bearer" | "oauth" | "signature"
    auth_config: str?              # JSON blob for mode-specific config
    allowed_pubkeys: str?          # comma-separated secp256k1 pubkeys
    oauth_provider: str?
    enabled: bool
    priority: i64
    notes: str?
}

TlsDomain = {
    sid: sid
    domain: str                    [index]
    cert_mode: str                 # "none" | "selfsigned" | "letsencrypt"
    acme_email: str?
    acme_production: bool
    status: str                    # "pending" | "active" | "failed"
    status_message: str?
}

OAuthProvider = {
    sid: sid
    name: str                      [index]
    provider_type: str             # "google" | "github" | "custom"
    client_id: str
    client_secret: str
    auth_url: str
    token_url: str
    userinfo_url: str
    scopes: str
    allowed_emails: str?
    enabled: bool
}

OAuthSession = {
    sid: sid
    user_email: str                [index]
    provider_name: str
    expires_at: u64
}

Listener = {
    sid: sid
    address: str                   [index]
    protocol: str                  # "http" | "https"
    tls_mode: str                  # "none" | "selfsigned" | "letsencrypt"
    enabled: bool
    auto_start: bool
    notes: str?
}

SshTunnel = {
    sid: sid
    name: str                      [index]
    ssh_host: str
    ssh_port: u16
    ssh_user: str
    auth_key_path: str
    remote_bind_addr: str
    remote_port: u16
    local_target_addr: str
    enabled: bool
    auto_start: bool
    notes: str?
}

Step 2: Add hero_rpc_osis dependency (same pattern as znzfreezone_backend)

hero_rpc_osis = { git = "...", branch = "development", features = ["rpc"] }

Step 3: Replace ProxyDb (SQLite) with generated OSIS domain, embed via AxumRpcServer

Step 4: Remove rusqlite dependency entirely

Practical considerations:

  • OSIS is file-based (OTOML) — each object is a separate file. For hero_proxy's workload (mostly reads on domain routes, occasional config updates) this is fine.
  • No SQL queries — filtering/matching (like find_route_for_host() with wildcard matching) needs to be done in application code. This is already a simple loop over ~10-50 routes, so no performance concern.
  • No ACID transactions — acceptable for config data. OAuth sessions are the most write-heavy, but individual session creates/deletes don't need transactional guarantees.
  • Storage becomes human-readable/debuggable — each route is a .otoml file you can inspect directly.
  • RPC access for free — other services can query proxy config via JSON-RPC over Unix socket.

2. Keypair-Only Auth (Drop API Keys)

Strongly agree. This simplifies everything:

What we need now:

  • auth_mode: "signature" on DomainRoute
  • hero_proxy verifies secp256k1(pubkey, SHA256(timestamp + body), signature) from X-Public-Key + X-Signature + X-Signature-Timestamp headers
  • Per-route allowed_pubkeys field — which pubkeys are allowed for this route (empty = any valid sig)
  • On success: inject X-Proxy-User-Pubkey + X-Proxy-Auth-Method: signature + X-Proxy-Signature-Verified: true and forward
  • Add k256 + sha2 dependencies to hero_proxy

What we defer:

  • API key tables, API key management, API key validation — not needed if resellers use keypairs
  • Session tokens for verified keypairs — future enhancement (once a keypair is verified, proxy could issue a short-lived session token so subsequent requests don't need full sig verification)

Reseller flow becomes:

1. Reseller generates secp256k1 keypair locally
2. Admin registers reseller pubkey via management API (stored in DomainRoute.allowed_pubkeys or separate AllowedPubkey OSIS type)
3. Reseller signs every request: SHA256(timestamp + body) with their private key
4. hero_proxy verifies signature + checks pubkey is allowed + checks timestamp freshness
5. hero_proxy injects X-Proxy-User-Pubkey header → forwards to backend
6. Backend does authorization only: "is this pubkey an admin?" or "which reseller context does this pubkey own?"

What stays in znzfreezone_backend:

  • Context isolation logic (pubkey → reseller_id mapping → context access)
  • Method-level authorization (admin methods need admin pubkey, reseller methods need registered reseller pubkey)
  • Business rules

What moves OUT of znzfreezone_backend:

  • verify_secp256k1_signature() from auth_middleware.rs — proxy does this now
  • verify_request_signature() (timestamp + body verification) from rpc_auth.rs — proxy does this now
  • k256, sha2, hex dependencies — move to hero_proxy

znzfreezone_backend's auth_middleware.rs simplifies to reading X-Proxy-User-Pubkey from headers and trusting it (since the connection is via Unix socket from proxy).

Revised Issue Scope

Phase 1: OSIS migration (data layer)
  - Define proxy.oschema
  - Code generation
  - Replace ProxyDb with OSIS domain
  - Remove rusqlite dependency

Phase 2: Signature auth mode
  - Add k256 + sha2 dependencies
  - Implement secp256k1 payload verification in dispatch_domain_route()
  - allowed_pubkeys on DomainRoute (or separate OSIS type)
  - Strip incoming X-Proxy-* headers
  - Inject verified identity headers

Phase 3: OAuth enforcement (existing scope)
  - Wire auth_mode checking for OAuth routes
  - /oauth/callback endpoint
  - Session cookie validation

Future: Session tokens for verified keypairs
  - After sig verification, optionally issue short-lived session token
  - Subsequent requests can use token instead of re-signing
  - Reduces overhead for high-frequency API callers
## Reply: OSIS as Data Layer + Keypair-Only Auth ### 1. OSIS Instead of SQLite for hero_proxy Agreed this is the right direction. znzfreezone_backend already proves the pattern — it embeds OSIS directly via `AxumRpcServer` with a 107KB `.oschema` file, full codegen, and SmartID-based storage. hero_proxy currently uses SQLite with 7 tables: - `domain_routes` — routing rules with auth_mode - `tls_domains` — Let's Encrypt / selfsigned config - `oauth_providers` — Google, GitHub, custom OAuth - `oauth_sessions` — cookie-based sessions - `listeners` — TCP listener configs - `ssh_tunnels` — remote port forwarding - `settings` — key/value store All of these map cleanly to OSIS types. The migration path: **Step 1**: Define `proxy.oschema` ``` DomainRoute = { sid: sid domain: str [index] target_type: str # "socket" | "http" | "https" target: str # socket path or upstream URL strip_prefix: str? https_redirect: bool auth_mode: str # "none" | "bearer" | "oauth" | "signature" auth_config: str? # JSON blob for mode-specific config allowed_pubkeys: str? # comma-separated secp256k1 pubkeys oauth_provider: str? enabled: bool priority: i64 notes: str? } TlsDomain = { sid: sid domain: str [index] cert_mode: str # "none" | "selfsigned" | "letsencrypt" acme_email: str? acme_production: bool status: str # "pending" | "active" | "failed" status_message: str? } OAuthProvider = { sid: sid name: str [index] provider_type: str # "google" | "github" | "custom" client_id: str client_secret: str auth_url: str token_url: str userinfo_url: str scopes: str allowed_emails: str? enabled: bool } OAuthSession = { sid: sid user_email: str [index] provider_name: str expires_at: u64 } Listener = { sid: sid address: str [index] protocol: str # "http" | "https" tls_mode: str # "none" | "selfsigned" | "letsencrypt" enabled: bool auto_start: bool notes: str? } SshTunnel = { sid: sid name: str [index] ssh_host: str ssh_port: u16 ssh_user: str auth_key_path: str remote_bind_addr: str remote_port: u16 local_target_addr: str enabled: bool auto_start: bool notes: str? } ``` **Step 2**: Add `hero_rpc_osis` dependency (same pattern as znzfreezone_backend) ```toml hero_rpc_osis = { git = "...", branch = "development", features = ["rpc"] } ``` **Step 3**: Replace `ProxyDb` (SQLite) with generated OSIS domain, embed via `AxumRpcServer` **Step 4**: Remove `rusqlite` dependency entirely **Practical considerations:** - OSIS is file-based (OTOML) — each object is a separate file. For hero_proxy's workload (mostly reads on domain routes, occasional config updates) this is fine. - No SQL queries — filtering/matching (like `find_route_for_host()` with wildcard matching) needs to be done in application code. This is already a simple loop over ~10-50 routes, so no performance concern. - No ACID transactions — acceptable for config data. OAuth sessions are the most write-heavy, but individual session creates/deletes don't need transactional guarantees. - Storage becomes human-readable/debuggable — each route is a `.otoml` file you can inspect directly. - RPC access for free — other services can query proxy config via JSON-RPC over Unix socket. ### 2. Keypair-Only Auth (Drop API Keys) Strongly agree. This simplifies everything: **What we need now:** - `auth_mode: "signature"` on DomainRoute - hero_proxy verifies `secp256k1(pubkey, SHA256(timestamp + body), signature)` from `X-Public-Key` + `X-Signature` + `X-Signature-Timestamp` headers - Per-route `allowed_pubkeys` field — which pubkeys are allowed for this route (empty = any valid sig) - On success: inject `X-Proxy-User-Pubkey` + `X-Proxy-Auth-Method: signature` + `X-Proxy-Signature-Verified: true` and forward - Add `k256` + `sha2` dependencies to hero_proxy **What we defer:** - ~~API key tables, API key management, API key validation~~ — not needed if resellers use keypairs - Session tokens for verified keypairs — future enhancement (once a keypair is verified, proxy could issue a short-lived session token so subsequent requests don't need full sig verification) **Reseller flow becomes:** ``` 1. Reseller generates secp256k1 keypair locally 2. Admin registers reseller pubkey via management API (stored in DomainRoute.allowed_pubkeys or separate AllowedPubkey OSIS type) 3. Reseller signs every request: SHA256(timestamp + body) with their private key 4. hero_proxy verifies signature + checks pubkey is allowed + checks timestamp freshness 5. hero_proxy injects X-Proxy-User-Pubkey header → forwards to backend 6. Backend does authorization only: "is this pubkey an admin?" or "which reseller context does this pubkey own?" ``` **What stays in znzfreezone_backend:** - Context isolation logic (pubkey → reseller_id mapping → context access) - Method-level authorization (admin methods need admin pubkey, reseller methods need registered reseller pubkey) - Business rules **What moves OUT of znzfreezone_backend:** - `verify_secp256k1_signature()` from `auth_middleware.rs` — proxy does this now - `verify_request_signature()` (timestamp + body verification) from `rpc_auth.rs` — proxy does this now - `k256`, `sha2`, `hex` dependencies — move to hero_proxy znzfreezone_backend's `auth_middleware.rs` simplifies to reading `X-Proxy-User-Pubkey` from headers and trusting it (since the connection is via Unix socket from proxy). ### Revised Issue Scope ``` Phase 1: OSIS migration (data layer) - Define proxy.oschema - Code generation - Replace ProxyDb with OSIS domain - Remove rusqlite dependency Phase 2: Signature auth mode - Add k256 + sha2 dependencies - Implement secp256k1 payload verification in dispatch_domain_route() - allowed_pubkeys on DomainRoute (or separate OSIS type) - Strip incoming X-Proxy-* headers - Inject verified identity headers Phase 3: OAuth enforcement (existing scope) - Wire auth_mode checking for OAuth routes - /oauth/callback endpoint - Session cookie validation Future: Session tokens for verified keypairs - After sig verification, optionally issue short-lived session token - Subsequent requests can use token instead of re-signing - Reduces overhead for high-frequency API callers ```
Author
Owner

This Issue is Phase 1 — Critical Path

Full roadmap posted on znzfreezone_backend#29. This issue (hero_proxy#8) is the critical path — everything downstream depends on it.

Execution order within this issue:

1a. OSIS migration        — Replace SQLite (db.rs) with proxy.oschema + embedded OSIS
                            Files: new schemas/proxy.oschema, replace src/db.rs,
                            update Cargo.toml (remove rusqlite, add hero_rpc_osis)

1b. Signature auth mode   — secp256k1 verification in dispatch_domain_route()
                            Files: src/proxy.rs, new src/signature.rs,
                            Cargo.toml (add k256, sha2)

1c. Identity injection    — Strip incoming X-Proxy-* headers, inject verified ones
                            Files: src/proxy.rs, weblib/src/proxy/forward.rs

1d. OAuth enforcement     — Wire auth_mode checking + /oauth/callback endpoint
                            Files: src/proxy.rs, src/lib.rs, src/oauth.rs

Blocked by this issue:

## This Issue is Phase 1 — Critical Path Full roadmap posted on [znzfreezone_backend#29](https://forge.ourworld.tf/znzfreezone_code/znzfreezone_backend/issues/29#issuecomment-15726). This issue (hero_proxy#8) is the critical path — everything downstream depends on it. ### Execution order within this issue: ``` 1a. OSIS migration — Replace SQLite (db.rs) with proxy.oschema + embedded OSIS Files: new schemas/proxy.oschema, replace src/db.rs, update Cargo.toml (remove rusqlite, add hero_rpc_osis) 1b. Signature auth mode — secp256k1 verification in dispatch_domain_route() Files: src/proxy.rs, new src/signature.rs, Cargo.toml (add k256, sha2) 1c. Identity injection — Strip incoming X-Proxy-* headers, inject verified ones Files: src/proxy.rs, weblib/src/proxy/forward.rs 1d. OAuth enforcement — Wire auth_mode checking + /oauth/callback endpoint Files: src/proxy.rs, src/lib.rs, src/oauth.rs ``` ### Blocked by this issue: - [hero_rpc#11](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/11) — can develop in parallel but can't integration-test without proxy headers - [hero_osis#16](https://forge.ourworld.tf/lhumina_code/hero_osis/issues/16) — needs RequestContext which needs proxy headers - [znzfreezone_backend#28](https://forge.ourworld.tf/znzfreezone_code/znzfreezone_backend/issues/28) — auth consolidation waits for proxy to inject headers - [znzfreezone_backend#29](https://forge.ourworld.tf/znzfreezone_code/znzfreezone_backend/issues/29) — cross-context auth uses all of the above - [znzfreezone_backend#30](https://forge.ourworld.tf/znzfreezone_code/znzfreezone_backend/issues/30) — OSIS migration benefits from stable auth
timur changed title from Wire OAuth enforcement into domain route dispatch + identity header injection to [Phase 1] Auth gateway: OSIS migration, secp256k1 signature auth, OAuth enforcement, identity injection 2026-03-26 13:04:37 +00:00
Author
Owner

Implemented: auth_mode enforcement + signature auth (commit a4aa070)

This commit wires auth_mode enforcement into dispatch_domain_route() as described in comment #15682 (Phase 2 & 3, OSIS migration deferred to separate PR).

What's new

Auth enforcement on domain routes — previously auth_mode was stored but never checked. Now every proxied request goes through auth verification:

auth_mode Behavior
none Forward directly
bearer Validate Authorization: Bearer <token> against server auth token
signature Verify secp256k1 ECDSA signature (X-Public-Key, X-Signature, X-Signature-Timestamp) with 5-minute replay window
oauth Validate session cookie, redirect to OAuth provider if no valid session

Security: header stripping + identity injection

  • All incoming X-Proxy-* headers are stripped (prevents spoofing)
  • After auth, verified identity headers are injected: X-Proxy-Auth-Method, X-Proxy-User-Email, X-Proxy-User-Pubkey, X-Proxy-Signature-Verified

New endpoint: /oauth/callback

  • Handles OAuth2 authorization code exchange
  • Creates session, sets cookie, redirects to original URL

Schema changes

  • allowed_pubkeys field on DomainRoute (comma-separated hex-encoded secp256k1 public keys)
  • OpenRPC spec updated with "signature" auth_mode and allowed_pubkeys parameter

Files changed (10 files, +853/-148)

  • Cargo.toml — k256, sha2, hex deps
  • src/signature.rs — new secp256k1 verification module (4 unit tests)
  • src/db.rs — allowed_pubkeys field + migration
  • src/proxy.rs — auth enforcement, header stripping, OAuth callback
  • src/lib.rs — module + route registration
  • src/auth.rs — /oauth/callback bypass
  • openrpc.json — spec updates

Remaining

  • OSIS migration (Phase 1 from comment #15682) deferred to separate PR — orthogonal to auth enforcement
## Implemented: auth_mode enforcement + signature auth (commit a4aa070) This commit wires `auth_mode` enforcement into `dispatch_domain_route()` as described in comment #15682 (Phase 2 & 3, OSIS migration deferred to separate PR). ### What's new **Auth enforcement on domain routes** — previously `auth_mode` was stored but never checked. Now every proxied request goes through auth verification: | auth_mode | Behavior | |-----------|----------| | `none` | Forward directly | | `bearer` | Validate `Authorization: Bearer <token>` against server auth token | | `signature` | Verify secp256k1 ECDSA signature (`X-Public-Key`, `X-Signature`, `X-Signature-Timestamp`) with 5-minute replay window | | `oauth` | Validate session cookie, redirect to OAuth provider if no valid session | **Security: header stripping + identity injection** - All incoming `X-Proxy-*` headers are stripped (prevents spoofing) - After auth, verified identity headers are injected: `X-Proxy-Auth-Method`, `X-Proxy-User-Email`, `X-Proxy-User-Pubkey`, `X-Proxy-Signature-Verified` **New endpoint: `/oauth/callback`** - Handles OAuth2 authorization code exchange - Creates session, sets cookie, redirects to original URL **Schema changes** - `allowed_pubkeys` field on DomainRoute (comma-separated hex-encoded secp256k1 public keys) - OpenRPC spec updated with `"signature"` auth_mode and `allowed_pubkeys` parameter ### Files changed (10 files, +853/-148) - `Cargo.toml` — k256, sha2, hex deps - `src/signature.rs` — new secp256k1 verification module (4 unit tests) - `src/db.rs` — allowed_pubkeys field + migration - `src/proxy.rs` — auth enforcement, header stripping, OAuth callback - `src/lib.rs` — module + route registration - `src/auth.rs` — /oauth/callback bypass - `openrpc.json` — spec updates ### Remaining - OSIS migration (Phase 1 from comment #15682) deferred to separate PR — orthogonal to auth enforcement
Sign in to join this conversation.
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_proxy#8
No description provided.