Watch
0
0
Fork
You've already forked hyperhive
0

swarm-authelia: run in its own network namespace

Refs #4815
This commit is contained in:
atlas 2026-10-21 21:26:13 +02:00 • committed by mara
commit 220b51d0a3

View file

@ -11,7 +11,8 @@
//! with root/`CAP_CHOWN`/a shared group (all examined and rejected — see
//! `swarmctl/README.md`'s own identical analysis of this same file).
//!
//! Listens on loopback; the authelia vhost proxies `/bridge/` to it, so a
//! Listens on a unix socket (`SWARM_AUTHELIA_BRIDGE_SOCKET`) in a directory
//! the host's nginx shares; the authelia vhost proxies `/bridge/` to it, so a
//! `swarm-controller` on any host reaches it at the same address.
//! Bearer-authenticated via authelia's own OIDC token introspection (RFC
//! 7662), and admits only `swarm-controller`'s **existing** machine-client
@ -45,7 +46,7 @@ use swarm_authelia_bridge_sock::{AgentIdentity, BridgeRequest, BridgeResponse};
/// `swarm-controller`'s (the token being checked). Set by the nix module
/// that provisions this bridge's own client alongside authelia.
struct Config {
bind: String,
socket: std::path::PathBuf,
users_file: std::path::PathBuf,
authelia_bin: std::path::PathBuf,
introspection_url: String,
@ -60,7 +61,7 @@ struct Config {
impl Config {
fn from_env() -> Result<Self> {
Ok(Self {
bind: env_var("SWARM_AUTHELIA_BRIDGE_BIND")?,
socket: env_var("SWARM_AUTHELIA_BRIDGE_SOCKET")?.into(),
users_file: env_var("SWARM_AUTHELIA_BRIDGE_USERS_FILE")?.into(),
authelia_bin: env_var("SWARM_AUTHELIA_BRIDGE_AUTHELIA_BIN")?.into(),
introspection_url: env_var("SWARM_AUTHELIA_BRIDGE_INTROSPECTION_URL")?,
@ -116,7 +117,7 @@ async fn main() -> Result<()> {
.init();
let config = Config::from_env()?;
let bind = config.bind.clone();
let socket = config.socket.clone();
let state = Arc::new(AppState {
config,
http: reqwest::Client::new(),
@ -127,15 +128,32 @@ async fn main() -> Result<()> {
.route("/requests", post(handle_request))
.with_state(state);
let listener = tokio::net::TcpListener::bind(&bind)
.await
.with_context(|| format!("binding {bind}"))?;
tracing::info!(%bind, "swarm-authelia-bridge listening");
let listener = bind_socket(&socket)?;
tracing::info!(socket = %socket.display(), "swarm-authelia-bridge listening");
axum::serve(listener, app)
.await
.context("serving swarm-authelia-bridge")
}
/// Bind the listening socket at `path`, replacing a stale one a crash left
/// behind (it would otherwise fail `bind` with `EADDRINUSE`).
///
/// The mode is `0660`: `connect(2)` needs write permission, and the
/// directory's group decides who can reach the socket at all.
fn bind_socket(path: &std::path::Path) -> Result<tokio::net::UnixListener> {
use std::os::unix::fs::PermissionsExt as _;
match std::fs::remove_file(path) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e).with_context(|| format!("removing stale {}", path.display())),
}
let listener = tokio::net::UnixListener::bind(path)
.with_context(|| format!("binding {}", path.display()))?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o660))
.with_context(|| format!("setting the mode of {}", path.display()))?;
Ok(listener)
}
/// The single endpoint — body is the wire-crate's [`BridgeRequest`]
/// verbatim (JSON), not a per-operation REST route. That is what let the
/// second operation arrive as a match arm rather than as another route
@ -448,7 +466,8 @@ async fn generate_password(bin: &std::path::Path) -> Result<String> {
#[cfg(test)]
mod tests {
use super::{
BridgeRequest, Config, Refusal, StatusCode, classify, handle, introspect::Verdict, respond,
BridgeRequest, Config, Refusal, StatusCode, bind_socket, classify, handle,
introspect::Verdict, respond,
};
const CONTROLLER: &str = "swarm-controller";
@ -565,7 +584,7 @@ mod tests {
)
.unwrap();
Config {
bind: String::new(),
socket: std::path::PathBuf::new(),
users_file,
authelia_bin: std::path::PathBuf::new(),
introspection_url: String::new(),
@ -576,6 +595,21 @@ mod tests {
}
}
#[tokio::test]
async fn the_socket_replaces_a_stale_one_and_is_group_only() {
use std::os::unix::fs::PermissionsExt as _;
let dir = std::env::temp_dir().join(format!("bridge-sock-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("bridge.sock");
// Dropping a listener leaves its file behind: the state a crash leaves.
drop(bind_socket(&path).unwrap());
assert!(path.exists(), "control: the stale socket is on disk");
let _listener = bind_socket(&path).unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o660);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn the_operator_list_is_the_operator_groups_members() {
let config = config_with_one_operator();