From 80364fa49655d36386261780c5823510816793ff Mon Sep 17 00:00:00 2001 From: oluceps Date: Wed, 13 Nov 2024 13:58:00 +0800 Subject: [PATCH 1/4] - symlink since has tmpfiled --- module/default.nix | 3 --- module/template.nix | 3 --- 2 files changed, 6 deletions(-) diff --git a/module/default.nix b/module/default.nix index 2a25b91..a283559 100644 --- a/module/default.nix +++ b/module/default.nix @@ -159,9 +159,6 @@ let Group of the decrypted secret. ''; }; - symlink = mkEnableOption "symlinking secrets to destination" // { - default = true; - }; }; }); in diff --git a/module/template.nix b/module/template.nix index 6cf35a5..2a70200 100644 --- a/module/template.nix +++ b/module/template.nix @@ -75,9 +75,6 @@ let Group of the built template. ''; }; - symlink = mkEnableOption "symlinking template to destination" // { - default = true; - }; }; }); From aa9910ba611e77f8a2c2a4090a820516d8d8aaaf Mon Sep 17 00:00:00 2001 From: oluceps Date: Wed, 13 Nov 2024 14:10:52 +0800 Subject: [PATCH 2/4] + deploy to specified location --- README.md | 6 ++- TODO.md | 2 +- module/default.nix | 1 - src/cmd/deploy.rs | 130 ++++++++++++++++++++++++++++----------------- src/profile.rs | 30 ++++------- 5 files changed, 95 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 301243a..59fde34 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,10 @@ Adding nixosModule config: owner = "root"; group = "users"; name = "example.toml"; - # symlink = true; # both not supported yet - # path = "/some/place"; + path = + # Optional. Secret will be extract to this place directly + # if user specified. + "/some/place"; }; }; diff --git a/TODO.md b/TODO.md index 53f18ce..6636d7d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,5 @@ - [ ] test with os -- [ ] deploy to specified location +- [x] deploy to specified location - [x] move storageInStore into flake module - [x] impl template - [x] [edit] or [add] secret with extra encrypt key diff --git a/module/default.nix b/module/default.nix index a283559..582d153 100644 --- a/module/default.nix +++ b/module/default.nix @@ -14,7 +14,6 @@ let readFile literalMD literalExpression - mkEnableOption mkIf assertMsg ; diff --git a/src/cmd/deploy.rs b/src/cmd/deploy.rs index e7180da..6d0faee 100644 --- a/src/cmd/deploy.rs +++ b/src/cmd/deploy.rs @@ -13,7 +13,7 @@ use crate::{ secret_buf::{Plain, SecBuf}, stored::{InStore, SecMap, SecPath}, }, - profile::{self, HostKey, Profile}, + profile::{self, DeployFactor, HostKey, Profile}, }; use crate::helper::parse_recipient::RawRecip; @@ -38,15 +38,10 @@ const KEY_TYPE: &str = "ed25519"; fn deploy_to_fs( ctx: SecBuf, item: impl crate::profile::DeployFactor, - generation_count: usize, - target_dir_ordered: PathBuf, + dst: PathBuf, ) -> Result<()> { - info!("{} -> generation {}", item.get_name(), generation_count); let mut the_file = { - let mut p = target_dir_ordered.clone(); - p.push(item.get_name().clone()); - - let mode = crate::parser::parse_octal_str(item.get_mode()) + let mode = crate::parser::parse_octal_str(item.mode()) .map_err(|e| eyre!("parse octal permission err: {}", e))?; let permissions = Permissions::from_mode(mode); @@ -54,11 +49,11 @@ fn deploy_to_fs( .create(true) .truncate(true) .write(true) - .open(p)?; + .open(dst)?; file.set_permissions(permissions)?; - helper::set_owner_group::set_owner_and_group(&file, item.get_owner(), item.get_group())?; + helper::set_owner_group::set_owner_and_group(&file, item.owner(), item.group())?; file }; @@ -185,18 +180,47 @@ impl Profile { .expect("set permission"); })? }; + macro_rules! generate_dst { + ($obj:expr, $settings:expr, $target_extract_dir:expr) => {{ + let default_path = { + let mut p: PathBuf = $settings.decrypted_dir.clone().into(); + p.push($obj.name()); + p + }; + if PathBuf::from($obj.path()) == default_path { + let mut ret = $target_extract_dir.clone(); + ret.push($obj.name()); + ret + } else { + if PathBuf::from($obj.path()).starts_with(&default_path) { + spdlog::warn!( + "extract to decryptedDir detected. recommend specify `name` instead of `path`." + ); + } + info!("specified decrypt path detected"); + $obj.path().into() + } + }}; + } // deploy general secrets - plain_map.inner_ref().iter().for_each(|(n, c)| { - let ctx = SecBuf::::new(c.clone()); - deploy_to_fs( - ctx, - *n, - generation_count, - target_extract_dir_with_gen.clone(), - ) - .expect("err"); - }); + plain_map + .inner_ref() + .iter() + .map(|(n, c)| { + let ctx = SecBuf::::new(c.clone()); + let item = n as &dyn DeployFactor; + let dst: PathBuf = generate_dst!(item, self.settings, target_extract_dir_with_gen); + + info!("secret {} -> {}", item.name(), dst.display(),); + + deploy_to_fs(ctx, *n, dst) + }) + .for_each(|res| { + if let Err(e) = res { + error!("{}", e); + } + }); if !self.templates.is_empty() { info!("start deploy templates"); @@ -215,41 +239,47 @@ impl Profile { .map(|(k, v)| (get_hashed_id(k), v)) .collect(); - self.templates.clone().iter().for_each(|(_, t)| { - let mut template = t.content.clone(); - let hashstrs_of_it = t.parse_hash_str_list().expect("parse template"); + 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; + let trim_the_insertial = t.trim; - hashstr_ctx_map - .iter() - .filter(|(k, _)| hashstrs_of_it.contains(k)) - .for_each(|(k, v)| { - // render and insert - trace!("template before process: {}", template); + hashstr_ctx_map + .iter() + .filter(|(k, _)| hashstrs_of_it.contains(k)) + .for_each(|(k, v)| { + // render and insert + trace!("template before process: {}", template); - let raw_composed_insertial = String::from_utf8_lossy(v).to_string(); + 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() - }; + let insertial = if trim_the_insertial { + raw_composed_insertial.trim() + } else { + raw_composed_insertial.as_str() + }; - template = template.replace( - format!("{{{{ {} }}}}", hex::encode(k.as_slice())).as_str(), - insertial, - ); - }); - - deploy_to_fs( - SecBuf::::new(template.into_bytes()), - t, - generation_count, - target_extract_dir_with_gen.clone(), - ) - .expect("extract template to target generation") - }); + template = template.replace( + format!("{{{{ {} }}}}", hex::encode(k.as_slice())).as_str(), + 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); + } + }); } // link back to /run/vaultix diff --git a/src/profile.rs b/src/profile.rs index a832785..78be496 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -21,7 +21,6 @@ pub struct Secret { pub name: String, pub owner: String, pub path: String, - pub symlink: bool, } #[derive(Debug, Deserialize, Clone, Hash, Eq, PartialEq, Default)] @@ -33,7 +32,6 @@ pub struct Template { pub mode: String, pub owner: String, pub path: String, - pub symlink: bool, } #[derive(Debug, Deserialize)] @@ -54,17 +52,18 @@ pub struct HostKey { } pub trait DeployFactor { - fn get_mode(&self) -> &String; - fn get_owner(&self) -> &String; - fn get_name(&self) -> &String; - fn get_group(&self) -> &String; + fn mode(&self) -> &String; + fn owner(&self) -> &String; + fn name(&self) -> &String; + fn group(&self) -> &String; + fn path(&self) -> &String; } macro_rules! impl_deploy_factor { - ($type:ty, { $($method:ident => $field:ident),+ $(,)? }) => { + ($type:ty, [ $($field:ident),+ $(,)? ]) => { impl DeployFactor for $type { $( - fn $method(&self) -> &String { + fn $field(&self) -> &String { &self.$field } )+ @@ -72,15 +71,6 @@ macro_rules! impl_deploy_factor { }; } -impl_deploy_factor!(&Secret, { - get_mode => mode, - get_owner => owner, - get_name => name, - get_group => group -}); -impl_deploy_factor!(&Template, { - get_mode => mode, - get_owner => owner, - get_name => name, - get_group => group -}); +impl_deploy_factor!(&Secret, [mode, owner, name, group, path]); + +impl_deploy_factor!(&Template, [mode, owner, name, group, path]); From e7d6528c4c0f7fab6dea5a36578b21ea8cd760ca Mon Sep 17 00:00:00 2001 From: oluceps Date: Wed, 13 Nov 2024 14:42:14 +0800 Subject: [PATCH 3/4] ~ default uname and groupname --- module/default.nix | 6 +++--- module/template.nix | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/module/default.nix b/module/default.nix index 582d153..e15c765 100644 --- a/module/default.nix +++ b/module/default.nix @@ -143,16 +143,16 @@ let }; owner = mkOption { type = types.str; - default = "0"; + default = "root"; description = '' User of the decrypted secret. ''; }; group = mkOption { type = types.str; - default = users.${submod.config.owner}.group or "0"; + default = users.${submod.config.owner}.group or "root"; defaultText = literalExpression '' - users.''${config.owner}.group or "0" + users.''${config.owner}.group or "root" ''; description = '' Group of the decrypted secret. diff --git a/module/template.nix b/module/template.nix index 2a70200..d5fe5a5 100644 --- a/module/template.nix +++ b/module/template.nix @@ -60,16 +60,16 @@ let }; owner = mkOption { type = types.str; - default = "0"; + default = "root"; description = '' User of the built template. ''; }; group = mkOption { type = types.str; - default = users.${submod.config.owner}.group or "0"; + default = users.${submod.config.owner}.group or "root"; defaultText = literalExpression '' - users.''${config.owner}.group or "0" + users.''${config.owner}.group or "root" ''; description = '' Group of the built template. From 41a228659d64e3dd1e760e04a7681467326f10a6 Mon Sep 17 00:00:00 2001 From: oluceps Date: Thu, 14 Nov 2024 00:23:26 +0800 Subject: [PATCH 4/4] + docs and description --- flake-module.nix | 29 +++++++++++++++++++++++++++++ module/default.nix | 35 ++++++++++++++++++++++++++++------- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/flake-module.nix b/flake-module.nix index 817c759..0b5b027 100644 --- a/flake-module.nix +++ b/flake-module.nix @@ -22,11 +22,19 @@ in cache = mkOption { type = types.addCheck types.str (s: (builtins.substring 0 1 s) == "."); default = "./secrets/cache"; + defaultText = lib.literalExpression "./secrets/cache"; + description = '' + `path str` that relative to flake root, used for storing host public key + re-encrypted secrets. + ''; }; nodes = mkOption { type = types.lazyAttrsOf types.unspecified; default = self.nixosConfigurations; defaultText = lib.literalExpression "self.nixosConfigurations"; + description = '' + nixos systems that vaultix to manage. + ''; }; identity = mkOption { type = @@ -37,6 +45,11 @@ in nullOr identityPathType; default = null; example = ./password-encrypted-identity.pub; + description = '' + `Age identity file`. + Able to use yubikey, see . + Supports age native secrets (recommend protected with passphrase) + ''; }; extraRecipients = mkOption { type = with types; listOf (coercedTo path toString str); @@ -44,6 +57,10 @@ in example = [ "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3290gq" ]; + description = '' + Recipients used for backup. Any of identity of them will able + to decrypt all secrets. + ''; }; app = mkOption { type = types.lazyAttrsOf (types.lazyAttrsOf types.package); @@ -71,10 +88,19 @@ in ) ) config.allSystems; readOnly = true; + defaultText = "Auto generate by flake module"; + description = '' + vaultix apps that auto generate by its flake module. + Run manually with `nix run .#vaultix.app.$system.` + ''; }; }; }); default = { }; + description = '' + A single-admin secret manage scheme for nixos, with support of templates and + agenix-like secret configuration layout. + ''; }; }; @@ -90,6 +116,9 @@ in type = types.unspecified; default = pkgs; defaultText = lib.literalExpression "pkgs"; + description = '' + pkgs that passed into vaultix apps. + ''; }; }; } diff --git a/module/default.nix b/module/default.nix index e15c765..6fa01be 100644 --- a/module/default.nix +++ b/module/default.nix @@ -35,13 +35,18 @@ let default = builtins.path { path = "/" + self + "/" + self.vaultix.cache + "/" + config.networking.hostName; }; + defaultText = literalExpression "path in store"; + description = '' + Secrets re-encrypted by each host public key. In nix store. + ''; }; decryptedDir = mkOption { type = types.path; default = "/run/vaultix"; + defaultText = literalExpression "/run/vaultix"; description = '' - Folder where secrets are symlinked to + Folder where secrets are symlinked to. ''; }; @@ -59,15 +64,28 @@ let } ); default = config.services.openssh.hostKeys; + defaultText = literalExpression "config.services.openssh.hostKeys"; readOnly = true; description = '' - `config.services.openssh.hostKeys` + Ed25519 host private ssh key (identity) path that used for decrypting secrets while deploying. + Default is `config.services.openssh.hostKeys`. + + Default format: + ```nix + [ + { + path = "/path/to/ssh_host_ed25519_key"; + type = "ed25519"; + } + ] + ``` ''; }; hostIdentifier = mkOption { type = types.str; default = config.networking.hostName; + defaultText = literalExpression "config.networking.hostName"; readOnly = true; description = '' Host identifier @@ -85,6 +103,7 @@ let description = "${types.str.description} (with check: non-empty without trailing slash)"; }; default = "/run/vaultix.d"; + defaultText = literalExpression "/run/vaultix.d"; description = '' Where secrets are created before they are symlinked to {option}`vaultix.settings.decryptedDir` ''; @@ -92,14 +111,16 @@ let hostPubkey = mkOption { type = with types; coercedTo path (x: if isPath x then readFile x else x) str; - #example = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI....."; - #example = "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3290gq"; example = literalExpression "./secrets/host1.pub"; - #example = "/etc/ssh/ssh_host_ed25519_key.pub"; + description = '' + str or path that contains host public key. + example: + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI....." + "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpq....." + "/etc/ssh/ssh_host_ed25519_key.pub" + ''; }; - }; - }); secretType = types.submodule (submod: {