Skip to main content

sysknife_daemon/actions/
sysctl.rs

1//! sysctl kernel-parameter actions.
2//!
3//! `GetSysctl` is a read-only query of one key (or the full table); `SetSysctl`
4//! changes a kernel parameter at runtime **and** persists it for the next boot.
5//!
6//! The read runs `sysctl` directly (the daemon is root). The write goes through
7//! the root-owned helper `/usr/lib/sysknife/sysctl-edit`, which applies the
8//! value with `sysctl -w` and rewrites the `/etc/sysctl.d/60-sysknife.conf`
9//! drop-in idempotently — a narrow sudoers grant, not a bare `sysctl` grant, so
10//! the daemon cannot use `sysctl -p <arbitrary-file>` to load an attacker file.
11
12use super::{command_mechanism, ActionSpec};
13use sysknife_types::RiskLevel;
14
15pub fn specs() -> Vec<ActionSpec> {
16    vec![
17        get_sysctl(Some("net.ipv4.ip_forward")),
18        set_sysctl("net.ipv4.ip_forward", "1"),
19    ]
20}
21
22/// Read one kernel parameter (`sysctl -- <key>`) or, when `key` is `None`, the
23/// entire table (`sysctl -a`). Read-only.
24///
25/// The `--` guard means a key can never be reparsed as an option even though
26/// the validator already forbids a leading dash.
27pub fn get_sysctl(key: Option<&str>) -> ActionSpec {
28    let args = match key {
29        Some(k) => vec!["--".to_string(), k.to_string()],
30        None => vec!["-a".to_string()],
31    };
32    ActionSpec {
33        action_name: "GetSysctl",
34        mechanism: command_mechanism("sysctl", args),
35        risk_level: RiskLevel::Low,
36        reboot_required: false,
37        rollback_available: false,
38    }
39}
40
41/// Set and persist a kernel parameter via the scoped helper.
42pub fn set_sysctl(key: &str, value: &str) -> ActionSpec {
43    ActionSpec {
44        action_name: "SetSysctl",
45        mechanism: command_mechanism(
46            "sudo",
47            [
48                "/usr/lib/sysknife/sysctl-edit",
49                "--key",
50                key,
51                "--value",
52                value,
53            ],
54        ),
55        risk_level: RiskLevel::High,
56        reboot_required: false,
57        rollback_available: false,
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use crate::actions::ActionMechanism;
65
66    fn args_of(spec: &ActionSpec) -> (&'static str, Vec<String>) {
67        match &spec.mechanism {
68            ActionMechanism::Command { program, args } => (program, args.clone()),
69            other => panic!("expected Command, got {other:?}"),
70        }
71    }
72
73    #[test]
74    fn get_one_key_uses_dash_dash_guard() {
75        let (program, args) = args_of(&get_sysctl(Some("net.ipv4.ip_forward")));
76        assert_eq!(program, "sysctl");
77        assert_eq!(args, vec!["--", "net.ipv4.ip_forward"]);
78    }
79
80    #[test]
81    fn get_all_uses_minus_a() {
82        let (_, args) = args_of(&get_sysctl(None));
83        assert_eq!(args, vec!["-a"]);
84    }
85
86    #[test]
87    fn set_delegates_to_scoped_helper() {
88        let spec = set_sysctl("vm.swappiness", "10");
89        let (program, args) = args_of(&spec);
90        assert_eq!(program, "sudo");
91        assert_eq!(
92            args,
93            vec![
94                "/usr/lib/sysknife/sysctl-edit",
95                "--key",
96                "vm.swappiness",
97                "--value",
98                "10"
99            ]
100        );
101        assert_eq!(spec.risk_level, RiskLevel::High);
102    }
103}