Skip to main content

aegis_tool/app/
enroll_install.rs

1use aegis_dto::{HostId, v1::AegisMeshConfig};
2use anyhow::Result;
3use capulus::shell::shell_quote as sh_quote;
4
5use crate::cli::AgentMode;
6
7use super::{
8    REMOTE_HOST_CERT_PATH, REMOTE_HOST_KEY_PATH, WIREGUARD_DIR, WIREGUARD_PRIVATE_KEY_PATH,
9    WIREGUARD_PUBLIC_KEY_PATH, install, mesh_bootstrap, system, wireguard,
10};
11
12pub(super) struct RemotePrepareHostScript;
13
14impl RemotePrepareHostScript {
15    pub(super) fn render(publish_ssh: bool) -> String {
16        let ssh_identity = if publish_ssh {
17            format!(
18                "sudo test -f {REMOTE_HOST_KEY_PATH} || sudo ssh-keygen -q -t ed25519 -N '' -f {REMOTE_HOST_KEY_PATH}\n"
19            )
20        } else {
21            String::new()
22        };
23        format!(
24            "source /etc/os-release\n\
25             [[ \"${{ID:-}}\" == \"ubuntu\" ]]\n\
26             sudo -v\n\
27             {bird3_repo}\
28             retry sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y \\\n\
29               --no-install-recommends build-essential bird3 ca-certificates curl \\\n\
30               libssl-dev pkg-config python3 wireguard\n\
31             sudo install -d -m 755 {WIREGUARD_DIR}\n\
32             if ! sudo test -f {WIREGUARD_PRIVATE_KEY_PATH}; then\n\
33               sudo sh -ceu 'umask 077; wg genkey > {WIREGUARD_PRIVATE_KEY_PATH}'\n\
34             fi\n\
35             if ! sudo test -f {WIREGUARD_PUBLIC_KEY_PATH}; then\n\
36               sudo sh -ceu 'wg pubkey < {WIREGUARD_PRIVATE_KEY_PATH} > {WIREGUARD_PUBLIC_KEY_PATH}'\n\
37             fi\n\
38             sudo chmod 600 {WIREGUARD_PRIVATE_KEY_PATH}\n\
39             sudo chmod 644 {WIREGUARD_PUBLIC_KEY_PATH}\n\
40             {ssh_identity}",
41            bird3_repo = system::Bird3Repository::with_sudo().setup_script(),
42            ssh_identity = ssh_identity,
43        )
44    }
45}
46
47pub(super) struct RemoteFinalizeInstall<'a> {
48    api_base: &'a str,
49    pending_host: &'a crate::config::CachedHost,
50    hub_peers: &'a [wireguard::HubPeer],
51    mesh: &'a AegisMeshConfig,
52    server_certificate: Option<&'a str>,
53    agent_token: &'a str,
54    login_principal: &'a str,
55    initial_oauth_principal: Option<&'a str>,
56    mode: AgentMode,
57    inbound_ssh: bool,
58}
59
60pub(super) struct RemoteFinalizeInstallParts<'a> {
61    pub(super) api_base: &'a str,
62    pub(super) pending_host: &'a crate::config::CachedHost,
63    pub(super) hub_peers: &'a [wireguard::HubPeer],
64    pub(super) mesh: &'a AegisMeshConfig,
65    pub(super) server_certificate: Option<&'a str>,
66    pub(super) agent_token: &'a str,
67    pub(super) login_principal: &'a str,
68    pub(super) initial_oauth_principal: Option<&'a str>,
69    pub(super) mode: AgentMode,
70    pub(super) inbound_ssh: bool,
71}
72
73impl<'a> RemoteFinalizeInstall<'a> {
74    pub(super) fn new(parts: RemoteFinalizeInstallParts<'a>) -> Self {
75        Self {
76            api_base: parts.api_base,
77            pending_host: parts.pending_host,
78            hub_peers: parts.hub_peers,
79            mesh: parts.mesh,
80            server_certificate: parts.server_certificate,
81            agent_token: parts.agent_token,
82            login_principal: parts.login_principal,
83            initial_oauth_principal: parts.initial_oauth_principal,
84            mode: parts.mode,
85            inbound_ssh: parts.inbound_ssh,
86        }
87    }
88
89    pub(super) fn render(&self) -> Result<String> {
90        let mut install_args = format!("--host-id {}", self.pending_host.host_id);
91        if self.server_certificate.is_some() {
92            install_args.push_str(&format!(
93                " --key {} --cert {}",
94                sh_quote(REMOTE_HOST_KEY_PATH),
95                sh_quote(REMOTE_HOST_CERT_PATH),
96            ));
97        }
98        install_args.push_str(&format!(
99            " --user {} --inbound-ssh {} --staged-enrollment",
100            sh_quote(self.login_principal),
101            if self.inbound_ssh { "yes" } else { "no" }
102        ));
103        if let Some(principal) = self.initial_oauth_principal {
104            install_args.push_str(&format!(
105                " --initial-oauth-principal {}",
106                sh_quote(principal)
107            ));
108        }
109        let agent_refresh_token_env =
110            install::agent_refresh_token_env_assignment(self.agent_token)?;
111        let wireguard_setup = if self.hub_peers.is_empty() {
112            String::new()
113        } else {
114            mesh_bootstrap::BootstrapMeshScript::new(
115                self.pending_host,
116                self.hub_peers,
117                self.mesh,
118                self.mode,
119            )
120            .render()?
121        };
122        let host_certificate_bootstrap = self
123            .server_certificate
124            .map(|server_certificate| {
125                format!(
126                    "cat <<'EOF_AEGIS_SERVER_CERT' | sudo tee {REMOTE_HOST_CERT_PATH} >/dev/null\n\
127{server_certificate}\n\
128EOF_AEGIS_SERVER_CERT\n\
129sudo chmod 644 {REMOTE_HOST_CERT_PATH}\n"
130                )
131            })
132            .unwrap_or_default();
133        Ok(format!(
134            "set -euo pipefail\n\
135             sudo -v\n\
136             login_user={login_user}\n\
137             login_home=\"$(getent passwd \"$login_user\" | cut -d: -f6)\"\n\
138             test -n \"$login_home\"\n\
139             {wireguard_setup}\n\
140             {tool_bootstrap}\
141             {host_certificate_bootstrap}\
142             sudo env {agent_refresh_token_env} {system_binary} --api-base {api_base} advanced install {install_args}\n",
143            api_base = sh_quote(self.api_base),
144            agent_refresh_token_env = agent_refresh_token_env,
145            host_certificate_bootstrap = host_certificate_bootstrap,
146            install_args = install_args,
147            tool_bootstrap = system_program_bootstrap_script(true),
148            login_user = sh_quote(self.login_principal),
149            system_binary = aegis_dto::layout::SYSTEM_BINARY_PATH,
150            wireguard_setup = wireguard_setup,
151        ))
152    }
153}
154
155pub fn system_program_bootstrap_script(use_sudo: bool) -> String {
156    format!(
157        "{sudo}bash -seuo pipefail <<'EOF_AEGIS_BOOTSTRAP'\naegis_bootstrap_version={version}\n{script}\nEOF_AEGIS_BOOTSTRAP\n",
158        sudo = if use_sudo { "sudo " } else { "" },
159        version = sh_quote(env!("CARGO_PKG_VERSION")),
160        script = include_str!("../../assets/bootstrap.sh"),
161    )
162}
163pub(super) struct LocalTargetInstall<'a> {
164    api_base: &'a str,
165    host_id: HostId,
166    inbound_ssh: bool,
167    install_host_certificate: bool,
168    agent_token: &'a str,
169    initial_oauth_principal: Option<&'a str>,
170    system_bootstrap: String,
171}
172
173pub(super) struct LocalTargetInstallOptions<'a> {
174    pub api_base: &'a str,
175    pub host_id: HostId,
176    pub inbound_ssh: bool,
177    pub install_host_certificate: bool,
178    pub agent_token: &'a str,
179    pub initial_oauth_principal: Option<&'a str>,
180}
181
182impl<'a> LocalTargetInstall<'a> {
183    pub(super) fn new(options: LocalTargetInstallOptions<'a>) -> Self {
184        Self {
185            api_base: options.api_base,
186            host_id: options.host_id,
187            inbound_ssh: options.inbound_ssh,
188            install_host_certificate: options.install_host_certificate,
189            agent_token: options.agent_token,
190            initial_oauth_principal: options.initial_oauth_principal,
191            system_bootstrap: system_program_bootstrap_script(false),
192        }
193    }
194
195    pub(super) fn run(&self) -> Result<()> {
196        system::LocalRoot::run_script(&self.system_bootstrap)?;
197        let mut install_args = vec![
198            "advanced".to_string(),
199            "install".to_string(),
200            "--staged-enrollment".to_string(),
201            "--host-id".to_string(),
202            self.host_id.to_string(),
203        ];
204        if self.install_host_certificate {
205            install_args.push("--key".to_string());
206            install_args.push(REMOTE_HOST_KEY_PATH.to_string());
207            install_args.push("--cert".to_string());
208            install_args.push(REMOTE_HOST_CERT_PATH.to_string());
209        }
210        install_args.push("--inbound-ssh".to_string());
211        install_args.push(if self.inbound_ssh {
212            "yes".to_string()
213        } else {
214            "no".to_string()
215        });
216        if let Some(principal) = self.initial_oauth_principal {
217            install_args.push("--initial-oauth-principal".to_string());
218            install_args.push(principal.to_string());
219        }
220        system::LocalRoot::run_aegis_command(self.api_base, &install_args, self.agent_token)
221    }
222}