Watch
0
0
Fork
You've already forked hyperhive
0

gateway: serve the hive dashboard on <hive>.hives.<swarm> behind authelia, drop htpasswd

The dashboard, its API, the swagger UI and the agent routes move to their
own vhost, `swarm.dashboardDomain` = `<hive>.hives.<swarm.domain>`, under
the swarm domain so authelia's session cookie reaches it. `/`, `/api/` and
`/api/docs/` ask authelia through `auth_request`, and authelia's
`operatorsOnly` rule for each hive's dashboard name lets only the admins
group in. The hive domain keeps matrix discovery and redirects every other
path to the new name.

The name rides as a SAN on the hive's own gateway leaf: the per-hive bao
role allows it, and hive-tls asks for it and checks coverage.

HTTP Basic auth is gone: the htpasswd writer and its test, the
`hivectl gateway` verbs and host-socket requests, the `gateway.auth`
options (now a removed-option error), the unauthorized page and the bcrypt
dependency. tmpfiles deletes a leftover gateway.htpasswd, which held
bcrypt hashes at 0644.

c0re hands out dashboard and agent URLs on the new name
(HYPERHIVE_DASHBOARD_DOMAIN), and the swarm controller's hive roster
carries each hive's dashboard name for the swarm UI's hive list.

Closes #3469
This commit is contained in:
atlas 2026-10-15 20:19:23 +02:00 • committed by mara
commit 92559af8e7
47 changed files with 351 additions and 623 deletions

View file

@ -115,6 +115,8 @@ let
nginxTree = import ./vhosts.nix {
gwLib = cfg.lib;
inherit (config.services.hyperhive.swarm) dashboardDomain;
autheliaDomain = config.services.hyperhive.swarm.authelia.domain;
inherit
lib
cfg
@ -236,19 +238,25 @@ in
# /var/lib/hyperhive.
"d /var/lib/hive-gateway 0755 root root - -"
"d /var/lib/hive-gateway/conf 0755 hive-core hive-core - -"
# Pre-created so nginx's config test can open it before c0re has
# ever written: nginx names it, and an `include` of a missing file is
# a fatal config error, not an empty one. tmpfiles runs before
# services, which is the whole ordering guarantee — content arrives
# when c0re writes and reloads, which it does on every topology
# change. `f` = create-if-absent, never overwrite.
"f /var/lib/hive-gateway/conf/agents.conf 0644 hive-core hive-core - # Generated by hive-c0re — do not edit.\n"
# Pre-create both files so nginx's config test can open them before
# c0re has ever written: nginx names them, and an `include` of a
# missing file is a fatal config error, not an empty one. tmpfiles
# runs before services, which is the whole ordering guarantee —
# content arrives when c0re writes and reloads, which it does on
# every topology change. `f` = create-if-absent, never overwrite.
#
# An empty htpasswd causes all auth checks to return 401 (no valid
# credentials), which is the correct no-users behaviour.
"f /var/lib/hive-gateway/conf/gateway.htpasswd 0644 hive-core hive-core - -"
# Nothing reads or writes this path. A copy left on an existing host
# holds bcrypt password hashes at 0644; tmpfiles' `--remove` pass, on
# switch and at boot, deletes it.
"r /var/lib/hive-gateway/conf/gateway.htpasswd"
];
# The dashboard's name, for agents on the bridge and (with
# `localHostsEntry`) the host's own browser.
services.hyperhive.gateway.localNames = lib.optional (
runsHive && config.services.hyperhive.swarm.dashboardDomain != null
) config.services.hyperhive.swarm.dashboardDomain;
# ACME (Let's Encrypt) integration. nginx vhosts set
# `enableACME = true` via the vhost builder; this provides the
# shared ACME config (acceptTerms + email).

View file

@ -67,17 +67,4 @@ in
<p class="hint">Check the container: <code>journalctl -M swarm-authelia -u authelia-swarm</code>. This page recovers on reload once the provider is up.</p>
'';
};
unauthorized = mkPage {
name = "unauthorized";
title = "unauthorized";
accent = "#f38ba8";
body = ''
<p>This hive is protected by HTTP Basic auth. Valid credentials are required.</p>
<p class="hint">Operator: add a user with <code>hivectl gateway create-user</code>:</p>
<pre>hivectl gateway create-user \
&lt;username&gt; --password-stdin</pre>
<p class="hint">Then reload your browser and enter the credentials when prompted.</p>
'';
};
}

View file

@ -18,6 +18,12 @@ in
mode. Remove the setting; configure `tls.certDir` or `tls.acme`
to override the self-signed default.
'')
(lib.mkRemovedOptionModule [ "services" "hyperhive" "gateway" "auth" ] ''
The dashboard is served on services.hyperhive.swarm.dashboardDomain
behind authelia, and only accounts in authelia's `admins` group can
open it. There is no HTTP Basic auth and no htpasswd file. Remove
the setting; add an operator with `swarmctl user add <name> --group admins`.
'')
];
options.services.hyperhive.gateway = {
@ -169,7 +175,7 @@ in
readOnly = true;
description = ''
Read-only: the gateway's styled static error pages, by name
(`notFound`, `unreachable`, `unauthorized`, `ssoUnavailable`).
(`notFound`, `unreachable`, `ssoUnavailable`).
Published so a service module can aim an `error_page` at one
instead of rendering its own — a service that built its own
@ -343,32 +349,6 @@ in
};
};
auth = {
enable = lib.mkEnableOption ''
HTTP basic auth on the gateway using an htpasswd file. When
enabled, every request to the gateway's main vhost requires a
valid username and password. nginx's built-in `auth_basic`
module validates credentials against
`/var/lib/hive-gateway/conf/gateway.htpasswd`. Off by default.
Manage users with `hivectl gateway create-user`, `delete-user`,
and `list-users` — see `hivectl gateway --help` for usage.
The htpasswd file is created automatically when auth is enabled;
add at least one user before enabling to avoid locking everyone out.
'';
realm = lib.mkOption {
type = lib.types.strMatching "[^\"$]*";
default = "hyperhive";
example = "my-hive";
description = ''
HTTP Basic auth `realm` value sent in the `WWW-Authenticate`
header when credentials are absent or rejected. Must not
contain `"` or `$` (nginx string metacharacters).
'';
};
};
swaggerUiTheme = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swagger-ui-theme";

View file

@ -22,7 +22,7 @@
svcCert, # swarm-services leaf, for names the hive leaf cannot carry
svcKey,
swarmServiceDomains, # which names those are (../swarm.nix derives it)
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable }
errorPages, # ./error-pages.nix: { notFound, unreachable, ssoUnavailable }
caBundle, # hive TLS trust bundle on the host
}:
let

View file

@ -1,8 +1,10 @@
# nginx virtual-host tree for the **hive's own** gateway surface: the `_`
# default server, which answers unmatched `Host` with a bare 444, and the
# vhost named for the hive domain, which carries the dashboard, per-agent
# routing, matrix discovery, the swagger UI and the legacy
# `<hive>/matrix/*` redirect.
# default server, which answers unmatched `Host` with a bare 444; the
# dashboard's vhost (`swarm.dashboardDomain`), which carries the dashboard
# behind authelia, per-agent routing and the swagger UI; and the vhost
# named for the hive domain, which carries matrix discovery, the legacy
# `<hive>/matrix/*` redirect and a redirect of everything else to the
# dashboard's name.
#
# Scope: a swarm service declares its own vhost and dns name in its own
# module, built from the kit this file also consumes
@ -18,10 +20,12 @@
matrixCfg, # services.hyperhive.swarm.matrix — what the homeserver IS
matrixDeployCfg, # services.hyperhive.deploy.matrix — what THIS host runs
hyperhiveDomain,
dashboardDomain, # services.hyperhive.swarm.dashboardDomain
autheliaDomain, # services.hyperhive.swarm.authelia.domain
runsHive, # deploy.hive-controller.enable — whether hive-c0re runs here
dashboardDist,
swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable }
errorPages, # ./error-pages.nix: { notFound, unreachable, ssoUnavailable }
gwLib, # `services.hyperhive.gateway.lib` — ./vhost-lib.nix's kit, via the option
}:
let
@ -126,15 +130,19 @@ let
};
};
# Shared auth block — separate locations don't inherit auth_basic, so
# each dashboard location (`/`, `/api/`) needs it or that surface is
# unauthed.
dashboardAuth = lib.optionalString cfg.auth.enable ''
auth_basic "${cfg.auth.realm}";
auth_basic_user_file /var/lib/hive-gateway/conf/gateway.htpasswd;
# `=401` keeps the status 401 so the login dialog shows; the
# internal page explains `hivectl gateway create-user`.
error_page 401 =401 /__hive_auth_unauthorized;
# Repeated verbatim by every operator-gated location (`/`, `/api/`,
# `/api/docs/`): nginx's `auth_request` does NOT inherit across sibling
# locations, so a location without it is reachable with no session.
#
# ⚠️ `auth_request` answers "is there a session", not "is this an
# operator". authelia's `operatorsOnly` rule for this name
# (../swarm-authelia.nix) is what refuses an agent's own account.
dashboardAuth = ''
auth_request /__hive_authelia;
# Captured BEFORE the error_page jump: inside the 401 handler
# `$request_uri` is the internal one.
auth_request_set $target_url $scheme://$http_host$request_uri;
error_page 401 =302 https://${autheliaDomain}/?rd=$target_url;
'';
# Dashboard: nginx static-serves the dist, c0re is API-only. Routing
@ -167,8 +175,8 @@ let
'';
};
"/health/" = {
# No dashboardAuth here: an external uptime monitor generally can't do
# interactive HTTP Basic. The endpoints themselves are scoped to
# No dashboardAuth here: an external uptime monitor can't do an
# interactive login. The endpoints themselves are scoped to
# status + warning kind/message (see hive-c0re/src/dashboard/
# health.rs) — no tokens, no agent detail — so exposing them
# unauthenticated isn't a new secret surface.
@ -206,6 +214,29 @@ let
'';
};
};
# The subrequest `dashboardAuth` makes, the same as the swarm UI's
# (../swarm-ui.nix), which documents the header set.
autheliaLocation = {
"= /__hive_authelia" = {
proxyPass = "https://${autheliaDomain}/api/authz/auth-request";
# nixpkgs appends its OWN `Host $host` after extraConfig, which would
# override verifiedProxyTo's — see the comment on verifiedProxyTo in
# ./vhost-lib.nix.
recommendedProxySettings = false;
extraConfig = ''
internal;
${gwLib.verifiedProxyTo autheliaDomain}
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
};
in
{
virtualHosts = {
@ -237,32 +268,26 @@ in
'';
};
}
// lib.optionalAttrs runsHive {
# The hive domain's own vhost, reachable by NAME, so the catch-all can
# reject everything else. Only where a hive runs: the dashboard, agent
# routes and swagger UI proxy to hive-c0re, which runs only with the hive
# controller, and the certificate naming the hive domain is the hive
# leaf, which exists only there.
${hyperhiveDomain} = (vhostTlsFor hyperhiveDomain) // {
// lib.optionalAttrs (runsHive && dashboardDomain != null) {
# The dashboard, reachable by its own NAME under the swarm domain, where
# authelia's session cookie reaches. Only where a hive runs: the
# dashboard, agent routes and swagger UI proxy to hive-c0re, which runs
# only with the hive controller, and the hive leaf carrying this name
# exists only there.
#
# The agent routes live here and not on the hive domain because the
# agent page calls the dashboard's `/api/` on its own origin
# (frontend/packages/agent/src/lib/dashboardBase.ts), and the dashboard
# links to them by path. They carry no `auth_request`.
#
# `forceSSL` for the reason ../swarm-ui.nix gives: authelia answers the
# subrequest for an `http://` target with 400, which `auth_request`
# cannot read. `removeAttrs` because nixos asserts on a vhost declaring
# both.
${dashboardDomain} = (builtins.removeAttrs (vhostTlsFor dashboardDomain) [ "addSSL" ]) // {
forceSSL = true;
listen = vhostListen;
locations =
matrixRedirectLocations
// wellKnownLocations
// agentLocations
// dashboardProxyLocation
// swaggerUiLocations
// lib.optionalAttrs cfg.auth.enable {
# Internal-only target for the dashboard's 401 error_page.
# `internal` prevents direct client access; `alias` serves
# the pre-built HTML from the Nix store.
"= /__hive_auth_unauthorized" = {
extraConfig = ''
internal;
alias ${errorPages.unauthorized};
default_type text/html;
'';
};
};
locations = agentLocations // dashboardProxyLocation // swaggerUiLocations // autheliaLocation;
# Per-agent location blocks, generated at runtime by
# hive-c0re and written to /var/lib/hive-gateway/conf/agents.conf
# on the host — the same machine nginx runs on. nginx parses
@ -275,5 +300,21 @@ in
include /var/lib/hive-gateway/conf/agents.conf;
'';
};
# The hive domain's own vhost: matrix discovery, which federation
# reads off the server name, and a redirect of every other path to the
# dashboard's name, so a bookmarked dashboard or agent link still lands.
${hyperhiveDomain} = (vhostTlsFor hyperhiveDomain) // {
listen = vhostListen;
locations =
matrixRedirectLocations
// wellKnownLocations
// {
"/".extraConfig = ''
return 301 ${publicScheme}://${dashboardDomain}${publicPortSuffix}$request_uri;
'';
};
extraConfig = securityHeaders;
};
};
}