feat(gateway): htpasswd Basic auth — close #1010
Replaces the earlier PAM+binary approach with nginx's built-in
`auth_basic` module. No new binary, no new systemd service, no PAM.
New option `services.hyperhive.gateway.auth`:
- `enable` — off by default
- `htpasswdFile` — host path to an htpasswd file (required when enable)
- `realm` — WWW-Authenticate realm string (default "hyperhive");
restricted to `strMatching "[^\"$]*"` to prevent nginx config injection
When enabled:
- the parent directory of `htpasswdFile` is bind-mounted read-only
into the gateway container at `/run/gateway-auth/`
- the `"/"` proxy location gets `auth_basic` + `auth_basic_user_file`
Create credentials: `htpasswd -Bc /path/to/file alice` (BCrypt).
See `docs/gateway.md` ("HTTP Basic auth") for the full setup guide.
This commit is contained in:
parent
d4409b27a3
commit
25d2951d1e
7 changed files with 128 additions and 532 deletions
|
|
@ -200,59 +200,43 @@ in
|
|||
|
||||
auth = {
|
||||
enable = lib.mkEnableOption ''
|
||||
HTTP basic auth on the gateway using host PAM. When enabled, every
|
||||
request to the gateway's main vhost requires a valid username and
|
||||
password from the host's user database. The user must also be a
|
||||
member of the `services.hyperhive.gateway.auth.group` host group
|
||||
(default: `hyperhive-operator`). A small `hive-gateway-auth`
|
||||
systemd service runs on the host, listens on loopback at
|
||||
`services.hyperhive.gateway.auth.port`, and performs the PAM
|
||||
authentication. nginx inside the gateway container calls it via
|
||||
`auth_request` (the container shares the host netns, so loopback
|
||||
is reachable directly). Off by default — local / single-operator
|
||||
setups may not need authentication.
|
||||
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.
|
||||
|
||||
Create the file with: `htpasswd -Bc /path/to/gateway.htpasswd <username>`
|
||||
(BCrypt recommended; `-c` creates a new file). Subsequent users:
|
||||
`htpasswd -B /path/to/gateway.htpasswd <user2>`.
|
||||
'';
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 7002;
|
||||
htpasswdFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
example = "/etc/hyperhive/gateway.htpasswd";
|
||||
description = ''
|
||||
TCP port for the `hive-gateway-auth` service on the host's
|
||||
loopback interface. nginx's `auth_request` sub-request is
|
||||
sent here. Change when 7002 is already in use.
|
||||
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).
|
||||
|
||||
Create with: `htpasswd -Bc <file> <username>`. BCrypt (`-B`) is
|
||||
strongly preferred over the legacy MD5/SHA1 algorithms.
|
||||
|
||||
Required when `enable = true`.
|
||||
'';
|
||||
};
|
||||
|
||||
realm = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
type = lib.types.strMatching "[^\"$]*";
|
||||
default = "hyperhive";
|
||||
example = "my-hive";
|
||||
description = ''
|
||||
HTTP Basic auth `realm` value sent in the `WWW-Authenticate`
|
||||
header when credentials are absent or rejected.
|
||||
'';
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "hyperhive-operator";
|
||||
example = "admins";
|
||||
description = ''
|
||||
Host Unix group every authenticated user must belong to.
|
||||
Create the group and add operator accounts before enabling
|
||||
auth. When using the default value, the group is
|
||||
automatically defined on the host by this module.
|
||||
'';
|
||||
};
|
||||
|
||||
pamService = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "hive-gateway";
|
||||
description = ''
|
||||
PAM service name. A matching `/etc/pam.d/hive-gateway` file
|
||||
is defined by this module when using the default value. Set to
|
||||
an existing service (e.g. `"login"`) to reuse a custom PAM
|
||||
stack instead of the generated one.
|
||||
header when credentials are absent or rejected. Must not
|
||||
contain `"` or `$` (nginx string metacharacters).
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
@ -269,53 +253,16 @@ in
|
|||
or leave `localHostsEntry` at its default of false.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = !cfg.auth.enable || cfg.auth ? htpasswdFile;
|
||||
message = ''
|
||||
services.hyperhive.gateway.auth.enable = true requires
|
||||
services.hyperhive.gateway.auth.htpasswdFile to be set.
|
||||
Create an htpasswd file with: htpasswd -Bc /path/to/file <username>
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
# HOST-SIDE: PAM auth service for the gateway.
|
||||
# Defined here so it co-locates with the nginx wiring below.
|
||||
# All three blocks are gated on `cfg.auth.enable`.
|
||||
|
||||
# Declare the hyperhive-operator group on the host so operators
|
||||
# can `usermod -aG hyperhive-operator <user>` out-of-the-box.
|
||||
# Only created when the default group name is in use; custom
|
||||
# groups are assumed to be managed externally.
|
||||
users.groups = lib.mkIf (cfg.auth.enable && cfg.auth.group == "hyperhive-operator") {
|
||||
hyperhive-operator = { };
|
||||
};
|
||||
|
||||
# PAM service used by `hive-gateway-auth`. Only emits the generated
|
||||
# `/etc/pam.d/hive-gateway` when the operator uses the default
|
||||
# service name, to avoid clobbering a custom PAM config they may
|
||||
# have defined elsewhere.
|
||||
security.pam.services.hive-gateway = lib.mkIf (cfg.auth.enable && cfg.auth.pamService == "hive-gateway") {
|
||||
text = ''
|
||||
# hive-gateway: authenticate via host Unix passwords, then check
|
||||
# group membership in ${cfg.auth.group}.
|
||||
auth required pam_unix.so
|
||||
auth required pam_succeed_if.so user ingroup ${cfg.auth.group}
|
||||
account required pam_unix.so
|
||||
'';
|
||||
};
|
||||
|
||||
# `hive-gateway-auth` systemd service. Runs as root so it can
|
||||
# call pam_unix.so against /etc/shadow (root-only readable).
|
||||
# Bound to 127.0.0.1 — the gateway container shares the host
|
||||
# netns, so it's reachable from nginx without any port-forward.
|
||||
systemd.services.hive-gateway-auth = lib.mkIf cfg.auth.enable {
|
||||
description = "hive-gateway HTTP basic auth validator";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = ''
|
||||
${config.services.hyperhive.c0re.package}/bin/hive-gateway-auth \
|
||||
--listen 127.0.0.1:${toString cfg.auth.port} \
|
||||
--pam-service ${lib.escapeShellArg cfg.auth.pamService} \
|
||||
--group ${lib.escapeShellArg cfg.auth.group}
|
||||
'';
|
||||
Restart = "on-failure";
|
||||
RestartSec = 2;
|
||||
};
|
||||
};
|
||||
|
||||
# Ensure bind-mount sources exist at host boot before the gateway
|
||||
# container's first start. nspawn would auto-create missing dirs
|
||||
|
|
@ -363,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 {
|
||||
hostPath = builtins.dirOf cfg.auth.htpasswdFile;
|
||||
isReadOnly = true;
|
||||
};
|
||||
config =
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
|
|
@ -616,11 +573,10 @@ in
|
|||
# headers stay set so SSE (`/dashboard/stream`,
|
||||
# `/events/stream`) + websocket (`/screen/ws`)
|
||||
# endpoints keep working transparently.
|
||||
# When auth is enabled, `auth_request` sub-requests
|
||||
# `/__hive_gateway_auth` before proxying. The 401
|
||||
# named-location handler (in vhost `extraConfig`)
|
||||
# adds the `WWW-Authenticate` header so browsers
|
||||
# show a login prompt.
|
||||
# When auth is enabled, nginx's built-in `auth_basic`
|
||||
# validates credentials against the htpasswd file
|
||||
# bind-mounted at `/run/gateway-auth/`. No extra
|
||||
# service or host-side daemon required.
|
||||
"/" = {
|
||||
proxyPass = "http://${cfg.upstreamHost}:${toString cfg.upstreamPort}";
|
||||
proxyWebsockets = true;
|
||||
|
|
@ -628,27 +584,11 @@ in
|
|||
proxy_buffering off;
|
||||
proxy_read_timeout 1d;
|
||||
${lib.optionalString cfg.auth.enable ''
|
||||
auth_request /__hive_gateway_auth;
|
||||
error_page 401 = @hive_auth_required;
|
||||
auth_basic "${cfg.auth.realm}";
|
||||
auth_basic_user_file /run/gateway-auth/${builtins.baseNameOf cfg.auth.htpasswdFile};
|
||||
''}
|
||||
'';
|
||||
};
|
||||
}
|
||||
# Internal auth sub-request location. Forwards the
|
||||
# `Authorization` header to `hive-gateway-auth` on
|
||||
# the host loopback; body is stripped (auth is
|
||||
# header-only). nginx reuses this location for every
|
||||
# `auth_request /__hive_gateway_auth;` directive.
|
||||
// lib.optionalAttrs cfg.auth.enable {
|
||||
"= /__hive_gateway_auth" = {
|
||||
extraConfig = ''
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:${toString cfg.auth.port}/;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
'';
|
||||
};
|
||||
};
|
||||
# Per-agent location blocks, generated at runtime by
|
||||
# hive-c0re and written to /var/lib/hyperhive/gateway/agents.conf
|
||||
|
|
@ -661,18 +601,6 @@ in
|
|||
# the `/agent/` catch-all above.
|
||||
extraConfig = ''
|
||||
include /run/hive-state/agents.conf;
|
||||
${lib.optionalString cfg.auth.enable ''
|
||||
# Named location for 401 responses from `auth_request`.
|
||||
# nginx does not propagate upstream `WWW-Authenticate`
|
||||
# headers automatically on auth failure, so we emit
|
||||
# it here. `always` ensures the header is added even
|
||||
# when nginx would otherwise suppress it on error
|
||||
# responses.
|
||||
location @hive_auth_required {
|
||||
add_header WWW-Authenticate 'Basic realm="${cfg.auth.realm}"' always;
|
||||
return 401;
|
||||
}
|
||||
''}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue