doc: add new doc website

* use mmdoc
* add github pages action to auto publish
* do not edit README for now, will follow up with a commit directs
people to the doc site
This commit is contained in:
Ryan Mulligan 2023-02-26 14:17:37 -08:00
parent 4828951d9d
commit 657789137c
22 changed files with 602 additions and 3 deletions

View file

@ -11,6 +11,7 @@ jobs:
with: with:
extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm" extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm"
- run: nix build - run: nix build
- run: nix build .#doc
- run: nix fmt . -- --check - run: nix fmt . -- --check
- run: nix flake check - run: nix flake check
tests-darwin: tests-darwin:
@ -21,6 +22,7 @@ jobs:
with: with:
extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm" extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm"
- run: nix build - run: nix build
- run: nix build .#doc
- run: nix fmt . -- --check - run: nix fmt . -- --check
- run: nix flake check - run: nix flake check
- run: | - run: |

36
.github/workflows/doc.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: doc
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
publish:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v20
- run: nix build .#doc
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: 'result/multi'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

3
doc/acknowledgements.md Normal file
View file

@ -0,0 +1,3 @@
# Acknowledgements {#acknowledgements}
This project is based off of [sops-nix](https://github.com/Mic92/sops-nix) created Mic92. Thank you to Mic92 for inspiration and advice.

View file

@ -0,0 +1,4 @@
# Community and Support {#community-and-support}
Support and development discussion is available here on GitHub and
also through [Matrix](https://matrix.to/#/#agenix:nixos.org).

28
doc/contributing.md Normal file
View file

@ -0,0 +1,28 @@
# Contributing {#contributing}
* The main branch is protected against direct pushes
* All changes must go through GitHub PR review and get at least one approval
* PR titles and commit messages should be prefixed with at least one of these categories:
* contrib - things that make the project development better
* doc - documentation
* feature - new features
* fix - bug fixes
* Please update or make integration tests for new features
* Use `nix fmt` to format nix code
## Tests
You can run the tests with
```ShellSession
nix flake check
```
You can run the integration tests in interactive mode like this:
```ShellSession
nix run .#checks.x86_64-linux.integration.driverInteractive
```
After it starts, enter `run_tests()` to run the tests.

8
doc/features.md Normal file
View file

@ -0,0 +1,8 @@
# Features {#features}
* Secrets are encrypted with SSH keys
* system public keys via `ssh-keyscan`
* can use public keys available on GitHub for users (for example, https://github.com/ryantm.keys)
* No GPG
* Very little code, so it should be easy for you to audit
* Encrypted secrets are stored in the Nix store, so a separate distribution mechanism is not necessary

View file

@ -0,0 +1,38 @@
# Install via fetchTarball {#install-via-fetchtarball}
#### Install module via fetchTarball
Add the following to your configuration.nix:
```nix
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age.nix" ];
}
```
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";
# update hash from nix build output
sha256 = "";
}}/modules/age.nix"
];
}
```
#### 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" {}) ];
}
```

39
doc/install-via-flakes.md Normal file
View file

@ -0,0 +1,39 @@
# Install via Flakes {#install-via-flakes}
## Install module via Flakes
```nix
{
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.nixosModules.default
];
};
};
}
```
## Install CLI via Flakes
You don't need to install it,
```ShellSession
nix run github:ryantm/agenix -- --help
```
but, if you want to (change the system based on your system):
```nix
{
environment.systemPackages = [ agenix.packages.x86_64-linux.default ];
}
```

27
doc/install-via-niv.md Normal file
View file

@ -0,0 +1,27 @@
# Install via [niv](https://github.com/nmattia/niv) {#install-via-niv}
First add it to niv:
```ShellSession
$ niv add ryantm/agenix
```
## Install module via niv
Then add the following to your `configuration.nix` in the `imports` list:
```nix
{
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age.nix" ];
}
```
## Install CLI via niv
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage "${(import ./nix/sources.nix).agenix}/pkgs/agenix.nix" {}) ];
}
```

View file

@ -0,0 +1,28 @@
# Install via nix-channel {#install-via-nix-channel}
As root run:
```ShellSession
$ sudo nix-channel --add https://github.com/ryantm/agenix/archive/main.tar.gz agenix
$ sudo nix-channel --update
```
## Install module via nix-channel
Then add the following to your `configuration.nix` in the `imports` list:
```nix
{
imports = [ <agenix/modules/age.nix> ];
}
```
## Install CLI via nix-channel
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage <agenix/pkgs/agenix.nix> {}) ];
}
```

3
doc/introduction.md Normal file
View file

@ -0,0 +1,3 @@
# agenix - [age](https://github.com/FiloSottile/age)-encrypted secrets for NixOS {#introduction}
`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.

3
doc/notices.md Normal file
View file

@ -0,0 +1,3 @@
# Notices {#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.

View file

@ -0,0 +1,12 @@
# Overriding age binary {#overriding-age-binary}
The agenix CLI uses `rage` by default as its age implemenation, you
can use the reference implementation `age` with Flakes like this:
```nix
{pkgs,agenix,...}:{
environment.systemPackages = [
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.age}/bin/age"; })
];
}
```

View file

@ -0,0 +1,5 @@
# Problem and solution {#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.

250
doc/reference.md Normal file
View file

@ -0,0 +1,250 @@
# Reference {#reference}
## `age` module 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>.path`
`age.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:
```nix
{
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:
```nix
{
users.users.ryantm = {
isNormalUser = true;
passwordFile = config.age.secrets.passwordfile-ryantm.path;
};
}
```
#### 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.
### `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
{pkgs, ...}:{
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. 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:
```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";
}
```
## agenix CLI reference {#agenix-cli-reference}
```
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'

13
doc/rekeying.md Normal file
View file

@ -0,0 +1,13 @@
# Rekeying {#rekeying}
If you change the public keys in `secrets.nix`, you should rekey your
secrets:
```ShellSession
$ 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.)

View file

@ -0,0 +1,14 @@
# Threat model/Warnings {#threat-model-warnings}
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](https://github.com/Mic92/sops-nix) have, however this was left
out for simplicity in `age`.

18
doc/toc.md Normal file
View file

@ -0,0 +1,18 @@
# agenix
* [Introduction](#introduction)
* [Problem and solution](#problem-and-solution)
* [Features](#features)
* Installation
* [flakes](#install-via-flakes)
* [niv](#install-via-niv)
* [fetchTarball](#install-via-fetchtarball)
* [nix-channel](#install-via-nix-channel)
* [Tutorial](#tutorial)
* [Reference](#reference)
* [`age` module reference](#age-module-reference)
* [agenix CLI reference](#agenix-cli-reference)
* [Community and Support](#community-and-support)
* [Threat model/Warnings](#threat-model-warnings)
* [Contributing](#contributing)
* [Acknowledgements](#acknowledgements)

51
doc/tutorial.md Normal file
View file

@ -0,0 +1,51 @@
# Tutorial {#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 (This file is **not** imported into your NixOS configuration. It is only used for the `agenix` CLI.):
```ShellSession
$ mkdir secrets
$ cd secrets
$ touch secrets.nix
```
3. Add public keys to `secrets.nix` file (hint: use `ssh-keyscan` or GitHub (for example, https://github.com/ryantm.keys)):
```nix
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;
}
```
4. Edit secret files (these instructions assume your SSH private key is in ~/.ssh/):
```ShellSession
$ agenix -e secret1.age
```
5. Add secret to a NixOS module config:
```nix
{
age.secrets.secret1.file = ../secrets/secret1.age;
}
```
6. Use the secret in your config:
```nix
{
users.users.user1 = {
isNormalUser = true;
passwordFile = config.age.secrets.secret1.path;
};
}
```
7. NixOS rebuild or use your deployment tool like usual.
The secret will be decrypted to the value of `config.age.secrets.secret1.path` (`/run/agenix/secret1` by default).

View file

@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1674641431, "lastModified": 1677676435,
"narHash": "sha256-qfo19qVZBP4qn5M5gXc/h1MDgAtPA5VxJm9s8RUAkVk=", "narHash": "sha256-6FxdcmQr5JeZqsQvfinIMr0XcTyTuR7EXX0H3ANShpQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9b97ad7b4330aacda9b2343396eb3df8a853b4fc", "rev": "a08d6979dd7c82c4cef0dcc6ac45ab16051c1169",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -15,6 +15,7 @@
darwin, darwin,
}: let }: let
agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {}; agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {};
doc = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/doc.nix {};
in { in {
nixosModules.age = import ./modules/age.nix; nixosModules.age = import ./modules/age.nix;
nixosModules.default = self.nixosModules.age; nixosModules.default = self.nixosModules.age;
@ -26,23 +27,28 @@
formatter.x86_64-darwin = nixpkgs.legacyPackages.x86_64-darwin.alejandra; formatter.x86_64-darwin = nixpkgs.legacyPackages.x86_64-darwin.alejandra;
packages.x86_64-darwin.agenix = agenix "x86_64-darwin"; packages.x86_64-darwin.agenix = agenix "x86_64-darwin";
packages.x86_64-darwin.doc = doc "x86_64-darwin";
packages.x86_64-darwin.default = self.packages.x86_64-darwin.agenix; packages.x86_64-darwin.default = self.packages.x86_64-darwin.agenix;
formatter.aarch64-darwin = nixpkgs.legacyPackages.aarch64-darwin.alejandra; formatter.aarch64-darwin = nixpkgs.legacyPackages.aarch64-darwin.alejandra;
packages.aarch64-darwin.agenix = agenix "aarch64-darwin"; packages.aarch64-darwin.agenix = agenix "aarch64-darwin";
packages.aarch64-darwin.doc = doc "aarch64-darwin";
packages.aarch64-darwin.default = self.packages.aarch64-darwin.agenix; packages.aarch64-darwin.default = self.packages.aarch64-darwin.agenix;
formatter.aarch64-linux = nixpkgs.legacyPackages.aarch64-linux.alejandra; formatter.aarch64-linux = nixpkgs.legacyPackages.aarch64-linux.alejandra;
packages.aarch64-linux.agenix = agenix "aarch64-linux"; packages.aarch64-linux.agenix = agenix "aarch64-linux";
packages.aarch64-linux.doc = doc "aarch64-linux";
packages.aarch64-linux.default = self.packages.aarch64-linux.agenix; packages.aarch64-linux.default = self.packages.aarch64-linux.agenix;
formatter.i686-linux = nixpkgs.legacyPackages.i686-linux.alejandra; formatter.i686-linux = nixpkgs.legacyPackages.i686-linux.alejandra;
packages.i686-linux.agenix = agenix "i686-linux"; packages.i686-linux.agenix = agenix "i686-linux";
packages.i686-linux.doc = doc "i686-linux";
packages.i686-linux.default = self.packages.i686-linux.agenix; packages.i686-linux.default = self.packages.i686-linux.agenix;
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.alejandra; formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.alejandra;
packages.x86_64-linux.agenix = agenix "x86_64-linux"; packages.x86_64-linux.agenix = agenix "x86_64-linux";
packages.x86_64-linux.default = self.packages.x86_64-linux.agenix; packages.x86_64-linux.default = self.packages.x86_64-linux.agenix;
packages.x86_64-linux.doc = doc "x86_64-linux";
checks.x86_64-linux.integration = import ./test/integration.nix { checks.x86_64-linux.integration = import ./test/integration.nix {
inherit nixpkgs; inherit nixpkgs;
pkgs = nixpkgs.legacyPackages.x86_64-linux; pkgs = nixpkgs.legacyPackages.x86_64-linux;

11
pkgs/doc.nix Normal file
View file

@ -0,0 +1,11 @@
{
stdenvNoCC,
mmdoc,
self,
}:
stdenvNoCC.mkDerivation rec {
name = "agenix-doc";
src = ../doc;
phases = ["mmdocPhase"];
mmdocPhase = "${mmdoc}/bin/mmdoc agenix $src $out";
}