037 - Multi-Network Serving¶
User Story¶
As an operator of an Infinito.Nexus node that runs the Tor provider next to a public clearnet domain, I want every web application to be reachable on clearnet and onion at the same time by default, configured from one place per role, so that clearnet users and Tor users reach the same deployment and every page stays inside the network it was requested on.
Implementing pull request: PR 414.
Decisions¶
Operator-confirmed. These MUST NOT be re-litigated during implementation.
# |
Decision |
|---|---|
1 |
A node runs in one of three network modes: |
2 |
The node mode defaults to |
3 |
|
4 |
|
5 |
Clearnet is canonical whenever it is active: |
6 |
SSO publishes exactly one issuer. In |
7 |
Clearnet vhosts of |
8 |
|
9 |
The CI network axis rotates |
10 |
Networks live in one registry, so Handshake and later networks plug in without a redesign. Only clearnet and onion are implemented. |
11 |
The generic mechanism replaces every Wazuh-specific dual-stack workaround of PR 414. |
Model¶
Network registry. One entry per network with its provider role, sibling-domain derivation, transport (TLS policy, scheme, port), client reachability (browser proxy) and CSP scheme.
clearnetis implicit and has no provider.onionis provided bysvc-net-tor, derives<sub>.<node>.onionfrom<sub>.<DOMAIN_PRIMARY>, serves plain HTTP on port 80 and needs the Tor SOCKS proxy in browsers.handshakeis reserved and not implemented.Mode presets.
clearnet= {clearnet},tor= {onion},multi= {clearnet, onion}.Effective networks of a role. The node preset intersected with the role’s
reachability.modes, then reduced to one network whensingle_modeis set:torif the node runs Tor,clearnetotherwise. An empty result fails the deploy.Roles without a Tor bond. A role that declares no
torservice (a server-side backend such asweb-app-seaweedfs) keeps its declared clearnet domains on every node and is never a mismatch.Network siblings. Every logical domain of a role expands to one domain per effective network. The clearnet sibling is canonical when present.
Per-vhost decisions. One vhost per sibling. TLS, injected snippets, CSP, redirects and health probes are decided by the vhost domain, and every cross-app URL rendered into a vhost comes from that vhost’s network.
Target Configuration¶
# roles/<role>/meta/networks.yml
local:
subnet: 192.168.81.0/24
reachability:
modes: [clearnet, tor]
single_mode: false
Both keys are optional. modes: [] or no modes key means every network, and no single_mode key means false.
# inventory (node)
NETWORK_MODE: multi
reachability is used instead of a top-level modes key because overlay.modes in the same file already lists deploy modes (compose, swarm).
Codebase Facts¶
Verified on the PR 414 worktree; build on these.
Fact |
Where |
|---|---|
Onion siblings are injected per app from |
domains.py |
The provider default is |
|
|
tls.py, tls_common.py |
Only the primary domain gets a vhost by default, and the TLS layer is switched on per app |
|
Roles registering explicit domains: bluesky, matrix, minio, seaweedfs; loops over all domains: mastodon, peertube, wordpress, joomla |
the roles’ |
Injected snippets live in one Lua file per app and CSP script hashes are stored per app, so two vhosts of one app share snippets |
|
The only runtime network rewrite is CDN clearnet to onion for onion hosts |
|
The CSP header is built per app without the vhost domain |
|
Keycloak redirect URIs already cover every domain of every SSO consumer with a per-domain scheme |
|
Keycloak pins its issuer to |
|
The web health check uses one global protocol from |
|
Redirect vhosts use the source domain’s scheme, and the node-onion apex redirects to the canonical homepage |
|
The CI network axis knows two states from |
|
Playwright runs one pass per role against |
|
The svc-net-tor README claims |
|
Fork run 35925283225 of the dual-stack Wazuh head served both vhosts, but its clearnet page loaded |
job 107412507989 log |
Acceptance Criteria¶
Configuration and model¶
[ ]
meta/networks.ymlacceptsreachability.modeswith values fromclearnetandtor, treats an empty list or a missing key as every network, and a lint rejects any other value includingmulti.[ ]
meta/networks.ymlacceptsreachability.single_modeas a boolean, and a role withsingle_mode: trueis served ontorwhen the node runs Tor and onclearnetotherwise.[ ]
NETWORK_MODEacceptsclearnet,torandmulti, defaults tomultiwhensvc-net-toris deployed and toclearnetotherwise.[ ] The effective networks of a role follow Decisions 3 and 4, covered by unit tests for every combination of node mode,
modesandsingle_modeincluding the explicit failure.[ ] No
meta/services.ymlcontainsexclusiveorprimaryundertor, and a lint rejects them.[ ] Network properties (sibling derivation, TLS policy, scheme, browser proxy) are read from the registry, and no
.onionsuffix check remains outside it in plugins, utils, filters and role scripts.[ ] The registry reserves
handshakewithout implementing it, and the design page documents the steps to add a network.
Domains and vhosts¶
[ ] In
multimode every canonical domain of a role has exactly one onion sibling inlookup('domains'), clearnet first.[ ]
sys-stk-front-proxyrenders one vhost per network sibling of the domain it receives, and no vhost is rendered twice.[ ] Roles that register explicit domains (bluesky, matrix, minio, seaweedfs, mastodon, peertube, wordpress, joomla) serve every network sibling without network-specific code in the role.
[ ] TLS is decided by the vhost domain in
sys-stk-front-proxy,sys-front-tlsandnginx_ssl_header.j2without adefault()fallback: onion vhosts listen on port 80 in plain HTTP, clearnet vhosts followTLS_MODE.[ ] Clearnet vhosts of
multiapps sendOnion-Location: http://<onion sibling>$request_uri, and no other vhost sends it.[ ] The shared location template forwards
X-Forwarded-Host, and the svc-net-tor README describes theX-Forwarded-Protovalue that is actually sent.
Per-vhost network alignment¶
[ ] Every cross-app URL rendered for a vhost (CDN, CSS, Matomo, logout, dashboard iframe origin, simpleicons, mirror) uses the provider domain of that vhost’s network, in both directions.
[ ] Injected snippets and their CSP hashes are rendered per vhost, so the clearnet and the onion vhost of one app serve their own correct snippets.
[ ] The CDN-only runtime rewrite in
body_filter.lua.j2is removed.[ ] An application that renders network-bound URLs into one config (the dashboard) runs one instance per network, each with the config and CDN scripts of its network.
[ ] The CSP header is built per vhost domain and lists only sources of that vhost’s network plus the single SSO issuer.
[ ] Redirect vhosts (aliases,
www, node-onion apex) redirect inside the source’s network, so source and target share one scheme.[ ] Templates rendered once per role (environment files, branding, web app manifests, Collabora configuration) resolve every own or peer host URL for the network of the vhost that serves them, either rendered once per network sibling and served by that sibling’s vhost or as same-origin paths, and no role carries a hand-written onion host or its own host rewrite.
[ ] A lint reports every role template rendered outside a vhost loop that resolves an own or peer host URL without that mechanism, and it fails on a negative control.
[ ] A web app manifest served on an onion vhost names only icons on that vhost’s network, and the onion pass of
web-app-mini-qrpasses.[ ] A cookie set on an http onion vhost carries no
Secureflag, and the login ofweb-app-n8npasses on an onion pick.[ ] Collabora accepts the WOPI host of every Nextcloud network sibling and may be embedded by it, and
addons/richdocuments.spec.jsopens a document on an onion pick ofweb-app-nextcloudvariant 0.
SSO¶
[ ] In
multimode Keycloak’s onion vhost answers every browser path with a 302 to the same path on the single issuerhttps://auth.<clearnet>, keeps its health locations, and the issuer sends noOnion-Location.[ ] The issuer’s
frame-ancestorsadmits the pages of every network the node serves, so an onion app can embed the issuer’s session iframes.[ ] In
tormode Keycloak keeps its onion issuer, and inclearnetmode its clearnet issuer.[ ] A
multiSSO app completes an OIDC login on its clearnet vhost (Playwright).[ ] Keycloak redirect URIs and web origins include both siblings of every SSO consumer, each with its own scheme.
[ ] An oauth2-proxy-gated role runs one proxy instance per network it is served on, so a login started on either sibling returns to that sibling and sets its session cookie there.
[ ] Keycloak registers every network sibling of an OIDC client as post-logout redirect URI, and an OIDC login started on the onion sibling of
web-app-joomlareturns to that onion sibling.[ ] A logout started on any network ends the role’s session on every network sibling: after logging out on the onion sibling of
web-app-nextcloudvariant 0,/loginon the onion host asks for credentials again andtest-login-biber-oidc.jspasses on an onion pick.
Checks¶
[ ]
sys-ctl-hlth-webserverprobes every vhost of every effective network with that vhost’s scheme, and no global protocol flag remains.[ ]
sys-ctl-hlth-cspcrawls the http and the https vhosts in one run.
CI¶
[ ] The CI network axis offers
clearnet🌐,tor🧅 andmulti🌈 (glyphs inutils/symbol_glossary.py), selected by one input that replacesINFINITO_TOR.[ ] A matrix row only takes node modes its role can be served in, and a
single_moderole never takes amultirow.[ ] On
multirows Playwright runs the full role suite once, on the clearnet or the onion URL picked at random, and keeps that pick for the flake retry.[ ] On
multirows the run includes the guest persona and a shared cross-network leak spec.[ ] The leak spec fails when a page requests a host of another network or loads blocked mixed content, proven by a negative control that reproduces the Wazuh mixed-content regression of fork run 35925283225.
[ ]
torrows keep running the full role suite on the onion URL.
Roles¶
[ ] Every role with a Tor bond either passes a
multirow or setsreachability.single_mode: trueor restrictsreachability.modes, each with a written reason that a lint enforces.[ ] web-app-matrix expresses its former
exclusive: trueasreachability.single_mode: true.[ ] web-app-wazuh contains no network-specific code (no onion variable, no second front-proxy include, no forked vhost template) and passes a
multirow including the onion leak pass.[ ]
network-leak.spec.jspasses on an onion pick ofweb-app-mobilizon, with its design and CDN assets loaded from the onion network or the same origin.[ ] No role whose onion pass fails on clearnet-rendered configuration is fixed by
networks.reachability.handover, a clearnet-only spec marker or a skipped assertion.[ ] The “dual-family providers” item in
TODO.mdis removed.
Quality¶
[ ] Unit tests cover the registry, sibling derivation, mode resolution, bidirectional alignment and per-vhost TLS.
[ ] A design page under
docs/contributing/design/documents the network modes,meta/networks.ymland the registry.[ ]
make quality-highis green.
Out of Scope¶
HTTPS on onion vhosts, blocked on a CA that issues for v3
.onion(seeTODO.md).Per-network OIDC issuers and mirrored SSO realms.
Implementing Handshake or any network beyond clearnet and onion.
Migrating identities of existing nodes between networks.
Implementation Order¶
Registry,
NETWORK_MODE, thereachability.modesandreachability.single_modeschema, network resolution and their lints and unit tests.Sibling injection from the effective mode, removal of
exclusiveandprimary, Matrix migration.Vhost fan-out in
sys-stk-front-proxyand per-vhost TLS.Per-vhost alignment: injected snippets, CSP, redirects,
Onion-Location, forwarded headers.Keycloak single clearnet issuer in
multimode.Health probe and CSP crawler per vhost.
CI network axis, the second Playwright pass and the leak spec with its negative control.
Wazuh cleanup, then
multirows for every role with a Tor bond and justified restrictions where a role fails.Design page,
TODO.mdcleanup,make quality-high.
Validation¶
make compose-deploy mode=reinstall apps=web-app-wazuh variant=0 full_cycle=false
make compose-playwright role=web-app-wazuh
curl -sI https://wazuh.infinito.test/ | grep -i '^onion-location'
Commit Policy¶
The agent MUST NOT push. When the criteria are green, the operator runs
git-sign-pushoutside the sandbox.