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-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -199,7 +199,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -210,7 +210,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -303,26 +303,6 @@ version = "1.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
|
|
@ -404,16 +384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex 2.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -477,16 +448,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
|
|
@ -518,7 +479,7 @@ dependencies = [
|
|||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -655,7 +616,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -678,7 +639,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -689,7 +650,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
|||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -793,7 +754,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1024,7 +985,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1106,12 +1067,6 @@ dependencies = [
|
|||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1247,9 +1202,7 @@ dependencies = [
|
|||
"base64",
|
||||
"clap",
|
||||
"hive-sh4re",
|
||||
"libc",
|
||||
"listenfd",
|
||||
"pam",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
|
|
@ -1686,15 +1639,6 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
|
|
@ -1772,12 +1716,6 @@ version = "1.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1879,7 +1817,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1917,7 +1855,7 @@ dependencies = [
|
|||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2098,12 +2036,6 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
|
|
@ -2131,16 +2063,6 @@ version = "1.0.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
|
|
@ -2213,40 +2135,6 @@ version = "0.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
|
|
@ -2406,7 +2294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2468,7 +2356,7 @@ dependencies = [
|
|||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2500,7 +2388,7 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.2",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
|
|
@ -2520,7 +2408,7 @@ dependencies = [
|
|||
"lru-slab",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"rustc-hash 2.1.2",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
|
|
@ -2674,7 +2562,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2795,7 +2683,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_json",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2973,7 +2861,7 @@ dependencies = [
|
|||
"quote",
|
||||
"ruma-identifiers-validation",
|
||||
"serde",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
"toml",
|
||||
]
|
||||
|
||||
|
|
@ -3007,12 +2895,6 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
|
|
@ -3111,7 +2993,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3163,7 +3045,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3174,7 +3056,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3266,12 +3148,6 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
|
|
@ -3384,17 +3260,6 @@ version = "2.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
|
|
@ -3423,7 +3288,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3476,7 +3341,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3487,7 +3352,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3567,7 +3432,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3757,7 +3622,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3904,16 +3769,6 @@ version = "2.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
|
|
@ -4066,7 +3921,7 @@ dependencies = [
|
|||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
|
@ -4204,7 +4059,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4215,7 +4070,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4452,7 +4307,7 @@ dependencies = [
|
|||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
|
|
@ -4468,7 +4323,7 @@ dependencies = [
|
|||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
|
@ -4553,7 +4408,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
|
|
@ -4574,7 +4429,7 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4594,7 +4449,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
|
|
@ -4615,7 +4470,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4648,7 +4503,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ must_use_candidate = "allow"
|
|||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
pam = "0.8"
|
||||
axum = { version = "0.8", features = ["ws"] }
|
||||
base64 = "0.22"
|
||||
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
|
||||
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.sqlite
|
||||
pkgs.pkg-config
|
||||
pkgs.linux-pam # libpam.so.0 for hive-gateway-auth
|
||||
];
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ base64.workspace = true
|
|||
reqwest.workspace = true
|
||||
clap.workspace = true
|
||||
hive-sh4re.workspace = true
|
||||
libc = "0.2"
|
||||
pam.workspace = true
|
||||
listenfd = "1"
|
||||
rusqlite.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 = {
|
||||
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