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.
This commit is contained in:
atlas 2026-06-01 23:42:03 +02:00 committed by mara
commit 167b4fa1f3
2 changed files with 39 additions and 64 deletions

View file

@ -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)] #[derive(Subcommand)]
enum GatewayCmd { enum GatewayCmd {
/// Add a new user or update the password of an existing user in an /// Add a new user or update the password of an existing user in the
/// htpasswd file. The password is hashed with BCrypt (cost 12). /// gateway htpasswd file. The password is hashed with BCrypt (cost 12).
/// ///
/// Pass `--password-stdin` when scripting or when you don't want the /// Pass `--password-stdin` when scripting or when you don't want the
/// password visible in shell history. The file is created if it does /// password visible in shell history. The file is created if it does
/// not exist; its parent directory must already exist. /// not exist; its parent directory must already exist.
CreateUser { 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 to add or update.
username: String, username: String,
/// Set the password inline. WARNING: visible in shell history and /// Set the password inline. WARNING: visible in shell history and
@ -171,20 +172,26 @@ enum GatewayCmd {
/// stripped). Mutually exclusive with `--password`. /// stripped). Mutually exclusive with `--password`.
#[arg(long)] #[arg(long)]
password_stdin: bool, password_stdin: bool,
}, /// Path to the htpasswd file. Defaults to the standard gateway
/// Remove a user from an htpasswd file. Exits with an error when the /// credential store at `/var/lib/hyperhive/gateway/gateway.htpasswd`.
/// user is not found so callers can detect the no-op case. #[arg(long, short = 'f', default_value = DEFAULT_HTPASSWD_FILE)]
DeleteUser {
/// Path to the htpasswd file.
#[arg(long, short = 'f')]
file: PathBuf, 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 to remove.
username: String, 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 { ListUsers {
/// Path to the htpasswd file. /// Path to the htpasswd file. Defaults to the standard gateway
#[arg(long, short = 'f')] /// credential store.
#[arg(long, short = 'f', default_value = DEFAULT_HTPASSWD_FILE)]
file: PathBuf, file: PathBuf,
}, },
} }

View file

@ -202,34 +202,18 @@ in
enable = lib.mkEnableOption '' enable = lib.mkEnableOption ''
HTTP basic auth on the gateway using an htpasswd file. When HTTP basic auth on the gateway using an htpasswd file. When
enabled, every request to the gateway's main vhost requires a enabled, every request to the gateway's main vhost requires a
valid username and password from the htpasswd file at valid username and password. nginx's built-in `auth_basic`
`services.hyperhive.gateway.auth.htpasswdFile`. nginx's built-in module validates credentials against
`auth_basic` module handles credential validation no extra `/var/lib/hyperhive/gateway/gateway.htpasswd` on the host
service or host-side daemon required. Off by default. (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`, Manage users with `hivectl gateway create-user`, `delete-user`,
and `list-users` see `hivectl gateway --help` for usage. 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 <path> <username>`.
BCrypt (cost 12) is used by default; no external `htpasswd` binary
required.
Required when `enable = true`.
'';
};
realm = lib.mkOption { realm = lib.mkOption {
type = lib.types.strMatching "[^\"$]*"; type = lib.types.strMatching "[^\"$]*";
default = "hyperhive"; default = "hyperhive";
@ -254,14 +238,6 @@ in
or leave `localHostsEntry` at its default of false. 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 <username>
'';
}
]; ];
# Ensure bind-mount sources exist at host boot before the gateway # 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 0755 root root - -"
"d /var/lib/hyperhive/gateway 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" "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 = { containers.hive-gateway = {
@ -310,16 +291,6 @@ in
hostPath = "/var/lib/hyperhive/gateway"; hostPath = "/var/lib/hyperhive/gateway";
isReadOnly = true; 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 = config =
{ pkgs, ... }: { pkgs, ... }:
let let
@ -583,9 +554,12 @@ in
extraConfig = '' extraConfig = ''
proxy_buffering off; proxy_buffering off;
proxy_read_timeout 1d; 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 "${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. # Serve a custom page when credentials are missing or wrong.
# `=401` forces the final status to remain 401 so browsers # `=401` forces the final status to remain 401 so browsers
# still present the login dialog on first visit; users who # 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-only target for the 401 error_page above.
# `internal` prevents direct client access; `alias` serves # `internal` prevents direct client access; `alias` serves
# the pre-built HTML from the Nix store. # 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" = "= /__hive_auth_unauthorized" =
let let
htpasswdPath = cfg.auth.htpasswdFile;
page = pkgs.writeText "hive-gateway-unauthorized.html" '' page = pkgs.writeText "hive-gateway-unauthorized.html" ''
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -630,7 +599,6 @@ in
<p>This hive is protected by HTTP Basic auth. Valid credentials are required.</p> <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> <p class="hint">Operator: add a user with <code>hivectl gateway create-user</code>:</p>
<pre>hivectl gateway create-user \ <pre>hivectl gateway create-user \
--file ${htpasswdPath} \
&lt;username&gt; --password-stdin</pre> &lt;username&gt; --password-stdin</pre>
<p class="hint">Then reload your browser and enter the credentials when prompted.</p> <p class="hint">Then reload your browser and enter the credentials when prompted.</p>
</body> </body>