package main // The sweep (novox/hq ADR 0415, issue 1017): ssh-client owns ~/.ssh, so it keeps it to what it knows. // Every path it does not know is moved, whole, into ~/.local/state/ssh-client/retired//, // keeping its path relative to ~/.ssh. Nothing is deleted. A key (private, public or a certificate), // at any depth and whatever its name, and authorized_keys are never moved (design 29 §3: moving one // can lock the operator out); a folder holding a key is gone into rather than moved whole. A socket // is ssh's own runtime and stays, and so do the files ssh and sshd read by themselves (SSHReads). A // path changed in the last Settle is being written (a temporary file of atomicWrite or ssh-keygen) and // waits for the next sweep. The process sweeps when it starts and every KeyRound (main.go); what the // last sweep did, or why it failed, is kept beside the retired folder for ssh_client_check (issue 1020). import ( "encoding/json" "fmt" "io/fs" "os" "path/filepath" "regexp" "sort" "strings" "syscall" "time" ) // Declared are the paths under ~/.ssh this module declares (module.json) or its own process makes // (keykeeper.go). A test checks the manifest against this list. var Declared = map[string]bool{ "config": true, "known_hosts": true, "config.d": true, "config.d/00-mesh": true, "config.d/10-forge": true, KeyFile: true, KeyFile + ".pub": true, "authorized_keys": true, } // SSHReads are the files ssh and sshd read in ~/.ssh by themselves, though this module writes none of // them (ssh(1), sshd(8) FILES): moving one loses a trust or a login's setup, or locks the operator out. var SSHReads = map[string]bool{ "authorized_keys2": true, // sshd's second AuthorizedKeysFile by default "known_hosts2": true, // ssh's second UserKnownHostsFile by default "rc": true, // run by sshd at every login "environment": true, // read by sshd at login when PermitUserEnvironment allows } // known is whether rel, relative to ~/.ssh, is a path this module declares or one ssh reads itself. func known(rel string) bool { return Declared[rel] || SSHReads[rel] } // copyOf is a copy of authorized_keys or known_hosts (authorized_keys.revoked-*, known_hosts.old): it // carries key lines but is no key, and sshd and ssh never read it. It is matched on a name, at any depth. var copyOf = regexp.MustCompile(`^(authorized_keys|known_hosts)[^/]+$`) // RetiredDir is where the sweep moves what it does not know, under the account's home. const RetiredDir = ".local/state/ssh-client/retired" // LastSweepFile is where each sweep writes what it did, or why it failed, under the account's home: // the sweep runs in the background, so a failure that is only logged is quiet (issue 1020). const LastSweepFile = ".local/state/ssh-client/last-sweep.json" // Moved is one path a sweep moved out of ~/.ssh. type Moved struct { From string `json:"from"` To string `json:"to"` // Missing is set by Retired when nothing lies at To: the sweep listed the move before making it and // stopped between the two. Missing bool `json:"missing,omitempty"` } // Sweep is what one sweep did: what it moved, and what it left because it could not tell what it is. type Sweep struct { At string `json:"at"` Moved []Moved `json:"moved"` Left []string `json:"left,omitempty"` } // LastSweep is what the last sweep did, as LastSweepFile holds it. type LastSweep struct { At string `json:"at"` Moved int `json:"moved"` Left []string `json:"left,omitempty"` Error string `json:"error,omitempty"` } // Sweep moves every path under ~/.ssh this module does not know into a retired folder named by now, // and writes beside that folder the list of what it moved (