Files
initrd/src/cmd/init/bootstrap.rs

391 lines
11 KiB
Rust
Raw Normal View History

2024-04-29 12:54:25 +02:00
use eyre::{format_err, Result};
use log::info;
use std::path::Path;
use tokio::{
fs,
io::{AsyncBufReadExt, BufReader},
};
use super::{exec, mount, retry, retry_or_ignore, try_exec};
use crate::bootstrap::config::Config;
use crate::{dkl, utils};
pub async fn bootstrap(cfg: Config) {
2025-07-06 15:43:42 +02:00
let verifier = retry(async || Verifier::from_config(&cfg)).await;
2024-04-29 12:54:25 +02:00
let bs = cfg.bootstrap;
retry_or_ignore(async || {
mount(Some(&bs.dev), "/bootstrap", "ext4", None).await;
Ok(())
})
.await;
let boot_version = utils::param("version", "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 || {
2025-07-06 15:43:42 +02:00
let sys_cfg_bytes = seed_config(base_dir, &bs.seed, &verifier).await?;
2024-04-29 12:54:25 +02:00
Ok(serde_yaml::from_slice(&sys_cfg_bytes)?)
})
.await;
2025-07-06 15:43:42 +02:00
mount_system(&sys_cfg, base_dir, &verifier).await;
2024-04-29 12:54:25 +02:00
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
}
2025-07-06 15:43:42 +02:00
struct Verifier {
pubkey: Option<Vec<u8>>,
}
impl Verifier {
fn from_config(cfg: &Config) -> Result<Self> {
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 Some(ref pubkey) = self.pubkey else {
return Ok(());
};
info!("verifying {path}");
let mut pubkey = std::io::Cursor::new(pubkey);
let sig = format!("{path}.sig");
use std::process::Stdio;
use tokio::process::Command;
let mut openssl = Command::new("openssl")
.stdin(Stdio::piped())
.args(&[
"dgst",
"-sha512",
"-verify",
"/dev/stdin",
"-signature",
&sig,
path,
])
.spawn()?;
tokio::io::copy(&mut pubkey, openssl.stdin.as_mut().unwrap()).await?;
let status = openssl.wait().await?;
if status.success() {
Ok(())
} else {
Err(format_err!(
"signature verification failed for {path}: {status}"
))
}
}
}
async fn seed_config(
base_dir: &str,
seed_url: &Option<String>,
verifier: &Verifier,
) -> Result<Vec<u8>> {
2024-04-29 12:54:25 +02:00
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"));
}
2025-07-06 15:43:42 +02:00
verifier.verify_path(&cfg_path).await?;
2024-04-29 12:54:25 +02:00
Ok(fs::read(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(())
}
2025-07-06 15:43:42 +02:00
async fn mount_system(cfg: &dkl::Config, bs_dir: &str, verifier: &Verifier) {
2024-04-29 12:54:25 +02:00
let mem_dir = "/mem";
mount(None, mem_dir, "tmpfs", Some("size=512m")).await;
let layers_dir = &format!("{mem_dir}/layers");
let mut lower_dir = String::new();
for layer in &cfg.layers {
let src = if layer == "modules" {
2025-07-06 15:43:42 +02:00
"/modules.sqfs".to_string()
2024-04-29 12:54:25 +02:00
} else {
2025-07-06 15:43:42 +02:00
let p = format!("{bs_dir}/{layer}.fs");
retry(async || verifier.verify_path(&p).await).await;
p
2024-04-29 12:54:25 +02:00
};
2025-07-06 15:43:42 +02:00
2024-04-29 12:54:25 +02:00
let tgt = &format!("{mem_dir}/{layer}.fs");
retry(async || {
info!("copying layer {layer} from {src}");
2025-07-06 15:43:42 +02:00
fs::copy(&src, tgt).await?;
2024-04-29 12:54:25 +02:00
Ok(())
})
.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;
mount(
None,
"/system",
"overlay",
Some(&format!(
"lowerdir={lower_dir},upperdir={upper_dir},workdir={work_dir}"
)),
)
.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;
}
fn chroot(root: &str, path: &str) -> String {
format!("{root}/{}", path.trim_start_matches(|c| c == '/'))
}
async fn apply_files(files: &[dkl::File], root: &str) -> Result<()> {
for file in files {
let path = chroot(root, &file.path);
let path = Path::new(&path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
use crate::dkl::FileKind as K;
match &file.kind {
K::Content(content) => fs::write(path, content.as_bytes()).await?,
K::Dir(true) => fs::create_dir(path).await?,
K::Dir(false) => {} // shouldn't happen, but semantic is to ignore
K::Symlink(tgt) => fs::symlink(tgt, path).await?,
}
match file.kind {
K::Symlink(_) => {}
_ => set_perms(path, file.mode).await?,
}
info!("created {}", file.path);
}
Ok(())
}
async fn set_perms(path: impl AsRef<Path>, mode: Option<u32>) -> std::io::Result<()> {
if let Some(mode) = mode.filter(|m| *m != 0) {
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::Permissions::from_mode(mode);
fs::set_permissions(path, mode).await?;
}
Ok(())
}
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_ref()
.filter(|s| !s.is_empty())
.map_or("ext4", |s| s.as_str()),
m.options
.as_ref()
.filter(|v| !v.is_empty())
.map(|s| s.as_str()),
)
.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(())
}