Skip to main content

sysknife_daemon/actions/
containers.rs

1use super::{command_mechanism, ActionMechanism, ActionSpec};
2use sysknife_types::RiskLevel;
3
4pub fn specs() -> Vec<ActionSpec> {
5    vec![
6        list_containers("testuser"),
7        create_container(
8            "testuser",
9            "sysknife-dev",
10            "registry.fedoraproject.org/fedora-toolbox:41",
11        ),
12        start_container("testuser", "sysknife-dev"),
13        stop_container("testuser", "sysknife-dev"),
14        remove_container("testuser", "sysknife-dev"),
15        get_container_info("testuser", "sysknife-dev"),
16    ]
17}
18
19/// Run a Podman command as the target user via `sudo runuser -l`.
20///
21/// Rootless Podman operates against the user's container storage, not a shared
22/// system store. The daemon runs as the `sysknife` system user whose container
23/// namespace is empty; we must switch to `username` to reach their containers.
24/// `runuser -l` establishes a full login context including XDG_RUNTIME_DIR and
25/// the sub-UID/GID ranges required by the kernel namespace code — those env
26/// vars are populated by `pam_systemd` which only fires for a login shell.
27///
28/// **Shell-injection safety:** the `-c "<podman_cmd>"` form passes a string to
29/// `/bin/sh`, so any metacharacter in an interpolated value would be expanded
30/// by the shell. Defence-in-depth:
31///   1. `username` flows through `validated_username` (`[A-Za-z0-9._-]`, no
32///      leading dash, ≤32 bytes).
33///   2. Container `name` and `image` flow through `validated_safe_arg`, which
34///      enforces `[A-Za-z0-9._:/+@-]`, no leading dash, ≤254 bytes — every
35///      shell metacharacter (`;`, `&`, `|`, `$`, backtick, quotes, whitespace,
36///      glob, brace, `\`, etc.) is rejected at the boundary.
37///   3. The format!-interpolated values are wrapped in single quotes so even
38///      a future validator regression cannot escape the surrounding quotes.
39fn podman_as(username: &str, podman_cmd: &str) -> ActionMechanism {
40    ActionMechanism::Command {
41        program: "sudo",
42        args: vec![
43            "runuser".to_string(),
44            "-l".to_string(),
45            username.to_string(),
46            "-c".to_string(),
47            podman_cmd.to_string(),
48        ],
49    }
50}
51
52pub fn list_containers(username: &str) -> ActionSpec {
53    ActionSpec {
54        action_name: "ListContainers",
55        mechanism: command_mechanism(
56            "sudo",
57            [
58                "runuser",
59                "-l",
60                username,
61                "-c",
62                "podman ps --all --format json",
63            ],
64        ),
65        risk_level: RiskLevel::Low,
66        reboot_required: false,
67        rollback_available: false,
68    }
69}
70
71pub fn create_container(username: &str, name: &str, image: &str) -> ActionSpec {
72    let cmd = format!("podman create --name '{}' '{}'", name, image);
73    ActionSpec {
74        action_name: "CreateContainer",
75        mechanism: podman_as(username, &cmd),
76        risk_level: RiskLevel::Medium,
77        reboot_required: false,
78        rollback_available: false,
79    }
80}
81
82pub fn start_container(username: &str, name: &str) -> ActionSpec {
83    let cmd = format!("podman start '{}'", name);
84    ActionSpec {
85        action_name: "StartContainer",
86        mechanism: podman_as(username, &cmd),
87        risk_level: RiskLevel::Medium,
88        reboot_required: false,
89        rollback_available: false,
90    }
91}
92
93pub fn stop_container(username: &str, name: &str) -> ActionSpec {
94    let cmd = format!("podman stop '{}'", name);
95    ActionSpec {
96        action_name: "StopContainer",
97        mechanism: podman_as(username, &cmd),
98        risk_level: RiskLevel::Medium,
99        reboot_required: false,
100        rollback_available: false,
101    }
102}
103
104pub fn remove_container(username: &str, name: &str) -> ActionSpec {
105    let cmd = format!("podman rm '{}'", name);
106    ActionSpec {
107        action_name: "RemoveContainer",
108        mechanism: podman_as(username, &cmd),
109        risk_level: RiskLevel::Medium,
110        reboot_required: false,
111        rollback_available: false,
112    }
113}
114
115pub fn get_container_info(username: &str, name: &str) -> ActionSpec {
116    let cmd = format!("podman inspect '{}'", name);
117    ActionSpec {
118        action_name: "GetContainerInfo",
119        mechanism: podman_as(username, &cmd),
120        risk_level: RiskLevel::Low,
121        reboot_required: false,
122        rollback_available: false,
123    }
124}