Compare commits

..
3 changed files with 70 additions and 102 deletions

View file

@ -351,38 +351,36 @@ 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
}; };
``` ```
The credential store lives at the fixed path Manage users with `hivectl gateway`:
`/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 alice --password-stdin hivectl gateway create-user --file /etc/hyperhive/gateway.htpasswd 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 bob --password hunter2 hivectl gateway create-user --file /etc/hyperhive/gateway.htpasswd bob --password hunter2
# Remove a user: # Remove a user:
hivectl gateway delete-user bob hivectl gateway delete-user --file /etc/hyperhive/gateway.htpasswd bob
# List current usernames: # List current usernames:
hivectl gateway list-users hivectl gateway list-users --file /etc/hyperhive/gateway.htpasswd
``` ```
`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. Pass `--file <path>` to target a `htpasswd` binary is required. The file is created on first add if absent;
non-default file. 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/<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,20 +147,19 @@ 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 the /// Add a new user or update the password of an existing user in an
/// gateway htpasswd file. The password is hashed with BCrypt (cost 12). /// 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
@ -172,26 +171,20 @@ 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
/// 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 /// Remove a user from an htpasswd file. Exits with an error when the
/// when the user is not found so callers can detect the no-op case. /// user is not found so callers can detect the no-op case.
DeleteUser { DeleteUser {
/// Path to the htpasswd file.
#[arg(long, short = 'f')]
file: PathBuf,
/// 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 the gateway htpasswd file, one per line. /// List all usernames in an htpasswd file, one per line.
ListUsers { ListUsers {
/// Path to the htpasswd file. Defaults to the standard gateway /// Path to the htpasswd file.
/// credential store. #[arg(long, short = 'f')]
#[arg(long, short = 'f', default_value = DEFAULT_HTPASSWD_FILE)]
file: PathBuf, file: PathBuf,
}, },
} }

View file

@ -202,18 +202,34 @@ 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. nginx's built-in `auth_basic` valid username and password from the htpasswd file at
module validates credentials against `services.hyperhive.gateway.auth.htpasswdFile`. nginx's built-in
`/var/lib/hyperhive/gateway/gateway.htpasswd` on the host `auth_basic` module handles credential validation no extra
(exposed as `/run/hive-state/gateway.htpasswd` inside the service or host-side daemon required. Off by default.
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";
@ -238,6 +254,14 @@ 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
@ -256,11 +280,6 @@ 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 = {
@ -291,6 +310,16 @@ 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
@ -554,64 +583,12 @@ in
extraConfig = '' extraConfig = ''
proxy_buffering off; proxy_buffering off;
proxy_read_timeout 1d; proxy_read_timeout 1d;
${lib.optionalString cfg.auth.enable '' ${lib.optionalString (cfg.auth.enable && cfg.auth.htpasswdFile != null) ''
auth_basic "${cfg.auth.realm}"; auth_basic "${cfg.auth.realm}";
# htpasswd file lives in the gateway state dir, auth_basic_user_file /run/gateway-auth/${builtins.baseNameOf cfg.auth.htpasswdFile};
# 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