age-encrypted secrets for NixOS and Home manager
2,482
stars
353
commits
Nix
primary language
Feb 4, 2026
updated
agenix is a small and convenient Nix library for securely managing and deploying secrets using common public-private SSH key pairs:
You can encrypt a secret (password, access-token, etc.) on a source machine using a number of public SSH keys,
and deploy that encrypted secret to any another target machine that has the corresponding private SSH key of one of those public keys.
This project contains two parts:
agenix commandline app (CLI) to encrypt secrets into secured .age files that can be copied into the Nix store.agenix NixOS module to conveniently
.age files) into the Nix store so that they can be deployed like any other Nix package using nixos-rebuild or similar tools./run/agenix/... to be consumed.All files in the Nix store are readable by any system user, so it is not a suitable place for including cleartext secrets. Many existing tools (like NixOps deployment.keys) deploy secrets separately from nixos-rebuild, making deployment, caching, and auditing more difficult. Out-of-band secret management is also less reproducible.
agenix solves these issues by using your pre-existing SSH key infrastructure and age to encrypt secrets into the Nix store. Secrets are decrypted using an SSH host private key during NixOS system activation.
ssh-keyscanFirst add it to niv:
$ niv add ryantm/agenix
Then add the following to your configuration.nix in the imports list:
{
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age.nix" ];
}
Add the following to your home configuration:
{
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age-home.nix" ];
}
To install the agenix binary:
{
environment.systemPackages = [ (pkgs.callPackage "${(import ./nix/sources.nix).agenix}/pkgs/agenix.nix" {}) ];
}
As root run:
$ sudo nix-channel --add https://github.com/ryantm/agenix/archive/main.tar.gz agenix
$ sudo nix-channel --update
Then add the following to your configuration.nix in the imports list:
{
imports = [ <agenix/modules/age.nix> ];
}
Add the following to your home configuration:
{
imports = [ <agenix/modules/age-home.nix> ];
}
To install the agenix binary:
{
environment.systemPackages = [ (pkgs.callPackage <agenix/pkgs/agenix.nix> {}) ];
}
Add the following to your configuration.nix:
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age.nix" ];
}
or with pinning:
{
imports = let
# replace this with an actual commit id or tag
commit = "298b235f664f925b433614dc33380f0662adfc3f";
in [
"${builtins.fetchTarball {
url = "https://github.com/ryantm/agenix/archive/${commit}.tar.gz";
# update hash from nix build output
sha256 = "";
}}/modules/age.nix"
];
}
Add the following to your home configuration:
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age-home.nix" ];
}
Or with pinning:
{
imports = let
# replace this with an actual commit id or tag
commit = "298b235f664f925b433614dc33380f0662adfc3f";
in [
"${builtins.fetchTarball {
url = "https://github.com/ryantm/agenix/archive/${commit}.tar.gz";
# update hash from nix build output
sha256 = "";
}}/modules/age-home.nix"
];
}
To install the agenix binary:
{
environment.systemPackages = [ (pkgs.callPackage "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/pkgs/agenix.nix" {}) ];
}
{
inputs.agenix.url = "github:ryantm/agenix";
# optional, not necessary for the module
#inputs.agenix.inputs.nixpkgs.follows = "nixpkgs";
# optionally choose not to download darwin deps (saves some resources on Linux)
#inputs.agenix.inputs.darwin.follows = "";
outputs = { self, nixpkgs, agenix }: {
# change `yourhostname` to your actual hostname
nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
# change to your system:
system = "x86_64-linux";
modules = [
./configuration.nix
agenix.nixosModules.default
];
};
};
}
{
inputs.agenix.url = "github:ryantm/agenix";
outputs = { self, nixpkgs, agenix, home-manager }: {
homeConfigurations."username" = home-manager.lib.homeManagerConfiguration {
# ...
modules = [
agenix.homeManagerModules.default
# ...
];
};
};
}
You can run the CLI tool ad-hoc without installing it:
nix run github:ryantm/agenix -- --help
But you can also add it permanently into a NixOS module (replace system "x86_64-linux" with your system):
{
environment.systemPackages = [ agenix.packages.x86_64-linux.default ];
}
e.g. inside your flake.nix file:
{
inputs.agenix.url = "github:ryantm/agenix";
# ...
outputs = { self, nixpkgs, agenix }: {
# change `yourhostname` to your actual hostname
nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
# ...
{
environment.systemPackages = [ agenix.packages.${system}.default ];
}
];
};
};
}
The system you want to deploy secrets to should already exist and
have sshd running on it so that it has generated SSH host keys in
/etc/ssh/.
Make a directory to store secrets and secrets.nix file for listing secrets and their public keys:
$ mkdir secrets
$ cd secrets
$ touch secrets.nix
This secrets.nix file is not imported into your NixOS configuration.
It's only used for the agenix CLI tool (example below) to know which public keys to use for encryption.
Add public keys to your secrets.nix file:
let
user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH";
user2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILI6jSq53F/3hEmSs+oq9L4TwOo1PrDMAgcA1uo1CCV/";
users = [ user1 user2 ];
system1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPJDyIr/FSz1cJdcoW69R+NrWzwGK/+3gJpqD1t8L2zE";
system2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1";
systems = [ system1 system2 ];
in
{
"secret1.age".publicKeys = [ user1 system1 ];
"secret2.age".publicKeys = users ++ systems;
"armored-secret.age" = {
publicKeys = [ user1 ];
armor = true;
};
}
These are the users and systems that will be able to decrypt the .age files later with their corresponding private keys.
The armor option may also be supplied here to ensure files are output in Base64 PEM text which is useful for more readable diffs.
You can obtain the public keys from
~/.ssh, e.g. ~/.ssh/id_ed25519.pub.ssh-keyscan:
$ ssh-keyscan <ip-address>
... ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1
...
Create a secret file:
$ agenix -e secret1.age
It will open a temporary file in the app configured in your $EDITOR environment variable.
When you save that file its content will be encrypted with all the public keys mentioned in the secrets.nix file.
Add secret to a NixOS module config:
{
age.secrets.secret1.file = ../secrets/secret1.age;
}
When the age.secrets attribute set contains a secret, the agenix NixOS module will later automatically decrypt and mount that secret under the default path /run/agenix/secret1.
Here the secret1.age file becomes part of your NixOS deployment, i.e. moves into the Nix store.
Reference the secrets' mount path in your config:
{
users.users.user1 = {
isNormalUser = true;
hashedPasswordFile = config.age.secrets.secret1.path;
};
}
You can reference the mount path to the (later) unencrypted secret already in your other configuration.
So config.age.secrets.secret1.path will contain the path /run/agenix/secret1 by default.
Use nixos-rebuild or another deployment tool of choice as usual.
The secret1.age file will be copied over to the target machine like any other Nix package.
Then it will be decrypted and mounted as described before.
Edit secret files:
$ agenix -e secret1.age
It assumes your SSH private key is in ~/.ssh/.
In order to decrypt and open a .age file for editing you need the private key of one of the public keys
it was encrypted with. You can pass the private key you want to use explicitly with -i, e.g.
$ agenix -e secret1.age -i ~/.ssh/id_ed25519
The home-manager module follows the same general principles as the NixOS module but is scoped to a single user. Here's how to use it:
{
age = {
identityPaths = [ "~/.ssh/id_ed25519" ];
secrets = {
example-secret = {
file = ../secrets/example-secret.age;
};
};
};
}
{
programs.some-program = {
enable = true;
hashedPasswordFile = config.age.secrets.example-secret.path;
};
}
When you run home-manager switch, your secrets will be decrypted to a user-specific directory (usually $XDG_RUNTIME_DIR/agenix on Linux or a temporary directory on Darwin) and can be referenced in your configuration.
age module referenceage.secretsage.secrets attrset of secrets. You always need to use this
configuration option. Defaults to {}.
age.secrets.<name>.fileage.secrets.<name>.file is the path to the encrypted .age for this
secret. This is the only required secret option.
Example:
{
age.secrets.monitrc.file = ../secrets/monitrc.age;
}
age.secrets.<name>.pathage.secrets.<name>.path is the path where the secret is decrypted
to. Defaults to /run/agenix/<name> (config.age.secretsDir/<name>).
Example defining a different path:
{
age.secrets.monitrc = {
file = ../secrets/monitrc.age;
path = "/etc/monitrc";
};
}
For many services, you do not need to set this. Instead, refer to the
decryption path in your configuration with
config.age.secrets.<name>.path.
Example referring to path:
{
users.users.ryantm = {
isNormalUser = true;
hashedPasswordFile = config.age.secrets.passwordfile-ryantm.path;
};
}
{
# Do not do this!
config.password = builtins.readFile config.age.secrets.secret1.path;
}
This can cause the cleartext to be placed into the world-readable Nix store. Instead, have your services read the cleartext path at runtime.
age.secrets.<name>.modeage.secrets.<name>.mode is permissions mode of the decrypted secret
in a format understood by chmod. Usually, you only need to use this in
combination with age.secrets.<name>.owner and
age.secrets.<name>.group
Example:
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
age.secrets.<name>.ownerage.secrets.<name>.owner is the username of the decrypted file's
owner. Usually, you only need to use this in combination with
age.secrets.<name>.mode and age.secrets.<name>.group
Example:
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
age.secrets.<name>.groupage.secrets.<name>.group is the name of the decrypted file's
group. Usually, you only need to use this in combination with
age.secrets.<name>.owner and age.secrets.<name>.mode
Example:
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
age.secrets.<name>.symlinkage.secrets.<name>.symlink is a boolean. If true (the default),
secrets are symlinked to age.secrets.<name>.path. If false, secrets
are copied to age.secrets.<name>.path. Usually, you want to keep
this as true, because it secure cleanup of secrets no longer
used. (The symlink will still be there, but it will be broken.) If
false, you are responsible for cleaning up your own secrets after you
stop using them.
Some programs do not like following symlinks (for example Java programs like Elasticsearch).
Example:
{
age.secrets."elasticsearch.conf" = {
file = ../secrets/elasticsearch.conf.age;
symlink = false;
};
}
age.secrets.<name>.nameage.secrets.<name>.name is the string of the name of the file after
it is decrypted. Defaults to the <name> in the attrpath, but can be
set separately if you want the file name to be different from the
attribute name part.
Example of a secret with a name different from its attrpath:
{
age.secrets.monit = {
name = "monitrc";
file = ../secrets/monitrc.age;
};
}
age.ageBinage.ageBin the string of the path to the age binary. Usually, you
don't need to change this. Defaults to age/bin/age.
Overriding age.ageBin example:
{pkgs, ...}:{
age.ageBin = "${pkgs.age}/bin/age";
}
age.identityPathsage.identityPaths is a list of paths to recipient keys to try to use to
decrypt the secrets. By default, it is the rsa and ed25519 keys in
config.services.openssh.hostKeys, and on NixOS you usually don't need to
change this. The list items should be strings ("/path/to/id_rsa"), not
nix paths (../path/to/id_rsa), as the latter would copy your private key to
the nix store, which is the exact situation agenix is designed to avoid. At
least one of the file paths must be present at runtime and able to decrypt the
secret in question. Overriding age.identityPaths example:
{
age.identityPaths = [ "/var/lib/persistent/ssh_host_ed25519_key" ];
}
age.secretsDirage.secretsDir is the directory where secrets are symlinked to by
default. Usually, you don't need to change this. Defaults to
/run/agenix.
Overriding age.secretsDir example:
{
age.secretsDir = "/run/keys";
}
age.secretsMountPointage.secretsMountPoint is the directory where the secret generations
are created before they are symlinked. Usually, you don't need to
change this. Defaults to /run/agenix.d.
Overriding age.secretsMountPoint example:
{
age.secretsMountPoint = "/run/secret-generations";
}
age-home module referenceThe home-manager module provides options similar to the NixOS module but scoped to a single user.
age.secretsage.secrets attrset of secrets. You always need to use this
configuration option. Defaults to {}.
age.secrets.<name>.fileage.secrets.<name>.file is the path to the encrypted .age for this
secret. This is the only required secret option.
age.secrets.<name>.pathage.secrets.<name>.path is the path where the secret is decrypted
to. Defaults to $XDG_RUNTIME_DIR/agenix/<name> on Linux and
$(getconf DARWIN_USER_TEMP_DIR)/agenix/<name> on Darwin.
age.secrets.<name>.modeage.secrets.<name>.mode is permissions mode of the decrypted secret
in a format understood by chmod.
age.secrets.<name>.symlinkage.secrets.<name>.symlink is a boolean. If true (the default),
secrets are symlinked to age.secrets.<name>.path. If false, secrets
are copied to age.secrets.<name>.path.
age.identityPathsage.identityPaths is a list of paths to SSH private keys to use for decryption.
This is a required option; there is no default value.
age.secretsDirage.secretsDir is the directory where secrets are symlinked to by
default. Defaults to $XDG_RUNTIME_DIR/agenix on Linux and
$(getconf DARWIN_USER_TEMP_DIR)/agenix on Darwin.
age.secretsMountPointage.secretsMountPoint is the directory where the secret generations
are created before they are symlinked. Defaults to $XDG_RUNTIME_DIR/agenix.d
on Linux and $(getconf DARWIN_USER_TEMP_DIR)/agenix.d on Darwin.
agenix - edit and rekey age secret files
agenix -e FILE [-i PRIVATE_KEY]
agenix -r [-i PRIVATE_KEY]
options:
-h, --help show help
-e, --edit FILE edits FILE using $EDITOR
-r, --rekey re-encrypts all secrets with specified recipients
-d, --decrypt FILE decrypts FILE to STDOUT
-i, --identity identity to use when decrypting
-v, --verbose verbose output
FILE an age-encrypted file
PRIVATE_KEY a path to a private SSH key used to decrypt file
EDITOR environment variable of editor to use when editing FILE
If STDIN is not interactive, EDITOR will be set to "cp /dev/stdin"
RULES environment variable with path to Nix file specifying recipient public keys.
Defaults to './secrets.nix'
If you change the public keys in secrets.nix, you should rekey your
secrets:
$ agenix --rekey
To rekey a secret, you have to be able to decrypt it. Because of
randomness in age's encryption algorithms, the files always change
when rekeyed, even if the identities do not. (This eventually could be
improved upon by reading the identities from the age file.)
The agenix CLI uses age by default as its age implemenation, you
can use the rage implementation with Flakes like this:
{pkgs,agenix,...}:{
environment.systemPackages = [
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
];
}
Support and development discussion is available here on GitHub and also through Matrix.
This project has not been audited by a security professional.
People unfamiliar with age might be surprised that secrets are not
authenticated. This means that every attacker that has write access to
the secret files can modify secrets because public keys are exposed.
This seems like not a problem on the first glance because changing the
configuration itself could expose secrets easily. However, reviewing
configuration changes is easier than reviewing random secrets (for
example, 4096-bit rsa keys). This would be solved by having a message
authentication code (MAC) like other implementations like GPG or
sops have, however this was left
out for simplicity in age.
Additionally you should only encrypt secrets that you are able to make useless in the event that they are decrypted in the future and be ready to rotate them periodically as age is as of 19th June 2024 NOT Post-Quantum Safe and so in case the threat actor can access your encrypted keys e.g. via their use in a public repository then they can utilize the strategy of Harvest Now, Decrypt Later to store your keys now for later decryption including the case where a major vulnerability is found that would expose the secrets. See https://github.com/FiloSottile/age/issues/578 for details.
nix fmt to format nix codeYou can run the tests with
nix flake check
You can run the integration tests in interactive mode like this:
nix run .#checks.x86_64-linux.integration.driverInteractive
After it starts, enter run_tests() to run the tests.
This project is based off of sops-nix created Mic92. Thank you to Mic92 for inspiration and advice.
Nix
84.7%
Shell
15.3%
age-encrypted secrets for NixOS and Home manager
2,482
stars
353
commits
Nix
primary language
Feb 4, 2026
updated
agenix is a small and convenient Nix library for securely managing and deploying secrets using common public-private SSH key pairs:
You can encrypt a secret (password, access-token, etc.) on a source machine using a number of public SSH keys,
and deploy that encrypted secret to any another target machine that has the corresponding private SSH key of one of those public keys.
This project contains two parts:
agenix commandline app (CLI) to encrypt secrets into secured .age files that can be copied into the Nix store.agenix NixOS module to conveniently
.age files) into the Nix store so that they can be deployed like any other Nix package using nixos-rebuild or similar tools./run/agenix/... to be consumed.All files in the Nix store are readable by any system user, so it is not a suitable place for including cleartext secrets. Many existing tools (like NixOps deployment.keys) deploy secrets separately from nixos-rebuild, making deployment, caching, and auditing more difficult. Out-of-band secret management is also less reproducible.
agenix solves these issues by using your pre-existing SSH key infrastructure and age to encrypt secrets into the Nix store. Secrets are decrypted using an SSH host private key during NixOS system activation.
ssh-keyscanFirst add it to niv:
$ niv add ryantm/agenix
Then add the following to your configuration.nix in the imports list:
{
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age.nix" ];
}
Add the following to your home configuration:
{
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age-home.nix" ];
}
To install the agenix binary:
{
environment.systemPackages = [ (pkgs.callPackage "${(import ./nix/sources.nix).agenix}/pkgs/agenix.nix" {}) ];
}
As root run:
$ sudo nix-channel --add https://github.com/ryantm/agenix/archive/main.tar.gz agenix
$ sudo nix-channel --update
Then add the following to your configuration.nix in the imports list:
{
imports = [ <agenix/modules/age.nix> ];
}
Add the following to your home configuration:
{
imports = [ <agenix/modules/age-home.nix> ];
}
To install the agenix binary:
{
environment.systemPackages = [ (pkgs.callPackage <agenix/pkgs/agenix.nix> {}) ];
}
Add the following to your configuration.nix:
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age.nix" ];
}
or with pinning:
{
imports = let
# replace this with an actual commit id or tag
commit = "298b235f664f925b433614dc33380f0662adfc3f";
in [
"${builtins.fetchTarball {
url = "https://github.com/ryantm/agenix/archive/${commit}.tar.gz";
# update hash from nix build output
sha256 = "";
}}/modules/age.nix"
];
}
Add the following to your home configuration:
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age-home.nix" ];
}
Or with pinning:
{
imports = let
# replace this with an actual commit id or tag
commit = "298b235f664f925b433614dc33380f0662adfc3f";
in [
"${builtins.fetchTarball {
url = "https://github.com/ryantm/agenix/archive/${commit}.tar.gz";
# update hash from nix build output
sha256 = "";
}}/modules/age-home.nix"
];
}
To install the agenix binary:
{
environment.systemPackages = [ (pkgs.callPackage "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/pkgs/agenix.nix" {}) ];
}
{
inputs.agenix.url = "github:ryantm/agenix";
# optional, not necessary for the module
#inputs.agenix.inputs.nixpkgs.follows = "nixpkgs";
# optionally choose not to download darwin deps (saves some resources on Linux)
#inputs.agenix.inputs.darwin.follows = "";
outputs = { self, nixpkgs, agenix }: {
# change `yourhostname` to your actual hostname
nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
# change to your system:
system = "x86_64-linux";
modules = [
./configuration.nix
agenix.nixosModules.default
];
};
};
}
{
inputs.agenix.url = "github:ryantm/agenix";
outputs = { self, nixpkgs, agenix, home-manager }: {
homeConfigurations."username" = home-manager.lib.homeManagerConfiguration {
# ...
modules = [
agenix.homeManagerModules.default
# ...
];
};
};
}
You can run the CLI tool ad-hoc without installing it:
nix run github:ryantm/agenix -- --help
But you can also add it permanently into a NixOS module (replace system "x86_64-linux" with your system):
{
environment.systemPackages = [ agenix.packages.x86_64-linux.default ];
}
e.g. inside your flake.nix file:
{
inputs.agenix.url = "github:ryantm/agenix";
# ...
outputs = { self, nixpkgs, agenix }: {
# change `yourhostname` to your actual hostname
nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
# ...
{
environment.systemPackages = [ agenix.packages.${system}.default ];
}
];
};
};
}
The system you want to deploy secrets to should already exist and
have sshd running on it so that it has generated SSH host keys in
/etc/ssh/.
Make a directory to store secrets and secrets.nix file for listing secrets and their public keys:
$ mkdir secrets
$ cd secrets
$ touch secrets.nix
This secrets.nix file is not imported into your NixOS configuration.
It's only used for the agenix CLI tool (example below) to know which public keys to use for encryption.
Add public keys to your secrets.nix file:
let
user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH";
user2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILI6jSq53F/3hEmSs+oq9L4TwOo1PrDMAgcA1uo1CCV/";
users = [ user1 user2 ];
system1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPJDyIr/FSz1cJdcoW69R+NrWzwGK/+3gJpqD1t8L2zE";
system2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1";
systems = [ system1 system2 ];
in
{
"secret1.age".publicKeys = [ user1 system1 ];
"secret2.age".publicKeys = users ++ systems;
"armored-secret.age" = {
publicKeys = [ user1 ];
armor = true;
};
}
These are the users and systems that will be able to decrypt the .age files later with their corresponding private keys.
The armor option may also be supplied here to ensure files are output in Base64 PEM text which is useful for more readable diffs.
You can obtain the public keys from
~/.ssh, e.g. ~/.ssh/id_ed25519.pub.ssh-keyscan:
$ ssh-keyscan <ip-address>
... ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1
...
Create a secret file:
$ agenix -e secret1.age
It will open a temporary file in the app configured in your $EDITOR environment variable.
When you save that file its content will be encrypted with all the public keys mentioned in the secrets.nix file.
Add secret to a NixOS module config:
{
age.secrets.secret1.file = ../secrets/secret1.age;
}
When the age.secrets attribute set contains a secret, the agenix NixOS module will later automatically decrypt and mount that secret under the default path /run/agenix/secret1.
Here the secret1.age file becomes part of your NixOS deployment, i.e. moves into the Nix store.
Reference the secrets' mount path in your config:
{
users.users.user1 = {
isNormalUser = true;
hashedPasswordFile = config.age.secrets.secret1.path;
};
}
You can reference the mount path to the (later) unencrypted secret already in your other configuration.
So config.age.secrets.secret1.path will contain the path /run/agenix/secret1 by default.
Use nixos-rebuild or another deployment tool of choice as usual.
The secret1.age file will be copied over to the target machine like any other Nix package.
Then it will be decrypted and mounted as described before.
Edit secret files:
$ agenix -e secret1.age
It assumes your SSH private key is in ~/.ssh/.
In order to decrypt and open a .age file for editing you need the private key of one of the public keys
it was encrypted with. You can pass the private key you want to use explicitly with -i, e.g.
$ agenix -e secret1.age -i ~/.ssh/id_ed25519
The home-manager module follows the same general principles as the NixOS module but is scoped to a single user. Here's how to use it:
{
age = {
identityPaths = [ "~/.ssh/id_ed25519" ];
secrets = {
example-secret = {
file = ../secrets/example-secret.age;
};
};
};
}
{
programs.some-program = {
enable = true;
hashedPasswordFile = config.age.secrets.example-secret.path;
};
}
When you run home-manager switch, your secrets will be decrypted to a user-specific directory (usually $XDG_RUNTIME_DIR/agenix on Linux or a temporary directory on Darwin) and can be referenced in your configuration.
age module referenceage.secretsage.secrets attrset of secrets. You always need to use this
configuration option. Defaults to {}.
age.secrets.<name>.fileage.secrets.<name>.file is the path to the encrypted .age for this
secret. This is the only required secret option.
Example:
{
age.secrets.monitrc.file = ../secrets/monitrc.age;
}
age.secrets.<name>.pathage.secrets.<name>.path is the path where the secret is decrypted
to. Defaults to /run/agenix/<name> (config.age.secretsDir/<name>).
Example defining a different path:
{
age.secrets.monitrc = {
file = ../secrets/monitrc.age;
path = "/etc/monitrc";
};
}
For many services, you do not need to set this. Instead, refer to the
decryption path in your configuration with
config.age.secrets.<name>.path.
Example referring to path:
{
users.users.ryantm = {
isNormalUser = true;
hashedPasswordFile = config.age.secrets.passwordfile-ryantm.path;
};
}
{
# Do not do this!
config.password = builtins.readFile config.age.secrets.secret1.path;
}
This can cause the cleartext to be placed into the world-readable Nix store. Instead, have your services read the cleartext path at runtime.
age.secrets.<name>.modeage.secrets.<name>.mode is permissions mode of the decrypted secret
in a format understood by chmod. Usually, you only need to use this in
combination with age.secrets.<name>.owner and
age.secrets.<name>.group
Example:
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
age.secrets.<name>.ownerage.secrets.<name>.owner is the username of the decrypted file's
owner. Usually, you only need to use this in combination with
age.secrets.<name>.mode and age.secrets.<name>.group
Example:
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
age.secrets.<name>.groupage.secrets.<name>.group is the name of the decrypted file's
group. Usually, you only need to use this in combination with
age.secrets.<name>.owner and age.secrets.<name>.mode
Example:
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
age.secrets.<name>.symlinkage.secrets.<name>.symlink is a boolean. If true (the default),
secrets are symlinked to age.secrets.<name>.path. If false, secrets
are copied to age.secrets.<name>.path. Usually, you want to keep
this as true, because it secure cleanup of secrets no longer
used. (The symlink will still be there, but it will be broken.) If
false, you are responsible for cleaning up your own secrets after you
stop using them.
Some programs do not like following symlinks (for example Java programs like Elasticsearch).
Example:
{
age.secrets."elasticsearch.conf" = {
file = ../secrets/elasticsearch.conf.age;
symlink = false;
};
}
age.secrets.<name>.nameage.secrets.<name>.name is the string of the name of the file after
it is decrypted. Defaults to the <name> in the attrpath, but can be
set separately if you want the file name to be different from the
attribute name part.
Example of a secret with a name different from its attrpath:
{
age.secrets.monit = {
name = "monitrc";
file = ../secrets/monitrc.age;
};
}
age.ageBinage.ageBin the string of the path to the age binary. Usually, you
don't need to change this. Defaults to age/bin/age.
Overriding age.ageBin example:
{pkgs, ...}:{
age.ageBin = "${pkgs.age}/bin/age";
}
age.identityPathsage.identityPaths is a list of paths to recipient keys to try to use to
decrypt the secrets. By default, it is the rsa and ed25519 keys in
config.services.openssh.hostKeys, and on NixOS you usually don't need to
change this. The list items should be strings ("/path/to/id_rsa"), not
nix paths (../path/to/id_rsa), as the latter would copy your private key to
the nix store, which is the exact situation agenix is designed to avoid. At
least one of the file paths must be present at runtime and able to decrypt the
secret in question. Overriding age.identityPaths example:
{
age.identityPaths = [ "/var/lib/persistent/ssh_host_ed25519_key" ];
}
age.secretsDirage.secretsDir is the directory where secrets are symlinked to by
default. Usually, you don't need to change this. Defaults to
/run/agenix.
Overriding age.secretsDir example:
{
age.secretsDir = "/run/keys";
}
age.secretsMountPointage.secretsMountPoint is the directory where the secret generations
are created before they are symlinked. Usually, you don't need to
change this. Defaults to /run/agenix.d.
Overriding age.secretsMountPoint example:
{
age.secretsMountPoint = "/run/secret-generations";
}
age-home module referenceThe home-manager module provides options similar to the NixOS module but scoped to a single user.
age.secretsage.secrets attrset of secrets. You always need to use this
configuration option. Defaults to {}.
age.secrets.<name>.fileage.secrets.<name>.file is the path to the encrypted .age for this
secret. This is the only required secret option.
age.secrets.<name>.pathage.secrets.<name>.path is the path where the secret is decrypted
to. Defaults to $XDG_RUNTIME_DIR/agenix/<name> on Linux and
$(getconf DARWIN_USER_TEMP_DIR)/agenix/<name> on Darwin.
age.secrets.<name>.modeage.secrets.<name>.mode is permissions mode of the decrypted secret
in a format understood by chmod.
age.secrets.<name>.symlinkage.secrets.<name>.symlink is a boolean. If true (the default),
secrets are symlinked to age.secrets.<name>.path. If false, secrets
are copied to age.secrets.<name>.path.
age.identityPathsage.identityPaths is a list of paths to SSH private keys to use for decryption.
This is a required option; there is no default value.
age.secretsDirage.secretsDir is the directory where secrets are symlinked to by
default. Defaults to $XDG_RUNTIME_DIR/agenix on Linux and
$(getconf DARWIN_USER_TEMP_DIR)/agenix on Darwin.
age.secretsMountPointage.secretsMountPoint is the directory where the secret generations
are created before they are symlinked. Defaults to $XDG_RUNTIME_DIR/agenix.d
on Linux and $(getconf DARWIN_USER_TEMP_DIR)/agenix.d on Darwin.
agenix - edit and rekey age secret files
agenix -e FILE [-i PRIVATE_KEY]
agenix -r [-i PRIVATE_KEY]
options:
-h, --help show help
-e, --edit FILE edits FILE using $EDITOR
-r, --rekey re-encrypts all secrets with specified recipients
-d, --decrypt FILE decrypts FILE to STDOUT
-i, --identity identity to use when decrypting
-v, --verbose verbose output
FILE an age-encrypted file
PRIVATE_KEY a path to a private SSH key used to decrypt file
EDITOR environment variable of editor to use when editing FILE
If STDIN is not interactive, EDITOR will be set to "cp /dev/stdin"
RULES environment variable with path to Nix file specifying recipient public keys.
Defaults to './secrets.nix'
If you change the public keys in secrets.nix, you should rekey your
secrets:
$ agenix --rekey
To rekey a secret, you have to be able to decrypt it. Because of
randomness in age's encryption algorithms, the files always change
when rekeyed, even if the identities do not. (This eventually could be
improved upon by reading the identities from the age file.)
The agenix CLI uses age by default as its age implemenation, you
can use the rage implementation with Flakes like this:
{pkgs,agenix,...}:{
environment.systemPackages = [
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
];
}
Support and development discussion is available here on GitHub and also through Matrix.
This project has not been audited by a security professional.
People unfamiliar with age might be surprised that secrets are not
authenticated. This means that every attacker that has write access to
the secret files can modify secrets because public keys are exposed.
This seems like not a problem on the first glance because changing the
configuration itself could expose secrets easily. However, reviewing
configuration changes is easier than reviewing random secrets (for
example, 4096-bit rsa keys). This would be solved by having a message
authentication code (MAC) like other implementations like GPG or
sops have, however this was left
out for simplicity in age.
Additionally you should only encrypt secrets that you are able to make useless in the event that they are decrypted in the future and be ready to rotate them periodically as age is as of 19th June 2024 NOT Post-Quantum Safe and so in case the threat actor can access your encrypted keys e.g. via their use in a public repository then they can utilize the strategy of Harvest Now, Decrypt Later to store your keys now for later decryption including the case where a major vulnerability is found that would expose the secrets. See https://github.com/FiloSottile/age/issues/578 for details.
nix fmt to format nix codeYou can run the tests with
nix flake check
You can run the integration tests in interactive mode like this:
nix run .#checks.x86_64-linux.integration.driverInteractive
After it starts, enter run_tests() to run the tests.
This project is based off of sops-nix created Mic92. Thank you to Mic92 for inspiration and advice.
Nix
84.7%
Shell
15.3%