feat(gateway): PAM auth against host — close #1010
Adds opt-in HTTP Basic auth to the hive-gateway backed by the host PAM stack + group membership check. New binary `hive-gateway-auth` (hive-c0re workspace): - Axum HTTP service on 127.0.0.1:7002 (host loopback) - Decodes Basic credentials, authenticates via pam_unix.so - Checks membership in `hyperhive-operator` group (or custom) - Returns 200 / 401 / 403; nginx `auth_request` consumes these New options under `services.hyperhive.gateway.auth`: - `enable` — off by default - `port` — auth service port (default 7002) - `realm` — WWW-Authenticate realm string (default "hyperhive") - `group` — required host group (default "hyperhive-operator") - `pamService` — PAM service name (default "hive-gateway") Host-side NixOS wiring: - `users.groups.hyperhive-operator` declared when default group used - `/etc/pam.d/hive-gateway` emitted via `security.pam.services` - `systemd.services.hive-gateway-auth` runs the auth binary as root (needs /etc/shadow access for pam_unix.so) Gateway container nginx wiring: - `location = /__hive_gateway_auth` — internal proxy to auth service - `auth_request /__hive_gateway_auth` on the `"/"` proxy location - `@hive_auth_required` named location adds WWW-Authenticate: Basic header on 401 so browsers display a login prompt Workspace deps: pam = "0.8"; flake.nix: linux-pam added to nativeBuildInputs so pkg-config can find libpam at build time.
This commit is contained in:
parent
4f684dc7c3
commit
d4409b27a3
6 changed files with 550 additions and 37 deletions
|
|
@ -198,6 +198,65 @@ 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.
|
||||
'';
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 7002;
|
||||
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.
|
||||
'';
|
||||
};
|
||||
|
||||
realm = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
@ -212,6 +271,52 @@ in
|
|||
}
|
||||
];
|
||||
|
||||
# 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
|
||||
# tmpfiles rules make the intent explicit
|
||||
|
|
@ -511,12 +616,37 @@ 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.
|
||||
"/" = {
|
||||
proxyPass = "http://${cfg.upstreamHost}:${toString cfg.upstreamPort}";
|
||||
proxyWebsockets = true;
|
||||
extraConfig = ''
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 1d;
|
||||
${lib.optionalString cfg.auth.enable ''
|
||||
auth_request /__hive_gateway_auth;
|
||||
error_page 401 = @hive_auth_required;
|
||||
''}
|
||||
'';
|
||||
};
|
||||
}
|
||||
# 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;
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
@ -531,6 +661,18 @@ 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