rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

services_hardening_systemd.rs (5411B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::collections::BTreeSet;
      4 
      5 use serde_json::Value;
      6 use sha2::{Digest, Sha256};
      7 
      8 const CONTRACT: &str =
      9     include_str!("../contracts/services_hardening/systemd_qualification.v1.json");
     10 const UNIT: &str = include_str!("../packaging/systemd/rhi@.service");
     11 const VERIFY_SCRIPT: &str = include_str!("../scripts/verify-systemd.sh");
     12 
     13 fn contract() -> Value {
     14     serde_json::from_str(CONTRACT).expect("systemd qualification contract")
     15 }
     16 
     17 fn sha256_hex(bytes: &[u8]) -> String {
     18     Sha256::digest(bytes)
     19         .iter()
     20         .map(|byte| format!("{byte:02x}"))
     21         .collect()
     22 }
     23 
     24 fn validate_unit(source: &str, authority: &Value) -> Result<(), String> {
     25     if source.len() > 16_384 || !source.ends_with('\n') || source.contains(['\0', '\r']) {
     26         return Err("invalid unit bytes".to_owned());
     27     }
     28 
     29     let expected = authority["canonical_lines"]
     30         .as_array()
     31         .ok_or_else(|| "missing canonical lines".to_owned())?
     32         .iter()
     33         .map(|line| {
     34             line.as_str()
     35                 .ok_or_else(|| "invalid canonical line".to_owned())
     36         })
     37         .collect::<Result<Vec<_>, _>>()?;
     38     let actual = source
     39         .lines()
     40         .filter(|line| !line.is_empty())
     41         .collect::<Vec<_>>();
     42 
     43     let forbidden = authority["forbidden_directives"]
     44         .as_array()
     45         .ok_or_else(|| "missing forbidden directives".to_owned())?
     46         .iter()
     47         .map(|directive| {
     48             directive
     49                 .as_str()
     50                 .ok_or_else(|| "invalid forbidden directive".to_owned())
     51         })
     52         .collect::<Result<BTreeSet<_>, _>>()?;
     53     let mut section = "";
     54     let mut sections = Vec::new();
     55     let mut keys = BTreeSet::new();
     56     for line in &actual {
     57         if line.starts_with('[') && line.ends_with(']') {
     58             section = &line[1..line.len() - 1];
     59             sections.push(section);
     60             continue;
     61         }
     62         let (key, _) = line
     63             .split_once('=')
     64             .ok_or_else(|| "invalid directive".to_owned())?;
     65         if section.is_empty() || forbidden.contains(key) {
     66             return Err("forbidden or unscoped directive".to_owned());
     67         }
     68         if !keys.insert(format!("{section}.{key}")) {
     69             return Err("duplicate directive".to_owned());
     70         }
     71     }
     72     if sections != ["Unit", "Service", "Install"] || actual != expected {
     73         return Err("unit differs from canonical contract".to_owned());
     74     }
     75     Ok(())
     76 }
     77 
     78 #[test]
     79 fn systemd_contract_binds_the_exact_fail_closed_unit() {
     80     let authority = contract();
     81     assert_eq!(authority["schema"], "radroots.rhi.systemd-qualification");
     82     assert_eq!(authority["schema_version"], 1);
     83     assert_eq!(authority["service"], "rhi");
     84     assert_eq!(authority["unit_path"], "packaging/systemd/rhi@.service");
     85     assert_eq!(
     86         authority["verification_script"],
     87         "scripts/verify-systemd.sh"
     88     );
     89     assert_eq!(authority["unit_sha256"], sha256_hex(UNIT.as_bytes()));
     90     validate_unit(UNIT, &authority).expect("canonical systemd unit");
     91 
     92     assert_eq!(authority["runtime_posture"]["type"], "simple");
     93     assert_eq!(
     94         authority["runtime_posture"]["readiness"],
     95         "cached_cli_no_sd_notify"
     96     );
     97     assert_eq!(
     98         authority["runtime_posture"]["restartable_exit_codes"],
     99         serde_json::json!([1, 3])
    100     );
    101     assert_eq!(
    102         authority["runtime_posture"]["nonrestartable_exit_codes"],
    103         serde_json::json!([2, 4, 5, 6])
    104     );
    105     assert_eq!(authority["runtime_posture"]["stop_timeout_seconds"], 310);
    106     assert_eq!(authority["verification"]["minimum_systemd_major"], 252);
    107     assert_eq!(authority["verification"]["maximum_exposure_score"], 3.0);
    108     assert_eq!(authority["verification"]["maximum_exposure_percent"], 30);
    109 }
    110 
    111 #[test]
    112 fn systemd_unit_rejects_aliases_environment_duplicates_and_weaker_posture() {
    113     let authority = contract();
    114     let mutations = [
    115         UNIT.replace("ConfigurationDirectory=", "ConfigDirectory="),
    116         format!("{UNIT}Environment=RHI_SECRET=forbidden\n"),
    117         UNIT.replace(
    118             "NoNewPrivileges=yes",
    119             "NoNewPrivileges=yes\nNoNewPrivileges=yes",
    120         ),
    121         UNIT.replace(
    122             "RuntimeDirectoryPreserve=restart",
    123             "RuntimeDirectoryPreserve=yes",
    124         ),
    125         UNIT.replace("CapabilityBoundingSet=\n", ""),
    126     ];
    127     for mutation in mutations {
    128         assert!(validate_unit(&mutation, &authority).is_err());
    129     }
    130 }
    131 
    132 #[test]
    133 fn systemd_verifier_is_linux_only_bounded_and_forge_agnostic() {
    134     for required in [
    135         "systemd 252 or newer is required",
    136         "systemd-analyze verify",
    137         "--offline=yes --threshold=30",
    138         "exact ExecStart was not admitted",
    139     ] {
    140         assert!(VERIFY_SCRIPT.contains(required), "missing `{required}`");
    141     }
    142     for forbidden in ["nix ", "oci", ".github", ".act", "_radroots", "docker"] {
    143         assert!(!VERIFY_SCRIPT.to_ascii_lowercase().contains(forbidden));
    144     }
    145     assert_eq!(
    146         contract()["deferred"],
    147         serde_json::json!([
    148             "compatibility_sensitive_memory_deny_write_execute",
    149             "compatibility_sensitive_system_call_filter",
    150             "resource_limits_step_231",
    151             "integration_wave_step_229",
    152             "rcld_promotion_step_235",
    153             "nix",
    154             "oci",
    155             "deployment",
    156             "production_activation"
    157         ])
    158     );
    159 }