difftreelog
feat secret installer
in: trunk
2 files changed
cmds/install-secrets/Cargo.tomldiffbeforeafterbothno changes
cmds/install-secrets/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/install-secrets/src/main.rs
@@ -0,0 +1,178 @@
+use age::Decryptor;
+use anyhow::{anyhow, bail, Context, Result};
+use log::{error, warn};
+use nix::fcntl::{renameat2, RenameFlags};
+use nix::sys::stat::Mode;
+use nix::unistd::{chown, Group, User};
+use serde::{Deserialize, Deserializer};
+use std::fs::{self, DirBuilder};
+use std::io::{self, Cursor, Read};
+use std::iter;
+use std::os::unix::prelude::PermissionsExt;
+use std::str::from_utf8;
+use std::{
+ collections::HashMap,
+ os::unix::fs::DirBuilderExt,
+ path::{Path, PathBuf},
+};
+use structopt::StructOpt;
+
+#[derive(StructOpt)]
+#[structopt(author)]
+struct Opts {
+ data: PathBuf,
+}
+
+#[derive(Deserialize)]
+struct DataItem {
+ group: String,
+ mode: String,
+ owner: String,
+ #[serde(deserialize_with = "from_z85")]
+ secret: Vec<u8>,
+}
+
+fn from_z85<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ use serde::de::Error;
+ String::deserialize(deserializer)
+ .and_then(|string| z85::decode(&string).map_err(|err| Error::custom(err.to_string())))
+}
+
+type Data = HashMap<String, DataItem>;
+
+fn init_secret(
+ identity: &age::ssh::Identity,
+ dir: &Path,
+ name: &str,
+ value: DataItem,
+) -> Result<()> {
+ let mut path = dir.to_path_buf();
+ path.push(name);
+ if path.strip_prefix(&dir).is_err() {
+ bail!("found escaping name");
+ }
+
+ let secret_dir = path
+ .parent()
+ .expect("path is in tempdir, so it should have parent");
+
+ if secret_dir != dir {
+ DirBuilder::new()
+ .recursive(true)
+ // o: xrw
+ // g: xr
+ // a: xr
+ .mode(0o755)
+ .create(
+ path.parent()
+ .expect("path is in tempdir, so it should have parent"),
+ )
+ .context("failed to create secret directory")?;
+ }
+
+ let mode = Mode::from_bits(
+ u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,
+ )
+ .context("failed to parse mode")?;
+ let user = User::from_name(&value.owner)
+ .context("failed to get user")?
+ .ok_or_else(|| anyhow!("user not found"))?;
+ let group = Group::from_name(&value.group)
+ .context("failed to get group")?
+ .ok_or_else(|| anyhow!("group not found"))?;
+ let mut tempfile =
+ tempfile::NamedTempFile::new_in(secret_dir).context("failed to create tempfile")?;
+ // File is owned by root, and only root can modify it
+
+ let decrypted = {
+ let mut input = Cursor::new(&value.secret);
+ let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;
+ let decryptor = match decryptor {
+ Decryptor::Recipients(r) => r,
+ Decryptor::Passphrase(_) => bail!("should be recipients"),
+ };
+ let mut decryptor = decryptor
+ .decrypt(iter::once(identity as &dyn age::Identity))
+ .context("failed to decrypt, wrong key?")?;
+
+ let mut decrypted = Vec::new();
+ decryptor
+ .read_to_end(&mut decrypted)
+ .context("failed to decrypt")?;
+ decrypted
+ };
+
+ io::copy(&mut Cursor::new(decrypted), &mut tempfile)
+ .context("failed to write decrypted file")?;
+
+ // Make file owned by specified user and group, then change mode
+ chown(tempfile.path(), Some(user.uid), Some(group.gid))
+ .context("failed to apply user/group")?;
+ fs::set_permissions(tempfile.path(), fs::Permissions::from_mode(mode.bits())).unwrap();
+ tempfile.persist(path).context("failed to persist")?;
+
+ Ok(())
+}
+
+fn main() -> anyhow::Result<()> {
+ env_logger::Builder::new()
+ .filter_level(log::LevelFilter::Info)
+ .init();
+
+ let opts = Opts::from_args();
+ let data = fs::read(&opts.data).context("failed to read secrets data")?;
+ let data_str = from_utf8(&data).context("failed to read data to string")?;
+ let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;
+
+ let tempdir =
+ tempfile::tempdir_in("/run/secrets.d").context("failed to create secrets tempdir")?;
+
+ let identity = age::ssh::Identity::from_buffer(
+ &mut Cursor::new(
+ fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,
+ ),
+ None,
+ )
+ .context("failed to parse identity")?;
+
+ let mut failed = false;
+ for (name, value) in data {
+ if let Err(e) = init_secret(&identity, tempdir.path(), &name, value) {
+ error!(
+ "{:?}",
+ e.context(format!("failed to initialize secret {}", name))
+ );
+ failed = true;
+ }
+ }
+ if failed {
+ bail!("one or more secrets failed");
+ }
+
+ if fs::metadata("/run/secrets.d/secrets.fleet")
+ .map(|m| m.is_dir())
+ .unwrap_or(false)
+ {
+ // Already linked
+ renameat2(
+ None,
+ tempdir.path(),
+ None,
+ "/run/secrets.d/secrets.fleet",
+ RenameFlags::RENAME_EXCHANGE,
+ )
+ .context("failed to exchange secret directories")?;
+ if tempdir.close().is_err() {
+ warn!("failed to unlink old secrets");
+ }
+ } else {
+ // Link now
+ let persisted = tempdir.into_path();
+ fs::rename(&persisted, "/run/secrets.d/secrets.fleet")
+ .context("failed to link secret directory")?;
+ }
+ Ok(())
+}