From a248c6bee14a30295826377428ae1932be6a0163 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 1 Jun 2026 23:33:16 +0200 Subject: [PATCH 1/4] 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. --- nix/modules/hive-gateway.nix | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 9925aeb9..edae09a8 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -59,6 +59,32 @@ let EOF + cat > $out/unauthorized.html <<'EOF' + + + + + unauthorized ◆ hyperhive + + + +

◆ unauthorized

+

This hive is protected by HTTP Basic auth. Valid credentials are required.

+

Operator: add a user with hivectl gateway create-user:

+
hivectl gateway create-user \
+      --file /etc/hyperhive/gateway.htpasswd \
+      <username> --password-stdin
+

Then reload your browser and enter the credentials when prompted.

+ + + EOF ''; in { @@ -586,9 +612,30 @@ in ${lib.optionalString (cfg.auth.enable && cfg.auth.htpasswdFile != null) '' auth_basic "${cfg.auth.realm}"; auth_basic_user_file /run/gateway-auth/${builtins.baseNameOf cfg.auth.htpasswdFile}; + # Serve a custom page when credentials are missing or wrong. + # `=401` forces the final status to remain 401 so browsers + # still present the login dialog on first visit; users who + # dismiss the dialog see a page explaining how to add users + # with `hivectl gateway create-user`. + # The exact-match location below beats `location /` in nginx's + # prefix ordering, so the internal subrequest does not loop back + # through auth_basic. + error_page 401 =401 /__hive_auth_unauthorized; ''} ''; }; + } + // lib.optionalAttrs (cfg.auth.enable && cfg.auth.htpasswdFile != null) { + # Internal-only target for the 401 error_page above. + # `internal` prevents direct client access; `alias` serves + # the pre-built HTML from the Nix store. + "= /__hive_auth_unauthorized" = { + extraConfig = '' + internal; + alias ${agentErrorPagesDir}/unauthorized.html; + default_type text/html; + ''; + }; }; # Per-agent location blocks, generated at runtime by # hive-c0re and written to /var/lib/hyperhive/gateway/agents.conf From ba1d09639136e23b8ae04f4bc250a2b99560023a Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 1 Jun 2026 23:38:17 +0200 Subject: [PATCH 2/4] 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. --- nix/modules/hive-gateway.nix | 74 ++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index edae09a8..7ac6a439 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -59,32 +59,6 @@ let EOF - cat > $out/unauthorized.html <<'EOF' - - - - - unauthorized ◆ hyperhive - - - -

◆ unauthorized

-

This hive is protected by HTTP Basic auth. Valid credentials are required.

-

Operator: add a user with hivectl gateway create-user:

-
hivectl gateway create-user \
-      --file /etc/hyperhive/gateway.htpasswd \
-      <username> --password-stdin
-

Then reload your browser and enter the credentials when prompted.

- - - EOF ''; in { @@ -629,13 +603,47 @@ in # Internal-only target for the 401 error_page above. # `internal` prevents direct client access; `alias` serves # the pre-built HTML from the Nix store. - "= /__hive_auth_unauthorized" = { - extraConfig = '' - internal; - alias ${agentErrorPagesDir}/unauthorized.html; - default_type text/html; - ''; - }; + # The page is built here (not in the top-level `let`) so + # that `cfg.auth.htpasswdFile` is in scope and known + # non-null — the operator sees the actual configured path + # in the `hivectl` example command, not a hardcoded guess. + "= /__hive_auth_unauthorized" = + let + htpasswdPath = cfg.auth.htpasswdFile; + page = pkgs.writeText "hive-gateway-unauthorized.html" '' + + + + + unauthorized ◆ hyperhive + + + +

◆ unauthorized

+

This hive is protected by HTTP Basic auth. Valid credentials are required.

+

Operator: add a user with hivectl gateway create-user:

+
hivectl gateway create-user \
+                          --file ${htpasswdPath} \
+                          <username> --password-stdin
+

Then reload your browser and enter the credentials when prompted.

+ + + ''; + in + { + extraConfig = '' + internal; + alias ${page}; + default_type text/html; + ''; + }; }; # Per-agent location blocks, generated at runtime by # hive-c0re and written to /var/lib/hyperhive/gateway/agents.conf From 167b4fa1f31eb940f804ea796d907185a93b8f34 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 1 Jun 2026 23:42:03 +0200 Subject: [PATCH 3/4] refactor(gateway): fixed htpasswd path, drop htpasswdFile option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- hive-c0re/src/bin/hivectl.rs | 37 ++++++++++++-------- nix/modules/hive-gateway.nix | 68 ++++++++++-------------------------- 2 files changed, 40 insertions(+), 65 deletions(-) diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index f5734b8b..e2fd156d 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -147,19 +147,20 @@ enum MatrixCmd { }, } +/// Default htpasswd file path — the host-side location of the gateway's +/// credential store, pre-created by a tmpfiles rule when +/// `services.hyperhive.gateway.auth.enable = true`. +const DEFAULT_HTPASSWD_FILE: &str = "/var/lib/hyperhive/gateway/gateway.htpasswd"; + #[derive(Subcommand)] enum GatewayCmd { - /// Add a new user or update the password of an existing user in an - /// htpasswd file. The password is hashed with BCrypt (cost 12). + /// Add a new user or update the password of an existing user in the + /// gateway htpasswd file. The password is hashed with BCrypt (cost 12). /// /// Pass `--password-stdin` when scripting or when you don't want the /// password visible in shell history. The file is created if it does /// not exist; its parent directory must already exist. CreateUser { - /// Path to the htpasswd file (the value of - /// `services.hyperhive.gateway.auth.htpasswdFile`). - #[arg(long, short = 'f')] - file: PathBuf, /// Username to add or update. username: String, /// Set the password inline. WARNING: visible in shell history and @@ -171,20 +172,26 @@ enum GatewayCmd { /// stripped). Mutually exclusive with `--password`. #[arg(long)] password_stdin: bool, - }, - /// Remove a user from an htpasswd file. Exits with an error when the - /// user is not found so callers can detect the no-op case. - DeleteUser { - /// Path to the htpasswd file. - #[arg(long, short = 'f')] + /// Path to the htpasswd file. Defaults to the standard gateway + /// credential store at `/var/lib/hyperhive/gateway/gateway.htpasswd`. + #[arg(long, short = 'f', default_value = DEFAULT_HTPASSWD_FILE)] file: PathBuf, + }, + /// Remove a user from the gateway htpasswd file. Exits with an error + /// when the user is not found so callers can detect the no-op case. + DeleteUser { /// Username to remove. username: String, + /// Path to the htpasswd file. Defaults to the standard gateway + /// credential store. + #[arg(long, short = 'f', default_value = DEFAULT_HTPASSWD_FILE)] + file: PathBuf, }, - /// List all usernames in an htpasswd file, one per line. + /// List all usernames in the gateway htpasswd file, one per line. ListUsers { - /// Path to the htpasswd file. - #[arg(long, short = 'f')] + /// Path to the htpasswd file. Defaults to the standard gateway + /// credential store. + #[arg(long, short = 'f', default_value = DEFAULT_HTPASSWD_FILE)] file: PathBuf, }, } diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 7ac6a439..851e8538 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -202,34 +202,18 @@ in 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 from the htpasswd file at - `services.hyperhive.gateway.auth.htpasswdFile`. nginx's built-in - `auth_basic` module handles credential validation — no extra - service or host-side daemon required. Off by default. + valid username and password. nginx's built-in `auth_basic` + module validates credentials against + `/var/lib/hyperhive/gateway/gateway.htpasswd` on the host + (exposed as `/run/hive-state/gateway.htpasswd` inside the + container via the existing gateway state bind-mount). 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. ''; - htpasswdFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - example = "/etc/hyperhive/gateway.htpasswd"; - description = '' - Path on the **host** to an htpasswd-format file whose - `username:hashed-password` entries nginx uses for Basic auth. - The parent directory is bind-mounted read-only into the gateway - container at `/run/gateway-auth/`. The file must be readable by - the `nginx` user inside the container (mode 0644 recommended). - - Manage with: `hivectl gateway create-user --file `. - BCrypt (cost 12) is used by default; no external `htpasswd` binary - required. - - Required when `enable = true`. - ''; - }; - realm = lib.mkOption { type = lib.types.strMatching "[^\"$]*"; default = "hyperhive"; @@ -254,14 +238,6 @@ in or leave `localHostsEntry` at its default of false. ''; } - { - assertion = !cfg.auth.enable || cfg.auth.htpasswdFile != null; - message = '' - services.hyperhive.gateway.auth.enable = true requires - services.hyperhive.gateway.auth.htpasswdFile to be set. - Create an htpasswd file with: hivectl gateway create-user --file /path/to/file - ''; - } ]; # Ensure bind-mount sources exist at host boot before the gateway @@ -280,6 +256,11 @@ in "d /var/lib/hyperhive 0755 root root - -" "d /var/lib/hyperhive/gateway 0755 root root - -" "f /var/lib/hyperhive/gateway/agents.conf 0644 root root - # Generated by hive-c0re — do not edit.\n" + # Pre-create the htpasswd file so nginx can open it even before any + # users have been added. An empty file causes all auth checks to + # return 401 (no valid credentials), which is the correct no-users + # behaviour. `f` = create-if-absent, never overwrite. + "f /var/lib/hyperhive/gateway/gateway.htpasswd 0644 root root - -" ]; containers.hive-gateway = { @@ -310,16 +291,6 @@ in hostPath = "/var/lib/hyperhive/gateway"; isReadOnly = true; }; - # When auth is enabled, bind-mount the parent directory of the - # htpasswd file read-only into the container at /run/gateway-auth/. - # nginx's `auth_basic_user_file` points at the file inside that dir. - # Using the parent directory (not the file itself) because nspawn - # bind-mounts need a pre-existing destination — binding a directory - # is always safe; nginx picks the file up by name inside. - bindMounts."/run/gateway-auth" = lib.mkIf (cfg.auth.enable && cfg.auth.htpasswdFile != null) { - hostPath = builtins.dirOf cfg.auth.htpasswdFile; - isReadOnly = true; - }; config = { pkgs, ... }: let @@ -583,9 +554,12 @@ in extraConfig = '' proxy_buffering off; proxy_read_timeout 1d; - ${lib.optionalString (cfg.auth.enable && cfg.auth.htpasswdFile != null) '' + ${lib.optionalString cfg.auth.enable '' auth_basic "${cfg.auth.realm}"; - auth_basic_user_file /run/gateway-auth/${builtins.baseNameOf cfg.auth.htpasswdFile}; + # htpasswd file lives in the gateway state dir, + # already bind-mounted read-only at /run/hive-state/. + # Host path: /var/lib/hyperhive/gateway/gateway.htpasswd + auth_basic_user_file /run/hive-state/gateway.htpasswd; # Serve a custom page when credentials are missing or wrong. # `=401` forces the final status to remain 401 so browsers # still present the login dialog on first visit; users who @@ -599,17 +573,12 @@ in ''; }; } - // lib.optionalAttrs (cfg.auth.enable && cfg.auth.htpasswdFile != null) { + // lib.optionalAttrs cfg.auth.enable { # Internal-only target for the 401 error_page above. # `internal` prevents direct client access; `alias` serves # the pre-built HTML from the Nix store. - # The page is built here (not in the top-level `let`) so - # that `cfg.auth.htpasswdFile` is in scope and known - # non-null — the operator sees the actual configured path - # in the `hivectl` example command, not a hardcoded guess. "= /__hive_auth_unauthorized" = let - htpasswdPath = cfg.auth.htpasswdFile; page = pkgs.writeText "hive-gateway-unauthorized.html" '' @@ -630,7 +599,6 @@ in

This hive is protected by HTTP Basic auth. Valid credentials are required.

Operator: add a user with hivectl gateway create-user:

hivectl gateway create-user \
-                          --file ${htpasswdPath} \
                           <username> --password-stdin

Then reload your browser and enter the credentials when prompted.

From 4bd0228de0ec43cf2e6c1e43edb9b14d7eda8047 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 1 Jun 2026 23:44:34 +0200 Subject: [PATCH 4/4] docs(gateway): update Basic auth section for fixed htpasswd path Remove stale htpasswdFile option from nix example (option no longer exists). Update hivectl command examples to drop --file flag (now optional with standard default). Add description of the fixed path and how it's exposed inside the container. --- docs/gateway.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/gateway.md b/docs/gateway.md index ebb0cf2f..1c59997a 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -351,36 +351,38 @@ required. ```nix services.hyperhive.gateway.auth = { enable = true; - htpasswdFile = "/etc/hyperhive/gateway.htpasswd"; # realm = "hyperhive"; # optional, default shown }; ``` -Manage users with `hivectl gateway`: +The credential store lives at the fixed path +`/var/lib/hyperhive/gateway/gateway.htpasswd` on the host. A tmpfiles +rule pre-creates the file on first boot; no manual path configuration +is required. The file is exposed inside the gateway container at +`/run/hive-state/gateway.htpasswd` via the existing gateway state +bind-mount. + +Manage users with `hivectl gateway` (defaults to the standard path — no +`--file` flag needed for the common case): ```sh # Add or update a user (prompted for password): -hivectl gateway create-user --file /etc/hyperhive/gateway.htpasswd alice --password-stdin +hivectl gateway create-user alice --password-stdin # Add with inline password (visible in shell history — avoid for sensitive creds): -hivectl gateway create-user --file /etc/hyperhive/gateway.htpasswd bob --password hunter2 +hivectl gateway create-user bob --password hunter2 # Remove a user: -hivectl gateway delete-user --file /etc/hyperhive/gateway.htpasswd bob +hivectl gateway delete-user bob # List current usernames: -hivectl gateway list-users --file /etc/hyperhive/gateway.htpasswd +hivectl gateway list-users ``` `hivectl gateway create-user` hashes passwords with BCrypt (cost 12) and writes `$2y$`-prefixed hashes that nginx accepts natively. No external -`htpasswd` binary is required. The file is created on first add if absent; -its parent directory must already exist. - -The file must be readable by the `nginx` user inside the container -(`chmod 0644`). The module bind-mounts the file's parent directory -read-only into the container at `/run/gateway-auth/`; nginx reads -`/run/gateway-auth/`. +`htpasswd` binary is required. Pass `--file ` to target a +non-default file. **What is not gated:** per-agent UI routes emitted into `agents.conf` (served under `/agent//`) inherit no auth from `/` — nginx