Skip to main content

sysknife_daemon/actions/
ssh.rs

1use super::{command_mechanism, ActionMechanism, ActionSpec};
2use sysknife_types::RiskLevel;
3
4pub fn specs() -> Vec<ActionSpec> {
5    vec![
6        get_authorized_keys("alice"),
7        add_authorized_key("alice", "ssh-ed25519 AAAA..."),
8        remove_authorized_key("alice", "ssh-ed25519 AAAA..."),
9        set_sshd_option("PermitRootLogin", "prohibit-password"),
10    ]
11}
12
13/// Installed path of the privileged sshd-option helper script.
14/// See `packaging/sysknife-sshd-option-edit` and the matching NOPASSWD grant in
15/// `packaging/sysknife-sudoers`.
16const SSHD_OPTION_HELPER: &str = "/usr/lib/sysknife/sshd-option-edit";
17
18/// Set an allowlisted sshd option via a drop-in under
19/// `/etc/ssh/sshd_config.d/`, validated with `sshd -t` and applied by reloading
20/// the ssh service.
21///
22/// Risk: High. A misconfigured sshd can lock out remote access; the helper
23/// gates every change on `sshd -t` and rolls back on failure. `option` and
24/// `value` are checked against a fixed allowlist by both the daemon and the
25/// helper — this is deliberately NOT an arbitrary `sshd_config` editor.
26pub fn set_sshd_option(option: &str, value: &str) -> ActionSpec {
27    ActionSpec {
28        action_name: "SetSshdOption",
29        mechanism: command_mechanism(
30            "sudo",
31            [SSHD_OPTION_HELPER, "--option", option, "--value", value],
32        ),
33        risk_level: RiskLevel::High,
34        reboot_required: false,
35        rollback_available: false,
36    }
37}
38
39pub fn get_authorized_keys(username: &str) -> ActionSpec {
40    ActionSpec {
41        action_name: "GetAuthorizedKeys",
42        mechanism: command_mechanism("cat", [&format!("/home/{username}/.ssh/authorized_keys")]),
43        risk_level: RiskLevel::Low,
44        reboot_required: false,
45        rollback_available: false,
46    }
47}
48
49pub fn add_authorized_key(username: &str, public_key: &str) -> ActionSpec {
50    let keys_path = format!("/home/{username}/.ssh/authorized_keys");
51    // Use sudo sh -c with grep to check idempotency: only append if not already present.
52    // sudo is required because the daemon runs as the sysknife system user, which has
53    // no write permission to user home directories (files are 600 owned by the target user).
54    let script = format!(
55        "grep -Fxq '{key}' '{path}' 2>/dev/null || echo '{key}' >> '{path}'",
56        key = public_key,
57        path = keys_path,
58    );
59
60    ActionSpec {
61        action_name: "AddAuthorizedKey",
62        mechanism: ActionMechanism::Command {
63            program: "sudo",
64            args: vec!["sh".to_string(), "-c".to_string(), script],
65        },
66        risk_level: RiskLevel::High,
67        reboot_required: false,
68        rollback_available: false,
69    }
70}
71
72pub fn remove_authorized_key(username: &str, public_key: &str) -> ActionSpec {
73    let keys_path = format!("/home/{username}/.ssh/authorized_keys");
74    // Use sudo sed to delete the exact matching line.
75    // sudo is required for the same reason as add_authorized_key.
76    let script = format!(
77        "sed -i '\\|^{key}$|d' '{path}'",
78        key = public_key,
79        path = keys_path,
80    );
81
82    ActionSpec {
83        action_name: "RemoveAuthorizedKey",
84        mechanism: ActionMechanism::Command {
85            program: "sudo",
86            args: vec!["sh".to_string(), "-c".to_string(), script],
87        },
88        // Revoking an authorized key is access-control + lockout-capable (remove
89        // the wrong/only key and you lose SSH access) and cannot be rolled back
90        // → High, symmetric with AddAuthorizedKey.
91        risk_level: RiskLevel::High,
92        reboot_required: false,
93        rollback_available: false,
94    }
95}