Commit graph hyperhive/nix/modules/hive-gateway.nix
Author SHA1 Message Date
damocles
c8ae3371e1 refactor(#2015): drop redundant domain != null checks (domain is required) + update docs 2026-06-27 21:02:55 +02:00
atlas
14ae7367cd fix(systemd): set explicit SyslogIdentifier on hyperhive-declared units
Follow-up to the harness fix: any unit whose ExecStart is a generated
wrapper (a NixOS `script =` block or a writeShellScript) gets its journal
SyslogIdentifier derived from the wrapper's store-path basename — an
opaque, unstable name. Pin an explicit stable identifier on every unit
hyperhive declares.

script/wrapper units (leaked store paths): forgejo-gpg-init,
hive-gateway-self-signed-cert, hive-tls-ca, hive-ci-prefetch,
hyperhive-isolated-dns, tea-login, forge-avatar-sync,
matrix-avatar-sync, hive-dashboard-links.

direct-binary units (basename already sane; explicit for tidiness +
future-proofing): hive-c0re, hive-priv, hive-matrix-daemon,
hive-bash-daemon, weston.

Only changes the displayed line identifier; journalctl -u <unit>
filtering (keys on _SYSTEMD_UNIT) is unaffected.
2026-06-23 22:53:33 +02:00
atlas
d4f106d590 hive-gateway: route dashboard by path, not Accept header
The dashboard vhost split static-vs-backend on the request Accept
header (map $http_accept $dashboard_spa_target), so the same URL
behaved differently by content-type — e.g. /api/state fetched with
Accept: text/html wrongly returned index.html.

Now that all hive-c0re routes live under /api/ plus the single
/webhook/knowledge endpoint, route by path instead: /api/ and
/webhook/ proxy to c0re (SSE settings on /api/), everything else
serves the dist with try_files $uri /index.html. Drops the
dashboard Accept-header map and the @c0re named location.

Updates docs/gateway.md accordingly.
2026-06-22 23:08:23 +02:00
atlas
f73bcc66eb hive-gateway.nix: nix fmt (collapse lib.optional block left dirty by the forge-fix) 2026-06-22 22:14:36 +02:00
müde
0a9bfe2b9b fix borked forge 2026-06-22 21:13:25 +02:00
atlas
2b4e928afe feat(#1867): make the internal forge mandatory (remove forge.enable)
The internal forge is the canonical store for the meta flake, every
agent's config repo, and the internal/* repos, so it can no longer be
optional. Remove the services.hyperhive.forge.enable toggle:

- hive-forge.nix: drop the `enable` option; the forge config now
  deploys gated on `services.hyperhive.enable` (it ships with hyperhive).
- hive-c0re.nix: HIVE_FORGE_URL env unconditional; forge-public-URL gate
  drops the enable check (keeps behindGateway).
- hive-gateway.nix: local /etc/hosts forge entry keyed on behindGateway.
- hive-ci.nix: drop the now-moot `forge.ci.enable requires forge.enable`
  assertion (forge is always present); reword the option doc.
- nix/docs/default.nix: drop the `forge.enable = mkForce false` stub
  (option gone); the options-doc eval stays light via hyperhive.enable.
- hive-c0re forge.rs / hivectl.rs: reword 'forge.enable = true' error
  text to 'wait for hive-c0re to start the container' (the runtime
  token-absent path is unchanged — it's a bootstrap-timing check, not
  the opt-out).
- docs/approvals.md, docs/ci.md: drop stale forge.enable references.

Migration: configs that set `services.hyperhive.forge.enable = false`
must drop the line — the forge is now mandatory.

Prereq/companion to #1838 (PR-based config flow, which assumes the forge
is always present).
2026-06-22 19:26:34 +02:00
atlas
4db8a8cd3d feat(#1843): static-serve the dashboard via the gateway, hive-c0re API-only
nginx proxied `<hive>/` straight to hive-c0re:7000, and hive-c0re served the
dashboard dist itself via `tower_http::ServeDir` (from `HIVE_STATIC_DIR` baked
into its service env). So a frontend-only change rebuilt the hive-c0re unit and
restarted the core daemon — every operator session dropped its SSE stream for a
pure CSS/JS change.

The gateway nginx now static-serves the dashboard dist directly; hive-c0re's
dashboard router is API-only. The split uses the Accept-header SPA fallback (the
same `map $http_accept` pattern the matrix/agent vhosts already use), so no
backend prefix has to be enumerated: a browser navigation (Accept: text/html)
whose path is not an on-disk asset gets the SPA index.html; everything else
(every /api route, the bare action/mutation routes, the two SSE streams, the
knowledge webhook — all Accept != text/html) falls through `try_files` to the
`@c0re` named location and is reverse-proxied to hive-c0re. A new c0re route
needs no gateway change.

- hive-c0re.nix: expose the themed dist as a new internal read-only option
  `services.hyperhive.c0re.servedFrontend`; drop `HIVE_STATIC_DIR` from the
  service env (the router no longer serves files).
- hive-gateway.nix: read that option in host-module scope (dashboardDist),
  static-serve `dashboard/` with the Accept-header `try_files ... @c0re` split;
  `@c0re` carries `proxy_buffering off` + a 1d read timeout for the SSE streams
  and a duplicated auth_basic block (named locations do not inherit it). The
  dashboard map is unconditional; the matrix map stays gated on the matrix GUI.
- dashboard.rs: drop the ServeDir fallback + the HIVE_STATIC_DIR resolution; the
  router 404s unmatched paths (the gateway only proxies non-static requests).
- hive-c0re/Cargo.toml: drop the now-unused tower-http dependency.
- docs/gateway.md: document the dashboard static split + the `@c0re` fall-through.

The store path is reachable inside the gateway nspawn container (shared
/nix/store), mirroring how HIVE_AGENT_FRONTEND_DIR already exposes the per-agent
UIs. The gateway and c0re changes must land together (atomic cutover) or the
dashboard 404s — this needs a watched gateway + c0re rebuild.
2026-06-22 01:18:01 +02:00
atlas
3b8e77c1c1 nix(gateway): factor the self-signed condition into a shared option
Per review: the `tls.certDir == null && !tls.acme.enable` derivation was
duplicated in hive-gateway, hive-tls, and hive-ci. Expose it once as a
read-only internal option `services.hyperhive.gateway.useSelfSigned` (the
gateway module's single source of truth) and have hive-tls and hive-ci
consume it instead of re-deriving.

Eval-proven: gateway.useSelfSigned is true on the self-signed default /
false with tls.certDir, and the hive-tls (HIVE_TLS_CA_PATH) + hive-ci
(NODE_EXTRA_CA_CERTS) wiring derives correctly from it.
2026-06-18 01:03:55 +02:00
atlas
1a3f82a459 nix(gateway): self-signed TLS as the implicit default, deprecate the toggle
Make self-signed TLS the gateway's default whenever no external TLS source
is configured, and deprecate the explicit selfSignedTls toggle. Self-signed
is now derived as `tls.certDir == null && !tls.acme.enable`, so an operator
selects a TLS mode by setting tls.certDir or tls.acme — or neither, for the
self-signed default. There is no http-only mode: matrix discovery hardcodes
https, so the gateway always terminates TLS.

The selfSignedTls option is kept as a deprecated no-op (warns when set to
false) so existing configs still eval. The two selfSignedTls mutual-
exclusion assertions and the HSTS-requires-TLS assertion are dropped — they
are impossible or vacuous now that self-signed is the floor. The hive-tls
module and the forge ROOT_URL scheme consume the derived value: the gateway
always terminates TLS, so behind the gateway the forge is always advertised
over https.

Updates docs/gateway.md (TLS-modes table, self-signed section, the removed
http-only section, firewall + discovery notes). Eval-proven: default →
self-signed (hive CA active, https ROOT_URL); tls.certDir → CA inactive;
selfSignedTls=false → deprecation warning fires.
2026-06-17 21:14:05 +02:00
atlas
74a90fd7d6 nix(tls): host hive-CA + gateway leaf for self-signed mode
Replace the gateway's bare in-container self-signed leaf with a leaf
signed by a host-held hive CA. A bare self-signed leaf is its own trust
anchor, so every regeneration is a fresh anchor every consumer would have
to re-trust, and a runtime-generated in-container leaf cannot be wired
into an agent's build-time trust store at all. A stable CA fixes both: a
single anchor that agents and federation peers trust once, surviving leaf
rotation.

New hive-tls module: a host oneshot generates a long-lived CA (default
~20y) under services.hyperhive.tls.stateDir and signs a gateway leaf
(default ~10y, SAN covering the bare domain, forge., matrix. and the
wildcard). It is ordered before the gateway container so the leaf exists
when nginx starts. Active only when the gateway uses self-signed TLS
(default) and a domain is set; inert under operator-cert or ACME modes.

Gateway: bind-mount the host CA dir read-only at /run/hive-ca; the
existing in-container cert unit now imports the host leaf into nginx's
state dir (copy as root, key left root:nginx 0640 for the pre-start
config test) instead of generating one. Cert/key paths nginx serves are
unchanged.

Foundational step toward agent + federation trust of self-signed hives;
no behaviour change for agents yet (they still reach the forge over plain
http on port 80). Eval-proven across self-signed, certDir and the inert
default paths.
2026-06-17 19:04:52 +02:00
atlas
eb61660d35 chore(nix): replace tracker tags with prose in nix comments
Part of the tracker-tag cleanup: the hive convention is prose, not
issue-tracker tags, in code. Reword the 21 tags in the nix tree
(flake.nix + the hive-c0re/ci/gateway/network modules) to describe
the thing they pointed at, preserving the context without the tag.

Comment-only — no eval or logic change. Validated with nix fmt
(no reformatting) and nix flake check --no-build (all checks
evaluate clean); the full build check was skipped locally because
the shared remote builder is degraded, so CI will exercise the
build derivations once the runner recovers.
2026-06-09 11:25:56 +02:00
atlas
fdf05c1673 refactor(gateway): make the gateway unconditional — remove gateway.enable
The gateway container starts alongside every hyperhive deployment, so
gating it behind a separate enable flag was a footgun: an operator who
set it false lost the only thing exposed to the outside while the
agent containers kept running. Re-gate the gateway config on the
top-level services.hyperhive.enable instead.

- hive-gateway.nix: drop the gateway.enable mkOption; gate the config
  block on config.services.hyperhive.enable.
- hive-forge.nix: behindGateway now defaults to services.hyperhive.enable;
  remove the behindGateway-requires-gateway assertion (now vacuous).
- hive-network.nix: remove both gateway.enable assertions (vacuous).
- hive-c0re.nix: drop the firewall.allowedTCPPortRanges 8100-8999
  fallback that opened agent ports when the gateway was off (the
  gateway is now the sole entry point); HIVE_GATEWAY_ENABLED is always
  set since the gateway always runs.
- nix/docs/default.nix: remove the gateway.enable = mkForce false stub
  (would be an eval error against the removed option; the gateway is
  already re-gated on hyperhive.enable, which docs force false).
- hive-matrix.nix, dashboard.rs: comment/prose updates only.

BREAKING: operators relying on services.hyperhive.gateway.enable = false
to suppress the gateway must instead point their own reverse proxy at
the gateway's port. NixOS errors clearly on the now-unknown option.
2026-06-09 00:11:10 +02:00
atlas
36e5e19d8e refactor(gateway): extract the _ vhost agent + dashboard location groups
Step 3 of the hive-gateway.nix vhost cleanup. Lift the /agent/ catch-all
(+ its two internal error-page targets) and the catch-all dashboard proxy
out of the inline //-chain into agentLocations / dashboardProxyLocation
bindings. The default _ server's locations now read as a flat composition
of named groups — matrixRedirectLocations // wellKnownLocations //
agentLocations // dashboardProxyLocation // <auth optionalAttrs> — instead
of a deep nested literal. The auth-401 group stays inline (a self-contained
lib.optionalAttrs already).

Pure readability refactor, eval-identical: generated virtualHosts toJSON is
byte-identical before/after (8888 bytes, diff empty).
2026-06-07 21:52:23 +02:00
atlas
d7e4028a16 refactor(gateway): extract the _ vhost matrix location groups into named bindings
Step 2 of the hive-gateway.nix vhost cleanup (follows the sub-domain
vhost extraction). Lift the two deepest-nested location groups of the
default _ server — the /matrix/ 301 redirect and the
.well-known/matrix/{client,server} discovery JSON, each carrying its own
let block — out of the inline //-chain into matrixRedirectLocations /
wellKnownLocations bindings. The _ vhost locations now open with
matrixRedirectLocations // wellKnownLocations // ... instead of two ~60-line
nested literals. agent/dashboard/auth groups stay inline (a later step).

Pure readability refactor, eval-identical: the generated
services.nginx.virtualHosts toJSON is byte-identical before/after (8888
bytes, diff empty) on a host with matrix+forge+gui+auth+tls enabled.
2026-06-07 20:40:55 +02:00
atlas
a03aafb004 refactor(gateway): extract forge + matrix sub-domain vhosts into named bindings
Step 1 of the hive-gateway.nix vhost-builder cleanup. The two
sub-domain vhosts (forge, matrix) were inline `lib.optionalAttrs`
blocks `//`-appended to the virtualHosts attrset, burying the
top-level structure. Lift them into `forgeVhost` / `matrixVhost`
bindings in the config-closure let so the composition reads as
`{ "_" = ...; } // forgeVhost // matrixVhost` — the three vhosts are
now visible at a glance. Also fixes a garbled merge-mangled comment on
the nginx-reload host-trigger.

Pure readability refactor, eval-identical: verified the generated
`services.nginx.virtualHosts` toJSON is byte-identical before/after
(8888 bytes, diff empty) on a host with matrix+forge+gui+auth+tls all
enabled. The deeper `_` vhost location-group extraction is a follow-up.
2026-06-07 20:40:55 +02:00
atlas
48b7c24265 fix(nix): emit explicit port in .well-known/matrix/server delegation
The matrix server-server spec resolves a portless m.server delegated
host to the federation default port 8448 (not 443 — the URL convention
that https implies 443 does not apply to the m.server field). The
gateway reused publicPortSuffix, which intentionally drops :443 for the
client base_url, so .well-known/matrix/server advertised the bare
gateway host. Peers then federated to <gatewayHost>:8448, which nothing
listens on, while the federation endpoint actually lives on the gateway
443 vhost (/_matrix/ proxied to tuwunel). Always emit the explicit port
in the federation delegation so peers reach the live 443 endpoint.
2026-06-06 00:37:07 +02:00
damocles
c5c47b29b4 fix: own /run/hive-agent as hive-core so the unprivileged daemon can create per-agent socket dirs on first launch 2026-06-05 23:30:50 +02:00
damocles
911dc5227b fix(#1263): repeat security headers in locations with their own add_header 2026-06-04 14:35:18 +02:00
atlas
5ec1306c7b feat(gateway): assert hsts.enable requires TLS
Addresses mara's review: add a NixOS assertion that rejects
hsts.enable = true when no TLS mode is configured. HSTS over
plain HTTP is silently ignored by browsers; the assertion turns
a silent misconfiguration into a build-time error.
2026-06-04 13:50:22 +02:00
atlas
5264ef7d5e feat(gateway): add gateway.hsts.enable option, disabled by default
HSTS was unconditionally tied to hasTls. This is risky: enabling it on a
deployment that later loses TLS locks browsers out until max-age expires.

Add three options under services.hyperhive.gateway.hsts:
  enable          — bool, default false
  maxAge          — seconds, default 31536000 (1 year)
  includeSubDomains — bool, default true

HSTS header is now only emitted when hsts.enable = true.
2026-06-04 13:50:22 +02:00
atlas
18941848dc feat(gateway): add security headers to all vhosts
X-Frame-Options, X-Content-Type-Options, Referrer-Policy at server
scope on _, forge, and matrix vhosts. HSTS added when TLS is active.

nginx inheritance rule: locations with their own add_header (CORS API
endpoints like /.well-known/matrix/client, /_matrix/) are unaffected —
they already carry the headers they need. HTML-serving and proxy
locations pick the security headers up automatically.
2026-06-04 13:50:22 +02:00
atlas
d2298e183b fix(#1155): enable recommendedTlsSettings + recommendedGzipSettings in gateway nginx 2026-06-03 16:48:33 +02:00
atlas
fb93cbf5c2 feat: add tls.acme mode — nginx inside container manages Let's Encrypt
per mara's suggestion: instead of bind-mounting operator certs, let
nginx handle ACME directly inside the gateway container.

- tls.acme.enable: lets nginx obtain + renew via HTTP-01 challenge
- tls.acme.email: ACME account contact (required when enable=true)
- security.acme in container config when acme.enable
- hasTls includes acme.enable → https, httpsPort listen, firewall
- mutual exclusion assertions: acme vs selfSignedTls vs certDir
- docs/gateway.md: four-mode TLS table + ACME section

typical setup:
  selfSignedTls = false; openFirewall = true;
  tls.acme = { enable = true; email = "admin@example.com"; };
2026-06-03 16:46:52 +02:00
atlas
44122c66de feat(#594): gateway operator-cert TLS mode (tls.certDir)
add services.hyperhive.gateway.tls.certDir option: operators with a
CA-signed cert (Let's Encrypt, corporate CA) point at the ACME output
dir instead of using the auto-generated self-signed cert.

- tls.certDir: host path bind-mounted r/o at /run/hive-tls/ in gateway
- tls.certName / tls.keyName: filenames within certDir (default: cert.pem / key.pem, matches nixpkgs security.acme layout)
- hasTls = selfSignedTls || certDir != null: publicScheme=https in both cases
- assertion: selfSignedTls=true + certDir set together is an error
- openFirewall: httpsPort opened in both TLS modes
- docs/gateway.md: TLS modes table + operator-cert section
- docs updated in swarm.md peer config reference in the cert TLS section

when using operator cert, swarm peers can omit certFingerprint —
standard CA bundle handles trust automatically.
2026-06-03 16:46:52 +02:00
damocles
9d11e5b6d6 fix(#1052): use systemctl reload nginx instead of systemd-run 2026-06-02 01:00:48 +02:00
damocles
637b170cc5 fix(#1052): use full nginx path in systemd-run reload (exit 203 = EXEC) 2026-06-02 00:52:56 +02:00
atlas
167b4fa1f3 refactor(gateway): fixed htpasswd path, drop htpasswdFile option
Remove the custom htpasswdFile option and bind-mount. The htpasswd file
now lives at the fixed path /var/lib/hyperhive/gateway/gateway.htpasswd
on the host, which is already exposed inside the container at
/run/hive-state/gateway.htpasswd via the existing gateway state
bind-mount — no extra bind-mount needed.

A tmpfiles rule pre-creates the file so nginx can open it even before
any users exist (empty file → all requests return 401, which is correct).

hivectl gateway commands default --file to the standard path so
`hivectl gateway create-user alice` just works without any flags.
2026-06-02 00:26:10 +02:00
atlas
ba1d096391 fix(gateway): interpolate actual htpasswdFile path in 401 page
Per argus review: the hardcoded /etc/hyperhive/gateway.htpasswd
example was wrong for operators with a custom htpasswdFile path.

Move the unauthorized.html from the static agentErrorPagesDir derivation
into a pkgs.writeText inside the lib.optionalAttrs guard where
cfg.auth.htpasswdFile is in scope and statically known non-null.
The rendered page now shows the operator's actual configured path.
2026-06-02 00:26:10 +02:00
atlas
a248c6bee1 feat(gateway): custom 401 page explaining how to add users
When HTTP Basic auth is enabled and credentials are absent or rejected,
nginx serves a Catppuccin-styled 401 page that tells the operator which
hivectl command to run to create a user. Uses error_page 401 =401 so
the browser still receives a 401 status (login dialog fires on first
visit) while getting a human-readable body when the dialog is dismissed.

The exact-match location (= /__hive_auth_unauthorized) beats location /
in nginx's prefix ordering so the internal subrequest does not loop back
through auth_basic.
2026-06-02 00:26:10 +02:00
atlas
6b6289c191 fix(gateway): point option descriptions at hivectl instead of raw htpasswd 2026-06-01 23:25:28 +02:00
atlas
4bff450343 feat(gateway): hivectl gateway user management + fix htpasswdFile assertion
Add `hivectl gateway {create-user,delete-user,list-users}` subcommands for
managing htpasswd files used by gateway Basic auth. Pure Rust bcrypt
(cost 12, $2y$ prefix nginx accepts). No external htpasswd binary required.

Also fix the NixOS module assertion: `cfg.auth ? htpasswdFile` is always
true in the module system (declared options always exist as keys); switch
to `nullOr path; default = null` + `!= null` check so the assertion
actually fires with a useful error when enable=true but no file is set.
Guard bind-mount and nginx config against null to prevent eval errors.

Update docs/gateway.md to show hivectl commands instead of raw htpasswd.
2026-06-01 23:25:28 +02:00
atlas
25d2951d1e feat(gateway): htpasswd Basic auth — close #1010
Replaces the earlier PAM+binary approach with nginx's built-in
`auth_basic` module. No new binary, no new systemd service, no PAM.

New option `services.hyperhive.gateway.auth`:
- `enable` — off by default
- `htpasswdFile` — host path to an htpasswd file (required when enable)
- `realm` — WWW-Authenticate realm string (default "hyperhive");
  restricted to `strMatching "[^\"$]*"` to prevent nginx config injection

When enabled:
- the parent directory of `htpasswdFile` is bind-mounted read-only
  into the gateway container at `/run/gateway-auth/`
- the `"/"` proxy location gets `auth_basic` + `auth_basic_user_file`

Create credentials: `htpasswd -Bc /path/to/file alice` (BCrypt).
See `docs/gateway.md` ("HTTP Basic auth") for the full setup guide.
2026-06-01 23:24:47 +02:00
atlas
d4409b27a3 feat(gateway): PAM auth against host — close #1010
Adds opt-in HTTP Basic auth to the hive-gateway backed by the host PAM
stack + group membership check.

New binary `hive-gateway-auth` (hive-c0re workspace):
- Axum HTTP service on 127.0.0.1:7002 (host loopback)
- Decodes Basic credentials, authenticates via pam_unix.so
- Checks membership in `hyperhive-operator` group (or custom)
- Returns 200 / 401 / 403; nginx `auth_request` consumes these

New options under `services.hyperhive.gateway.auth`:
- `enable`      — off by default
- `port`        — auth service port (default 7002)
- `realm`       — WWW-Authenticate realm string (default "hyperhive")
- `group`       — required host group (default "hyperhive-operator")
- `pamService`  — PAM service name (default "hive-gateway")

Host-side NixOS wiring:
- `users.groups.hyperhive-operator` declared when default group used
- `/etc/pam.d/hive-gateway` emitted via `security.pam.services`
- `systemd.services.hive-gateway-auth` runs the auth binary as root
  (needs /etc/shadow access for pam_unix.so)

Gateway container nginx wiring:
- `location = /__hive_gateway_auth` — internal proxy to auth service
- `auth_request /__hive_gateway_auth` on the `"/"` proxy location
- `@hive_auth_required` named location adds WWW-Authenticate: Basic
  header on 401 so browsers display a login prompt

Workspace deps: pam = "0.8"; flake.nix: linux-pam added to
nativeBuildInputs so pkg-config can find libpam at build time.
2026-06-01 23:24:47 +02:00
atlas
d45b2f0293 chore: scrub issue/PR cookies from gateway + network nix modules and gateway_nginx.rs 2026-05-31 23:09:46 +02:00
atlas
01d7c37af2 gateway: fix nginx reload — trigger from host via systemd-run (#889)
The #872 path-unit approach was silently broken: IN_MOVED_TO from an
atomic rename on the host does not propagate across the nspawn
mount-namespace boundary into the container, so the watcher inside
the gateway container never fired.

Fix: after each agents.conf write, c0re calls
`systemd-run --machine=hive-gateway -- nginx -s reload` from the host.
The reload is best-effort (logged on failure, not fatal).

Remove the now-unused `hive-gateway-agents-conf.path` +
`hive-gateway-nginx-reload.service` from the gateway container config.
Update docs/gateway.md + comments to reflect the host-side approach.
2026-05-31 22:42:52 +02:00
atlas
9eb8012c9b gateway: scope bind-mount to gateway/ subdir, scrub #869 cookie (argus 🟡)
- gateway_nginx.rs: HOST_CONF_PATH → /var/lib/hyperhive/gateway/agents.conf
- hive-gateway.nix: hostPath = /var/lib/hyperhive/gateway (not whole parent
  dir — avoids exposing forge tokens or other credentials to the gateway
  container)
- tmpfiles: add /var/lib/hyperhive/gateway/ dir rule + seed agents.conf there
- scrub "(#869)" from hive-gateway-nginx-reload service description
2026-05-31 20:29:56 +02:00
atlas
07434e8f50 gateway: hot-reload agents.conf at runtime (#869)
Replace eval-time per-agent nginx location baking with a runtime
include file. c0re writes /var/lib/hyperhive/agents.conf (nginx
location blocks, UDS or TCP per agent) on every topology change and
on the 10s marker poll. The gateway container bind-mounts
/var/lib/hyperhive/ at /run/hive-state/ and nginx includes
/run/hive-state/agents.conf. A systemd path unit inside the container
watches the file for changes and fires `nginx -s reload` on each
atomic rename from c0re — no nixos-rebuild switch needed when agents
start, stop, or flip useUnixSocket.

  - new hive-c0re/src/gateway_nginx.rs: write() + render()
  - lib.rs + meta.rs + agent_sockets::spawn_poll: hook in write()
  - hive-gateway.nix: drop agentPortsTable/agentSocketsTable/
    agentUpstreamFor/lib.mapAttrs', add /run/hive-state bind-mount,
    include directive, systemd path unit + reload service, tmpfiles
    for /var/lib/hyperhive + agents.conf seed
  - docs/gateway.md: update vhost table + Per-agent UDS section
2026-05-31 20:29:56 +02:00
müde
adf89a1f25 hive-gateway: chown key.pem root:nginx 0640 for pre-start nginx -t
nginx-pre-start runs the config test as the nginx user; a 0600
root-owned key fails that check with BIO_new_file Permission denied
even though the master process would later load it as root.
2026-05-31 19:50:43 +02:00
müde
d10f7d4c13 hive-gateway: chmod cert parent dir so nginx worker can traverse
Parent /var/lib/hive-gateway came out 0700, blocking the nginx-user
worker from reaching the 0755 tls/ subdir and surfacing as a generic
cert-load failure at nginx start.
2026-05-31 19:44:16 +02:00
damocles
80dd4babd4 hive-gateway: also gate cert gen on key existence (argus 🟡 on #865) 2026-05-31 17:55:20 +02:00
damocles
0aa0e5b95a hive-gateway: idempotent cert oneshot, always-run + parse check (#856 follow-up) 2026-05-31 17:55:20 +02:00
damocles
50a1eb41cf hive-gateway: nginx Requires= cert oneshot (fixes #856 from #848) 2026-05-31 17:34:23 +02:00
damocles
c7777f3d58 hive-gateway: hoist publicScheme/Port helpers to outer let (argus 🟡 on #848) 2026-05-31 17:17:58 +02:00
damocles
314f1774d8 hive-gateway: self-signed TLS for matrix-client well-known fetch (closes #837) 2026-05-31 17:17:58 +02:00
atlas
b50f182396 hive-gateway: note common-case dnsmasq address redundancy (argus 🟡 on #845)
forge/matrix subdomain address rules are redundant when those are
sub-domains of hyperhive.domain (dnsmasq's /<domain>/ matches
sub-domains). Keep explicit for the cross-domain case (e.g.
forge.domain = "git.example.com") + add a comment explaining
the trade-off.
2026-05-31 16:56:59 +02:00
atlas
ed3b9d853e nix/hive-network: bridge + dnsmasq resolver, opt-in (#805 v1)
Stand up the host-side bridge interface + per-agent DNS resolver
ahead of #14 (netns isolation). Mara on #805#11541: "we need it
before #14 so nothing breaks when we switch over". v1 ships the
endpoint live but containers stay on shared host netns — when #14
flips them to private netns the DNS contract is already there.

Shape:

- new `nix/modules/hive-network.nix` with `services.hyperhive.network.*`
  options (enable + bridgeName + bridgeIp + bridgePrefixLength +
  upstreamDns). Default off. Imported from `hive-c0re.nix`.
- bridge interface via `networking.bridges` (no slave NICs at v1;
  per-agent veth pairs attach once #14 lands).
- bridge IP assigned via `networking.interfaces`.
- `networking.firewall.interfaces.<bridge>.allowed{UDP,TCP}Ports =
  [ 53 ]` opens the resolver on the bridge interface only —
  other interfaces stay closed.
- dnsmasq config added to the existing `hive-gateway` container
  (mara on #805:10957: "put the resolver into the gateway container").
  Listens only on `bridgeName` + `lo`; authoritative for
  `<hive-domain>`, `forge.<hive>`, `matrix.<hive>` answering with
  the bridge IP; forwards everything else to upstream.
  `resolveLocalQueries = false` keeps the gateway container's own
  resolver untouched.

Asserts `services.hyperhive.domain != null` + `gateway.enable =
true` — both required for the resolver to be meaningful.

Docs: new `docs/network.md` covering v1 vs v2 split, container shape
rationale, default addressing, resolver behaviour, firewall posture.

`nix flake check` clean.
2026-05-31 16:56:59 +02:00
atlas
e1bc9e42f1 hive-gateway: tmpfiles rule for /run/hive-agent (argus 🟡 on #829)
Defensive against the fresh-boot-before-any-agent-spawn window where
the bind-mount source wouldn't exist from c0re's per-agent
`set_nspawn_flags` mkdir chain yet. nspawn auto-creates missing
sources, so this is intent-explicit rather than functional fix.
2026-05-31 16:14:02 +02:00
atlas
3a29aee001 nix/hive-gateway: UDS upstream for /agent/<name>/ (#784 phase 2 step 3)
Switch per-agent gateway upstreams from TCP loopback to unix-domain
socket when the agent has opted in via `hyperhive.web.useUnixSocket`
(#822). Coexists with the TCP path during rollout.

Changes:

- New `agentSocketsFile` option (default
  `/var/lib/hyperhive/agent-sockets.json`) — c0re writes the map
  there via `hive_c0re::agent_sockets::write` (#809).
- `agentSocketsTable = lib.importJSON ...` (graceful empty when
  file missing).
- `agentUpstreamFor name port` picks `http://unix:<path>:/` when the
  socket has a JSON entry AND the file exists at eval time; else
  `http://127.0.0.1:<port>/`. Path-exists gate guards against
  c0re's blanket-emit shape during the canary window (agents in
  `agent-sockets.json` who haven't actually flipped have no
  bound socket on disk → fall back to TCP). Damocles will ship a
  `.bound` marker filter on the c0re side (#784 step 2d
  follow-up); once that's in, the path-exists check is redundant
  but harmless. Step 4 drops it entirely along with the TCP
  fallback.
- `containers.hive-gateway.bindMounts."/run/hive-agent"` —
  read-only, unconditional. Inert when no agents have opted in.
  Required so nginx inside the gateway container can `connect(2)`
  to the per-agent sockets damocles's #813 bind-mounts into agent
  containers at the same paths.

Docs:

- `docs/gateway.md::Per-agent UDS upstream (#784)` — full rollout
  flow, subdir-bind rationale (damocles #813), eval-time gate
  explainer, step 4 drop plan.

`nix flake check` clean; `nix fmt` clean.

Canary plan: once #822 (`useUnixSocket` option) lands + this PR
merges, manager flips atlas's agent.nix to `useUnixSocket = true`
via the config-update flow. End-to-end validation against atlas
before broader rollout.
2026-05-31 16:14:02 +02:00
atlas
24775845a3 nix/hive-gateway: static not-found + unreachable pages for /agent/<name>/ (#755)
mara on #755: "e.g. /agent/name should show an error page stating
that the agent could not be found if missing in json or that it is
not reachable if we get a connection error. we dont want a fully
generic fallback, only for routes already special cased in the
nginx config."

Adds two static HTML pages built at deploy time via
`pkgs.runCommand "hyperhive-agent-error-pages"`:

- **not-found.html** — served when `/agent/<unknown>/...` hits the
  bare `/agent/` catch-all. The catch-all `return 404`s, and
  `error_page 404 = /__hive_agent_not_found` rewrites to the static
  page.
- **unreachable.html** — served when `/agent/<known>/...` proxy_pass
  to the harness returns 502 / 503 / 504. `proxy_intercept_errors
  on` + `error_page 502 503 504 = /__hive_agent_unreachable` on each
  per-agent location block rewrites to the static page.

Mechanics:

- `agentErrorPagesDir` (in the `let` block) is a `runCommand` that
  emits two HTML files using a `<<EOF` heredoc — no template engine
  needed.
- Two `internal` nginx locations (`= /__hive_agent_not_found`,
  `= /__hive_agent_unreachable`) `alias` the exact files. `internal`
  keeps the URIs unreachable from direct operator request — only
  nginx's own error-handling can hit them.
- Per-agent location blocks pick up the `error_page` directive
  through the existing `lib.mapAttrs'` over `agentPortsTable`. No
  per-agent generated content; same static page for all.
- `/agent/` catch-all generates from a tiny optionalAttrs alongside
  the per-agent block — both are no-op when the agent table is
  empty (matches the pre-#15 shape).

Pages: minimal inline CSS, catppuccin palette matching the
dashboard (`#1e1e2e` bg, `#cdd6f4` text, `#cba6f7` not-found heading,
`#f9e2af` unreachable heading). No frontend-dist dependency — render
even when hive-c0re is down. Both link back to `/`.

Per mara's "only for routes already special cased" — scope stays
narrow. Forge / matrix / fluffychat keep nginx defaults; extending
the custom-error pattern to other vhosts is a separate follow-up
if/when needed.

Verified:
- nginx location attrset has `["/", "/agent/", "= /__hive_agent_not_found", "= /__hive_agent_unreachable"]`
- container toplevel builds clean (`nixos-system-hive-gateway-26.05pre-git`)
- `docs/gateway.md::Per-agent error pages` section captures the
  design + rationale + intentional narrowness

Closes #755.
2026-05-31 15:04:54 +02:00
atlas
922057e81b docs/gateway.md: extract big-picture gateway docs, trim in-code comments (mara #775:9988)
mara on PR #775: "this is too much docs in code - move bigger picture
stuff to md files and put refs in code"

New `docs/gateway.md` consolidates the gateway architecture story
that was spreading across long inline comments in `hive-gateway.nix`,
`hive-matrix.nix`, and `hive-forge.nix`:

- vhost map (which URL serves what, which upstream, which option)
- matrix discovery flow (.well-known → sub-domain delegation
  sequence)
- Accept-header SPA fallback pattern (#686 / #729 design history)
- local-dev `localHostsEntry` story
- sub-domain rationale (mara verdict tracking) + when sub-path is
  right (hyperhive-internal apps)
- per-vhost tuning knobs (forge LFS, matrix long-poll, agent SSE)
- sequencing history (which PR added which routing piece)

In-code comments in the two nix modules get trimmed to short refs
into the doc — keeps the *why* in the markdown while the *what*
stays alongside the code:

- hive-gateway.nix: top-of-file comment, `agentPortsTable`,
  `appendHttpConfig`, every location block + vhost
- hive-matrix.nix: `fluffychat-web-fixed`, `fluffychat-web-imaging`,
  the dart compile postInstall

README.md gets a new row in the docs table pointing at gateway.md.

Verified `nix eval` still resolves the same vhost + location layout
after the comment trim — no behavioral change, just less in-code
prose.
2026-05-31 14:29:40 +02:00