Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Templating Support #2

Merged
merged 3 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

Secret management for NixOS.

Highly inspired by agenix-rekey. Based on rust age crate.
Highly inspired by agenix-rekey and sops-nix. Based on rust age crate.

+ AGE Support Only
+ PIV Card (Yubikey) Support
+ Age Plugin Compatible
+ Support Template
+ Support identity with passphase
+ Support PIV Card (Yubikey)
+ No Bash

## Setup
Expand Down Expand Up @@ -80,14 +81,24 @@ Adding nixosModule config:
# path = "/some/place";
};
};

# the templating function acts the same as sops-nix
templates = {
test = {
name = "template.txt";
# to be notice that the source secret file may have trailing `\n`
content = "this is a template for testing ${config.vaultix.placeholder.example}";
};
}
};
}
```

After this you could reference the decrypted secret path by:
After this you could reference the path by:

```
<AnyPathOption> = config.vaultix.secrets.example.path;
<A-PathOption> = config.vaultix.secrets.example.path;
<B-PathOption> = config.vaultix.templates.test.path;
# ...
```

Expand Down Expand Up @@ -138,3 +149,4 @@ See [TODO](./TODO.md)

+ [agenix](https://github.com/ryantm/agenix)
+ [agenix-rekey](https://github.com/oddlama/agenix-rekey)
+ [sops-nix](https://github.com/Mic92/sops-nix)
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
- [ ] template
- [x] template
- [ ] [edit] or [add] secret with extra encrypt key
- [ ] deploy to specified location
- [x] check command
Expand Down
9 changes: 7 additions & 2 deletions module/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,15 @@ let
Group of the decrypted secret.
'';
};
symlink = mkEnableOption "symlinking secrets to their destination" // {
symlink = mkEnableOption "symlinking secrets to destination" // {
default = true;
};
};
});

in
{
imports = [ ./template.nix ];

options.vaultix = {

package = mkOption { defaultText = literalMD "`packages.vaultix` from this flake"; };
Expand All @@ -254,7 +255,11 @@ in
Attrset of secrets.
'';
};
};

options.vaultix-debug = mkOption {
type = types.unspecified;
default = cfg;
};

config =
Expand Down
114 changes: 114 additions & 0 deletions module/template.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
config,
lib,
...
}:
# this part basically inherit from
# https://github.com/Mic92/sops-nix/tree/60e1bce1999f126e3b16ef45f89f72f0c3f8d16f/modules/sops/templates
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
'';
};
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 = "0";
description = ''
User of the built template.
'';
};
group = mkOption {
type = types.str;
default = users.${submod.config.owner}.group or "0";
defaultText = literalExpression ''
users.''${config.owner}.group or "0"
'';
description = ''
Group of the built template.
'';
};
symlink = mkEnableOption "symlinking template to destination" // {
default = true;
};
};
});

in
{
options.vaultix = {

templates = mkOption {
type = types.attrsOf templateType;
default = { };
description = ''
Attrset of templates.
'';
};

placeholder = mkOption {
type = types.attrsOf (
types.mkOptionType {
name = "coercibleToString";
description = "value that can be coerced to string";
check = lib.strings.isConvertibleWithToString;
merge = lib.mergeEqualOption;
}
);
default = { };
visible = false;
description = ''
Identical with the attribute name of secrets, NOTICE this if you
defined the `name` in secrets submodule.
'';
};
};
config = mkIf (config.vaultix.templates != { }) {
vaultix.placeholder = mapAttrs (
n: _: mkDefault "{{ ${builtins.hashString "sha256" n} }}"
) cfg.secrets;
};
}
115 changes: 86 additions & 29 deletions src/cmd/deploy.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::{
collections::HashMap,
fs::{self, OpenOptions, Permissions, ReadDir},
io::{ErrorKind, Write},
os::unix::fs::PermissionsExt,
Expand All @@ -10,10 +11,10 @@ use std::{
use crate::{
helper::{
self,
secret_buf::{HostEnc, SecBuf},
secret_buf::{Plain, SecBuf},
stored::{InStore, SecMap, SecPath},
},
profile::{HostKey, Profile},
profile::{self, HostKey, Profile},
};

use age::{ssh, x25519, Recipient};
Expand All @@ -33,6 +34,34 @@ impl HostKey {
}

const KEY_TYPE: &str = "ed25519";

fn deploy_to_fs(
ctx: SecBuf<Plain>,
item: impl crate::profile::DeployFactor,
generation_count: usize,
target_dir_ordered: 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 = helper::parse_permission::parse_octal_string(item.get_mode())
.map_err(|e| eyre!("parse octal permission err: {}", e))?;
let permissions = Permissions::from_mode(mode);

let file = OpenOptions::new().create(true).write(true).open(p)?;

file.set_permissions(permissions)?;

helper::set_owner_group::set_owner_and_group(&file, item.get_owner(), item.get_group())?;

file
};
the_file.write_all(ctx.buf_ref())?;
Ok(())
}

impl Profile {
pub fn get_decrypted_mount_point_path(&self) -> String {
self.settings.decrypted_mount_point.to_string()
Expand Down Expand Up @@ -149,15 +178,17 @@ impl Profile {
extract secrets to `/run/vaultix.d/$num` and link to `/run/vaultix`
*/
pub fn deploy(self) -> Result<()> {
let sec_instore_map = SecMap::<SecPath<_, InStore>>::create(&self.secrets)
let host_prv_key = &self.get_host_key_identity()?;
let plain_map: SecMap<Vec<u8>> = SecMap::<SecPath<_, InStore>>::create(&self.secrets)
.renced_stored(
self.settings.storage_in_store.clone().into(),
self.settings.host_pubkey.as_str(),
)
.bake_ctx()?
.inner();

trace!("{:?}", sec_instore_map);
.inner()
.into_iter()
.map(|(s, c)| (s, c.decrypt(host_prv_key).expect("err").inner()))
.collect();

let generation_count = self.init_decrypted_mount_point()?;

Expand All @@ -179,36 +210,62 @@ impl Profile {
})?
};

let host_prv_key = &self.get_host_key_identity()?;

sec_instore_map.into_iter().for_each(|(n, c)| {
let ctx = SecBuf::<HostEnc>::new(c)
.decrypt(host_prv_key)
.expect("err");
// deploy general secrets
plain_map.inner_ref().iter().for_each(|(n, c)| {
let ctx = SecBuf::<Plain>::new(c.clone());
deploy_to_fs(
ctx,
*n,
generation_count,
target_extract_dir_with_gen.clone(),
)
.expect("err");
});

info!("{} -> generation {}", n.name, generation_count);
let mut the_file = {
let mut p = target_extract_dir_with_gen.clone();
p.push(n.name.clone());
if self.templates.len() != 0 {
info!("start deploy templates");
use sha2::{Digest, Sha256};

let mode = helper::parse_permission::parse_octal_string(&n.mode).unwrap();
let permissions = Permissions::from_mode(mode);
let get_hashed_id = |s: &profile::Secret| -> String {
let mut hasher = Sha256::new();
hasher.update(s.id.as_str());
format!("{:X}", hasher.finalize()).to_lowercase()
};

let file = OpenOptions::new().create(true).write(true).open(p).unwrap();
// new map with sha256 hashed secret id str as key, ctx as value
let hashstr_ctx_map: HashMap<String, &Vec<u8>> = plain_map
.inner_ref()
.iter()
.map(|(k, v)| (get_hashed_id(*k), v))
.collect();

file.set_permissions(permissions).unwrap();
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");

helper::set_owner_group::set_owner_and_group(&file, &n.owner, &n.group)
.expect("good report");
hashstr_ctx_map
.iter()
.filter(|(k, _)| hashstrs_of_it.contains(*k))
.for_each(|(k, v)| {
// render
trace!("template before process: {}", template);
template = template.replace(
format!("{{{{ {} }}}}", k).as_str(),
String::from_utf8_lossy(v).to_string().as_str(),
);
trace!("processed template: {}", template);
});

file
};
the_file
.write_all(ctx.buf_ref())
.expect("write decrypted file error")
});
deploy_to_fs(
SecBuf::<Plain>::new(template.into_bytes()),
t,
generation_count,
target_extract_dir_with_gen.clone(),
)
.expect("extract template to target generation")
});
}

let _ = fs::remove_file(self.get_decrypt_dir_path());
// link back to /run/vaultix
if std::os::unix::fs::symlink(target_extract_dir_with_gen, self.get_decrypt_dir_path())
.wrap_err("create symlink error")
Expand Down
1 change: 1 addition & 0 deletions src/helper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod parse_permission;
pub mod secret_buf;
pub mod set_owner_group;
pub mod stored;
pub mod template;
13 changes: 13 additions & 0 deletions src/helper/secret_buf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct HostEnc;
#[derive(Debug, Clone)]
pub struct Plain;

#[derive(Debug, Clone)]
pub struct SecBuf<T> {
buf: Vec<u8>,
_marker: PhantomData<T>,
Expand All @@ -23,6 +24,9 @@ impl<T> SecBuf<T> {
_marker: PhantomData,
}
}
pub fn inner(self) -> Vec<u8> {
self.buf
}
}

use eyre::Result;
Expand All @@ -44,6 +48,15 @@ impl<T> SecBuf<T> {
}
}

impl<T> From<Vec<u8>> for SecBuf<T> {
fn from(value: Vec<u8>) -> Self {
Self {
buf: value,
_marker: PhantomData,
}
}
}

impl SecBuf<AgeEnc> {
pub fn renc(&self, ident: &dyn Identity, recips: Rc<dyn Recipient>) -> Result<SecBuf<HostEnc>> {
self.decrypt(ident).and_then(|d| d.encrypt(vec![recips]))
Expand Down
Loading