nixfleet_cli/commands/
mint_operator_cert.rs

1//! Operator-side helper: mints a clientAuth-EKU X.509 cert from the offline
2//! fleet root CA. Pure offline crypto, no network access.
3
4use std::path::PathBuf;
5
6use anyhow::{Context, Result, bail};
7// Alias avoids clashing with `struct Args` below.
8use clap::Args as ClapArgs;
9
10use crate::{MintOperatorCertArgs, mint_operator_cert};
11
12#[derive(ClapArgs, Debug)]
13#[command(
14    about = "Mint an mTLS client cert for an operator workstation, signed by the offline fleet root CA."
15)]
16pub struct Args {
17    /// Offline fleet root CA cert PEM. Falls back to
18    /// $NIXFLEET_OPERATOR_FLEET_ROOT_CERT_FILE then to
19    /// ~/.config/nixfleet/fleet-root.cert.pem.
20    #[arg(long)]
21    root_cert: Option<PathBuf>,
22
23    /// Offline fleet root CA private key PEM. Falls back to
24    /// $NIXFLEET_OPERATOR_FLEET_ROOT_KEY_FILE then to
25    /// ~/.config/nixfleet/fleet-root.key.pem.
26    #[arg(long)]
27    root_key: Option<PathBuf>,
28
29    /// Common Name on the operator cert. Default: operator-${USER}@${HOSTNAME}.
30    #[arg(long)]
31    cn: Option<String>,
32
33    /// Output cert path. Default: ~/.config/nixfleet/operator.pem.
34    #[arg(long)]
35    output_cert: Option<PathBuf>,
36
37    /// Output key path. Default: ~/.config/nixfleet/operator.key.
38    #[arg(long)]
39    output_key: Option<PathBuf>,
40
41    /// Validity in days.
42    #[arg(long, default_value_t = 365)]
43    days: u32,
44
45    /// Overwrite existing operator.pem / operator.key.
46    #[arg(long)]
47    force: bool,
48}
49
50pub fn run(args: Args) -> Result<()> {
51    let cfg_dir = crate::config::default_config_path()
52        .parent()
53        .map(|p| p.to_path_buf())
54        .context("resolve ~/.config/nixfleet directory")?;
55
56    let root_cert = args
57        .root_cert
58        .or_else(|| std::env::var_os("NIXFLEET_OPERATOR_FLEET_ROOT_CERT_FILE").map(PathBuf::from))
59        .unwrap_or_else(|| cfg_dir.join("fleet-root.cert.pem"));
60    let root_key = args
61        .root_key
62        .or_else(|| std::env::var_os("NIXFLEET_OPERATOR_FLEET_ROOT_KEY_FILE").map(PathBuf::from))
63        .unwrap_or_else(|| cfg_dir.join("fleet-root.key.pem"));
64    let output_cert = args
65        .output_cert
66        .unwrap_or_else(|| cfg_dir.join("operator.pem"));
67    let output_key = args
68        .output_key
69        .unwrap_or_else(|| cfg_dir.join("operator.key"));
70
71    let cn = match args.cn {
72        Some(c) => c,
73        None => {
74            let user = std::env::var("USER").unwrap_or_default();
75            let host = whoami::fallible::hostname().unwrap_or_default();
76            if user.is_empty() || host.is_empty() {
77                bail!("operator CN is empty (USER={user:?}, HOSTNAME={host:?}); pass --cn");
78            }
79            format!("operator-{user}@{host}")
80        }
81    };
82
83    let outcome = mint_operator_cert(MintOperatorCertArgs {
84        root_cert_path: root_cert,
85        root_key_path: root_key,
86        cn,
87        output_cert_path: output_cert,
88        output_key_path: output_key,
89        validity_days: args.days,
90        overwrite: args.force,
91    })?;
92
93    eprintln!(
94        "minted operator cert
95  cn:          {}
96  valid until: {} ({} days)
97  cert:        {}
98  key:         {}
99
100next: nixfleet config init --client-cert {} --client-key {}",
101        outcome.cn,
102        outcome.not_after.to_rfc3339(),
103        args.days,
104        outcome.cert_path.display(),
105        outcome.key_path.display(),
106        outcome.cert_path.display(),
107        outcome.key_path.display(),
108    );
109    Ok(())
110}