Commit graph hyperhive/nix/modules/hive-gateway.nix
Author SHA1 Message Date
müde
3a3f318779 fix(#2363): grant hive-gateway CAP_NET_ADMIN — dnsmasq DHCP mode requires it 2026-07-13 13:57:37 +02:00
atlas
4cdbbafc44 feat(#2363): full-DHCP for all agents — drop static agent_network_ip
All agent containers now receive their bridge IP dynamically via DHCP
from the dnsmasq pool instead of a hash-derived static address:

- nix/templates/harness-base.nix: networking.useDHCP = true
- nix/modules/hive-gateway.nix: expand DHCP pool to full usable range
  (.2 to .254 on /24) — was last-14-IPs-only
- hive-sh4re/src/priv_proto.rs: remove agent_ip from NetworkIsolation
- hive-c0re/src/lifecycle/mod.rs: drop agent_network_ip + DHCP_POOL_SIZE
- hive-c0re/src/lifecycle/host_config.rs: remove agent_network_ip call
- hive-priv/src/main.rs: LOCAL_ADDRESS= empty (DHCP assigns IP);
  HOST_ADDRESS still set so nixos-container installs default route
  before the DHCP lease arrives
- nix/dhcp-pool-size: deleted (no longer needed)

The nix/dhcp-pool-size single-source-of-truth file and all associated
Rust/Nix dual-constant plumbing are gone — there is no static map.
bridge_gateway_ip() is retained (still needed for HOST_ADDRESS).

Closes #2363
2026-07-13 11:57:14 +02:00
atlas
eb94b2aae6 docs(#2363): fix stale 'remap' comments in hive-gateway.nix
The DHCP pool exclusion is now by construction (agent_slots = usable -
dhcpPoolSize), not a secondary-hash remap. Update two comment blocks
that still referenced the old approach.
2026-07-13 11:57:14 +02:00
atlas
3068034463 refactor(#2363): single source of truth for DHCP pool size
Move the DHCP pool size constant out of the two separate definitions
(Nix literal + Rust const) into a shared data file: nix/dhcp-pool-size.

- nix/dhcp-pool-size: new file, contains '14'
- hive-gateway.nix: reads via builtins.readFile + toIntBase10
- lifecycle/mod.rs: parses via include_bytes! const block at compile time

Cargo automatically tracks include_bytes! as a file dependency so a
change to nix/dhcp-pool-size triggers recompilation without build.rs.
2026-07-13 11:57:14 +02:00
atlas
a3796890f5 feat(#2363): DHCP pool for bridge service containers
Add a DHCP pool to the gateway's dnsmasq so bridge-attached service
containers (hive-ci and future equivalents) get their addresses from
a proper DHCP server instead of a brittle static derivation.

gateway (hive-gateway.nix):
- Add IPv4 arithmetic helpers (ipToInt, intToIp, pow2) to compute the
  DHCP pool range at nix eval time from bridgeIp + bridgePrefixLength.
- Reserve the last dhcpPoolSize (14) usable host addresses as the DHCP
  pool (e.g. .241-.254 on a /24 with 10.42.0.0 network).
- Add dhcp-range and dhcp-leasefile to the dnsmasq settings block.
  The pool is active whenever services.hyperhive.network.enable is true.

hive-ci (hive-ci.nix):
- Remove the ciBridgeIp / ciBridgeOctets static derivation and the
  brittle top-of-/24 comment block.
- Switch networking.interfaces.eth0 to useDHCP = true so hive-ci gets
  its address from the gateway DHCP pool.

lifecycle (mod.rs, tests.rs):
- Add DHCP_POOL_SIZE = 14 constant (must stay in sync with
  dhcpPoolSize in hive-gateway.nix).
- Remap agents whose FNV-1a hash falls in the DHCP pool into the
  agent-only window [2, dhcp_start - 1]. Only the rare agent whose
  name hashes into the pool is affected; all others keep their IPs.
- Update and extend tests: agent range is now .2-.240 on /24;
  add agent_network_ip_never_in_dhcp_pool covering 18 agent names.
2026-07-13 11:57:14 +02:00
atlas
79a29873e3 fix(#2164): domain-URL webhooks + HMAC + config-PR polling fallback
Both webhook registrations (knowledge push + config-PR pull_request) now
use the public hive domain instead of loopback:
  https://<HYPERHIVE_HIVE_DOMAIN>/webhook/{knowledge,config-pr}

This routes deliveries through the gateway, bypassing the Forgejo SSRF
guard that blocked loopback delivery and silently broke the config-PR
merge flow since launch.

Changes:
- webhook_secret: new module — auto-generate + persist a 32-byte HMAC
  secret to STATE_ROOT/webhook-secret on first startup; verify
  X-Hub-Signature-256 on every incoming webhook POST (HMAC-SHA256).
- forge/mod.rs: ensure_config_pr_webhook now takes hive_domain +
  webhook_secret; sets secret in Forgejo hook config.
- workers/knowledge.rs: ensure_webhook same update.
- dashboard/webhook.rs: both handlers read raw Bytes first, verify HMAC,
  then parse JSON. Returns 401 on signature mismatch.
- dashboard/mod.rs: AppState carries webhook_secret; serve() takes it.
- main.rs: load/generate secret at startup; pass to registration tasks
  + dashboard; add 5-minute config-PR polling fallback task.
- forge/config_pr_poll.rs: new — scan agent-configs/* for open PRs with
  no pending MergeConfigPr approval; queue them. Idempotent.
- stores/approvals.rs: has_pending_merge_config_pr() for poll dedup.
- nix/modules/hive-gateway.nix: remove dashboardAuth from /webhook/
  location (HMAC replaces basic auth for webhook endpoints; Forgejo
  cannot send HTTP Basic credentials with webhook deliveries).
2026-07-11 23:28:16 +02:00
atlas
1e986a0270 chore(nix): remove stale 'privsep phase 2' labels from comments
privsep is complete and always on. the phased-rollout framing
is historical — drop it from the inline comments, keeping the
accurate description of what the user + delegation actually do.
2026-07-04 21:04:18 +02:00
atlas
460ed50a80 chore(nix): remove stale phased-rollout comments in network + gateway modules 2026-07-04 20:44:50 +02:00
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