use eyre::{format_err, Result}; use log::{info, warn}; use tokio::{ fs, io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, }; use dkl::{ self, apply::{self, chroot, set_perms}, bootstrap::Config, }; use super::{exec, mount, retry, retry_or_ignore, try_exec}; use crate::utils; pub async fn bootstrap(cfg: Config) { let verifier = retry(async || Verifier::from_config(&cfg)).await; let bs = cfg.bootstrap; mount(Some(&bs.dev), "/bootstrap", "ext4", None).await; let boot_version = utils::param("version").unwrap_or("current"); let base_dir = &format!("/bootstrap/{boot_version}"); retry_or_ignore(async || { if !fs::try_exists(&base_dir).await? { info!("creating {base_dir}"); fs::create_dir_all(&base_dir).await? } Ok(()) }) .await; let sys_cfg: dkl::Config = retry(async || { let sys_cfg_bytes = seed_config(base_dir, &bs.seed, &verifier).await?; Ok(serde_yaml::from_slice(&sys_cfg_bytes)?) }) .await; mount_system(&sys_cfg, base_dir, &verifier).await; retry_or_ignore(async || { let path = "/etc/resolv.conf"; if fs::try_exists(path).await? { info!("cp /etc/resolv.conf"); fs::copy(path, &format!("/system{path}")).await?; } Ok(()) }) .await; retry_or_ignore(async || apply::files(&sys_cfg.files, "/system").await).await; apply_groups(&sys_cfg.groups, "/system").await; apply_users(&sys_cfg.users, "/system").await; // TODO VPNs mount_filesystems(&sys_cfg.mounts, "/system").await; retry_or_ignore(async || { info!("setting up root user"); setup_root_user(&sys_cfg.root_user, "/system").await }) .await; exec("chroot", &["/system", "update-ca-certificates"]).await } struct Verifier { pubkey: Option>, } impl Verifier { fn from_config(cfg: &Config) -> Result { let Some(ref pubkey) = cfg.signer_public_key else { return Ok(Self { pubkey: None }); }; use base64::{prelude::BASE64_STANDARD, Engine}; let pubkey = BASE64_STANDARD.decode(pubkey)?; let pubkey = Some(pubkey); return Ok(Self { pubkey }); } async fn verify_path(&self, path: &str) -> Result> { let data = (fs::read(path).await).map_err(|e| format_err!("failed to read {path}: {e}"))?; let Some(ref pubkey) = self.pubkey else { return Ok(data); }; info!("verifying {path}"); let sig = &format!("{path}.sig"); let sig = (fs::read(sig).await).map_err(|e| format_err!("failed to read {sig}: {e}"))?; use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier}; let pubkey = PKey::public_key_from_der(pubkey)?; let sig_ok = Verifier::new(MessageDigest::sha512(), &pubkey)? .verify_oneshot(&sig, &data) .map_err(|e| format_err!("verify failed: {e}"))?; if sig_ok { Ok(data) } else { Err(format_err!("signature verification failed for {path}")) } } } async fn seed_config( base_dir: &str, seed_url: &Option, verifier: &Verifier, ) -> Result> { let cfg_path = &format!("{base_dir}/config.yaml"); if fs::try_exists(cfg_path).await? { return Ok(fs::read(cfg_path).await?); } let bs_tar = "/bootstrap.tar"; if !fs::try_exists(bs_tar).await? { if let Some(seed_url) = seed_url.as_ref() { fetch_bootstrap(seed_url, bs_tar).await?; } else { return Err(format_err!( "no {cfg_path}, no {bs_tar} and no seed, can't bootstrap" )); } } try_exec("tar", &["xf", bs_tar, "-C", base_dir]).await?; if !fs::try_exists(cfg_path).await? { return Err(format_err!("{cfg_path} does not exist after seeding")); } verifier.verify_path(&cfg_path).await } async fn fetch_bootstrap(seed_url: &str, output_file: &str) -> Result<()> { let tmp_file = &format!("{output_file}.new"); let _ = fs::remove_file(tmp_file).await; try_exec("wget", &["-O", tmp_file, seed_url]).await?; fs::rename(tmp_file, output_file) .await .map_err(|e| format_err!("seed rename failed: {e}"))?; Ok(()) } fn default_root_tmpfs_opts() -> Option { let mem = sys_info::mem_info() .inspect_err(|e| warn!("failed to get system memory info, using default tmpfs size: {e}")) .ok()?; let mem_size = mem.total /* kiB */ / 1024; let fs_size = 1024.min(mem_size / 2); info!("system has {mem_size} MiB of memory, allowing {fs_size} MiB for root tmpfs"); Some(format!("size={fs_size}m")) } async fn mount_system(cfg: &dkl::Config, bs_dir: &str, verifier: &Verifier) { let opts = match utils::param("root-opts") { Some(s) => Some(s.to_string()), None => default_root_tmpfs_opts(), }; let mem_dir = "/mem"; mount(None, mem_dir, "tmpfs", opts.as_deref()).await; let layers_dir = &format!("{mem_dir}/layers"); let mut lower_dir = String::new(); for layer in &cfg.layers { let src = retry(async || { if layer == "modules" { (fs::read("/modules.sqfs").await) .map_err(|e| format_err!("read /modules.sqfs failed: {e}")) } else { verifier.verify_path(&format!("{bs_dir}/{layer}.fs")).await } }) .await; let tgt = &format!("{mem_dir}/{layer}.fs"); retry(async || { info!("copying layer {layer}"); let mut out = (fs::File::create(tgt).await) .map_err(|e| format_err!("create {tgt} failed: {e}"))?; (out.write_all(&src).await).map_err(|e| format_err!("write failed: {e}"))?; (out.flush().await).map_err(|e| format_err!("write failed: {e}")) }) .await; let layer_dir = &format!("{layers_dir}/{layer}"); mount(Some(tgt), layer_dir, "squashfs", None).await; if !lower_dir.is_empty() { lower_dir.push(':'); } lower_dir.push_str(&layer_dir); } let upper_dir = &format!("{mem_dir}/upper"); let work_dir = &format!("{mem_dir}/work"); retry_or_ignore(async || { fs::create_dir_all(upper_dir).await?; fs::create_dir_all(work_dir).await?; Ok(()) }) .await; let opts = format!("lowerdir={lower_dir},upperdir={upper_dir},workdir={work_dir}"); mount(None, "/system", "overlay", Some(&opts)).await; // make root rshared (default in systemd, required by Kubernetes 1.10+) // equivalent to "mount --make-rshared /" // see kernel's Documentation/sharedsubtree.txt (search rshared) retry_or_ignore(async || { use nix::mount::MsFlags as M; const NONE: Option<&str> = None; nix::mount::mount(NONE, "/system", NONE, M::MS_SHARED | M::MS_REC, NONE)?; Ok(()) }) .await; } async fn apply_groups(groups: &[dkl::Group], root: &str) { for group in groups { let mut args = vec![root, "groupadd", "-r"]; let gid = group.gid.map(|s| s.to_string()); if let Some(gid) = gid.as_ref() { args.extend(&["-g", gid]); } args.push(group.name.as_str()); exec("chroot", &args).await; } } async fn apply_users(users: &[dkl::User], root: &str) { for user in users { let mut args = vec![root, "useradd", "-r"]; let uid = user.uid.map(|s| s.to_string()); if let Some(uid) = uid.as_ref() { args.extend(&["-u", uid]); } let gid = user.gid.map(|s| s.to_string()); if let Some(gid) = gid.as_ref() { args.extend(&["-g", gid]); } args.push(user.name.as_str()); exec("chroot", &args).await; } } async fn mount_filesystems(mounts: &[dkl::Mount], root: &str) { for m in mounts { let path = chroot(root, &m.path); mount( Some(&m.dev), &path, (m.r#type.as_deref()) .filter(|s| !s.is_empty()) .unwrap_or("ext4"), m.options.as_deref().filter(|v| !v.is_empty()), ) .await; } } async fn setup_root_user(user: &dkl::RootUser, root: &str) -> Result<()> { if let Some(pw_hash) = user.password_hash.as_ref().filter(|v| !v.is_empty()) { set_user_password("root", &pw_hash, root).await?; } let mut authorized_keys = Vec::new(); for ak in &user.authorized_keys { authorized_keys.extend(ak.as_bytes()); authorized_keys.push(b'\n'); } let ssh_dir = &chroot(root, "root/.ssh"); fs::create_dir_all(ssh_dir) .await .map_err(|e| format_err!("mkdir -p {ssh_dir} failed: {e}"))?; set_perms(ssh_dir, Some(0o700)) .await .map_err(|e| format_err!("chmod {ssh_dir} failed: {e}"))?; let ak_path = &format!("{ssh_dir}/authorized_keys"); fs::write(ak_path, authorized_keys) .await .map_err(|e| format_err!("write {ak_path} failed: {e}"))?; Ok(()) } async fn set_user_password(user: &str, password_hash: &str, root: &str) -> Result<()> { info!("setting password for {user}"); let user = user.as_bytes(); let password_hash = password_hash.as_bytes(); let mut buf = Vec::new(); let pw_file = &chroot(root, "etc/shadow"); let rd = fs::File::open(pw_file) .await .map_err(|e| format_err!("open {pw_file} failed: {e}"))?; let mut rd = BufReader::new(rd); let mut line = Vec::new(); while (rd.read_until(b'\n', &mut line).await) .map_err(|e| format_err!("read {pw_file} failed: {e}"))? != 0 { let mut split: Vec<_> = line.split(|c| *c == b':').collect(); if split.len() > 2 && split[0] == user { split[1] = password_hash; buf.extend(split.join(&b':')); } else { buf.extend(&line); } line.clear(); } fs::write(pw_file, buf).await?; Ok(()) }