difftreelog
refactor new secrets structure
in: trunk
2 files changed
cmds/install-secrets/src/main.rsdiffbeforeafterboth1use age::Decryptor;2use anyhow::{anyhow, bail, Context, Result};3use clap::Parser;4use log::{error, warn};5use nix::fcntl::{renameat2, RenameFlags};6use nix::sys::stat::Mode;7use nix::unistd::{chown, Group, User};8use serde::{Deserialize, Deserializer};9use std::fs::{self, DirBuilder};10use std::io::{self, Cursor, Read};11use std::iter;12use std::os::unix::prelude::PermissionsExt;13use std::str::from_utf8;14use std::{15 collections::HashMap,16 os::unix::fs::DirBuilderExt,17 path::{Path, PathBuf},18};1920#[derive(Parser)]21#[clap(author)]22struct Opts {23 data: PathBuf,24}2526#[derive(Deserialize)]27struct DataItem {28 group: String,29 mode: String,30 owner: String,31 #[serde(deserialize_with = "from_z85")]32 secret: Option<Vec<u8>>,33}3435fn from_z85<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>36where37 D: Deserializer<'de>,38{39 use serde::de::Error;40 if let Some(v) = <Option<String>>::deserialize(deserializer)? {41 Ok(Some(42 z85::decode(&v).map_err(|err| Error::custom(err.to_string()))?,43 ))44 } else {45 Ok(None)46 }47}4849type Data = HashMap<String, DataItem>;5051fn init_secret(52 identity: &age::ssh::Identity,53 dir: &Path,54 name: &str,55 value: DataItem,56) -> Result<()> {57 if value.secret.is_none() {58 return Ok(());59 }60 let secret = value.secret.as_ref().unwrap();6162 let mut path = dir.to_path_buf();63 path.push(name);64 if path.strip_prefix(&dir).is_err() {65 bail!("found escaping name");66 }6768 let secret_dir = path69 .parent()70 .expect("path is in tempdir, so it should have parent");7172 if secret_dir != dir {73 DirBuilder::new()74 .recursive(true)75 // o: xrw76 // g: xr77 // a: xr78 .mode(0o755)79 .create(80 path.parent()81 .expect("path is in tempdir, so it should have parent"),82 )83 .context("failed to create secret directory")?;84 }8586 let mode = Mode::from_bits(87 u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,88 )89 .context("failed to parse mode")?;90 let user = User::from_name(&value.owner)91 .context("failed to get user")?92 .ok_or_else(|| anyhow!("user not found"))?;93 let group = Group::from_name(&value.group)94 .context("failed to get group")?95 .ok_or_else(|| anyhow!("group not found"))?;96 let mut tempfile =97 tempfile::NamedTempFile::new_in(secret_dir).context("failed to create tempfile")?;98 // File is owned by root, and only root can modify it99100 let decrypted = {101 let mut input = Cursor::new(&secret);102 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;103 let decryptor = match decryptor {104 Decryptor::Recipients(r) => r,105 Decryptor::Passphrase(_) => bail!("should be recipients"),106 };107 let mut decryptor = decryptor108 .decrypt(iter::once(identity as &dyn age::Identity))109 .context("failed to decrypt, wrong key?")?;110111 let mut decrypted = Vec::new();112 decryptor113 .read_to_end(&mut decrypted)114 .context("failed to decrypt")?;115 decrypted116 };117118 io::copy(&mut Cursor::new(decrypted), &mut tempfile)119 .context("failed to write decrypted file")?;120121 // Make file owned by specified user and group, then change mode122 chown(tempfile.path(), Some(user.uid), Some(group.gid))123 .context("failed to apply user/group")?;124 fs::set_permissions(tempfile.path(), fs::Permissions::from_mode(mode.bits())).unwrap();125 tempfile.persist(path).context("failed to persist")?;126127 Ok(())128}129130fn main() -> anyhow::Result<()> {131 env_logger::Builder::new()132 .filter_level(log::LevelFilter::Info)133 .init();134135 let opts = Opts::parse();136 let data = fs::read(&opts.data).context("failed to read secrets data")?;137 let data_str = from_utf8(&data).context("failed to read data to string")?;138 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;139140 let tempdir = tempfile::tempdir_in("/run/").context("failed to create secrets tempdir")?;141142 let identity = age::ssh::Identity::from_buffer(143 &mut Cursor::new(144 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,145 ),146 None,147 )148 .context("failed to parse identity")?;149150 let mut failed = false;151 for (name, value) in data {152 if let Err(e) = init_secret(&identity, tempdir.path(), &name, value) {153 error!(154 "{:?}",155 e.context(format!("failed to initialize secret {}", name))156 );157 failed = true;158 }159 }160 if failed {161 bail!("one or more secrets failed");162 }163164 if fs::metadata("/run/secrets")165 .map(|m| m.is_dir())166 .unwrap_or(false)167 {168 // Already linked169 renameat2(170 None,171 tempdir.path(),172 None,173 "/run/secrets",174 RenameFlags::RENAME_EXCHANGE,175 )176 .context("failed to exchange secret directories")?;177 if tempdir.close().is_err() {178 warn!("failed to unlink old secrets");179 }180 } else {181 // Link now182 let persisted = tempdir.into_path();183 fs::rename(&persisted, "/run/secrets").context("failed to link secret directory")?;184 }185 Ok(())186}1use age::Decryptor;2use anyhow::{anyhow, bail, Context, Result};3use clap::Parser;4use log::{error, warn};5use nix::fcntl::{renameat2, RenameFlags};6use nix::sys::stat::Mode;7use nix::unistd::{chown, Group, User};8use serde::{Deserialize, Deserializer};9use std::fs::{self, DirBuilder};10use std::io::{self, Cursor, Read};11use std::iter;12use std::os::unix::prelude::PermissionsExt;13use std::str::from_utf8;14use std::{15 collections::HashMap,16 os::unix::fs::DirBuilderExt,17 path::{Path, PathBuf},18};1920#[derive(Parser)]21#[clap(author)]22struct Opts {23 data: PathBuf,24}2526#[derive(Deserialize)]27#[serde(rename_all = "camelCase")]28struct DataItem {29 group: String,30 mode: String,31 owner: String,3233 #[serde(deserialize_with = "from_z85")]34 secret: Option<Vec<u8>>,35 public: String,3637 secret_hash: String,38 public_path: String,39}4041fn from_z85<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>42where43 D: Deserializer<'de>,44{45 use serde::de::Error;46 if let Some(v) = <Option<String>>::deserialize(deserializer)? {47 Ok(Some(48 z85::decode(&v).map_err(|err| Error::custom(err.to_string()))?,49 ))50 } else {51 Ok(None)52 }53}5455type Data = HashMap<String, DataItem>;5657fn init_secret(58 identity: &age::ssh::Identity,59 dir: &Path,60 name: &str,61 value: DataItem,62) -> Result<()> {63 if value.secret.is_none() {64 return Ok(());65 }66 let secret = value.secret.as_ref().unwrap();6768 let mut path = dir.to_path_buf();69 path.push(name);70 if path.strip_prefix(&dir).is_err() {71 bail!("found escaping name");72 }7374 let secret_dir = path75 .parent()76 .expect("path is in tempdir, so it should have parent");7778 if secret_dir != dir {79 DirBuilder::new()80 .recursive(true)81 // o: xrw82 // g: xr83 // a: xr84 .mode(0o755)85 .create(86 path.parent()87 .expect("path is in tempdir, so it should have parent"),88 )89 .context("failed to create secret directory")?;90 }9192 let mode = Mode::from_bits(93 u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,94 )95 .context("failed to parse mode")?;96 let user = User::from_name(&value.owner)97 .context("failed to get user")?98 .ok_or_else(|| anyhow!("user not found"))?;99 let group = Group::from_name(&value.group)100 .context("failed to get group")?101 .ok_or_else(|| anyhow!("group not found"))?;102 let mut tempfile =103 tempfile::NamedTempFile::new_in(secret_dir).context("failed to create tempfile")?;104 // File is owned by root, and only root can modify it105106 let decrypted = {107 let mut input = Cursor::new(&secret);108 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;109 let decryptor = match decryptor {110 Decryptor::Recipients(r) => r,111 Decryptor::Passphrase(_) => bail!("should be recipients"),112 };113 let mut decryptor = decryptor114 .decrypt(iter::once(identity as &dyn age::Identity))115 .context("failed to decrypt, wrong key?")?;116117 let mut decrypted = Vec::new();118 decryptor119 .read_to_end(&mut decrypted)120 .context("failed to decrypt")?;121 decrypted122 };123124 io::copy(&mut Cursor::new(decrypted), &mut tempfile)125 .context("failed to write decrypted file")?;126127 // Make file owned by specified user and group, then change mode128 chown(tempfile.path(), Some(user.uid), Some(group.gid))129 .context("failed to apply user/group")?;130 fs::set_permissions(tempfile.path(), fs::Permissions::from_mode(mode.bits())).unwrap();131 tempfile.persist(path).context("failed to persist")?;132133 Ok(())134}135136fn main() -> anyhow::Result<()> {137 env_logger::Builder::new()138 .filter_level(log::LevelFilter::Info)139 .init();140141 let opts = Opts::parse();142 let data = fs::read(&opts.data).context("failed to read secrets data")?;143 let data_str = from_utf8(&data).context("failed to read data to string")?;144 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;145146 let tempdir = tempfile::tempdir_in("/run/").context("failed to create secrets tempdir")?;147148 let identity = age::ssh::Identity::from_buffer(149 &mut Cursor::new(150 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,151 ),152 None,153 )154 .context("failed to parse identity")?;155156 let mut failed = false;157 for (name, value) in data {158 if let Err(e) = init_secret(&identity, tempdir.path(), &name, value) {159 error!(160 "{:?}",161 e.context(format!("failed to initialize secret {}", name))162 );163 failed = true;164 }165 }166 if failed {167 bail!("one or more secrets failed");168 }169170 if fs::metadata("/run/secrets")171 .map(|m| m.is_dir())172 .unwrap_or(false)173 {174 // Already linked175 renameat2(176 None,177 tempdir.path(),178 None,179 "/run/secrets",180 RenameFlags::RENAME_EXCHANGE,181 )182 .context("failed to exchange secret directories")?;183 if tempdir.close().is_err() {184 warn!("failed to unlink old secrets");185 }186 } else {187 // Link now188 let persisted = tempdir.into_path();189 fs::rename(&persisted, "/run/secrets").context("failed to link secret directory")?;190 }191 Ok(())192}nixos/secrets.nixdiffbeforeafterboth--- a/nixos/secrets.nix
+++ b/nixos/secrets.nix
@@ -5,9 +5,15 @@
let
sysConfig = config;
secretType = types.submodule ({ config, ... }: {
- config = {
- path = mkOptionDefault "/run/secrets/${config._module.args.name}";
- publicPath = mkOptionDefault (pkgs.writeText "pub-${config._module.args.name}" config.public);
+ config = rec {
+ path = warn "use .stableSecretPath instead of .path (at config.secrets.${config._module.args.name})" stableSecretPath;
+ stableSecretPath = mkOptionDefault "/run/secrets/secret-stable-${config._module.args.name}";
+ secretPath = mkOptionDefault "/run/secrets/secret-${config.secretHash}-${config._module.args.name}";
+ secretHash = mkOptionDefault (if config.secret != null then (builtins.hashString "sha1" config.secret) else "<missingno>");
+
+ stablePublicPath = mkOptionDefault "/run/secrets/public-stable-${config._module.args.name}";
+ publicPath = mkOptionDefault "/run/secrets/public-${config.publicHash}-${config._module.args.name}";
+ publicHash = mkOptionDefault (if config.public != null then (builtins.hashString "sha1" config.public) else "<missingno>");
};
options = {
public = mkOption {
@@ -36,12 +42,40 @@
default = sysConfig.users.users.${config.owner}.group;
};
+ secretHash = mkOption {
+ type = types.str;
+ description = "Hash of .secret field";
+ };
+ publicHash = mkOption {
+ type = types.str;
+ description = "Hash of .public field";
+ };
+
path = mkOption {
type = types.str;
description = "Path to the decrypted secret";
};
+ stableSecretPath = mkOption {
+ type = types.str;
+ description = """
+ Use this, if target process supports re-reading of secret from disk,
+ and doesn't needs to be restarted when secret is updated in file
+ """;
+ };
+ secretPath = mkOption {
+ type = types.str;
+ description = "Path to decrypted secret, suffixed with contents hash";
+ };
+
+ stablePublicPath = mkOption {
+ type = types.str;
+ description = """
+ Use this, if target process supports re-reading of secret from disk,
+ and doesn't needs to be restarted when secret is updated in file
+ """;
+ };
publicPath = mkOption {
- type = types.package;
+ type = types.str;
description = "Path to the public part of secret";
};
};