parent
ef156d9d23
commit
220b51d0a3
12 changed files with 204 additions and 105 deletions
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue