render squid's users file from this hive's proxy credentials
This commit is contained in:
parent
ec06a91deb
commit
52d532ff5a
12 changed files with 562 additions and 6 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
|
@ -1777,6 +1777,7 @@ dependencies = [
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"forgejo-api",
|
"forgejo-api",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"getrandom 0.3.4",
|
||||||
"hive-core-agent-sock",
|
"hive-core-agent-sock",
|
||||||
"hive-host-sock",
|
"hive-host-sock",
|
||||||
"hive-jobq",
|
"hive-jobq",
|
||||||
|
|
@ -1800,6 +1801,7 @@ dependencies = [
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha-crypt",
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
"strum",
|
"strum",
|
||||||
"swarm-matrix-client",
|
"swarm-matrix-client",
|
||||||
|
|
@ -3082,6 +3084,15 @@ dependencies = [
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mcf"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2044bdbade272ded4a1529324f4f30ca7b0c5b496541f2a9095bbd1c9d03cb06"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
|
|
@ -3357,6 +3368,12 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
|
@ -4567,6 +4584,19 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha-crypt"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32ee31e0190f9dc366d26b032ac36a99328532f40a237a79385a20d2d7f29810"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"ctutils",
|
||||||
|
"mcf",
|
||||||
|
"password-hash",
|
||||||
|
"sha2 0.11.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,9 @@ matrix-sdk = { version = "0.18", default-features = false, features = [
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
hmac = "0.13"
|
hmac = "0.13"
|
||||||
sha2 = "0.11"
|
sha2 = "0.11"
|
||||||
|
# SHA-512 crypt (`$6$`) for the users file hive-c0re renders; squid's `basic_ncsa_auth`
|
||||||
|
# hands a `$` hash to the system `crypt()`.
|
||||||
|
sha-crypt = "0.6"
|
||||||
# Verifies the cache signature over a narinfo (hive-c0re). Already in the lock
|
# Verifies the cache signature over a narinfo (hive-c0re). Already in the lock
|
||||||
# through nkeys.
|
# through nkeys.
|
||||||
ed25519-dalek = "2"
|
ed25519-dalek = "2"
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ the rest of the cell says how.
|
||||||
| `swarm/agents/<agent>/claude-credentials` | `swarm-claude-login`, when you log the agent in from its card; one version per login | the agent, while it waits for a login | ❌ log in again | ✅ every 30 seconds while waiting |
|
| `swarm/agents/<agent>/claude-credentials` | `swarm-claude-login`, when you log the agent in from its card; one version per login | the agent, while it waits for a login | ❌ log in again | ✅ every 30 seconds while waiting |
|
||||||
| `swarm/hives/<hive>/matrix/sender-token` | `swarm-controller`, every 5 minutes | `swarm-controller` and the hive | ✅ when missing or rejected by the homeserver | ✅ the hive re-reads on every sweep |
|
| `swarm/hives/<hive>/matrix/sender-token` | `swarm-controller`, every 5 minutes | `swarm-controller` and the hive | ✅ when missing or rejected by the homeserver | ✅ the hive re-reads on every sweep |
|
||||||
| `swarm/hives/<hive>/forge/token` | `swarm-controller`, every 5 minutes | `swarm-controller` and the hive | ✅ when missing or no longer matching the forge | ✅ read on every forge call |
|
| `swarm/hives/<hive>/forge/token` | `swarm-controller`, every 5 minutes | `swarm-controller` and the hive | ✅ when missing or no longer matching the forge | ✅ read on every forge call |
|
||||||
|
| `swarm/agents/<agent>/proxy-credential` | `swarm-controller`: 32 random bytes, at start and every 5 minutes for every agent that holds a store identity | the hive (`hive-c0re`); the agent can read it, nothing sends it yet | ❌ never replaced: written only while the path holds nothing | ✅ `hive-c0re` re-reads it every 30 seconds |
|
||||||
|
| `swarm/hives/<hive>/squid-selfcheck` | `swarm-controller`, in the same pass as the agents' credentials | the hive: `hive-c0re` and `squid-selfcheck-credential` (root) | ❌ never replaced: written only while the path holds nothing | ✅ `hive-c0re` every 30 seconds, the root unit every 10 minutes |
|
||||||
| `swarm/hives/<hive>/oidc/client` | authelia, first boot; `swarm-secret-publish` copies it in | the hive | ❌ minted once; `swarm-secret-publish` republishes a re-mint | ✅ re-read when authelia refuses it |
|
| `swarm/hives/<hive>/oidc/client` | authelia, first boot; `swarm-secret-publish` copies it in | the hive | ❌ minted once; `swarm-secret-publish` republishes a re-mint | ✅ re-read when authelia refuses it |
|
||||||
| `swarm/services/<clientId>/oidc/client` | authelia | that service | must be stated | must be stated |
|
| `swarm/services/<clientId>/oidc/client` | authelia | that service | must be stated | must be stated |
|
||||||
| `swarm/services/ci-runner/registration-token` | `swarm-controller`, from the forge, at start and every 5 minutes | `swarm-bao-ci-runner` on the runner's host | ❌ the forge keeps one token until an admin resets it; the controller then writes the new one within 5 minutes | ✅ every 10 minutes; restarts the runner on a change |
|
| `swarm/services/ci-runner/registration-token` | `swarm-controller`, from the forge, at start and every 5 minutes | `swarm-bao-ci-runner` on the runner's host | ❌ the forge keeps one token until an admin resets it; the controller then writes the new one within 5 minutes | ✅ every 10 minutes; restarts the runner on a change |
|
||||||
|
|
@ -154,6 +156,7 @@ pieces fit, and the reasoning behind them, is in [`credentials.md`](credentials.
|
||||||
| the forge's copy of its OIDC secret | `swarm-bao-forge-oidc.service` reads it out of the swarm secret store under the forge publisher's leaf, wherever authelia runs | `/run/hive-forge-oidc/<id>.secret` on the host (tmpfs), bound into the forge container, handed to `forgejo-sso-source` through `LoadCredential` |
|
| the forge's copy of its OIDC secret | `swarm-bao-forge-oidc.service` reads it out of the swarm secret store under the forge publisher's leaf, wherever authelia runs | `/run/hive-forge-oidc/<id>.secret` on the host (tmpfs), bound into the forge container, handed to `forgejo-sso-source` through `LoadCredential` |
|
||||||
| the homeserver's copy of its OIDC secret | `swarm-bao-matrix-oidc.service` reads it out of the swarm secret store under matrix-ctl's leaf, wherever authelia runs | `/run/hive-matrix-oidc/<id>.secret` on the host (tmpfs), bound into the homeserver container, handed to tuwunel through `LoadCredential` |
|
| the homeserver's copy of its OIDC secret | `swarm-bao-matrix-oidc.service` reads it out of the swarm secret store under matrix-ctl's leaf, wherever authelia runs | `/run/hive-matrix-oidc/<id>.secret` on the host (tmpfs), bound into the homeserver container, handed to tuwunel through `LoadCredential` |
|
||||||
| this hive's forge read token | `swarm-controller`, into the secret store at `swarm/hives/<hive>/forge/token`, for the `hive-<hive>` account; a pass at start and every five minutes re-mints a missing or stale token | `hive-c0re` reads it under the hive's own store identity, and copies it (account on the first line, token on the second) to `/run/hyperhive/forge-hive-token` (tmpfs, `0600` `hive-core`) for nix's `git`, which `hive-priv` also runs. Each `internal/meta` read and each boot rewrites the copy when it differs; where they differ after a rotation, a build that starts before the next read fails to authenticate, and a retry reads the new token |
|
| this hive's forge read token | `swarm-controller`, into the secret store at `swarm/hives/<hive>/forge/token`, for the `hive-<hive>` account; a pass at start and every five minutes re-mints a missing or stale token | `hive-c0re` reads it under the hive's own store identity, and copies it (account on the first line, token on the second) to `/run/hyperhive/forge-hive-token` (tmpfs, `0600` `hive-core`) for nix's `git`, which `hive-priv` also runs. Each `internal/meta` read and each boot rewrites the copy when it differs; where they differ after a rotation, a build that starts before the next read fails to authenticate, and a retry reads the new token |
|
||||||
|
| squid's users file | `hive-c0re` (`workers::squid_users`), every 30 seconds, from the proxy credentials of its agents and the self-check | `services.hyperhive.squid.usersFile` (`/var/lib/hive-squid/users`, `hive-core:squid`, `0640`): `<user>:<hash>` lines, SHA-512 crypt hashes only, renamed into place when it changed. `systemd-tmpfiles` creates it empty so squid's helper never sees a missing file. A hive whose agents have no credential yet (an older controller) has an empty file: every login fails and no agent sends a credential and squid doesn't read the file yet, so no traffic changes |
|
||||||
|
|
||||||
### Store identities, one per principal
|
### Store identities, one per principal
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ axum.workspace = true
|
||||||
backon.workspace = true
|
backon.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
getrandom.workspace = true
|
||||||
|
sha-crypt.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
forgejo-api.workspace = true
|
forgejo-api.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,8 @@ async fn cmd_serve(
|
||||||
// not a part of the above; see swarm_agent_status's module doc for why
|
// not a part of the above; see swarm_agent_status's module doc for why
|
||||||
// the two buckets stay separate.
|
// the two buckets stay separate.
|
||||||
swarm_agent_status::spawn(coord.shutdown_rx());
|
swarm_agent_status::spawn(coord.shutdown_rx());
|
||||||
|
// Squid's users file: the hashes of this hive's agents' proxy credentials.
|
||||||
|
workers::squid_users::spawn(coord.shutdown_rx());
|
||||||
// Per-agent events.sqlite + bash-tasks file cleanup now runs
|
// Per-agent events.sqlite + bash-tasks file cleanup now runs
|
||||||
// agent-side in the harness (`hive_agent::vacuum`): the files are
|
// agent-side in the harness (`hive_agent::vacuum`): the files are
|
||||||
// agent-owned, so host-side deletes hit PermissionDenied / readonly-db
|
// agent-owned, so host-side deletes hit PermissionDenied / readonly-db
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
//! [`write_secret_0600`]: the one way hive-c0re puts a credential on disk.
|
//! [`write_secret_0600`] and [`write_secret_mode`]: the ways hive-c0re puts a
|
||||||
|
//! credential, or a hash of one, on disk.
|
||||||
|
|
||||||
use std::io::Write as _;
|
use std::io::Write as _;
|
||||||
use std::os::unix::fs::OpenOptionsExt as _;
|
use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
|
@ -13,6 +14,12 @@ use anyhow::{Context as _, Result};
|
||||||
/// reader (or a crash) never sees half of it. Errors carry paths, never
|
/// reader (or a crash) never sees half of it. Errors carry paths, never
|
||||||
/// `contents`.
|
/// `contents`.
|
||||||
pub fn write_secret_0600(path: &Path, contents: &[u8]) -> Result<()> {
|
pub fn write_secret_0600(path: &Path, contents: &[u8]) -> Result<()> {
|
||||||
|
write_secret_mode(path, contents, 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`write_secret_0600`] with the file's mode given, for a file another user
|
||||||
|
/// has to read (the group comes from the directory).
|
||||||
|
pub fn write_secret_mode(path: &Path, contents: &[u8], mode: u32) -> Result<()> {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||||
}
|
}
|
||||||
|
|
@ -27,9 +34,13 @@ pub fn write_secret_0600(path: &Path, contents: &[u8]) -> Result<()> {
|
||||||
let mut f = std::fs::OpenOptions::new()
|
let mut f = std::fs::OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.mode(0o600)
|
.mode(mode)
|
||||||
.open(&tmp)
|
.open(&tmp)
|
||||||
.with_context(|| format!("create {}", tmp.display()))?;
|
.with_context(|| format!("create {}", tmp.display()))?;
|
||||||
|
// `open` masks the mode with the process umask; the mode is a property of
|
||||||
|
// the writer, not of the unit's umask.
|
||||||
|
f.set_permissions(std::fs::Permissions::from_mode(mode))
|
||||||
|
.with_context(|| format!("chmod {}", tmp.display()))?;
|
||||||
f.write_all(contents)
|
f.write_all(contents)
|
||||||
.and_then(|()| f.sync_all())
|
.and_then(|()| f.sync_all())
|
||||||
.with_context(|| format!("write {}", tmp.display()))?;
|
.with_context(|| format!("write {}", tmp.display()))?;
|
||||||
|
|
@ -38,7 +49,7 @@ pub fn write_secret_0600(path: &Path, contents: &[u8]) -> Result<()> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::write_secret_0600;
|
use super::{write_secret_0600, write_secret_mode};
|
||||||
use std::os::unix::fs::PermissionsExt as _;
|
use std::os::unix::fs::PermissionsExt as _;
|
||||||
|
|
||||||
fn mode(path: &std::path::Path) -> u32 {
|
fn mode(path: &std::path::Path) -> u32 {
|
||||||
|
|
@ -76,4 +87,26 @@ mod tests {
|
||||||
write_secret_0600(&path, b"new\n").expect("writes");
|
write_secret_0600(&path, b"new\n").expect("writes");
|
||||||
assert_eq!(std::fs::read(&path).expect("read"), b"new\n");
|
assert_eq!(std::fs::read(&path).expect("read"), b"new\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A restrictive umask doesn't change the mode asked for. The umask is
|
||||||
|
/// process-wide, so the write happens in a child run of this test.
|
||||||
|
#[test]
|
||||||
|
fn the_mode_survives_a_restrictive_umask() {
|
||||||
|
const DIR: &str = "SECRET_FILE_UMASK_TEST_DIR";
|
||||||
|
if let Some(dir) = std::env::var_os(DIR) {
|
||||||
|
write_secret_mode(&std::path::Path::new(&dir).join("shared"), b"x\n", 0o640)
|
||||||
|
.expect("writes");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let status = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(r#"umask 077 && exec "$0" --exact secret_file::tests::the_mode_survives_a_restrictive_umask"#)
|
||||||
|
.arg(std::env::current_exe().expect("test binary"))
|
||||||
|
.env(DIR, dir.path())
|
||||||
|
.status()
|
||||||
|
.expect("spawns the child");
|
||||||
|
assert!(status.success());
|
||||||
|
assert_eq!(mode(&dir.path().join("shared")), 0o640);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Background tasks and periodic sweeps: crash/login watcher, boot-time
|
//! Background tasks and periodic sweeps: crash/login watcher, boot-time
|
||||||
//! auto-update reconcile, the agent-sockets.json writer loop, the MCP socket listener
|
//! auto-update reconcile, the agent-sockets.json writer loop, the MCP socket listener
|
||||||
//! reconcile loop, knowledge-repo sync, the swarm wanted-state pull, and the
|
//! reconcile loop, knowledge-repo sync, the swarm wanted-state pull, and the
|
||||||
//! swarm resource-cap and permissions reads.
|
//! swarm resource-cap and permissions reads, and the squid users file.
|
||||||
//! Each submodule is re-exported
|
//! Each submodule is re-exported
|
||||||
//! at the crate root, so `crate::crash_watch::…` etc. keep working unchanged.
|
//! at the crate root, so `crate::crash_watch::…` etc. keep working unchanged.
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ pub mod crash_watch;
|
||||||
pub mod knowledge;
|
pub mod knowledge;
|
||||||
pub mod mcp_sockets;
|
pub mod mcp_sockets;
|
||||||
pub mod permissions_import;
|
pub mod permissions_import;
|
||||||
|
pub mod squid_users;
|
||||||
pub mod swarm_limits;
|
pub mod swarm_limits;
|
||||||
pub mod swarm_permissions;
|
pub mod swarm_permissions;
|
||||||
pub mod wanted;
|
pub mod wanted;
|
||||||
|
|
|
||||||
440
hive-c0re/src/workers/squid_users.rs
Normal file
440
hive-c0re/src/workers/squid_users.rs
Normal file
|
|
@ -0,0 +1,440 @@
|
||||||
|
//! Squid's users file: the hashes of the proxy credentials this hive's agents
|
||||||
|
//! authenticate to its egress proxy with.
|
||||||
|
//!
|
||||||
|
//! swarm-controller mints one credential per agent and one for this hive's own
|
||||||
|
//! squid self-check ([`swarm_secret_client::proxy`]). Every [`INTERVAL`] this
|
||||||
|
//! reads the credentials of the agents this hive has, plus the self-check's,
|
||||||
|
//! and renders `<user>:<hash>` lines into the file named by
|
||||||
|
//! [`ENV_USERS_FILE`], which squid's `basic_ncsa_auth` re-reads when its mtime
|
||||||
|
//! moves. The agents are the container list, not the running containers: a
|
||||||
|
//! stopped agent stays in the file.
|
||||||
|
//!
|
||||||
|
//! Only salted SHA-512 crypt hashes reach the disk. A hash is kept as it is
|
||||||
|
//! while it still verifies its credential, so the file changes only when a
|
||||||
|
//! user is added, removed or given a new credential.
|
||||||
|
//!
|
||||||
|
//! A failed read leaves the file as it is: dropping a user on a store outage
|
||||||
|
//! would read exactly like the swarm revoking it.
|
||||||
|
|
||||||
|
use std::future::Future;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use sha_crypt::{PasswordHasher as _, PasswordVerifier as _, ShaCrypt};
|
||||||
|
use swarm_secret_client::{SecretStore, client, proxy};
|
||||||
|
|
||||||
|
use crate::stats::sweep_health::{self, SweepHealth};
|
||||||
|
|
||||||
|
/// How often the file is re-rendered.
|
||||||
|
pub const INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
/// At [`INTERVAL`] this is ~90 seconds of failing passes.
|
||||||
|
const FAILURES_BEFORE_WARNING: u32 = 3;
|
||||||
|
|
||||||
|
/// The environment variable naming the file. Set by `nix/host-modules/hive-c0re`
|
||||||
|
/// from `services.hyperhive.squid.usersFile`; unset means this host runs no squid.
|
||||||
|
pub const ENV_USERS_FILE: &str = "HIVE_SQUID_USERS_FILE";
|
||||||
|
|
||||||
|
/// Raw salt bytes: twelve of them encode to the sixteen characters `crypt()`
|
||||||
|
/// accepts.
|
||||||
|
const SALT_BYTES: usize = 12;
|
||||||
|
|
||||||
|
/// The file's mode. The group is squid's, from the setgid directory
|
||||||
|
/// `nix/host-modules/hive-squid.nix` creates.
|
||||||
|
const FILE_MODE: u32 = 0o640;
|
||||||
|
|
||||||
|
/// One squid user and the credential it logs in with. No `Debug`: `secret` is
|
||||||
|
/// live.
|
||||||
|
struct User {
|
||||||
|
name: String,
|
||||||
|
secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the pass loop, when this host has a squid users file, a store and a
|
||||||
|
/// hive name.
|
||||||
|
pub fn spawn(mut shutdown: tokio::sync::watch::Receiver<bool>) {
|
||||||
|
let Some(path) = std::env::var_os(ENV_USERS_FILE)
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(hive) = crate::container_view::hive_swarm_names().0 else {
|
||||||
|
tracing::info!("squid users: HYPERHIVE_HIVE_NAME is unset; no users file is written");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if std::env::var_os(client::ENV_ADDR).is_none_or(|v| v.is_empty()) {
|
||||||
|
tracing::info!(
|
||||||
|
"squid users: no swarm secret store is configured; no users file is written"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut health = SweepHealth::new("squid_users_render", "warn", FAILURES_BEFORE_WARNING);
|
||||||
|
loop {
|
||||||
|
match pass(&hive, &path).await {
|
||||||
|
Ok(()) => health.record_ok(),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = ?e, "squid users: the pass failed");
|
||||||
|
let err = format!("{e:#}");
|
||||||
|
health.record_err(|ctx| {
|
||||||
|
let age = ctx.since_last_ok.map_or_else(
|
||||||
|
|| "no success this session".to_owned(),
|
||||||
|
|d| format!("last ok {} ago", sweep_health::fmt_age(d)),
|
||||||
|
);
|
||||||
|
format!(
|
||||||
|
"rendering squid's users file is failing ({} consecutive, {age}) \
|
||||||
|
— squid keeps the users last written: {err}",
|
||||||
|
ctx.consecutive
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::select! {
|
||||||
|
() = tokio::time::sleep(INTERVAL) => {}
|
||||||
|
_ = shutdown.changed() => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read this hive's credentials from the store and render them into `file`.
|
||||||
|
async fn pass(hive: &str, file: &Path) -> Result<()> {
|
||||||
|
let agents: Vec<String> = crate::lifecycle::agents_for_meta_listing()
|
||||||
|
.await
|
||||||
|
.context("enumerating this hive's agents")?
|
||||||
|
.into_iter()
|
||||||
|
.map(|spec| spec.name)
|
||||||
|
.collect();
|
||||||
|
let store = SecretStore::from_env(hive)
|
||||||
|
.await
|
||||||
|
.context("connecting to the store")?;
|
||||||
|
sync_with(hive, &agents, file, |path| {
|
||||||
|
let store = &store;
|
||||||
|
async move {
|
||||||
|
let held: Option<proxy::Credential> = store
|
||||||
|
.read_optional(&path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("reading {path}"))?;
|
||||||
|
Ok(held.map(|c| c.value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`pass`] with the store read and the file given. Nothing is written unless
|
||||||
|
/// every read succeeds and the rendered file differs from the current one.
|
||||||
|
async fn sync_with<F, Fut>(hive: &str, agents: &[String], file: &Path, read: F) -> Result<()>
|
||||||
|
where
|
||||||
|
F: Fn(String) -> Fut,
|
||||||
|
Fut: Future<Output = Result<Option<String>>>,
|
||||||
|
{
|
||||||
|
let mut wanted = Vec::new();
|
||||||
|
for agent in agents {
|
||||||
|
wanted.push((agent.clone(), proxy::agent_credential_path(agent)?));
|
||||||
|
}
|
||||||
|
wanted.push((
|
||||||
|
proxy::SELFCHECK_USER.to_owned(),
|
||||||
|
proxy::selfcheck_path(hive)?,
|
||||||
|
));
|
||||||
|
let mut users = Vec::new();
|
||||||
|
for (name, path) in wanted {
|
||||||
|
if let Some(secret) = read(path).await? {
|
||||||
|
users.push(User { name, secret });
|
||||||
|
} else {
|
||||||
|
tracing::debug!(name, "squid users: no proxy credential minted yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let current = match std::fs::read_to_string(file) {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
|
||||||
|
Err(e) => return Err(e).with_context(|| format!("reading {}", file.display())),
|
||||||
|
};
|
||||||
|
let rendered = render(¤t, &users)?;
|
||||||
|
if rendered == current {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
crate::secret_file::write_secret_mode(file, rendered.as_bytes(), FILE_MODE)
|
||||||
|
.with_context(|| format!("writing {}", file.display()))?;
|
||||||
|
tracing::info!(
|
||||||
|
users = users.len(),
|
||||||
|
agents = users.iter().filter(|u| u.name != proxy::SELFCHECK_USER).count(),
|
||||||
|
file = %file.display(),
|
||||||
|
"squid users: wrote the users file"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The users file for `users`, sorted by name, one `<user>:<hash>` line each.
|
||||||
|
///
|
||||||
|
/// A user whose line in `current` still verifies against its credential keeps
|
||||||
|
/// that line. Every other line of `current` is dropped, so a destroyed agent,
|
||||||
|
/// or one that belongs to another hive, does not outlive the pass.
|
||||||
|
fn render(current: &str, users: &[User]) -> Result<String> {
|
||||||
|
let mut lines: Vec<String> = Vec::with_capacity(users.len());
|
||||||
|
for user in users {
|
||||||
|
if user.name.contains([':', '\n']) {
|
||||||
|
bail!("{:?} cannot be a squid user name", user.name);
|
||||||
|
}
|
||||||
|
let kept = current
|
||||||
|
.lines()
|
||||||
|
.filter_map(|l| l.split_once(':'))
|
||||||
|
.find(|(name, hash)| *name == user.name && verifies(&user.secret, hash))
|
||||||
|
.map(|(_, hash)| hash.to_owned());
|
||||||
|
let hash = match kept {
|
||||||
|
Some(hash) => hash,
|
||||||
|
None => hash_secret(&user.secret)?,
|
||||||
|
};
|
||||||
|
lines.push(format!("{}:{hash}\n", user.name));
|
||||||
|
}
|
||||||
|
lines.sort();
|
||||||
|
Ok(lines.concat())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fresh salted SHA-512 crypt hash of `secret`: `$6$rounds=5000$<salt>$<hash>`.
|
||||||
|
fn hash_secret(secret: &str) -> Result<String> {
|
||||||
|
let mut salt = [0u8; SALT_BYTES];
|
||||||
|
getrandom::fill(&mut salt).context("drawing a salt from the kernel's CSPRNG")?;
|
||||||
|
let hash = ShaCrypt::SHA512
|
||||||
|
.hash_password_with_salt(secret.as_bytes(), &salt)
|
||||||
|
.map_err(|e| anyhow::anyhow!("hashing a proxy credential: {e}"))?;
|
||||||
|
Ok(hash.as_str().to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `hash` is `secret`'s SHA-crypt hash.
|
||||||
|
fn verifies(secret: &str, hash: &str) -> bool {
|
||||||
|
ShaCrypt::SHA512
|
||||||
|
.verify_password(secret.as_bytes(), hash)
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::os::unix::fs::PermissionsExt as _;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const HIVE: &str = "h1";
|
||||||
|
|
||||||
|
/// What the store holds, by path.
|
||||||
|
fn store(entries: &[(&str, &str)]) -> HashMap<String, String> {
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.map(|(p, v)| ((*p).to_owned(), (*v).to_owned()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn names(names: &[&str]) -> Vec<String> {
|
||||||
|
names.iter().map(|n| (*n).to_owned()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync(held: &HashMap<String, String>, agents: &[&str], file: &Path) -> Result<()> {
|
||||||
|
sync_with(HIVE, &names(agents), file, |path| {
|
||||||
|
let found = held.get(&path).cloned();
|
||||||
|
async move { Ok(found) }
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lines(file: &Path) -> Vec<(String, String)> {
|
||||||
|
std::fs::read_to_string(file)
|
||||||
|
.expect("the file is there")
|
||||||
|
.lines()
|
||||||
|
.map(|l| {
|
||||||
|
let (name, hash) = l.split_once(':').expect("a name and a hash");
|
||||||
|
(name.to_owned(), hash.to_owned())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn two_agents_and_a_stranger() -> HashMap<String, String> {
|
||||||
|
store(&[
|
||||||
|
("swarm/agents/atlas/proxy-credential", "atlas-secret"),
|
||||||
|
("swarm/agents/argus/proxy-credential", "argus-secret"),
|
||||||
|
("swarm/agents/other-hive-agent/proxy-credential", "stranger"),
|
||||||
|
("swarm/hives/h1/squid-selfcheck", "check-secret"),
|
||||||
|
("swarm/hives/h2/squid-selfcheck", "other-check"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn only_this_hives_agents_and_its_selfcheck_are_rendered() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let file = dir.path().join("users");
|
||||||
|
sync(&two_agents_and_a_stranger(), &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
let users: Vec<String> = lines(&file).into_iter().map(|(n, _)| n).collect();
|
||||||
|
assert_eq!(users, ["argus", "atlas", "selfcheck.squid"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn each_line_is_a_salted_sha512_crypt_hash_of_its_users_credential() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let file = dir.path().join("users");
|
||||||
|
sync(&two_agents_and_a_stranger(), &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
let by_name: HashMap<_, _> = lines(&file).into_iter().collect();
|
||||||
|
assert!(by_name["atlas"].starts_with("$6$"), "{}", by_name["atlas"]);
|
||||||
|
assert!(verifies("atlas-secret", &by_name["atlas"]));
|
||||||
|
assert!(verifies("argus-secret", &by_name["argus"]));
|
||||||
|
assert!(verifies("check-secret", &by_name["selfcheck.squid"]));
|
||||||
|
// Control: a hash is of its own user's credential only.
|
||||||
|
assert!(!verifies("argus-secret", &by_name["atlas"]));
|
||||||
|
// Salted: the same credential hashes differently twice.
|
||||||
|
assert_ne!(
|
||||||
|
hash_secret("atlas-secret").expect("hashes"),
|
||||||
|
hash_secret("atlas-secret").expect("hashes")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn no_credential_is_written_to_the_file_or_its_directory() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let file = dir.path().join("users");
|
||||||
|
sync(&two_agents_and_a_stranger(), &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
// Control: the search finds a name that is in the file.
|
||||||
|
let mut found = 0;
|
||||||
|
for entry in std::fs::read_dir(dir.path()).expect("lists") {
|
||||||
|
let text = std::fs::read_to_string(entry.expect("entry").path()).expect("text");
|
||||||
|
assert!(text.contains("atlas:"), "control: {text}");
|
||||||
|
for secret in ["atlas-secret", "argus-secret", "check-secret"] {
|
||||||
|
assert!(!text.contains(secret), "{secret} reached the disk");
|
||||||
|
}
|
||||||
|
found += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(found, 1, "only the users file is left behind");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn the_file_is_group_readable_and_replaced_atomically() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let file = dir.path().join("users");
|
||||||
|
sync(&two_agents_and_a_stranger(), &["atlas"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
let mode = std::fs::metadata(&file).expect("stat").permissions().mode() & 0o777;
|
||||||
|
assert_eq!(mode, 0o640);
|
||||||
|
let inode = std::os::unix::fs::MetadataExt::ino(&std::fs::metadata(&file).expect("stat"));
|
||||||
|
sync(&two_agents_and_a_stranger(), &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
let after = std::os::unix::fs::MetadataExt::ino(&std::fs::metadata(&file).expect("stat"));
|
||||||
|
assert_ne!(
|
||||||
|
inode, after,
|
||||||
|
"a rename onto the path, not a rewrite in place"
|
||||||
|
);
|
||||||
|
assert!(!dir.path().join("users.tmp").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn an_unchanged_pass_leaves_the_file_alone() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let file = dir.path().join("users");
|
||||||
|
let held = two_agents_and_a_stranger();
|
||||||
|
sync(&held, &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
let first = std::fs::read_to_string(&file).expect("read");
|
||||||
|
let inode = std::os::unix::fs::MetadataExt::ino(&std::fs::metadata(&file).expect("stat"));
|
||||||
|
sync(&held, &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
assert_eq!(std::fs::read_to_string(&file).expect("read"), first);
|
||||||
|
let again = std::os::unix::fs::MetadataExt::ino(&std::fs::metadata(&file).expect("stat"));
|
||||||
|
assert_eq!(inode, again, "nothing was written");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_changed_credential_gets_a_new_hash_and_the_others_keep_theirs() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let file = dir.path().join("users");
|
||||||
|
let mut held = two_agents_and_a_stranger();
|
||||||
|
sync(&held, &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
let before: HashMap<_, _> = lines(&file).into_iter().collect();
|
||||||
|
held.insert(
|
||||||
|
"swarm/agents/atlas/proxy-credential".to_owned(),
|
||||||
|
"rotated".to_owned(),
|
||||||
|
);
|
||||||
|
sync(&held, &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
let after: HashMap<_, _> = lines(&file).into_iter().collect();
|
||||||
|
assert!(verifies("rotated", &after["atlas"]));
|
||||||
|
assert_ne!(before["atlas"], after["atlas"]);
|
||||||
|
assert_eq!(before["argus"], after["argus"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_stopped_agent_stays_and_a_destroyed_one_goes() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let file = dir.path().join("users");
|
||||||
|
let held = two_agents_and_a_stranger();
|
||||||
|
// The agent list is the container list, so an agent whose container
|
||||||
|
// is down is in it: nothing here looks at whether anything runs.
|
||||||
|
sync(&held, &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
assert!(lines(&file).iter().any(|(n, _)| n == "argus"));
|
||||||
|
// Control: once the container is gone the user is dropped, and a line
|
||||||
|
// for an agent this hive never had does not survive a pass either.
|
||||||
|
let mut text = std::fs::read_to_string(&file).expect("read");
|
||||||
|
text.push_str("stray:$6$rounds=5000$saltsaltsaltsalt$x\n");
|
||||||
|
std::fs::write(&file, text).expect("write");
|
||||||
|
sync(&held, &["atlas"], &file).await.expect("renders");
|
||||||
|
let users: Vec<String> = lines(&file).into_iter().map(|(n, _)| n).collect();
|
||||||
|
assert_eq!(users, ["atlas", "selfcheck.squid"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn an_agent_without_a_credential_yet_is_left_out() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let file = dir.path().join("users");
|
||||||
|
// The empty file squid's unit creates before its helper starts.
|
||||||
|
std::fs::write(&file, "").expect("write");
|
||||||
|
sync(&store(&[]), &["atlas"], &file).await.expect("renders");
|
||||||
|
assert_eq!(std::fs::read_to_string(&file).expect("read"), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_failed_read_leaves_the_file_as_it_was() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let file = dir.path().join("users");
|
||||||
|
sync(&two_agents_and_a_stranger(), &["atlas", "argus"], &file)
|
||||||
|
.await
|
||||||
|
.expect("renders");
|
||||||
|
let before = std::fs::read_to_string(&file).expect("read");
|
||||||
|
let failed = sync_with(
|
||||||
|
HIVE,
|
||||||
|
&names(&["atlas", "argus"]),
|
||||||
|
&file,
|
||||||
|
|path| async move {
|
||||||
|
if path.contains("argus") {
|
||||||
|
bail!("the store is down")
|
||||||
|
}
|
||||||
|
Ok(Some("atlas-secret".to_owned()))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(failed.is_err());
|
||||||
|
assert_eq!(std::fs::read_to_string(&file).expect("read"), before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_name_that_would_break_the_line_is_refused() {
|
||||||
|
for bad in ["a:b", "a\nb"] {
|
||||||
|
let user = User {
|
||||||
|
name: bad.to_owned(),
|
||||||
|
secret: "s".to_owned(),
|
||||||
|
};
|
||||||
|
assert!(render("", &[user]).is_err(), "{bad:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -347,7 +347,11 @@ in
|
||||||
# needs access to the rest of c0re's state — that separation
|
# needs access to the rest of c0re's state — that separation
|
||||||
# means c0re itself needs an explicit carve-out to write there.
|
# means c0re itself needs an explicit carve-out to write there.
|
||||||
ProtectSystem = "strict";
|
ProtectSystem = "strict";
|
||||||
ReadWritePaths = [ "/var/lib/hive-gateway/conf" ];
|
# `-` so a host without the squid users directory still starts the daemon.
|
||||||
|
ReadWritePaths = [
|
||||||
|
"/var/lib/hive-gateway/conf"
|
||||||
|
"-${builtins.dirOf config.services.hyperhive.squid.usersFile}"
|
||||||
|
];
|
||||||
ProtectKernelTunables = true; # no sysctl writes
|
ProtectKernelTunables = true; # no sysctl writes
|
||||||
ProtectKernelLogs = true; # no /dev/kmsg access needed
|
ProtectKernelLogs = true; # no /dev/kmsg access needed
|
||||||
ProtectControlGroups = true; # cgroup writes go through hive-priv, not c0re directly
|
ProtectControlGroups = true; # cgroup writes go through hive-priv, not c0re directly
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,9 @@ in
|
||||||
# See the LoadCredential in ./default.nix.
|
# See the LoadCredential in ./default.nix.
|
||||||
BAO_CLIENT_CERT = "%d/bao-client.pem";
|
BAO_CLIENT_CERT = "%d/bao-client.pem";
|
||||||
BAO_CLIENT_KEY = "%d/bao-client-key.pem";
|
BAO_CLIENT_KEY = "%d/bao-client-key.pem";
|
||||||
|
# Where `workers::squid_users` renders the hashes of this hive's agents' proxy
|
||||||
|
# credentials for squid; it reads them from the store under the identity above.
|
||||||
|
HIVE_SQUID_USERS_FILE = config.services.hyperhive.squid.usersFile;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (haveBaoClientIdentity && baoDeploy.serverCaFile != null) {
|
// lib.optionalAttrs (haveBaoClientIdentity && baoDeploy.serverCaFile != null) {
|
||||||
# Absent means the system trust store — right for a deployment with a real
|
# Absent means the system trust store — right for a deployment with a real
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ let
|
||||||
|
|
||||||
allowed = lib.unique (cfg.allowedDomains ++ [ cfg.selfcheck.canary ]);
|
allowed = lib.unique (cfg.allowedDomains ++ [ cfg.selfcheck.canary ]);
|
||||||
|
|
||||||
|
usersDir = builtins.dirOf cfg.usersFile;
|
||||||
|
|
||||||
# Evaluated top to bottom, first match wins. The three destination denies
|
# Evaluated top to bottom, first match wins. The three destination denies
|
||||||
# sit before the only allow: squid resolves names on the host, so a name
|
# sit before the only allow: squid resolves names on the host, so a name
|
||||||
# that answers 127.0.0.1, 169.254.169.254 or a bridge address would
|
# that answers 127.0.0.1, 169.254.169.254 or a bridge address would
|
||||||
|
|
@ -136,6 +138,17 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
usersFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "/var/lib/hive-squid/users";
|
||||||
|
description = ''
|
||||||
|
The `<user>:<hash>` file hive-c0re renders from the credentials in the
|
||||||
|
swarm secret store: one line per agent of this hive and one for the
|
||||||
|
self-check, as SHA-512 crypt hashes. Created empty, owned by `hive-core`
|
||||||
|
and readable by the `squid` group. Squid doesn't read it yet.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
selfcheck.canary = lib.mkOption {
|
selfcheck.canary = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = hyperhiveCfg.swarm.forge.domain;
|
default = hyperhiveCfg.swarm.forge.domain;
|
||||||
|
|
@ -167,6 +180,13 @@ in
|
||||||
configText = squidConf;
|
configText = squidConf;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# The directory is setgid so the file hive-c0re renames into it is the
|
||||||
|
# squid group's.
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${usersDir} 2750 hive-core squid - -"
|
||||||
|
"f ${cfg.usersFile} 0640 hive-core squid - -"
|
||||||
|
];
|
||||||
|
|
||||||
systemd.services.squid = {
|
systemd.services.squid = {
|
||||||
serviceConfig = sandbox // {
|
serviceConfig = sandbox // {
|
||||||
User = "squid";
|
User = "squid";
|
||||||
|
|
|
||||||
|
|
@ -505,6 +505,21 @@ let
|
||||||
&& builtins.elem "cache deny all" lines
|
&& builtins.elem "cache deny all" lines
|
||||||
&& lib.last (lib.filter (l: l != "") lines) == "http_access deny all";
|
&& lib.last (lib.filter (l: l != "") lines) == "http_access deny all";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# The file hive-c0re renders exists, owned by it and readable by squid's
|
||||||
|
# group, from boot; hive-c0re gets its path only on a host with a store
|
||||||
|
# identity.
|
||||||
|
name = "squid's users file exists for hive-c0re to write, and hive-c0re is told where it is";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
users = hiveOtel.services.hyperhive.squid.usersFile;
|
||||||
|
rules = hiveOtel.systemd.tmpfiles.rules;
|
||||||
|
in
|
||||||
|
users == "/var/lib/hive-squid/users"
|
||||||
|
&& builtins.elem "d /var/lib/hive-squid 2750 hive-core squid - -" rules
|
||||||
|
&& builtins.elem "f ${users} 0640 hive-core squid - -" rules
|
||||||
|
&& hiveOtelAllLocal.systemd.services.hive-c0re.environment.HIVE_SQUID_USERS_FILE == users;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
name = "squid's port is open on the bridge interface and on no other";
|
name = "squid's port is open on the bridge interface and on no other";
|
||||||
ok =
|
ok =
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue