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
219
Cargo.lock
generated
219
Cargo.lock
generated
|
|
@ -120,7 +120,7 @@ dependencies = [
|
||||||
"proc-macro-error2",
|
"proc-macro-error2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -199,7 +199,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -210,7 +210,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -303,26 +303,6 @@ version = "1.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bindgen"
|
|
||||||
version = "0.69.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cexpr",
|
|
||||||
"clang-sys",
|
|
||||||
"itertools 0.12.1",
|
|
||||||
"lazy_static",
|
|
||||||
"lazycell",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"regex",
|
|
||||||
"rustc-hash 1.1.0",
|
|
||||||
"shlex 1.3.0",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
|
|
@ -404,16 +384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex 2.0.1",
|
"shlex",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cexpr"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
|
||||||
dependencies = [
|
|
||||||
"nom",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -477,16 +448,6 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clang-sys"
|
|
||||||
version = "1.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
|
||||||
dependencies = [
|
|
||||||
"glob",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.1"
|
version = "4.6.1"
|
||||||
|
|
@ -518,7 +479,7 @@ dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -655,7 +616,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -678,7 +639,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -689,7 +650,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -793,7 +754,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1024,7 +985,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1106,12 +1067,6 @@ dependencies = [
|
||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "glob"
|
|
||||||
version = "0.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-timers"
|
name = "gloo-timers"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -1247,9 +1202,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
"hive-sh4re",
|
"hive-sh4re",
|
||||||
"libc",
|
|
||||||
"listenfd",
|
"listenfd",
|
||||||
"pam",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1686,15 +1639,6 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
@ -1772,12 +1716,6 @@ version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazycell"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leb128fmt"
|
name = "leb128fmt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -1879,7 +1817,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1917,7 +1855,7 @@ dependencies = [
|
||||||
"proc-macro-error2",
|
"proc-macro-error2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2098,12 +2036,6 @@ dependencies = [
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimal-lexical"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -2131,16 +2063,6 @@ version = "1.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "7.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"minimal-lexical",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
|
|
@ -2213,40 +2135,6 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pam"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"memchr",
|
|
||||||
"pam-macros",
|
|
||||||
"pam-sys",
|
|
||||||
"users",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pam-macros"
|
|
||||||
version = "0.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pam-sys"
|
|
||||||
version = "1.0.0-alpha5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b"
|
|
||||||
dependencies = [
|
|
||||||
"bindgen",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -2406,7 +2294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2468,7 +2356,7 @@ dependencies = [
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2500,7 +2388,7 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn-proto",
|
"quinn-proto",
|
||||||
"quinn-udp",
|
"quinn-udp",
|
||||||
"rustc-hash 2.1.2",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2",
|
"socket2",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
|
@ -2520,7 +2408,7 @@ dependencies = [
|
||||||
"lru-slab",
|
"lru-slab",
|
||||||
"rand 0.9.4",
|
"rand 0.9.4",
|
||||||
"ring",
|
"ring",
|
||||||
"rustc-hash 2.1.2",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"slab",
|
"slab",
|
||||||
|
|
@ -2674,7 +2562,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2795,7 +2683,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2973,7 +2861,7 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"ruma-identifiers-validation",
|
"ruma-identifiers-validation",
|
||||||
"serde",
|
"serde",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3007,12 +2895,6 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc-hash"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
|
|
@ -3111,7 +2993,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_derive_internals",
|
"serde_derive_internals",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3163,7 +3045,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3174,7 +3056,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3266,12 +3148,6 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "shlex"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
|
|
@ -3384,17 +3260,6 @@ version = "2.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "1.0.109"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
|
|
@ -3423,7 +3288,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3476,7 +3341,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3487,7 +3352,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3567,7 +3432,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3757,7 +3622,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3904,16 +3769,6 @@ version = "2.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "users"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf-8"
|
name = "utf-8"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
|
|
@ -4066,7 +3921,7 @@ dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4204,7 +4059,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4215,7 +4070,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4452,7 +4307,7 @@ dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
"wasm-metadata",
|
"wasm-metadata",
|
||||||
"wit-bindgen-core",
|
"wit-bindgen-core",
|
||||||
"wit-component",
|
"wit-component",
|
||||||
|
|
@ -4468,7 +4323,7 @@ dependencies = [
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
"wit-bindgen-core",
|
"wit-bindgen-core",
|
||||||
"wit-bindgen-rust",
|
"wit-bindgen-rust",
|
||||||
]
|
]
|
||||||
|
|
@ -4553,7 +4408,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4574,7 +4429,7 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4594,7 +4449,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4615,7 +4470,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4648,7 +4503,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ must_use_candidate = "allow"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
pam = "0.8"
|
|
||||||
axum = { version = "0.8", features = ["ws"] }
|
axum = { version = "0.8", features = ["ws"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
|
||||||
|
|
@ -339,3 +339,44 @@ the nginx config get custom error pages. Other gateway routes
|
||||||
(forge / matrix / fluffychat) get nginx defaults — extending the
|
(forge / matrix / fluffychat) get nginx defaults — extending the
|
||||||
custom-error pattern there is a separate follow-up.
|
custom-error pattern there is a separate follow-up.
|
||||||
|
|
||||||
|
## HTTP Basic auth
|
||||||
|
|
||||||
|
`services.hyperhive.gateway.auth.enable = true` gates every request to
|
||||||
|
the main vhost (`_`) behind HTTP Basic auth. nginx's built-in `auth_basic`
|
||||||
|
module validates credentials; no extra service or host-side daemon is
|
||||||
|
required.
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.hyperhive.gateway.auth = {
|
||||||
|
enable = true;
|
||||||
|
htpasswdFile = "/etc/hyperhive/gateway.htpasswd";
|
||||||
|
# realm = "hyperhive"; # optional, default shown
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the htpasswd file on the host:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Create new file with first user (BCrypt, recommended):
|
||||||
|
htpasswd -Bc /etc/hyperhive/gateway.htpasswd alice
|
||||||
|
|
||||||
|
# Add subsequent users:
|
||||||
|
htpasswd -B /etc/hyperhive/gateway.htpasswd bob
|
||||||
|
```
|
||||||
|
|
||||||
|
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`
|
||||||
|
(served under `/agent/<name>/`) inherit no auth from `/` — nginx
|
||||||
|
applies `auth_basic` per-location. Full per-agent coverage is a
|
||||||
|
follow-up.
|
||||||
|
|
||||||
|
**Realm:** the `WWW-Authenticate: Basic realm="..."` string browsers
|
||||||
|
display in the credential dialog. Defaults to `"hyperhive"`. Must not
|
||||||
|
contain `"` or `$`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,6 @@
|
||||||
pkgs.git
|
pkgs.git
|
||||||
pkgs.sqlite
|
pkgs.sqlite
|
||||||
pkgs.pkg-config
|
pkgs.pkg-config
|
||||||
pkgs.linux-pam # libpam.so.0 for hive-gateway-auth
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ base64.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
hive-sh4re.workspace = true
|
hive-sh4re.workspace = true
|
||||||
libc = "0.2"
|
|
||||||
pam.workspace = true
|
|
||||||
listenfd = "1"
|
listenfd = "1"
|
||||||
rusqlite.workspace = true
|
rusqlite.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
//! `hive-gateway-auth` — host-side HTTP basic auth validator for the hive gateway.
|
|
||||||
//!
|
|
||||||
//! Runs as a systemd service on the host. The gateway container's nginx
|
|
||||||
//! calls this via `auth_request` for every incoming request; this binary
|
|
||||||
//! validates the `Authorization: Basic` credentials against the host's PAM
|
|
||||||
//! stack and checks that the authenticated user is a member of the
|
|
||||||
//! `hyperhive-operator` group (configurable via `--group`).
|
|
||||||
//!
|
|
||||||
//! Listens on `127.0.0.1:PORT` (default 7002, host loopback only — the
|
|
||||||
//! gateway container shares the host netns, so it can reach this directly).
|
|
||||||
//!
|
|
||||||
//! Response codes nginx cares about:
|
|
||||||
//! - `200 OK` — auth passed; nginx proxies the request upstream.
|
|
||||||
//! - `401 Unauthorized` — missing/invalid credentials; nginx returns 401
|
|
||||||
//! with a `WWW-Authenticate: Basic realm="…"` header added by the nginx
|
|
||||||
//! config. The body from this service is discarded by nginx.
|
|
||||||
//! - `403 Forbidden` — valid credentials but not in the required group.
|
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::str::FromStr as _;
|
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
|
||||||
use axum::Router;
|
|
||||||
use axum::extract::State;
|
|
||||||
use axum::http::{HeaderMap, StatusCode};
|
|
||||||
use axum::routing::any;
|
|
||||||
use base64::Engine as _;
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(
|
|
||||||
name = "hive-gateway-auth",
|
|
||||||
about = "Host-side HTTP basic-auth validator for hive-gateway"
|
|
||||||
)]
|
|
||||||
struct Cli {
|
|
||||||
/// Address to listen on. Bind to 127.0.0.1 (loopback-only) so
|
|
||||||
/// only the gateway container (shared host netns) can reach it.
|
|
||||||
#[arg(long, default_value = "127.0.0.1:7002")]
|
|
||||||
listen: String,
|
|
||||||
/// PAM service name. A matching `/etc/pam.d/<service>` file must
|
|
||||||
/// exist on the host and include `pam_unix.so` for password auth.
|
|
||||||
#[arg(long, default_value = "hive-gateway")]
|
|
||||||
pam_service: String,
|
|
||||||
/// Host group that every authenticated user must belong to.
|
|
||||||
#[arg(long, default_value = "hyperhive-operator")]
|
|
||||||
group: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct AppState {
|
|
||||||
pam_service: String,
|
|
||||||
required_group: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_env_filter(
|
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
||||||
.unwrap_or_else(|_| "info".into()),
|
|
||||||
)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
|
||||||
let addr = SocketAddr::from_str(&cli.listen)
|
|
||||||
.with_context(|| format!("invalid --listen address: {}", cli.listen))?;
|
|
||||||
let state = AppState {
|
|
||||||
pam_service: cli.pam_service,
|
|
||||||
required_group: cli.group,
|
|
||||||
};
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/{*path}", any(handle_auth))
|
|
||||||
.route("/", any(handle_auth))
|
|
||||||
.with_state(state);
|
|
||||||
|
|
||||||
tracing::info!(addr = %addr, "hive-gateway-auth listening");
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("bind {addr}"))?;
|
|
||||||
axum::serve(listener, app).await.context("serve")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate the `Authorization: Basic` header. Returns the decoded
|
|
||||||
/// `(username, password)` pair, or `None` when the header is absent,
|
|
||||||
/// malformed, or not Basic-scheme.
|
|
||||||
fn parse_basic_auth(headers: &HeaderMap) -> Option<(String, String)> {
|
|
||||||
let value = headers.get("authorization")?.to_str().ok()?;
|
|
||||||
let encoded = value.strip_prefix("Basic ")?;
|
|
||||||
let decoded = base64::engine::general_purpose::STANDARD
|
|
||||||
.decode(encoded)
|
|
||||||
.ok()?;
|
|
||||||
let s = String::from_utf8(decoded).ok()?;
|
|
||||||
let (user, pass) = s.split_once(':')?;
|
|
||||||
Some((user.to_owned(), pass.to_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether `username` is a member of `group` by reading the host's
|
|
||||||
/// group database. Uses `getgrouplist(3)` (via `nix::unistd`) if available,
|
|
||||||
/// or falls back to scanning `/etc/group` entries directly.
|
|
||||||
///
|
|
||||||
/// Returns `true` when the user's primary GID matches OR when the user
|
|
||||||
/// appears in the supplementary member list of the target group.
|
|
||||||
fn user_in_group(username: &str, group_name: &str) -> bool {
|
|
||||||
use std::ffi::CString;
|
|
||||||
// SAFETY: all `libc` calls here follow the documented calling
|
|
||||||
// conventions for `getpwnam_r` / `getgrnam_r` / `getgrouplist`.
|
|
||||||
unsafe {
|
|
||||||
// Resolve the target group's GID.
|
|
||||||
let c_group = match CString::new(group_name) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
let mut grp_storage = std::mem::zeroed::<libc::group>();
|
|
||||||
let mut grp_ptr: *mut libc::group = std::ptr::null_mut();
|
|
||||||
let mut buf = vec![0i8; 4096];
|
|
||||||
let ret = libc::getgrnam_r(
|
|
||||||
c_group.as_ptr(),
|
|
||||||
&mut grp_storage,
|
|
||||||
buf.as_mut_ptr(),
|
|
||||||
buf.len(),
|
|
||||||
&mut grp_ptr,
|
|
||||||
);
|
|
||||||
if ret != 0 || grp_ptr.is_null() {
|
|
||||||
return false; // group not found
|
|
||||||
}
|
|
||||||
let target_gid = (*grp_ptr).gr_gid;
|
|
||||||
|
|
||||||
// Resolve the user's primary GID.
|
|
||||||
let c_user = match CString::new(username) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
let mut pwd_storage = std::mem::zeroed::<libc::passwd>();
|
|
||||||
let mut pwd_ptr: *mut libc::passwd = std::ptr::null_mut();
|
|
||||||
let mut pwd_buf = vec![0i8; 4096];
|
|
||||||
let ret = libc::getpwnam_r(
|
|
||||||
c_user.as_ptr(),
|
|
||||||
&mut pwd_storage,
|
|
||||||
pwd_buf.as_mut_ptr(),
|
|
||||||
pwd_buf.len(),
|
|
||||||
&mut pwd_ptr,
|
|
||||||
);
|
|
||||||
if ret != 0 || pwd_ptr.is_null() {
|
|
||||||
return false; // user not found
|
|
||||||
}
|
|
||||||
|
|
||||||
// Primary group match.
|
|
||||||
if (*pwd_ptr).pw_gid == target_gid {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan gr_mem list for the username.
|
|
||||||
let mut mem = (*grp_ptr).gr_mem;
|
|
||||||
if mem.is_null() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
while !(*mem).is_null() {
|
|
||||||
let member = std::ffi::CStr::from_ptr(*mem);
|
|
||||||
if member.to_bytes() == username.as_bytes() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
mem = mem.add(1);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Authenticate `username` / `password` against the host PAM stack using
|
|
||||||
/// the configured service. Returns `true` on success. The PAM call is
|
|
||||||
/// synchronous and blocking — runs on the tokio thread pool via
|
|
||||||
/// `spawn_blocking`.
|
|
||||||
fn pam_authenticate_sync(service: &str, username: &str, password: &str) -> bool {
|
|
||||||
let mut client = match pam::Client::with_password(service) {
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(service, error = %e, "pam: client init failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
client
|
|
||||||
.conversation_mut()
|
|
||||||
.set_credentials(username, password);
|
|
||||||
if let Err(e) = client.authenticate() {
|
|
||||||
tracing::debug!(service, username, error = %e, "pam: authenticate failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if let Err(e) = client.account_mgmt() {
|
|
||||||
tracing::debug!(service, username, error = %e, "pam: account_mgmt failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_auth(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
) -> StatusCode {
|
|
||||||
let Some((username, password)) = parse_basic_auth(&headers) else {
|
|
||||||
return StatusCode::UNAUTHORIZED;
|
|
||||||
};
|
|
||||||
|
|
||||||
// PAM blocks — run off the async executor.
|
|
||||||
let service = state.pam_service.clone();
|
|
||||||
let user_clone = username.clone();
|
|
||||||
let pass_clone = password.clone();
|
|
||||||
let authed =
|
|
||||||
tokio::task::spawn_blocking(move || pam_authenticate_sync(&service, &user_clone, &pass_clone))
|
|
||||||
.await
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if !authed {
|
|
||||||
tracing::info!(username, "auth: bad credentials");
|
|
||||||
return StatusCode::UNAUTHORIZED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user_in_group(&username, &state.required_group) {
|
|
||||||
tracing::info!(username, group = %state.required_group, "auth: user not in required group");
|
|
||||||
return StatusCode::FORBIDDEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!(username, "auth: ok");
|
|
||||||
StatusCode::OK
|
|
||||||
}
|
|
||||||
|
|
@ -200,59 +200,43 @@ in
|
||||||
|
|
||||||
auth = {
|
auth = {
|
||||||
enable = lib.mkEnableOption ''
|
enable = lib.mkEnableOption ''
|
||||||
HTTP basic auth on the gateway using host PAM. When enabled, every
|
HTTP basic auth on the gateway using an htpasswd file. When
|
||||||
request to the gateway's main vhost requires a valid username and
|
enabled, every request to the gateway's main vhost requires a
|
||||||
password from the host's user database. The user must also be a
|
valid username and password from the htpasswd file at
|
||||||
member of the `services.hyperhive.gateway.auth.group` host group
|
`services.hyperhive.gateway.auth.htpasswdFile`. nginx's built-in
|
||||||
(default: `hyperhive-operator`). A small `hive-gateway-auth`
|
`auth_basic` module handles credential validation — no extra
|
||||||
systemd service runs on the host, listens on loopback at
|
service or host-side daemon required. Off by default.
|
||||||
`services.hyperhive.gateway.auth.port`, and performs the PAM
|
|
||||||
authentication. nginx inside the gateway container calls it via
|
Create the file with: `htpasswd -Bc /path/to/gateway.htpasswd <username>`
|
||||||
`auth_request` (the container shares the host netns, so loopback
|
(BCrypt recommended; `-c` creates a new file). Subsequent users:
|
||||||
is reachable directly). Off by default — local / single-operator
|
`htpasswd -B /path/to/gateway.htpasswd <user2>`.
|
||||||
setups may not need authentication.
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
port = lib.mkOption {
|
htpasswdFile = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.path;
|
||||||
default = 7002;
|
example = "/etc/hyperhive/gateway.htpasswd";
|
||||||
description = ''
|
description = ''
|
||||||
TCP port for the `hive-gateway-auth` service on the host's
|
Path on the **host** to an htpasswd-format file whose
|
||||||
loopback interface. nginx's `auth_request` sub-request is
|
`username:hashed-password` entries nginx uses for Basic auth.
|
||||||
sent here. Change when 7002 is already in use.
|
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 {
|
realm = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.strMatching "[^\"$]*";
|
||||||
default = "hyperhive";
|
default = "hyperhive";
|
||||||
example = "my-hive";
|
example = "my-hive";
|
||||||
description = ''
|
description = ''
|
||||||
HTTP Basic auth `realm` value sent in the `WWW-Authenticate`
|
HTTP Basic auth `realm` value sent in the `WWW-Authenticate`
|
||||||
header when credentials are absent or rejected.
|
header when credentials are absent or rejected. Must not
|
||||||
'';
|
contain `"` or `$` (nginx string metacharacters).
|
||||||
};
|
|
||||||
|
|
||||||
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.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -269,53 +253,16 @@ in
|
||||||
or leave `localHostsEntry` at its default of false.
|
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
|
# Ensure bind-mount sources exist at host boot before the gateway
|
||||||
# container's first start. nspawn would auto-create missing dirs
|
# container's first start. nspawn would auto-create missing dirs
|
||||||
|
|
@ -363,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 {
|
||||||
|
hostPath = builtins.dirOf cfg.auth.htpasswdFile;
|
||||||
|
isReadOnly = true;
|
||||||
|
};
|
||||||
config =
|
config =
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
let
|
let
|
||||||
|
|
@ -616,11 +573,10 @@ in
|
||||||
# headers stay set so SSE (`/dashboard/stream`,
|
# headers stay set so SSE (`/dashboard/stream`,
|
||||||
# `/events/stream`) + websocket (`/screen/ws`)
|
# `/events/stream`) + websocket (`/screen/ws`)
|
||||||
# endpoints keep working transparently.
|
# endpoints keep working transparently.
|
||||||
# When auth is enabled, `auth_request` sub-requests
|
# When auth is enabled, nginx's built-in `auth_basic`
|
||||||
# `/__hive_gateway_auth` before proxying. The 401
|
# validates credentials against the htpasswd file
|
||||||
# named-location handler (in vhost `extraConfig`)
|
# bind-mounted at `/run/gateway-auth/`. No extra
|
||||||
# adds the `WWW-Authenticate` header so browsers
|
# service or host-side daemon required.
|
||||||
# show a login prompt.
|
|
||||||
"/" = {
|
"/" = {
|
||||||
proxyPass = "http://${cfg.upstreamHost}:${toString cfg.upstreamPort}";
|
proxyPass = "http://${cfg.upstreamHost}:${toString cfg.upstreamPort}";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
|
|
@ -628,27 +584,11 @@ in
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
proxy_read_timeout 1d;
|
proxy_read_timeout 1d;
|
||||||
${lib.optionalString cfg.auth.enable ''
|
${lib.optionalString cfg.auth.enable ''
|
||||||
auth_request /__hive_gateway_auth;
|
auth_basic "${cfg.auth.realm}";
|
||||||
error_page 401 = @hive_auth_required;
|
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
|
# 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
|
||||||
|
|
@ -661,18 +601,6 @@ in
|
||||||
# the `/agent/` catch-all above.
|
# the `/agent/` catch-all above.
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
include /run/hive-state/agents.conf;
|
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