P2 slice 7: backups and restore (D-30, D-31)
The server backs each book up itself: a per-book git archive, pushed fast-forward only to a git remote and to a folder, when the books open, after a quiet spell, before they close and on request; every commit, push and drill is a record in ops/backup.jsonl. Drills clone each target and verify the chain, the blobs, the vault and the head, weekly and after a failure. `wanderbooks books restore` previews, then restores beside or (with --replace) over, never moving books something holds. The archive refuses anything but a plain bare repository and pins git against the owner's configuration. books.backup and books.back_up_now for the agent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
1 parent
5bc0633cdd
commit
019d8d76a3
29 files changed
+6443
-136
No files matched your search
@@ -9,8 +9,11 @@
|
||||
//! 1. The host's environment is a fixed list: PATH (fixed), HOME (from
|
||||
//! the account's record, never the opener's), and XDG_RUNTIME_DIR
|
||||
//! and DBUS_SESSION_BUS_ADDRESS only when they point at this
|
||||
//! account's own private runtime folder. Nothing else passes (a
|
||||
//! scope's own identifiers, such as INVOCATION_ID, are systemd's).
|
||||
//! account's own private runtime folder, and SSH_AUTH_SOCK only when
|
||||
//! it names a socket this account owns (the owner's ssh agent, so the
|
||||
//! server's backups reach a private git repository, D-30). Nothing
|
||||
//! else passes (a scope's own identifiers, such as INVOCATION_ID, are
|
||||
//! systemd's).
|
||||
//! 2. Its standard input and output are null, its errors go to
|
||||
//! run/host.log (0600); it starts a new session with umask 077. Every
|
||||
//! other descriptor is closed (std opens them close-on-exec, and the
|
||||
@@ -112,8 +115,20 @@ fn session_bus(runtime: &Path) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The host's environment (INVARIANT 1).
|
||||
fn host_env(home: &Path) -> Vec<(String, std::ffi::OsString)> {
|
||||
/// The ssh agent's socket named in `named`, if it is a socket this
|
||||
/// account owns at an absolute path (INVARIANT 1).
|
||||
fn own_agent(named: Option<&std::ffi::OsStr>) -> Option<std::ffi::OsString> {
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
let path = Path::new(named?);
|
||||
(path.is_absolute()
|
||||
&& std::fs::symlink_metadata(path)
|
||||
.is_ok_and(|m| m.file_type().is_socket() && m.uid() == own_uid()))
|
||||
.then(|| path.as_os_str().to_os_string())
|
||||
}
|
||||
|
||||
/// The host's environment (INVARIANT 1), given the opener's
|
||||
/// `SSH_AUTH_SOCK`.
|
||||
fn host_env(home: &Path, ssh_agent: Option<&std::ffi::OsStr>) -> Vec<(String, std::ffi::OsString)> {
|
||||
let mut env = vec![
|
||||
("PATH".to_string(), PATH.into()),
|
||||
("HOME".to_string(), home.as_os_str().to_os_string()),
|
||||
@@ -124,6 +139,9 @@ fn host_env(home: &Path) -> Vec<(String, std::ffi::OsString)> {
|
||||
}
|
||||
env.push(("XDG_RUNTIME_DIR".into(), runtime.into_os_string()));
|
||||
}
|
||||
if let Some(agent) = own_agent(ssh_agent) {
|
||||
env.push(("SSH_AUTH_SOCK".into(), agent));
|
||||
}
|
||||
env
|
||||
}
|
||||
|
||||
@@ -199,7 +217,10 @@ pub fn start_host(root: Option<&Path>, knobs: &Knobs) -> Result<(), String> {
|
||||
};
|
||||
c.args(&args)
|
||||
.env_clear()
|
||||
.envs(host_env(&home))
|
||||
.envs(host_env(
|
||||
&home,
|
||||
std::env::var_os("SSH_AUTH_SOCK").as_deref(),
|
||||
))
|
||||
.current_dir("/")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
@@ -357,12 +378,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_host_environment_carries_nothing_else() {
|
||||
let env = host_env(Path::new("/home/someone"));
|
||||
let env = host_env(Path::new("/home/someone"), None);
|
||||
let names: Vec<&str> = env.iter().map(|(n, _)| n.as_str()).collect();
|
||||
assert_eq!(&names[..2], ["PATH", "HOME"]);
|
||||
for n in &names[2..] {
|
||||
assert!(
|
||||
["XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS"].contains(n),
|
||||
[
|
||||
"XDG_RUNTIME_DIR",
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
"SSH_AUTH_SOCK"
|
||||
]
|
||||
.contains(n),
|
||||
"{n}"
|
||||
);
|
||||
}
|
||||
@@ -370,6 +396,54 @@ mod tests {
|
||||
assert_eq!(env[1].1, "/home/someone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_an_own_ssh_agent_socket_passes() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let sock = dir.path().join("agent.sock");
|
||||
let _listener = std::os::unix::net::UnixListener::bind(&sock).unwrap();
|
||||
assert_eq!(
|
||||
own_agent(Some(sock.as_os_str())),
|
||||
Some(sock.clone().into_os_string())
|
||||
);
|
||||
// It reaches the host's environment, once.
|
||||
let env = host_env(Path::new("/home/someone"), Some(sock.as_os_str()));
|
||||
let agents: Vec<_> = env.iter().filter(|(n, _)| n == "SSH_AUTH_SOCK").collect();
|
||||
assert_eq!(agents.len(), 1);
|
||||
assert_eq!(agents[0].1, sock.as_os_str());
|
||||
// A link to it is not it (whoever made the link chose where it
|
||||
// points).
|
||||
let link = dir.path().join("link.sock");
|
||||
std::os::unix::fs::symlink(&sock, &link).unwrap();
|
||||
assert_eq!(own_agent(Some(link.as_os_str())), None);
|
||||
let env = host_env(Path::new("/home/someone"), Some(link.as_os_str()));
|
||||
assert!(env.iter().all(|(n, _)| n != "SSH_AUTH_SOCK"));
|
||||
// Named relative to here: not taken, even where it leads to one.
|
||||
let here = std::env::current_dir().unwrap();
|
||||
let up = here.components().count() - 1;
|
||||
let relative: std::path::PathBuf = std::iter::repeat_n("..", up)
|
||||
.collect::<std::path::PathBuf>()
|
||||
.join(sock.strip_prefix("/").unwrap());
|
||||
assert!(relative.exists() && relative.is_relative(), "{relative:?}");
|
||||
assert_eq!(own_agent(Some(relative.as_os_str())), None);
|
||||
// Another account's socket (the system's bus, owned by root, where
|
||||
// there is one): not ours.
|
||||
let system = Path::new("/run/dbus/system_bus_socket");
|
||||
if system.exists() && own_uid() != 0 {
|
||||
assert_eq!(own_agent(Some(system.as_os_str())), None);
|
||||
}
|
||||
let file = dir.path().join("plain");
|
||||
std::fs::write(&file, "x").unwrap();
|
||||
for bad in [
|
||||
file.as_os_str(),
|
||||
"agent.sock".as_ref(),
|
||||
"/nonexistent/agent.sock".as_ref(),
|
||||
"".as_ref(),
|
||||
] {
|
||||
assert_eq!(own_agent(Some(bad)), None, "{bad:?}");
|
||||
}
|
||||
assert_eq!(own_agent(None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn this_executable_is_a_file_on_disk() {
|
||||
let exe = this_executable().unwrap();
|
||||
|
||||
Reference in new issue
Block a user