swarm-archive: back up every agent's state to an append-only restic repository per agent
swarm-archive serves restic repositories with rclone, append-only, one login and one repository per agent. swarm-controller writes each placed agent's login and restic password to the store, replaces them on a move and removes them when the agent is destroyed. Every hive backs its agents up hourly under its own store identity. The archive host prunes by retention.
This commit is contained in:
parent
409e7ee5a0
commit
306aae0378
2 changed files with 2214 additions and 5 deletions
124
swarm-secret-client/src/archive.rs
Normal file
124
swarm-secret-client/src/archive.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
//! The archive agreement: where `swarm-controller` keeps each agent's archive
|
||||
//! credentials, and where its hive and the archive host read them.
|
||||
//!
|
||||
//! Three paths per agent, all written by the controller:
|
||||
//!
|
||||
//! - [`grant_path`], `swarm/hives/<hive>/archive/<agent>`: the REST password the
|
||||
//! agent's hive logs in to the archive with, and the restic repository
|
||||
//! password. The hive's own `swarm/hives/<hive>/*` stanza reads it, and only
|
||||
//! the hive that places the agent holds one.
|
||||
//! - [`user_path`], `swarm/services/archive/users/<agent>`: the htpasswd line
|
||||
//! the archive host renders its users file from. A hash, never the password.
|
||||
//! - [`restic_path`], `swarm/services/archive/restic/<agent>`: the same restic
|
||||
//! password, kept where it outlives a move. The archive host reads it to
|
||||
//! prune the repository.
|
||||
//!
|
||||
//! The nix half is `swarm-bao.nix`'s archive grants and `swarm-archive.nix`.
|
||||
|
||||
use crate::{
|
||||
Error,
|
||||
path::{Kind, checked_segment, principal_prefix},
|
||||
};
|
||||
|
||||
/// The service segment under `swarm/services/`.
|
||||
pub const SERVICE: &str = "archive";
|
||||
|
||||
/// The field of [`grant_path`] holding the REST password.
|
||||
pub const REST_PASSWORD: &str = "rest-password";
|
||||
|
||||
/// The field of [`grant_path`] holding the restic repository password.
|
||||
pub const RESTIC_PASSWORD: &str = "restic-password";
|
||||
|
||||
/// The field of [`restic_path`] holding the restic repository password.
|
||||
pub const PASSWORD: &str = "password";
|
||||
|
||||
/// The field of [`user_path`] holding the htpasswd line.
|
||||
pub const HTPASSWD: &str = "htpasswd";
|
||||
|
||||
/// The directory holding every [`user_path`], to list.
|
||||
///
|
||||
/// # Errors
|
||||
/// Never in practice: the segments are constants.
|
||||
pub fn users_prefix() -> Result<String, Error> {
|
||||
Ok(format!(
|
||||
"{}/users",
|
||||
principal_prefix(Kind::Service, SERVICE)?
|
||||
))
|
||||
}
|
||||
|
||||
/// Where the archive host reads `agent`'s htpasswd line.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `agent` contains anything but `[A-Za-z0-9_-]`.
|
||||
pub fn user_path(agent: &str) -> Result<String, Error> {
|
||||
checked_segment(Kind::Agent.label(), agent)?;
|
||||
Ok(format!("{}/{agent}", users_prefix()?))
|
||||
}
|
||||
|
||||
/// Where the archive host reads `agent`'s restic password.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `agent` contains anything but `[A-Za-z0-9_-]`.
|
||||
pub fn restic_path(agent: &str) -> Result<String, Error> {
|
||||
checked_segment(Kind::Agent.label(), agent)?;
|
||||
Ok(format!(
|
||||
"{}/restic/{agent}",
|
||||
principal_prefix(Kind::Service, SERVICE)?
|
||||
))
|
||||
}
|
||||
|
||||
/// The directory holding every [`grant_path`] of `hive`, to list.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `hive` contains anything but `[A-Za-z0-9_-]`.
|
||||
pub fn grants_prefix(hive: &str) -> Result<String, Error> {
|
||||
Ok(format!("{}/archive", principal_prefix(Kind::Hive, hive)?))
|
||||
}
|
||||
|
||||
/// Where `hive` reads the credentials of `agent`, which it places.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `hive` or `agent` contains anything but
|
||||
/// `[A-Za-z0-9_-]`, which keeps one name from addressing another's secret.
|
||||
pub fn grant_path(hive: &str, agent: &str) -> Result<String, Error> {
|
||||
checked_segment(Kind::Agent.label(), agent)?;
|
||||
Ok(format!("{}/{agent}", grants_prefix(hive)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_paths_are_the_ones_the_store_grants() {
|
||||
// Spelled out: `swarm-bao.nix` grants each pattern.
|
||||
assert_eq!(
|
||||
user_path("atlas").expect("a plain name"),
|
||||
"swarm/services/archive/users/atlas"
|
||||
);
|
||||
assert_eq!(
|
||||
restic_path("atlas").expect("a plain name"),
|
||||
"swarm/services/archive/restic/atlas"
|
||||
);
|
||||
assert_eq!(
|
||||
grant_path("alpha", "atlas").expect("plain names"),
|
||||
"swarm/hives/alpha/archive/atlas"
|
||||
);
|
||||
assert_eq!(
|
||||
users_prefix().expect("constant"),
|
||||
"swarm/services/archive/users"
|
||||
);
|
||||
assert_eq!(
|
||||
grants_prefix("alpha").expect("a plain name"),
|
||||
"swarm/hives/alpha/archive"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_cannot_address_another_principals_credentials() {
|
||||
assert!(grant_path("alpha", "atlas/../argus").is_err());
|
||||
assert!(grant_path("alpha/../beta", "atlas").is_err());
|
||||
assert!(user_path("../restic/atlas").is_err());
|
||||
assert!(restic_path("").is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
//! needs only the provider and the store links nothing else.
|
||||
|
||||
pub mod acp;
|
||||
pub mod archive;
|
||||
pub mod attic;
|
||||
pub mod backend_env;
|
||||
pub mod builder;
|
||||
|
|
|
|||
Loading…
Reference in a new issue