From 194026f0fe13da10800c7a797d437de1e0ceaf2f Mon Sep 17 00:00:00 2001 From: oluceps Date: Fri, 15 Nov 2024 17:41:28 +0800 Subject: [PATCH] + refine needByUser module options --- dev/check.nix | 2 + dev/test.nix | 3 +- doc/src/nixos-option.md | 13 +++- module/default.nix | 100 +++---------------------- module/secretType.nix | 79 ++++++++++++++++++++ module/template.nix | 62 +--------------- module/templateType.nix | 90 +++++++++++++++++++++++ src/cmd/deploy.rs | 158 +++++++++++++++++++++------------------- src/cmd/mod.rs | 10 ++- src/helper/stored.rs | 19 ++++- src/profile.rs | 5 +- 11 files changed, 308 insertions(+), 233 deletions(-) create mode 100644 module/secretType.nix create mode 100644 module/templateType.nix diff --git a/dev/check.nix b/dev/check.nix index afb3f0b..37b5698 100644 --- a/dev/check.nix +++ b/dev/check.nix @@ -3,11 +3,13 @@ disko.tests = { extraChecks = '' machine.succeed("test -e /run/vaultix.d/0") + machine.succeed("test -e /run/vaultix.d/1") machine.succeed("test -e ${config.vaultix.secrets.test-secret-1.path}") machine.succeed("test -e ${config.vaultix.secrets.test-secret-2.path}") machine.succeed("test -e ${config.vaultix.templates.template-test.path}") machine.succeed("md5sum -c ${pkgs.writeText "checksum-list" '' 2e57c2db0f491eba1d4e496a076cdff7 ${config.vaultix.secrets.test-secret-1.path} + 2e57c2db0f491eba1d4e496a076cdff7 ${config.vaultix.secrets.test-secret-2.path} ba1efe71bd3d4a9a491d74df5c23e177 ${config.vaultix.templates.template-test.path} ''}") ''; diff --git a/dev/test.nix b/dev/test.nix index b61698a..7e136a7 100644 --- a/dev/test.nix +++ b/dev/test.nix @@ -42,6 +42,8 @@ vaultix = { settings.hostPubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEu8luSFCts3g367nlKBrxMdLyOy4Awfo5Rb397ef2AR"; + needByUser = [ "test-secret-2" ]; + # secret example secrets.test-secret-1 = { file = ./secrets/there-is-a-secret.age; @@ -56,7 +58,6 @@ owner = "root"; group = "users"; path = "/home/1.txt"; - neededForUser = true; }; # template example diff --git a/doc/src/nixos-option.md b/doc/src/nixos-option.md index 90ab399..6c0d5b8 100644 --- a/doc/src/nixos-option.md +++ b/doc/src/nixos-option.md @@ -11,6 +11,7 @@ Configurable option could be divided into 3 parts: settings = { ... }; secrets = { ... }; templates = { ... }; + needByUser = [...]; }; } ``` @@ -93,7 +94,6 @@ secrets = { group = "users"; name = "example.toml"; path = "/some/place"; - neededForUser = false; }; }; ``` @@ -101,7 +101,6 @@ secrets = { This part basically keeps identical with `agenix`. But has few diffs: + no `symlink: bool` option, since it has an systemd function called [tmpfiles.d](https://www.freedesktop.org/software/systemd/man/latest/tmpfiles.d.html). -+ added `neededForUser: bool` option, for deploying secrets and templates that required before user init. ### path: path str @@ -129,7 +128,6 @@ templates = { group = "users"; name = "example.toml"; path = "/some/place"; - neededForUser = false; }; } ``` @@ -142,6 +140,8 @@ To insert secrets in this string, insert `config.vaultix.placeholder.example`. This pretend the secret which `id` (the keyof attribute of secrets) was defined. +
+ ```nix secrets = { # the id is 'example'. despite `name`. @@ -167,3 +167,10 @@ TO BE NOTICE that the source secret file may have trailing `\n`: default true; Removing trailing and leading whitespace by default. + + +## needByUser: [str] + +For deploying secrets and templates that required before user init. + +List of [id](#id-state) of templates or secrets. diff --git a/module/default.nix b/module/default.nix index e3c3235..997e660 100644 --- a/module/default.nix +++ b/module/default.nix @@ -10,12 +10,10 @@ let inherit (lib) types mkOption - filterAttrs isPath readFile literalMD warn - mkEnableOption literalExpression mkIf assertMsg @@ -146,69 +144,7 @@ let }; }); - secretType = types.submodule (submod: { - options = { - id = mkOption { - type = types.str; - default = submod.config._module.args.name; - readOnly = true; - description = "The true identifier of this secret as used in `age.secrets`."; - }; - name = mkOption { - type = types.str; - default = submod.config._module.args.name; - defaultText = literalExpression "submod.config._module.args.name"; - description = '' - Name of the file used in {option}`vaultix.settings.decryptedDir` - ''; - }; - file = mkOption { - type = types.path; - description = '' - Age file the secret is loaded from. - ''; - }; - path = mkOption { - type = types.str; - default = - if submod.config.neededForUser then - "${cfg.settings.decryptedDirForUser}/${submod.config.name}" - else - "${cfg.settings.decryptedDir}/${submod.config.name}"; - defaultText = literalExpression '' - "''${cfg.settings.decryptedDir}/''${config.name}" - ''; - description = '' - Path where the decrypted secret is installed. - ''; - }; - mode = mkOption { - type = types.str; - default = "0400"; - description = '' - Permissions mode of the decrypted secret in a format understood by chmod. - ''; - }; - owner = mkOption { - type = types.str; - default = "root"; - description = '' - User of the decrypted secret. - ''; - }; - group = mkOption { - type = types.str; - default = users.${submod.config.owner}.group or "root"; - defaultText = literalExpression '' - users.''${config.owner}.group or "root" - ''; - description = '' - Group of the decrypted secret. - ''; - }; - neededForUser = mkEnableOption { }; - }; - }); + inherit (import ./secretType.nix { inherit lib cfg users; }) secretType; in { imports = [ ./template.nix ]; @@ -232,6 +168,14 @@ in Attrset of secrets. ''; }; + + needByUser = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + List of id of items needed before user init + ''; + }; }; options.vaultix-debug = mkOption { @@ -247,28 +191,8 @@ in name = "secret-meta-${config.networking.hostName}"; text = builtins.toJSON partial; }; - whatIfPreUser = what: need: filterAttrs (_: v: v.neededForUser == need) what; - - secretsPreUser = whatIfPreUser cfg.secrets true; - templatesPreUser = whatIfPreUser cfg.templates true; - - regularSecrets = whatIfPreUser cfg.secrets false; - regularTemplates = whatIfPreUser cfg.templates false; - profilePreUser = mkProfile ( - cfg - // { - secrets = secretsPreUser; - templates = templatesPreUser; - } - ); - profileRegular = mkProfile ( - cfg - // { - secrets = regularSecrets; - templates = regularTemplates; - } - ); + profile = mkProfile cfg; checkRencSecsReport = pkgs.runCommandNoCCLocal "secret-check-report" { } @@ -289,7 +213,7 @@ in serviceConfig = { Type = "oneshot"; Environment = deployRequisits; - ExecStart = "${lib.getExe cfg.package} -p ${profileRegular} deploy"; + ExecStart = "${lib.getExe cfg.package} -p ${profile} deploy"; RemainAfterExit = true; }; }; @@ -302,7 +226,7 @@ in serviceConfig = { Type = "oneshot"; Environment = deployRequisits; - ExecStart = "${lib.getExe cfg.package} -p ${profilePreUser} deploy"; + ExecStart = "${lib.getExe cfg.package} -p ${profile} deploy --early"; RemainAfterExit = true; }; }; diff --git a/module/secretType.nix b/module/secretType.nix new file mode 100644 index 0000000..34f2307 --- /dev/null +++ b/module/secretType.nix @@ -0,0 +1,79 @@ +{ + lib, + cfg, + users, + ... +}: + +let + inherit (lib) + types + elem + mkOption + literalExpression + ; +in +{ + secretType = types.submodule (submod: { + options = { + id = mkOption { + type = types.str; + default = submod.config._module.args.name; + readOnly = true; + description = "The true identifier of this secret as used in `age.secrets`."; + }; + name = mkOption { + type = types.str; + default = submod.config._module.args.name; + defaultText = literalExpression "submod.config._module.args.name"; + description = '' + Name of the file used in {option}`vaultix.settings.decryptedDir` + ''; + }; + file = mkOption { + type = types.path; + description = '' + Age file the secret is loaded from. + ''; + }; + path = mkOption { + type = types.str; + default = + if elem submod.config._module.args.name cfg.needByUser then + "${cfg.settings.decryptedDirForUser}/${submod.config.name}" + else + "${cfg.settings.decryptedDir}/${submod.config.name}"; + defaultText = literalExpression '' + "''${cfg.settings.decryptedDir}/''${config.name}" + ''; + description = '' + Path where the decrypted secret is installed. + ''; + }; + mode = mkOption { + type = types.str; + default = "0400"; + description = '' + Permissions mode of the decrypted secret in a format understood by chmod. + ''; + }; + owner = mkOption { + type = types.str; + default = "root"; + description = '' + User of the decrypted secret. + ''; + }; + group = mkOption { + type = types.str; + default = users.${submod.config.owner}.group or "root"; + defaultText = literalExpression '' + users.''${config.owner}.group or "root" + ''; + description = '' + Group of the decrypted secret. + ''; + }; + }; + }); +} diff --git a/module/template.nix b/module/template.nix index 7ec7aab..a5159c5 100644 --- a/module/template.nix +++ b/module/template.nix @@ -8,76 +8,16 @@ let inherit (lib) mkOption - mkEnableOption mkDefault mapAttrs types - literalExpression mkIf ; inherit (config.users) users; cfg = config.vaultix; - templateType = types.submodule (submod: { - options = { - content = mkOption { - type = types.str; - default = ""; - defaultText = literalExpression ""; - description = '' - Content of the template - ''; - }; - trim = (mkEnableOption { }) // { - default = true; - description = "remove trailing and leading whitespace of the secret content to insert"; - }; - name = mkOption { - type = types.str; - default = submod.config._module.args.name; - defaultText = literalExpression "submod.config._module.args.name"; - description = '' - Name of the file used in {option}`vaultix.settings.decryptedDir` - ''; - }; - path = mkOption { - type = types.str; - default = "${cfg.settings.decryptedDir}/${submod.config.name}"; - defaultText = literalExpression '' - "''${cfg.settings.decryptedDir}/''${config.name}" - ''; - description = '' - Path where the built template is installed. - ''; - }; - mode = mkOption { - type = types.str; - default = "0400"; - description = '' - Permissions mode of the built template in a format understood by chmod. - ''; - }; - owner = mkOption { - type = types.str; - default = "root"; - description = '' - User of the built template. - ''; - }; - group = mkOption { - type = types.str; - default = users.${submod.config.owner}.group or "root"; - defaultText = literalExpression '' - users.''${config.owner}.group or "root" - ''; - description = '' - Group of the built template. - ''; - }; - neededForUser = mkEnableOption { }; - }; - }); + inherit (import ./templateType.nix { inherit lib cfg users; }) templateType; in { diff --git a/module/templateType.nix b/module/templateType.nix new file mode 100644 index 0000000..c47f55b --- /dev/null +++ b/module/templateType.nix @@ -0,0 +1,90 @@ +{ + lib, + cfg, + users, + ... +}: + +let + inherit (lib) + types + elem + mkOption + literalExpression + mkEnableOption + ; +in +{ + templateType = types.submodule (submod: { + options = { + type = mkOption { + type = types.str; + default = "template"; + readOnly = true; + description = "Identifier of option type"; + }; + content = mkOption { + type = types.str; + default = ""; + defaultText = literalExpression ""; + description = '' + Content of the template + ''; + }; + trim = (mkEnableOption { }) // { + default = true; + description = "remove trailing and leading whitespace of the secret content to insert"; + }; + name = mkOption { + type = types.str; + default = submod.config._module.args.name; + defaultText = literalExpression "submod.config._module.args.name"; + description = '' + Name of the file used in {option}`vaultix.settings.decryptedDir` + ''; + }; + path = mkOption { + type = types.str; + default = + if elem submod.config._module.args.name cfg.needByUser then + "${cfg.settings.decryptedDirForUser}/${submod.config.name}" + else + "${cfg.settings.decryptedDir}/${submod.config.name}"; + + defaultText = literalExpression '' + if elem submod.config._module.args.name cfg.needbyUser then + "${cfg.settings.decryptedDirForUser}/${submod.config.name}" + else + "${cfg.settings.decryptedDir}/${submod.config.name}"; + ''; + description = '' + Path where the built template is installed. + ''; + }; + mode = mkOption { + type = types.str; + default = "0400"; + description = '' + Permissions mode of the built template in a format understood by chmod. + ''; + }; + owner = mkOption { + type = types.str; + default = "root"; + description = '' + User of the built template. + ''; + }; + group = mkOption { + type = types.str; + default = users.${submod.config.owner}.group or "root"; + defaultText = literalExpression '' + users.''${config.owner}.group or "root" + ''; + description = '' + Group of the built template. + ''; + }; + }; + }); +} diff --git a/src/cmd/deploy.rs b/src/cmd/deploy.rs index 85fe8c7..b622891 100644 --- a/src/cmd/deploy.rs +++ b/src/cmd/deploy.rs @@ -153,18 +153,29 @@ impl Profile { /** extract secrets to `/run/vaultix.d/$num` and link to `/run/vaultix` */ - pub fn deploy(self) -> Result<()> { + pub fn deploy(self, early: bool) -> Result<()> { + if self.secrets.is_empty() && self.templates.is_empty() { + return Ok(()); + } let host_prv_key = &self.get_host_key_identity()?; - let plain_map: SecMap> = SecMap::>::create(&self.secrets) - .renced_stored( - self.settings.cache_in_store.clone().into(), - self.settings.host_pubkey.as_str(), - ) - .bake_ctx()? - .inner() - .into_iter() - .map(|(s, c)| (s, c.decrypt(host_prv_key).expect("err").inner())) - .collect(); + + let if_early = |i: &String| -> bool { self.need_by_user.contains(i) == early }; + + let secrets_to_deploy = self.secrets.iter().filter(|i| if_early(i.0)); + + let templates_map_iter = self.templates.iter().filter(|i| if_early(i.0)); + + let plain_map: SecMap> = + SecMap::>::from_iter(secrets_to_deploy.into_iter().map(|(_, v)| v)) + .renced_stored( + self.settings.cache_in_store.clone().into(), + self.settings.host_pubkey.as_str(), + ) + .bake_ctx()? + .inner() + .into_iter() + .map(|(s, c)| (s, c.decrypt(host_prv_key).expect("err").inner())) + .collect(); let generation_count = self.init_decrypted_mount_point()?; @@ -226,72 +237,71 @@ impl Profile { error!("{}", e); } }); + info!("finish secrets deployment"); - if !self.templates.is_empty() { - info!("start deploy templates"); - - // new map with {{ hash }} String as key, ctx as value - let hashstr_ctx_map: HashMap<&str, &Vec> = plain_map - .inner_ref() - .iter() - .map(|(k, v)| { - self.placeholder - .get_braced_from_id(k.id.as_str()) - .wrap_err_with(|| { - eyre!("secrets corresponding to the template placeholder id not found") - }) - .map(|i| (i, v)) - .expect("found secret from placeholder id") - }) - .collect(); + info!("start templates deployment"); + // new map with {{ hash }} String as key, ctx as value + let hashstr_ctx_map: HashMap<&str, &Vec> = plain_map + .inner_ref() + .iter() + .map(|(k, v)| { + self.placeholder + .get_braced_from_id(k.id.as_str()) + .wrap_err_with(|| { + eyre!("secrets corresponding to the template placeholder id not found") + }) + .map(|i| (i, v)) + .expect("found secret from placeholder id") + }) + .collect(); - self.templates - .values() - .map(|t| { - let mut template = t.content.clone(); - let hashstrs_of_it = t.parse_hash_str_list().expect("parse template"); - - let trim_the_insertial = t.trim; - - hashstr_ctx_map - .iter() - .filter(|(k, _)| { - let mut v = Vec::new(); - extract_all_hashes(k, &mut v); - hashstrs_of_it - // promised by nixos module - .contains(&decode(v.first().expect("only one")).expect("decoded")) - }) - .for_each(|(k, v)| { - // render and insert - trace!("template before process: {}", template); - - let raw_composed_insertial = String::from_utf8_lossy(v).to_string(); - - let insertial = if trim_the_insertial { - raw_composed_insertial.trim() - } else { - raw_composed_insertial.as_str() - }; - - template = template.replace(k, insertial); - }); - - let item = &t as &dyn DeployFactor; - - let dst = generate_dst!(item, self.settings, target_extract_dir_with_gen); - - info!("template {} -> {}", item.name(), dst.display(),); - deploy_to_fs(SecBuf::::new(template.into_bytes()), t, dst) - }) - .for_each(|res| { - if let Err(e) = res { - error!("{}", e); - } - }); - } + templates_map_iter + .map(|(_, t)| { + let mut template = t.content.clone(); + let hashstrs_of_it = t.parse_hash_str_list().expect("parse template"); + + let trim_the_insertial = t.trim; + + hashstr_ctx_map + .iter() + .filter(|(k, _)| { + let mut v = Vec::new(); + extract_all_hashes(k, &mut v); + hashstrs_of_it + // promised by nixos module + .contains(&decode(v.first().expect("only one")).expect("decoded")) + }) + .for_each(|(k, v)| { + // render and insert + trace!("template before process: {}", template); + + let raw_composed_insertial = String::from_utf8_lossy(v).to_string(); + + let insertial = if trim_the_insertial { + raw_composed_insertial.trim() + } else { + raw_composed_insertial.as_str() + }; + + template = template.replace(k, insertial); + }); + + let item = &t as &dyn DeployFactor; + + let dst = generate_dst!(item, self.settings, target_extract_dir_with_gen); + + info!("template {} -> {}", item.name(), dst.display(),); + deploy_to_fs(SecBuf::::new(template.into_bytes()), t, dst) + }) + .for_each(|res| { + if let Err(e) = res { + error!("{}", e); + } + }); + + info!("finish templates deployment"); - let symlink_dst = if self.secrets.values().any(|i| i.needed_for_user) { + let symlink_dst = if early { self.get_decrypt_dir_path_for_user() } else { self.get_decrypt_dir_path() diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 1ad4af2..059ad9c 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -61,7 +61,11 @@ pub struct EditSubCmd { #[derive(FromArgs, PartialEq, Debug)] /// Decrypt and deploy cipher credentials #[argh(subcommand, name = "deploy")] -pub struct DeploySubCmd {} +pub struct DeploySubCmd { + #[argh(switch, short = 'e')] + /// deploy for user + early: bool, +} #[derive(FromArgs, PartialEq, Debug)] /// Check secret status @@ -96,10 +100,10 @@ impl Args { let profile = profile()?; profile.renc(flake_root, identity.clone(), cache.into()) } - SubCmd::Deploy(DeploySubCmd {}) => { + SubCmd::Deploy(DeploySubCmd { early }) => { info!("deploying secrets"); let profile = profile()?; - profile.deploy() + profile.deploy(*early) } SubCmd::Edit(e) => { info!("editing secrets"); diff --git a/src/helper/stored.rs b/src/helper/stored.rs index 41792b3..11dbc90 100644 --- a/src/helper/stored.rs +++ b/src/helper/stored.rs @@ -13,7 +13,7 @@ use spdlog::{debug, trace}; use crate::{ helper::secret_buf::{AgeEnc, SecBuf}, - profile::{self, SecretSet}, + profile::{self, Secret, SecretSet}, }; use eyre::{eyre, Result}; use std::marker::PhantomData; @@ -93,6 +93,7 @@ macro_rules! impl_from_iterator_for_secmap { )* }; } + impl_from_iterator_for_secmap!(Vec, blake3::Hash, UniPath, SecBuf); macro_rules! impl_from_for_secmap { @@ -134,6 +135,12 @@ impl SecMap<'_, SecPath> { } } +impl FromIterator for SecMap<'_, SecPBWith> { + fn from_iter>(iter: T) -> Self { + iter.into_iter().collect() + } +} + impl<'a> SecMap<'a, SecPBWith> { pub fn create(secrets: &'a SecretSet) -> Self { SecMap::>( @@ -147,6 +154,16 @@ impl<'a> SecMap<'a, SecPBWith> { ) } + pub fn from_iter(iter: impl Iterator) -> Self { + SecMap::>( + iter.map(|s| { + let secret_path = SecPath::<_, InStore>::new(PathBuf::from(s.file.clone())); + (s, secret_path) + }) + .collect(), + ) + } + /// return self but processed the path to produce in-store storageInStore/[hash] map pub fn renced_stored(self, per_host_dir: PathBuf, host_pubkey: &str) -> Self { self.inner() diff --git a/src/profile.rs b/src/profile.rs index 3fed948..07b9162 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -6,10 +6,12 @@ pub type SecretSet = HashMap; pub type TemplateSet = HashMap; #[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Profile { - pub secrets: SecretSet, pub settings: Settings, + pub secrets: SecretSet, pub templates: TemplateSet, + pub need_by_user: Vec, pub placeholder: PlaceHolderSet, } @@ -26,7 +28,6 @@ pub struct Secret { pub name: String, pub owner: String, pub path: String, - pub needed_for_user: bool, } #[derive(Debug, Deserialize, Clone, Hash, Eq, PartialEq, Default)]