Skip to main content

sysknife_daemon/actions/
certbot.rs

1//! certbot / ACME certificate actions.
2//!
3//! `GetCertificates` (`certbot certificates`) is read-only. `ObtainCertificate`
4//! and `RenewCertificates` run `certbot` directly (scoped argv). Every input is
5//! validated before it reaches `certbot`.
6//!
7//! Network-gated: `certbot` is not on the base cloud image and ACME issuance
8//! needs outbound network + a DNS/HTTP challenge, so these could not be
9//! live-validated in the sandbox — only the argv construction (unit tests).
10
11use super::{command_mechanism, ActionSpec};
12use sysknife_types::RiskLevel;
13
14pub fn specs() -> Vec<ActionSpec> {
15    vec![
16        get_certificates(),
17        obtain_certificate(
18            &["example.com".to_string()],
19            "admin@example.com",
20            "standalone",
21        ),
22        renew_certificates(),
23    ]
24}
25
26/// List managed certificates (`certbot certificates`). Read-only.
27pub fn get_certificates() -> ActionSpec {
28    ActionSpec {
29        action_name: "GetCertificates",
30        mechanism: command_mechanism("certbot", ["certificates"]),
31        risk_level: RiskLevel::Low,
32        reboot_required: false,
33        rollback_available: false,
34    }
35}
36
37/// Obtain a certificate non-interactively. `domains` is a pre-validated list;
38/// `challenge` is one of `standalone`/`nginx`/`apache` (validated upstream).
39pub fn obtain_certificate(domains: &[String], email: &str, challenge: &str) -> ActionSpec {
40    let mut args = vec![
41        "certbot".to_string(),
42        "certonly".to_string(),
43        "--non-interactive".to_string(),
44        "--agree-tos".to_string(),
45        format!("--{challenge}"),
46        "-m".to_string(),
47        email.to_string(),
48    ];
49    for d in domains {
50        args.push("-d".to_string());
51        args.push(d.clone());
52    }
53    ActionSpec {
54        action_name: "ObtainCertificate",
55        mechanism: command_mechanism("sudo", args),
56        risk_level: RiskLevel::High,
57        reboot_required: false,
58        rollback_available: false,
59    }
60}
61
62/// Renew all due certificates (`certbot renew`).
63pub fn renew_certificates() -> ActionSpec {
64    ActionSpec {
65        action_name: "RenewCertificates",
66        mechanism: command_mechanism("sudo", ["certbot", "renew"]),
67        risk_level: RiskLevel::Medium,
68        reboot_required: false,
69        rollback_available: false,
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::actions::ActionMechanism;
77
78    fn args_of(spec: &ActionSpec) -> (&'static str, Vec<String>) {
79        match &spec.mechanism {
80            ActionMechanism::Command { program, args } => (program, args.clone()),
81            other => panic!("expected Command, got {other:?}"),
82        }
83    }
84
85    #[test]
86    fn get_certs_read_only() {
87        let (program, args) = args_of(&get_certificates());
88        assert_eq!(program, "certbot");
89        assert_eq!(args, vec!["certificates"]);
90        assert_eq!(get_certificates().risk_level, RiskLevel::Low);
91    }
92
93    #[test]
94    fn obtain_builds_multi_domain_argv() {
95        let (program, args) = args_of(&obtain_certificate(
96            &["a.example.com".to_string(), "b.example.com".to_string()],
97            "ops@example.com",
98            "standalone",
99        ));
100        assert_eq!(program, "sudo");
101        assert_eq!(
102            args,
103            vec![
104                "certbot",
105                "certonly",
106                "--non-interactive",
107                "--agree-tos",
108                "--standalone",
109                "-m",
110                "ops@example.com",
111                "-d",
112                "a.example.com",
113                "-d",
114                "b.example.com",
115            ]
116        );
117        assert_eq!(
118            obtain_certificate(&["x.io".to_string()], "e@x.io", "nginx").risk_level,
119            RiskLevel::High
120        );
121    }
122
123    #[test]
124    fn renew_shape() {
125        let (program, args) = args_of(&renew_certificates());
126        assert_eq!(program, "sudo");
127        assert_eq!(args, vec!["certbot", "renew"]);
128    }
129}