Skip to main content

isoloom_plan/
secrets.rs

1//! A machine's secrets as files: which secret, at what path, owned by whom, with what mode.
2//! The plan never holds a value: `isoloom run` reads each from its store and hands it to the
3//! target's own mechanism (Compose secrets, a Kubernetes Secret).
4
5use std::fmt;
6
7use isoloom_spec::model::{MachineSecret, SecretSource, Spec};
8use isoloom_spec::syntax::Mode;
9
10/// Where a value file's contents come from.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum ValueSource {
13    /// A secret, read from its store at each start.
14    Secret(String),
15    /// An input, given at each start (`files: [{ path, input }]`).
16    Input(String),
17}
18
19/// One secret file of a machine.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SecretFile {
22    /// The secret, as the top-level `secrets` names it (an input's file is the secret
23    /// `input-<name>` lowering makes of it).
24    pub name: String,
25    /// Where the value comes from.
26    pub source: ValueSource,
27    /// The file's absolute path in the machine.
28    pub path: String,
29    /// Its owner's user id, when the spec sets one (root otherwise).
30    pub uid: Option<u32>,
31    /// Its group id, when the spec sets one (root otherwise).
32    pub gid: Option<u32>,
33    /// Its mode, when the spec sets one ([`DEFAULT_MODE`] in a container, [`VM_DEFAULT_MODE`] on
34    /// a Linux VM otherwise).
35    pub mode: Option<u32>,
36}
37
38/// The mode a container's secret file has when the spec sets none: readable by every user of
39/// the container, as Compose and Kubernetes both default to. A container usually runs one
40/// process as one user, so this is what its image expects.
41pub const DEFAULT_MODE: u32 = 0o444;
42
43/// The mode a Linux VM's secret file has when the spec sets none: readable by its owner only
44/// (root, or the spec's `uid`). A VM has other users (its login account, its services), and a
45/// secret readable by all of them would be one any of them could leak; the process that needs
46/// it runs as root or as the `uid` the spec gives the file. A Windows VM's file has no mode: it
47/// is readable by SYSTEM and Administrators only.
48pub const VM_DEFAULT_MODE: u32 = 0o400;
49
50impl SecretFile {
51    /// The file a machine's `secrets` entry places.
52    #[must_use]
53    pub fn of(spec: &Spec, s: &MachineSecret) -> SecretFile {
54        let m = s.mount();
55        let source = match spec.secrets.get(&m.source) {
56            Some(SecretSource::Input { input }) => ValueSource::Input(input.clone()),
57            _ => ValueSource::Secret(m.source.clone()),
58        };
59        SecretFile {
60            source,
61            name: m.source,
62            path: s.path(),
63            uid: m.uid,
64            gid: m.gid,
65            mode: m.mode.map(Mode::bits),
66        }
67    }
68}
69
70/// Why a target with no machine to deliver to refuses machine `machine`'s secrets.
71pub(crate) fn no_delivery(f: &mut fmt::Formatter<'_>, machine: &str) -> fmt::Result {
72    write!(
73        f,
74        "machine `{machine}` gets `secrets`, and this target has no machine `isoloom run` can hand them to"
75    )
76}
77
78/// Why a target that places a secret's file as root's refuses one with its own owner.
79pub(crate) fn no_owner(f: &mut fmt::Formatter<'_>, machine: &str) -> fmt::Result {
80    write!(
81        f,
82        "a secret of machine `{machine}` sets `uid` or `gid`, and this target places a secret's file as root's"
83    )
84}
85
86#[cfg(test)]
87mod tests {
88    use super::{SecretFile, ValueSource};
89    use crate::testing;
90
91    #[test]
92    fn a_file_from_an_input_is_given_at_each_start_and_one_from_a_secret_read_from_its_store() {
93        let spec = testing::spec(
94            "version: 1\nname: t\ninputs: [FLAG]\nsecrets: { db: { env: DB } }\nmachines:\n  web: { app: { image: nginx }, files: [{ path: /flag, input: FLAG, mode: '0440' }, { path: /run/db, secret: db, uid: 70 }] }\n",
95        );
96        let files: Vec<SecretFile> = spec.machines["web"].secrets.iter().map(|s| SecretFile::of(&spec, s)).collect();
97        let at = |i: usize| files.get(i).map(|f| (f.name.as_str(), &f.source, f.path.as_str(), f.uid, f.mode));
98        assert_eq!(at(0), Some(("input-flag", &ValueSource::Input("FLAG".into()), "/flag", None, Some(0o440))));
99        assert_eq!(
100            at(1),
101            Some(("db", &ValueSource::Secret("db".into()), "/run/db", Some(70), Some(0o400))),
102            "root-only by default"
103        );
104    }
105}