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}