diff --git a/docs/gateway.md b/docs/gateway.md index 1c59997a..ebb0cf2f 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -351,38 +351,36 @@ required. ```nix services.hyperhive.gateway.auth = { enable = true; + htpasswdFile = "/etc/hyperhive/gateway.htpasswd"; # realm = "hyperhive"; # optional, default shown }; ``` -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): +Manage users with `hivectl gateway`: ```sh # 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): -hivectl gateway create-user bob --password hunter2 +hivectl gateway create-user --file /etc/hyperhive/gateway.htpasswd bob --password hunter2 # Remove a user: -hivectl gateway delete-user bob +hivectl gateway delete-user --file /etc/hyperhive/gateway.htpasswd bob # 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 writes `$2y$`-prefixed hashes that nginx accepts natively. No external -`htpasswd` binary is required. Pass `--file ` to target a -non-default file. +`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/`. **What is not gated:** per-agent UI routes emitted into `agents.conf` (served under `/agent//`) inherit no auth from `/` — nginx diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index e2fd156d..f5734b8b 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -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)] enum GatewayCmd { - /// 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). + /// Add a new user or update the password of an existing user in an + /// 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 @@ -172,26 +171,20 @@ enum GatewayCmd { /// stripped). Mutually exclusive with `--password`. #[arg(long)] 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 - /// when the user is not found so callers can detect the no-op case. + /// 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')] + file: PathBuf, /// 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 the gateway htpasswd file, one per line. + /// List all usernames in an htpasswd file, one per line. ListUsers { - /// Path to the htpasswd file. Defaults to the standard gateway - /// credential store. - #[arg(long, short = 'f', default_value = DEFAULT_HTPASSWD_FILE)] + /// Path to the htpasswd file. + #[arg(long, short = 'f')] file: PathBuf, }, } diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 851e8538..9925aeb9 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -202,18 +202,34 @@ 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. 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. + 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. 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"; @@ -238,6 +254,14 @@ 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 @@ -256,11 +280,6 @@ 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 = { @@ -291,6 +310,16 @@ 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 @@ -554,64 +583,12 @@ in extraConfig = '' proxy_buffering off; proxy_read_timeout 1d; - ${lib.optionalString cfg.auth.enable '' + ${lib.optionalString (cfg.auth.enable && cfg.auth.htpasswdFile != null) '' auth_basic "${cfg.auth.realm}"; - # 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; + auth_basic_user_file /run/gateway-auth/${builtins.baseNameOf cfg.auth.htpasswdFile}; ''} ''; }; - } - // 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" '' - - - - - 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 \
-                          <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