HTTP/HTTPS reverse proxy routing traffic to Hero Unix sockets with TLS and OAuth.
- Rust 80.3%
- JavaScript 11.2%
- HTML 5.1%
- CSS 3.4%
|
All checks were successful
lab publish / publish (push) Successful in 30m2s
Forgejo SSO login failed on the callback with "OAuth code exchange failed:
validate id_token". Three stacked bugs in the OIDC path, fixed here:
1. id_token `aud` shape. Forgejo emits `aud` as a single-element JSON array
(`["<client_id>"]`), but IdTokenClaims.aud was typed `String`, so serde
deserialization failed before any check ran ("invalid type: sequence,
expected a string"). `aud` now deserializes from string-or-array per
RFC 7519 section 4.1.3. The audience membership check is still enforced by
jsonwebtoken's Validation::set_audience (which parses the raw claim
independently of our struct), so this does not weaken validation.
2. Identity from userinfo. Forgejo's id_token carries only sub/iss/aud/exp/
iat/nonce -- never preferred_username/email/name (even with profile/email
scope); those live solely in the userinfo endpoint. The OIDC path read
identity only from the id_token, so the local username fell back to the
numeric `sub` and never matched the username-based access allowlist. After
validating the id_token, the OIDC path now enriches the identity from
userinfo, binding the userinfo `sub` to the validated id_token `sub`
(OIDC Core section 5.3.2). userinfo failure is non-fatal.
3. openid scope durability. The boot-time provider seed defaulted scopes to
`read:user`, dropping `openid` on a proxy restart so Forgejo stopped
returning an id_token. The seed now ensures `openid` is present for the
OIDC provider regardless of the stored scope secret.
Also: the OAuth callback now logs the full error chain ({:#}) and id_token
validation failures include the token's iss/aud vs the required values, so a
mismatch is diagnosable from the proxy log without a second login attempt.
Verified end to end against forge.ourworld.tf SSO: a real login now returns
302 with the username resolved from userinfo. fmt/clippy -D warnings clean,
69 unit tests pass (8 new). An independent security review confirmed the
audience/issuer/signature/nonce checks remain intact and the userinfo
subject-binding is not bypassable.
Fixes #61
Closes #60
Signed-by: mik-tf <mik-tf@noreply.invalid>
|
||
|---|---|---|
| .forgejo/workflows | ||
| .hero | ||
| crates | ||
| docs | ||
| patches | ||
| schema | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| PURPOSE.md | ||
| README.md | ||
| rust-toolchain.toml | ||
Hero Proxy
HTTP/HTTPS reverse proxy and service discovery for the Hero ecosystem. Routes incoming traffic to Hero services via URL-prefix → Unix domain socket forwarding, with TLS termination (self-signed or Let's Encrypt), SSH tunnels, OAuth, and a full management API.
Architecture
Internet / Browser
│
:80 (HTTP) / :443 (HTTPS)
│
hero_proxy_server
├── {domain}/* → ~/hero/var/sockets/{service}/*.sock
├── TLS (self-signed or Let's Encrypt / ACME)
├── SSH reverse-port tunnels
├── OAuth2 / bearer / signature auth per route
└── OpenRPC JSON-RPC 2.0 management API
│
~/hero/var/sockets/hero_proxy/rpc.sock
Crates
| Crate | Type | Description |
|---|---|---|
hero_proxy |
binary | CLI — registers and starts/stops all services via hero_proc |
hero_proxy_server |
binary | TCP proxy + service discovery + OpenRPC API |
hero_proxy_admin |
binary | Admin dashboard |
hero_proxy_sdk |
library | Auto-generated typed client |
weblib |
library | Reusable web / TLS / ACME library |
Sockets
All sockets under $PATH_SOCKET/hero_proxy/ (default ~/hero/var/sockets/hero_proxy/).
| Socket | Protocol | Description |
|---|---|---|
rpc.sock |
OpenRPC / JSON-RPC 2.0 | Management API |
admin.sock |
HTTP | Admin dashboard |
Ports (TCP)
| Port | Protocol | Description |
|---|---|---|
| 9997 | HTTP | Proxy ingress (dev / LAN) |
| 9996 | HTTPS | Proxy ingress (TLS) |
| 443 | HTTPS | Production ingress (Let's Encrypt) |
| 80 | HTTP | Redirect → HTTPS (Let's Encrypt mode) |
Service Management
All service lifecycle operations are driven by lab on top of hero_proc.
Install and start
lab service proxy --install # build + install all declared binaries
lab service proxy --start # register with hero_proc and start
Stop / restart
lab service proxy --stop
lab service proxy --stop && lab service proxy --start
Status
lab service proxy --status
TLS / ACME Configuration
TLS settings are persisted in the database so they survive restarts without environment variables. DB values always take precedence over env vars.
Set via the management API (recommended)
# Set the domain for Let's Encrypt (required)
proxy system config set --dns-name proxy.example.com
# Set contact email (strongly recommended — expiry notifications)
proxy system config set --acme-email admin@example.com
# Switch to production CA (default is staging — safe to test first)
proxy system config set --acme-production true
# View current effective config and where each value comes from
proxy system config get
# Clear a field (reverts to env var fallback, or unset)
proxy system config remove dns_name
Response from proxy system config get
{
dns_name: "proxy.example.com"
acme_email: "admin@example.com"
acme_production: true
dns_name_source: "db" # db | env | unset
acme_email_source: "db" # db | env | unset
acme_production_source: "db" # db | env | default
}
Env var fallbacks (legacy / alternative)
| DB key (via API) | Environment variable | Default |
|---|---|---|
acme_dns_name |
HERO_PROXY_DNS_NAME |
— (self-signed) |
acme_email |
HERO_PROXY_ACME_EMAIL |
— (no email) |
acme_production |
HERO_PROXY_ACME_PRODUCTION |
false (staging) |
Changes take effect the next time a listener with tls_mode=letsencrypt is started. Running listeners are not hot-reloaded — restart the listener after changing config.
Quick-start: public HTTPS with Let's Encrypt
# 1. Install and start the service
lab service proxy --install
lab service proxy --start
# 2. Configure ACME (while service is running — no restart needed for config)
proxy system config set --dns-name proxy.example.com --acme-email admin@example.com
proxy system config set --acme-production true
# 3. Add an HTTPS listener (starts the ACME challenge immediately)
proxy listener add "0.0.0.0:443" --protocol https --tls-mode letsencrypt
# 4. Verify
proxy listener status
proxy tls check proxy.example.com
Quick-start: SSH tunnel to a public server
# Create HTTP + HTTPS + DNS-bridge tunnels in one call and start them
proxy tunnel quick_add edge.example.com admin --auth-key-path ~/.ssh/id_ed25519
# Check status
proxy tunnel status
proxy tunnel check_dns 3
Domain routing
# Route a hostname to a local service socket
proxy domain add app.example.com socket /run/myapp.sock
# Route with bearer-token auth
proxy domain add api.example.com https https://127.0.0.1:9000 --auth-mode bearer
# Route with OAuth (Google)
proxy oauth set google google $CLIENT_ID $CLIENT_SECRET \
--scopes "openid email profile"
proxy domain add dash.example.com http http://127.0.0.1:8080 \
--auth-mode oauth --oauth-provider google
# List / inspect routes
proxy domain list
proxy domain get 3
Development
# Fast check without producing a binary
cargo check -p hero_proxy_server
# Run tests
cargo test
# Clippy
cargo clippy --all-targets -- -D warnings
# Format
cargo fmt
Documentation
- docs/configuration.md — all env vars and DB settings
- docs/api.md — management API reference
- docs/architecture.md — system design
- docs/setup.md — installation and deployment
- docs/releasing.md — release process