Skip to main content

aegis_tool/
lib.rs

1mod agent;
2mod agent_credentials;
3mod api;
4mod app;
5mod apparmor;
6mod cli;
7pub mod client;
8mod command;
9mod config;
10mod egress_probe;
11mod invitation;
12mod locks;
13mod managed;
14mod metadata;
15mod platform;
16mod principal_grants;
17mod redeploy_version;
18mod release;
19mod ssh_service;
20mod system_user;
21mod tunnel_operation;
22pub mod ui;
23mod wireguard_endpoint;
24mod wireguard_keys;
25
26use std::sync::Arc;
27
28use anyhow::{Result, bail};
29use clap::Parser;
30
31use crate::cli::{AgentCommands, Cli, Commands};
32
33pub fn run_cli() -> capulus::CliTermination {
34    let Cli {
35        api_base,
36        namespace,
37        ui: ui_options,
38        command,
39    } = Cli::parse();
40    if matches!(command, Commands::Agent(_)) && (api_base.is_some() || namespace.is_some()) {
41        return capulus::CliTermination::without_ui(Err(anyhow::anyhow!(
42            "agent commands use the enrolled context in their configuration; --api-base and --namespace apply to CLI operations"
43        )));
44    }
45    let ui_configuration = ui_options.options();
46    match command {
47        Commands::Agent(agent) => match agent.command {
48            AgentCommands::DirectSsh => {
49                if let Err(error) = ui::init(ui_configuration) {
50                    return capulus::CliTermination::without_ui(Err(error));
51                }
52                capulus::CliTermination::with_ui(ui::current(), app::run_direct_ssh())
53            }
54            command => capulus::CliTermination::without_ui(run_agent(command).map(|()| 0)),
55        },
56        command => {
57            if let Err(error) = ui::init(ui_configuration) {
58                return capulus::CliTermination::without_ui(Err(error));
59            }
60            capulus::CliTermination::with_ui(
61                ui::current(),
62                app::run(Cli {
63                    api_base,
64                    namespace,
65                    ui: ui_options,
66                    command,
67                }),
68            )
69        }
70    }
71}
72
73fn run_agent(command: AgentCommands) -> Result<()> {
74    match command {
75        AgentCommands::PrepareIdentity { inbound_ssh } => {
76            require_agent_root()?;
77            crate::platform::detect()?;
78            wireguard_keys::Keypair::ensure(
79                std::path::Path::new("/etc/aegis/wireguard/wg-aegis.key"),
80                std::path::Path::new("/etc/aegis/wireguard/wg-aegis.pub"),
81            )?;
82            if inbound_ssh {
83                let key = std::path::Path::new("/etc/ssh/ssh_host_ed25519_key");
84                if !key.exists() {
85                    command::require_success(
86                        "create SSH host identity",
87                        std::process::Command::new("/usr/bin/ssh-keygen")
88                            .args(["-q", "-t", "ed25519", "-N", "", "-f"])
89                            .arg(key),
90                    )?;
91                }
92            }
93            Ok(())
94        }
95        #[cfg(target_os = "macos")]
96        AgentCommands::WireguardWorker { interface } => {
97            require_agent_root()?;
98            agent::macos::wireguard_worker(&interface)
99        }
100        AgentCommands::Serve(args) => {
101            require_agent_root()?;
102            let status = agent::run(&args)?;
103            if status == 0 {
104                Ok(())
105            } else {
106                bail!("aegis-agent exited with status {status}")
107            }
108        }
109        AgentCommands::DirectSsh => {
110            unreachable!("the direct SSH endpoint is dispatched with the interactive UI")
111        }
112        AgentCommands::Lifecycle(command) => {
113            let product = Arc::new(managed::product()?);
114            let health_product = Arc::clone(&product);
115            command.run(product, move || {
116                app::application_agent_info(&health_product)
117            })
118        }
119        AgentCommands::EgressProbeWorker => {
120            require_agent_root()?;
121            // The supervising agent receives stderr, so include the complete cause in
122            // the worker's display message while preserving typed interruption.
123            egress_probe::run_worker().map_err(|error| {
124                let detail = format!("{error:#}");
125                error.context(detail)
126            })
127        }
128    }
129}
130
131fn require_agent_root() -> Result<()> {
132    if rustix::process::geteuid().is_root() {
133        Ok(())
134    } else {
135        bail!("aegis agent operations must run as root")
136    }
137}