Vaultix
Secret management for NixOS.
This project is highly inspired by agenix-rekey and sops-nix. Based on rust age crate.
- Support Template
- Age Plugin Compatible
- Support PIV Card (Yubikey)
- Support identity with passphrase
- Compatible with
userborn
module option - No Bash
Prerequisits
It basically require:
nix-command
feature enabledflake-parts
structured configself
as one ofspecialArgs
for nixosSystemsystemd.sysusers
orservices.userborn
option enabled
See following for reason and details.
enable
nix-command
flakes
features
Which is almost every user will do.
Vaultix depends on nix flake and nix apps to perform basic function.
nix.settings = {
experimental-features = [
"nix-command"
"flakes"
];
}
flake-parts
structured config
flake-parts provides modulized flake config, vaultix using flake module to produce nix apps and hidding complexity.
self
as one ofspecialArgs
for nixosSystem
For passing top-level flake arguments to nixos module.
This requirement may change in the future, with backward compatiblility. Looking forward for a better implementation in nixpkgs that more gracefully to do so.
enable
systemd.sysusers
orservices.userborn
sysusers
was comes with Perlless Activation.
userborn
was introduced in Aug 30 2024
Vaultix using systemd instead of old perl script for activating on system startup or switch. It meams that you need on nixos-24.05
or newer version for using it.
setup
You could also find the minimal complete nixos configuration on CI VM test.
Layout Preview
{
withSystem,
self,
inputs,
...
}:
{
flake = {
vaultix = {
nodes = self.nixosConfigurations;
identity = "/home/who/key";
};
nixosConfigurations.host-name = withSystem "x86_64-linux" ({ system, ... }:
inputs.nixpkgs.lib.nixosSystem (
{
inherit system;
specialArgs = {
inherit self; # Required
};
modules = [
inputs.oluceps.nixosModules.vaultix # import nixosModule
(
{ config, ... }:
{
services.userborn.enable = true; # or systemd.sysuser, required
vaultix = {
settings.hostPubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEu8luSFCts3g367nlKBrxMdLyOy4Awfo5Rb397ef2BC";
secrets.test-secret-1 = {
file = ./secrets/there-is-a-secret.age;
};
};
}
)
./configuration.nix
];
}
)
);
};
}
And you will be able to use secret on any module with, e.g.:
{
services.proxy1.environmentFile = config.vaultix.secrets.example.path;
}
# ...
flakeModule Options
The Vaultix configuration option has two parts: in flake level and nixos module level. You need to set both to make it work.
Here is a flake module configuration, it should be written in your flake top-level or in flake module.
Commented options means its default value.
You could find the definition here
flake.vaultix = {
nodes = self.nixosConfigurations;
identity = "/where/sec/age-yubikey-identity-7d5d5540.txt.pub";
# extraRecipients = [ ];
# cache = "./secrets/cache";
};
node =
NixOS systems that allow vaultix to manage. Generally pass self.nixosConfigurations
will work, if you're using framework like colmena
that produced unstandard system outputs, you need manually convertion, there always some way. For example, for colmena
:
nodes = inherit ((colmena.lib.makeHive self.colmena).introspect (x: x)) nodes;
identity =
Age identity file
.
Supports age native secrets (recommend protected with passphrase), this could be a:
-
string, of absolute path to your local age identity. Thus it can avoid loading identity to nix store.
-
path, relative to your age identity in your configuration repository. Note that writting path directly will copy your secrets into nix store, with Global READABLE.
This is THE identity that could decrypt all of your secret, take care of it.
Every
path
in your nix configuration will load file to nix store, eventually shows as string of absolute path to nix store.
Since it inherited great compatibility of age
, you could use yubikey. Feel free to test other plugins like age tpm.
extraRecipients =
Recipients used for backup. Any of identity of them will able to decrypt all secrets, like the identity
.
Changing this will not take effect to
renc
command output. The hash of host pub key re-encrypted filename isblake3(encrypted secret content + host public key)
.
cache =
String of path that relative to flake root, used for storing host public key
re-encrypted secrets. It's default ./secrets/cache
.
NixOS Module Options
Configurable option could be divided into 3 parts:
# configuration.nix
{
imports = [ inputs.vaultix.nixosModules.default ];
vaultix = {
settings = { ... };
secrets = { ... };
templates = { ... };
needByUser = [...];
};
}
Settings =
Literally.
decryptedDir: path str
Folder where secrets are symlinked to. Default is /run/vaultix
.
decryptedDirForUser: path str
Same as above, but for secrets and templates that required by user, which means needs to be initialize before user born.
decryptedMountPoint: path str with no trailing slash
default is /run/vaultix.d
Where secrets are created before they are symlinked to vaultix.settings.decryptedDir
Vaultix use this machenism to implement atomic manage, like other secret managing schemes.
It decrypting secrets into this directory, with generation number like /run/vaultix.d/1
, then symlink it to decryptedDir
.
hostKeys
{ path: str, type: str }
default is config.services.openssh.hostKeys
This generally has no need to manually change, unless you know clearly what you're doing.
Ed25519 host private ssh key (identity) path that used for decrypting secrets while deploying.
format:
[
{
path = "/path/to/ssh_host_ed25519_key";
type = "ed25519";
}
]
hostPubkey: str
ssh public key of the private key, which defines above. This is different from every host, since each generates host key while initial booting.
Get this by: ssh-keyscan ip
. It supports ed25519
type.
Secrets =
Here is a secrets:
secrets = {
example = {
file = ./secret/example.age;
};
};
The secret is expected to appear in /run/vaultix/
with 0400
and own by uid0.
Here is full options that configurable:
secrets = {
example = {
file = ./secret/example.age;
mode = "640"; # default 400
owner = "root";
group = "users";
name = "example.toml";
path = "/some/place";
};
};
This part basically keeps identical with agenix
. But has few diffs:
- no
symlink: bool
option, since it has an systemd function called tmpfiles.d.
path: path str
If you manually set this, it will deploy to specified location instead of to /run/vaultix.d
(default value of decryptedMountPoint).
If you still set the path to directory to /run/vaultix
(default value of decryptedDir), you will receive a warning, because you should use the name
option instead of doing that.
Templates
Vaultix
provides templating function. This makes it able to insert secrets content into plaintext config while deploying.
Overview of this option:
templates = {
test-template = {
name = "template.txt";
content = "this is a template for testing ${config.vaultix.placeholder.example}";
trim = true;
# permission options like secrets
mode = "640"; # default 400
owner = "root";
group = "users";
name = "example.toml";
path = "/some/place";
};
}
content: str
To insert secrets in this string, insert config.vaultix.placeholder.example
.
This pretend the secret which id
(the keyof attribute of secrets) was defined.
secrets = {
# the id is 'example'. despite `name`.
example = {
file = ./secret/example.age;
};
};
The content could also be multiline:
''
this is a template for testing ${config.vaultix.placeholder.example}
this is another ${config.vaultix.placeholder.what}
${config.vaultix.placeholder.some} here
''
TO BE NOTICE that the source secret file may have trailing \n
:
trim: bool
default true;
Removing trailing and leading whitespace by default.
needByUser: [str]
For deploying secrets and templates that required before user init.
List of id of templates or secrets.
Nix Apps
Provided user friendly cli tools:
renc
This is needed every time the host key or secret content changed.
The wrapped vaultix will decrypt cipher content to plaintext and encrypt it with target host public key, finally stored in cache
.
nix run .#vaultix.app.x86_64-linux.renc
edit
This will decrypt and open file with $EDITOR
. Will encrypt it after editing finished.
nix run .#vaultix.app.x86_64-linux.edit -- ./secrets/some.age
Workflows
Common used workflow with vaultix.
Add new secret
1. Run edit:
nix run .#vaultix.app.x86_64-linux.edit -- ./where/new-to-add.age
2. Add a secret to nixos module:
secrets = {
#...
new-to-add.file = ./where/new-to-add.age;
};
3. Run renc:
nix run .#vaultix.app.x86_64-linux.renc
4. Add new produced stuff to git.
Modify existed secret
nix run .#vaultix.app.x86_64-linux.edit -- ./where/to-edit.age
nix run .#vaultix.app.x86_64-linux.renc
Then add changes to git.
Remove secret
secrets = {
#...
- new-to-add.file = ./where/new-to-add.age;
};
rm ./where/new-to-add.age
nix run .#vaultix.app.x86_64-linux.renc
Then add changes to git.
Development
DevShell
nix develop
Test
For testing basic functions with virtual machine:
nix run github:nix-community/nixos-anywhere -- --flake .#tester --vm-test
Run full test with just full-test
Format
This repo follows nixfmt-rfc-style
style, reformat with running nixfmt .
.
Lint
Lint with statix.
/|、
(˙、.7
|、~ヽ
じしf_,)ノ
Frequent Asked Questions
Q. Rebooting and unit failed with could not found ssh private key, but it indeed just there.
A. Check if using root on tmpfs
, and modify hostKeys path to Absolute path string to your REAL private key location (not bind mounted or symlinked etc.)