agenix/README.md

475 lines
13 KiB
Markdown
Raw Normal View History

2020-12-18 20:49:50 +03:00
# agenix - [age](https://github.com/FiloSottile/age)-encrypted secrets for NixOS
2020-09-03 22:03:01 +03:00
2020-12-18 20:49:50 +03:00
`agenix` is a commandline tool for managing secrets encrypted with your existing SSH keys. This project also includes the NixOS module `age` for adding encrypted secrets into the Nix store and decrypting them.
2020-09-03 22:03:01 +03:00
## Contents
* [Problem and solution](#problem-and-solution)
* [Features](#features)
* [Installation](#installation)
* [niv](#install-via-niv) (Current recommendation)
* [module](#install-module-via-niv)
* [CLI](#install-cli-via-niv)
* [nix-channel](#install-via-nix-channel)
* [module](#install-module-via-nix-channel)
* [CLI](#install-cli-via-nix-channel)
* [fetchTarball](#install-via-fetchtarball)
* [module](#install-module-via-fetchtarball)
* [CLI](#install-cli-via-fetchTarball)
* [flakes](#install-via-flakes)
* [module](#install-module-via-flakes)
* [CLI](#install-cli-via-flakes)
* [Tutorial](#tutorial)
2022-09-03 21:46:45 +03:00
* [Reference](#reference)
2022-03-01 06:34:22 +03:00
* [Community and Support](#community-and-support)
* [Rekeying](#rekeying)
* [Don't symlink secret](#dont-symlink-secret)
* [Use other implementations](#use-other-implementations)
* [Threat model/Warnings](#threat-modelwarnings)
* [Acknowledgements](#acknowledgements)
2020-12-18 20:49:50 +03:00
## Problem and solution
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.
2020-09-03 23:16:44 +03:00
## Features
2020-09-03 22:03:01 +03:00
* Secrets are encrypted with SSH keys
2020-09-03 23:18:21 +03:00
* system public keys via `ssh-keyscan`
* can use public keys available on GitHub for users (for example, https://github.com/ryantm.keys)
2020-09-03 22:03:01 +03:00
* No GPG
* Very little code, so it should be easy for you to audit
2020-09-04 07:12:02 +03:00
* Encrypted secrets are stored in the Nix store, so a separate distribution mechanism is not necessary
2020-09-03 22:03:01 +03:00
2020-12-18 21:09:17 +03:00
## Notices
* Password-protected ssh keys: since the underlying tool age/rage do not support ssh-agent, password-protected ssh keys do not work well. For example, if you need to rekey 20 secrets you will have to enter your password 20 times.
2020-12-18 21:09:17 +03:00
2020-09-03 23:16:44 +03:00
## Installation
2020-09-03 22:03:01 +03:00
Choose one of the following methods:
* [niv](#install-via-niv) (Current recommendation)
* [nix-channel](#install-via-nix-channel)
* [fetchTarball](#install-via-fetchTarball)
* [flakes](#install-via-flakes)
### Install via [niv](https://github.com/nmattia/niv)
2020-09-03 22:03:01 +03:00
First add it to niv:
2020-12-18 20:49:50 +03:00
```ShellSession
2020-09-03 22:03:01 +03:00
$ niv add ryantm/agenix
```
#### Install module via niv
2020-09-03 23:16:44 +03:00
Then add the following to your `configuration.nix` in the `imports` list:
2020-09-03 22:03:01 +03:00
```nix
{
2021-12-01 02:08:57 +03:00
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age.nix" ];
2020-09-03 22:03:01 +03:00
}
```
#### Install CLI via niv
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage "${(import ./nix/sources.nix).agenix}/pkgs/agenix.nix" {}) ];
}
```
### Install via nix-channel
2020-09-03 22:03:01 +03:00
As root run:
2020-09-03 22:03:01 +03:00
2020-12-18 20:49:50 +03:00
```ShellSession
$ sudo nix-channel --add https://github.com/ryantm/agenix/archive/main.tar.gz agenix
$ sudo nix-channel --update
2020-09-03 22:03:01 +03:00
```
#### Install module via nix-channel
Then add the following to your `configuration.nix` in the `imports` list:
2020-09-03 22:03:01 +03:00
```nix
{
imports = [ <agenix/modules/age.nix> ];
2020-09-03 22:03:01 +03:00
}
```
#### Install CLI via nix-channel
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage <agenix/pkgs/agenix.nix> {}) ];
}
```
### Install via fetchTarball
#### Install module via fetchTarball
2020-09-03 22:03:01 +03:00
Add the following to your configuration.nix:
2020-09-03 22:03:01 +03:00
2020-09-03 23:16:44 +03:00
```nix
2020-09-03 22:03:01 +03:00
{
2021-11-21 04:30:45 +03:00
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age.nix" ];
2020-09-03 22:03:01 +03:00
}
```
or with pinning:
```nix
{
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";
# replace this with an actual hash
sha256 = "0000000000000000000000000000000000000000000000000000";
2021-12-01 02:08:57 +03:00
}}/modules/age.nix"
2020-09-03 22:03:01 +03:00
];
}
```
#### Install CLI via fetchTarball
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/pkgs/agenix.nix" {}) ];
}
```
### Install via Flakes
2020-09-03 23:16:44 +03:00
#### Install module via Flakes
2020-09-03 22:03:01 +03:00
2020-09-03 23:16:44 +03:00
```nix
2020-09-03 22:03:01 +03:00
{
inputs.agenix.url = "github:ryantm/agenix";
# optional, not necessary for the module
#inputs.agenix.inputs.nixpkgs.follows = "nixpkgs";
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.nixosModule
2020-09-03 22:03:01 +03:00
];
};
};
}
```
#### Install CLI via Flakes
2020-09-03 23:16:44 +03:00
2020-09-04 17:13:03 +03:00
You don't need to install it,
2020-09-03 23:16:44 +03:00
2020-12-18 20:49:50 +03:00
```ShellSession
2020-09-03 23:16:44 +03:00
nix run github:ryantm/agenix -- --help
```
2020-09-04 17:13:03 +03:00
but, if you want to (change the system based on your system):
2020-09-04 07:12:02 +03:00
```nix
{
environment.systemPackages = [ agenix.defaultPackage.x86_64-linux ];
}
```
2020-09-03 23:16:44 +03:00
## Tutorial
1. 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/`.
2. Make a directory to store secrets and `secrets.nix` file for listing secrets and their public keys:
2020-09-03 23:16:44 +03:00
2020-12-18 20:49:50 +03:00
```ShellSession
2020-09-03 23:16:44 +03:00
$ mkdir secrets
2020-12-18 22:37:23 +03:00
$ cd secrets
2020-09-04 01:18:20 +03:00
$ touch secrets.nix
2020-09-03 23:16:44 +03:00
```
3. Add public keys to `secrets.nix` file (hint: use `ssh-keyscan` or GitHub (for example, https://github.com/ryantm.keys)):
2020-09-04 01:18:20 +03:00
```nix
let
user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH";
2020-12-18 20:49:50 +03:00
user2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILI6jSq53F/3hEmSs+oq9L4TwOo1PrDMAgcA1uo1CCV/";
users = [ user1 user2 ];
2020-09-04 01:18:20 +03:00
system1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPJDyIr/FSz1cJdcoW69R+NrWzwGK/+3gJpqD1t8L2zE";
2020-12-18 20:49:50 +03:00
system2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1";
systems = [ system1 system2 ];
2020-09-04 01:18:20 +03:00
in
{
"secret1.age".publicKeys = [ user1 system1 ];
2020-12-18 20:49:50 +03:00
"secret2.age".publicKeys = users ++ systems;
2020-09-04 01:18:20 +03:00
}
2020-09-03 23:16:44 +03:00
```
4. Edit secret files (these instructions assume your SSH private key is in ~/.ssh/):
2020-12-18 20:49:50 +03:00
```ShellSession
2020-09-03 23:16:44 +03:00
$ agenix -e secret1.age
```
5. Add secret to a NixOS module config:
2020-09-03 23:16:44 +03:00
```nix
2020-09-04 07:12:02 +03:00
age.secrets.secret1.file = ../secrets/secret1.age;
2020-09-03 23:16:44 +03:00
```
6. NixOS rebuild or use your deployment tool like usual.
2020-09-03 23:16:44 +03:00
The secret will be decrypted to the value of `config.age.secrets.secret1.path` (`/run/agenix/secret1` by default). For per-secret options controlling ownership etc, see [modules/age.nix](modules/age.nix).
2021-04-08 21:47:48 +03:00
2022-09-03 21:46:45 +03:00
## Reference
### `age` module reference
#### `age.secrets`
`age.secrets` attrset of secrets. You always need to use this configuration option. Defaults to `{}`.
#### `age.secrets.<name>.file`
`age.secrets.<name>.file` is the path to the encrypted `.age` for this secret. This is the only required secret option.
Example:
```nix
{
age.secrets.monitrc.file = ../secrets/monitrc.age;
}
```
#### `age.secrets.<name>.mode`
`age.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:
```nix
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
```
#### `age.secrets.<name>.owner`
`age.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:
```nix
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
```
#### `age.secrets.<name>.group`
`age.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:
```nix
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
```
#### `age.secrets.<name>.symlink`
`age.secrets.<name>.symlink` is a boolean. If true (the default), secrets are symlinked to `age.secrets.<name>.path`. If false, secerts 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:
```nix
{
age.secrets."elasticsearch.conf" = {
file = ../secrets/elasticsearch.conf.age;
symlink = false;
};
}
```
#### `age.secrets.<name>.name`
`age.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:
```nix
{
age.secrets.monit = {
name = "monitrc";
file = ../secrets/monitrc.age;
};
}
```
#### `age.ageBin`
`age.ageBin` the string of the path to the `age` binary. Usually, you don't need to change this. Defaults to `rage/bin/rage`.
Overriding `age.ageBin` example:
```nix
{
age.ageBin = "${pkgs.age}/bin/age";
}
```
#### `age.identityPaths`
`age.identityPaths` is a list of paths to recipient keys to try to use to decrypt the secrets. All of the file paths must be present, but only one needs to be able to decrypt the secret. Usually, you don't need to change this. By default, this is the `rsa` and `ed25519` keys in `config.services.openssh.hostKeys`.
Overriding `age.identityPaths` example:
```nix
{
age.identityPaths = [ "/var/lib/persistent/ssh_host_ed25519_key" ];
}
```
#### `age.secretsDir`
`age.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:
```nix
{
age.secretsDir = "/run/keys";
}
```
#### `age.secretsMountPoint`
`age.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:
```nix
{
age.secretsMountPoint = "/run/secret-generations";
}
```
2022-03-01 06:34:22 +03:00
## Community and Support
Support and development discussion is available here on GitHub and
also through [Matrix](https://matrix.to/#/#agenix:nixos.org).
2020-09-03 23:16:44 +03:00
## Rekeying
2020-09-04 01:18:20 +03:00
If you change the public keys in `secrets.nix`, you should rekey your
2020-09-03 23:16:44 +03:00
secrets:
2020-12-18 20:49:50 +03:00
```ShellSession
2020-09-03 23:16:44 +03:00
$ 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
2020-12-18 20:49:50 +03:00
when rekeyed, even if the identities do not. (This eventually could be
improved upon by reading the identities from the age file.)
2020-09-03 23:16:44 +03:00
## Don't symlink secret
If your secret cannot be a symlink, you should set the `symlink` option to `false`:
```nix
{
age.secrets.some-secret = {
file = ./secret;
path = "/var/lib/some-service/some-secret";
symlink = false;
};
}
```
Instead of first decrypting the secret to `/run/agenix` and then symlinking to its `path`, the secret will instead be forcibly moved to its `path`. Please note that, currently, there are no cleanup mechanisms for secrets that are not symlinked by agenix.
2021-12-06 02:18:47 +03:00
## Use other implementations
This project uses the Rust implementation of age, [rage](https://github.com/str4d/rage), by default. You can change it to use the [official implementation](https://github.com/FiloSottile/age).
### Module
```nix
{
age.ageBin = "${pkgs.age}/bin/age";
}
```
### CLI
```nix
{
environment.systemPackages = [
(agenix.defaultPackage.x86_64-linux.override { ageBin = "${pkgs.age}/bin/age"; })
];
}
```
2020-09-03 23:16:44 +03:00
## Threat model/Warnings
2022-02-03 00:53:46 +03:00
This project has not been audited by a security professional.
2020-09-03 23:16:44 +03:00
People unfamiliar with `age` might be surprised that secrets are not
authenticated. This means that every attacker that has write access to
2020-12-18 20:49:50 +03:00
the secret files can modify secrets because public keys are exposed.
2020-09-03 23:16:44 +03:00
This seems like not a problem on the first glance because changing the
2020-12-18 20:49:50 +03:00
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
2020-09-03 23:16:44 +03:00
authentication code (MAC) like other implementations like GPG or
[sops](https://github.com/Mic92/sops-nix) have, however this was left
out for simplicity in `age`.
2022-04-03 01:10:25 +03:00
### builtins.readFile anti-pattern
```nix
{
# 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.
2020-09-03 23:16:44 +03:00
## Acknowledgements
2020-09-03 22:03:01 +03:00
2020-12-18 20:49:50 +03:00
This project is based off of [sops-nix](https://github.com/Mic92/sops-nix) created Mic92. Thank you to Mic92 for inspiration and advice.