Compare commits

..
Author SHA1 Message Date
atlas
4bd0228de0 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.
2026-06-02 00:26:10 +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
3 changed files with 102 additions and 70 deletions

View file

@ -351,36 +351,38 @@ required.
```nix ```nix
services.hyperhive.gateway.auth = { services.hyperhive.gateway.auth = {
enable = true; enable = true;
htpasswdFile = "/etc/hyperhive/gateway.htpasswd";
# realm = "hyperhive"; # optional, default shown # 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 ```sh
# Add or update a user (prompted for password): # 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): # 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: # Remove a user:
hivectl gateway delete-user --file /etc/hyperhive/gateway.htpasswd bob hivectl gateway delete-user bob
# List current usernames: # 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 `hivectl gateway create-user` hashes passwords with BCrypt (cost 12) and
writes `$2y$`-prefixed hashes that nginx accepts natively. No external writes `$2y$`-prefixed hashes that nginx accepts natively. No external
`htpasswd` binary is required. The file is created on first add if absent; `htpasswd` binary is required. Pass `--file <path>` to target a
its parent directory must already exist. non-default file.
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/<filename>`.
**What is not gated:** per-agent UI routes emitted into `agents.conf` **What is not gated:** per-agent UI routes emitted into `agents.conf`
(served under `/agent/<name>/`) inherit no auth from `/` — nginx (served under `/agent/<name>/`) inherit no auth from `/` — nginx

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,12 +554,64 @@ 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.
# `=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 {
# 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" =
let
page = pkgs.writeText "hive-gateway-unauthorized.html" ''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>unauthorized hyperhive</title>
<style>
body { background: #1e1e2e; color: #cdd6f4; font: 14px/1.5 -apple-system, system-ui, sans-serif; margin: 0; padding: 4rem 1rem; text-align: center; }
h1 { color: #f38ba8; font-size: 1.5rem; margin: 0 0 0.5rem; }
p { max-width: 36rem; margin: 0.5rem auto; color: #a6adc8; }
code { background: #313244; color: #f5c2e7; padding: 0.1rem 0.35rem; border-radius: 0.2rem; font-size: 0.92em; }
pre { background: #181825; color: #cdd6f4; text-align: left; display: inline-block; padding: 0.75rem 1.25rem; border-radius: 0.4rem; margin: 0.75rem 0; font-size: 0.88em; line-height: 1.6; }
.hint { color: #a6adc8; font-size: 0.9em; margin-top: 1.5rem; }
</style>
</head>
<body>
<h1> unauthorized</h1>
<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>
</body>
</html>
'';
in
{
extraConfig = ''
internal;
alias ${page};
default_type text/html;
'';
};
}; };
# Per-agent location blocks, generated at runtime by # Per-agent location blocks, generated at runtime by
# hive-c0re and written to /var/lib/hyperhive/gateway/agents.conf # hive-c0re and written to /var/lib/hyperhive/gateway/agents.conf