−
100%
+
fit
scroll
# portal-ingress — Chronicle **Day 355** —
portal-ingress conceived, designed, and built
in one session. ## Why axum over pingora/sozu
Three candidates evaluated: - **Pingora**
(Cloudflare) — most capable proxy framework
but runs as a separate daemon. Introduces a
second process, a second config layer, no
native Things integration. - **sozu** —
nginx drop-in with hot-reload but still a
separate config file daemon. Same problem: two
systems to keep in sync. - **axum + hyper**
— Portal already runs axum 0.7. hyper 1.x is
a transitive dep. Adding a proxy layer to the
existing binary means: one process, one DB
pool, one config source (route Things), zero
inter-process coordination. **Decision:**
extend Portal. The ingress is not a sidecar
— it IS Portal, listening on a second port.
## How route Things map to nginx location
blocks nginx `server {}` block → one route
Thing (`sapphire=route`). | nginx | route
Thing gem | |-------|----------------| |
`server_name` | `citrine.host` (array,
wildcard supported) | | `location /` |
`citrine.path` | | `proxy_pass` |
`citrine.upstream` | | `proxy_*` handler |
`ruby.handler = "proxy"` | | `root` +
`try_files` | `ruby.handler = "static"` | |
`return 301` | `ruby.handler = "redirect"` | |
per-server SSL cert | `obsidian.cert` → cert
Thing UUID | | (implicit) node |
`citrine.hearth` | Route Things live in the
`route` ring. Priority (`topaz.priority`)
resolves overlaps — higher wins. Path
specificity breaks ties (longer path prefix
wins). ## Cert-as-Thing lifecycle ``` Route
Thing (sapphire=route,
citrine.host=cocotrix.stream, ruby.tls=auto)
↓ ACME manager reads domain list from route
Things ↓ Runs HTTP-01 challenge (token
written to ChallengeStore, served on :80) ↓
Let's Encrypt validates, issues cert ↓ cert
stored as Thing (sapphire=cert,
onyx=cocotrix.stream) pearl.cert = chain PEM
pearl.key = private key PEM topaz.expiry =
unix timestamp ↓ CertStore (in-memory)
loaded, SniFinder resolves at TLS handshake
time ↓ Daily renewal check: expiry - now <
30 days → re-provision ``` ACME account
credentials stored as `sapphire=acme-account`
Thing (`pearl.credentials=<json>`). Activate
via `PORTAL_ACME=1`. ## Env vars | Var |
Effect | |-----|--------| |
`PORTAL_INGRESS_PORT=4444` | HTTP proxy on
test port (parallel run) | |
`PORTAL_INGRESS_HTTP=1` | Bind :80 — ACME
challenges + HTTPS redirect | |
`PORTAL_INGRESS_TLS=1` | Bind :443 — TLS
termination via cert Things | |
`PORTAL_ACME=1` | Enable cert manager
background task | | `PORTAL_ACME_EMAIL` |
Contact email for Let's Encrypt account | |
`PORTAL_ACME_STAGING` | Use Let's Encrypt
staging (testing) | | `PORTAL_NODE_NAME` |
Node identity for hearth-based route filtering
| ## nginx-mesh gaps retired - **nm-001**
(cert SAN expansion) — obsolete. ACME
manager provisions certs for all domains in
route Things automatically. No manual cert
renewal or SAN list needed. - **nm-002**
(nginx server_name expansion) — obsolete.
Route Things replace nginx vhosts.
Portal-ingress reads from DB; no config file
to update. - **nm-tendril** (ingress event
observability) — fulfilled. pi-006 proxy
handler logs upstream errors; moon emission
can be added to `proxy_handler` as a one-liner
when the event-stream FID lands. ## Sunset
sequence (per-domain) 1. Add route Thing for
domain (`sapphire=route`, `ruby.tls=auto`) 2.
Set `PORTAL_ACME=1`, let cert manager
provision cert Thing 3. Set
`PORTAL_INGRESS_HTTP=1` (nginx steps off :80
for that domain, or nginx proxies ACME path)
4. Set `PORTAL_INGRESS_TLS=1` — Portal binds
:443 5. Remove nginx vhost for domain 6.
Repeat per domain until nginx has nothing left
→ stop nginx ## Files -
`src/server/ingress.rs` — RouteTable, proxy
handler, ChallengeStore, SniFinder,
:80/:443/:XXXX servers - `src/server/acme.rs`
— ACME cert manager, cert Thing CRUD -
`src/server/mod.rs` — startup wiring
(env-gated ingress spawning)
△