mirror of https://github.com/ryantm/agenix.git
Compare commits
169 Commits
Author | SHA1 | Date |
---|---|---|
Nathan Henrie | 8d37c5bdea | |
hansemschnokeloch | 63a57d8dfb | |
Jörg Thalheim | 07479c2e73 | |
Ryan Mulligan | 24a7ea3905 | |
Ellis Gibbons | 2c1d1fb134 | |
Cole Helbling | 1381a759b2 | |
oluceps | 3fd98a2c3b | |
Ryan Mulligan | 8cb01a0e71 | |
kraem | 1f62cef426 | |
Ryan Mulligan | 417caa847f | |
Ryan Mulligan | a23aa271be | |
Ryan Mulligan | bc24f2e510 | |
Ryan Mulligan | 457669db42 | |
Nathan Henrie | 6ce42cc768 | |
Ryan Mulligan | 23d4d5d291 | |
Ryan Mulligan | b6aa6180db | |
Ryan Mulligan | 58017c0c93 | |
Ryan Mulligan | bd86c06961 | |
Ryan Mulligan | eb3b5cf4fd | |
Ryan Mulligan | 5c1198a352 | |
Ryan Mulligan | 9bc80dc4ce | |
Ryan Mulligan | d0d4ad5be6 | |
Ryan Mulligan | 08dc5068e6 | |
Ryan Mulligan | 17090d105a | |
Ryan Mulligan | 097aa18b59 | |
Ryan Mulligan | 344f985526 | |
Ryan Mulligan | 564595d0ad | |
Ryan Mulligan | b7e0494b10 | |
Samuele Facenda | 9d3b37a117 | |
Ryan Mulligan | 93cec0ce6e | |
Ryan Mulligan | 221a1f22e5 | |
Ryan Mulligan | 6cb7cd66c2 | |
Ryan Mulligan | 13ac9ac6d6 | |
Shiva Kaul | 4c48606094 | |
Charles Hall | 65fe5959c3 | |
Charles Hall | 05591973d7 | |
Ryan Mulligan | daf42cb35b | |
Ryan Mulligan | dbc533ddc2 | |
Ryan Mulligan | e2f339274d | |
Tim Häring | b5fa96a90e | |
Ryan Mulligan | 1f677b3e16 | |
William McKinnon | 115e561054 | |
Ryan Mulligan | 7f9dfa309f | |
Nathan Henrie | da763b2c4b | |
Nathan Henrie | eb1386f3b2 | |
Ryan Mulligan | 572baca9b0 | |
Nathan Henrie | b76899f4c1 | |
Nathan Henrie | 7f30f9b4b3 | |
Nathan Henrie | da5d6f05f9 | |
Ryan Mulligan | 20deb735cc | |
Ryan Mulligan | 2ed2dc7582 | |
Ryan Mulligan | 54693c91d9 | |
Ryan Mulligan | 7d39a26d73 | |
Ryan Mulligan | 1698ed385d | |
Nicolas Lenz | fe4f564f13 | |
Ryan Mulligan | d8c973fd22 | |
malteneuss | 91220a701d | |
malteneuss | 2bee5c988c | |
malteneuss | 1d7fd15690 | |
malteneuss | 6d20bf81f8 | |
malteneuss | b91dfbaf76 | |
malteneuss | 78733d6d09 | |
Ryan Mulligan | 0d8c5325fc | |
Lin Jian | 6e8a48c2dc | |
Lin Jian | 0d94960783 | |
Ryan Mulligan | db5637d10f | |
Sefa Eyeoglu | 72205a86ca | |
Sefa Eyeoglu | 758cdc98f4 | |
Ryan Mulligan | 92197270a1 | |
Nathan Henrie | 6b4ff3d191 | |
Nathan Henrie | 50743bd117 | |
Nathan Henrie | 19bf5a20d8 | |
Nathan Henrie | 3fbc22fe43 | |
Bruno BELANYI | 0155c5710e | |
Bruno BELANYI | 1f43d94d52 | |
Bruno BELANYI | 9274b82816 | |
Cole Helbling | 2994d002dc | |
Ryan Mulligan | 0e3a237c5a | |
Winston (Winny) Weinert | 8722cf94f1 | |
Nathan Henrie | e64961977f | |
Wanja Hentze | 40550f0619 | |
Ryan Mulligan | 03b51fe8e4 | |
Ryan Mulligan | b1d6d764e1 | |
Ryan Mulligan | 1abf0ade92 | |
Ryan Mulligan | 2fb0a74be3 | |
Ryan Mulligan | 36986c8fed | |
Ryan Mulligan | 119fac65b4 | |
Ryan Mulligan | 6a2757101d | |
Ryan Mulligan | 657789137c | |
Ryan Mulligan | 4828951d9d | |
Ryan Mulligan | b67873854d | |
Ryan Mulligan | faf978f7f3 | |
Ryan Mulligan | 1141c36c26 | |
Ryan Mulligan | 9225d56306 | |
Nathan Henrie | 37dcc5f5e7 | |
Ryan Mulligan | 833f87c8ff | |
Wanja Hentze | 7dae15b7bc | |
Ryan Mulligan | c2a71c83c7 | |
muvlon | 9cf1967039 | |
Ryan Mulligan | 2d735d6518 | |
Ryan Mulligan | 2c0ae7d44f | |
Ryan Mulligan | 0c50bbe60e | |
Nathan Henrie | 283c178469 | |
Nathan Henrie | d84a99d0b8 | |
Nathan Henrie | 5f66c8aa77 | |
Ryan Mulligan | 53da86e976 | |
Matthias Putz | ec66ebe0ee | |
Ryan Mulligan | b0721be0c6 | |
Ryan Mulligan | 344c8e41d2 | |
Ryan Mulligan | 2c56a93426 | |
Ryan Mulligan | c602dc4ffb | |
Nathan Henrie | 78a22dbc0d | |
Ryan Mulligan | 16c6ccef09 | |
Ryan Mulligan | ec396f7a76 | |
Ryan Mulligan | e4f0dcc8d3 | |
Ryan Mulligan | de657061b1 | |
Nathan Henrie | 0b5c4b8c8f | |
Nathan Henrie | 9e361f8b39 | |
Nathan Henrie | 0efac6bcf0 | |
Nathan Henrie | effb43cb63 | |
Ryan Mulligan | ea17cc71b4 | |
Ryan Mulligan | d0b75ddf9a | |
Ryan Mulligan | 6053c559c5 | |
Nathan Henrie | 37c7297956 | |
Nathan Henrie | 578794f528 | |
Ryan Mulligan | b7ffcfe77f | |
Nathan Henrie | d7fd31756e | |
Nathan Henrie | 6ec0b0f7c7 | |
Nathan Henrie | 9779a98f1e | |
Nathan Henrie | 4b2b6fa111 | |
Nathan Henrie | 4c315d9683 | |
Nathan Henrie | 9b94b43971 | |
Nathan Henrie | c69689da58 | |
Nathan Henrie | b818ac2e7d | |
Nathan Henrie | 019784cb7e | |
Nathan Henrie | 8867c12d72 | |
Nathan Henrie | 4532604741 | |
Nathan Henrie | 351e874918 | |
Ryan Mulligan | 49798e535e | |
Ryan Mulligan | c695ebce9a | |
Ryan Mulligan | 6d3a415637 | |
Ryan Mulligan | 16bef569f4 | |
Ryan Mulligan | 99e0963743 | |
Ryan Mulligan | bf537f5b72 | |
Ryan Mulligan | 64b0574514 | |
Cole Helbling | 42d371d861 | |
Erik Arvstedt | 822f71b8d8 | |
Ryan Mulligan | a630400067 | |
Chris Montgomery | ffbca4ae7e | |
Ryan Mulligan | ff2dc4fb88 | |
Chris Montgomery | a8ccd5bfa8 | |
Ryan Mulligan | 6acb1fe5f8 | |
Ryan Mulligan | 78d871220f | |
Ryan Mulligan | d51af86302 | |
Ryan Mulligan | a4ad67c46e | |
Ryan Mulligan | 84f0dc0a4f | |
Ryan Mulligan | edf0d09012 | |
Ryan Mulligan | c96da5835b | |
Ryan Mulligan | 68a8bc2951 | |
Ryan Mulligan | 5d802d251c | |
Ryan Mulligan | d13c6d3bb7 | |
Ryan Mulligan | 7ebd7d741d | |
Ryan Mulligan | 3c34edaf65 | |
Ryan Mulligan | b352e6b70f | |
Ryan Mulligan | e05a49ee30 | |
Ryan Mulligan | bce59868a2 | |
Ryan Mulligan | 9f136ecfa5 | |
Ryan Mulligan | f86b56229b | |
Jeroen Simonetti | fe206b4306 |
|
@ -0,0 +1,35 @@
|
|||
|
||||
name-template: '$RESOLVED_VERSION'
|
||||
tag-template: '$RESOLVED_VERSION'
|
||||
categories:
|
||||
- title: '🚀 Features'
|
||||
labels:
|
||||
- 'feature'
|
||||
- 'enhancement'
|
||||
- title: '🐛 Bug Fixes'
|
||||
labels:
|
||||
- 'fix'
|
||||
- 'bugfix'
|
||||
- 'bug'
|
||||
- title: '🧰 Development'
|
||||
label: 'dev'
|
||||
- title: '🤖 Dependencies'
|
||||
label: 'dependencies'
|
||||
- title: '🔒 Security'
|
||||
label: 'security'
|
||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- 'major'
|
||||
minor:
|
||||
labels:
|
||||
- 'minor'
|
||||
patch:
|
||||
labels:
|
||||
- 'patch'
|
||||
default: patch
|
||||
template: |
|
||||
## Changes
|
||||
$CHANGES
|
|
@ -4,20 +4,48 @@ on:
|
|||
push:
|
||||
jobs:
|
||||
tests-linux:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: cachix/install-nix-action@v15
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm"
|
||||
extra_nix_config: |
|
||||
system-features = nixos-test recursive-nix benchmark big-parallel kvm
|
||||
extra-experimental-features = recursive-nix nix-command flakes
|
||||
- run: nix build
|
||||
- run: nix build .#doc
|
||||
- run: nix fmt . -- --check
|
||||
- run: nix flake check
|
||||
tests-darwin:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: cachix/install-nix-action@v15
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v24
|
||||
with:
|
||||
extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm"
|
||||
extra_nix_config: |
|
||||
system-features = nixos-test recursive-nix benchmark big-parallel kvm
|
||||
extra-experimental-features = recursive-nix nix-command flakes
|
||||
- run: nix build
|
||||
- run: nix build .#doc
|
||||
- run: nix fmt . -- --check
|
||||
- run: nix flake check
|
||||
- name: "Install nix-darwin module"
|
||||
run: |
|
||||
# https://github.com/ryantm/agenix/pull/230#issuecomment-1867025385
|
||||
|
||||
sudo mv /etc/nix/nix.conf{,.bak}
|
||||
nix \
|
||||
--extra-experimental-features 'nix-command flakes' \
|
||||
build .#checks.x86_64-darwin.integration
|
||||
|
||||
./result/activate-user
|
||||
sudo ./result/activate
|
||||
- name: "Test nix-darwin module"
|
||||
run: |
|
||||
sudo /run/current-system/sw/bin/agenix-integration
|
||||
- name: "Test home-manager module"
|
||||
run: |
|
||||
# Do the job of `home-manager switch` in-line to avoid rate limiting
|
||||
nix build .#homeConfigurations.integration-darwin.activationPackage
|
||||
./result/activate
|
||||
~/agenix-home-integration/bin/agenix-home-integration
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy static content to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: [$default-branch]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- uses: cachix/install-nix-action@v20
|
||||
- run: nix build .#doc && mkdir -p _site/ && cp -r ./result/multi/* _site/
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
|
@ -0,0 +1,27 @@
|
|||
name: "Publish tags to FlakeHub"
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v?[0-9]+.[0-9]+.[0-9]+*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "The existing tag to publish to FlakeHub"
|
||||
type: "string"
|
||||
required: true
|
||||
jobs:
|
||||
flakehub-publish:
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
id-token: "write"
|
||||
contents: "read"
|
||||
steps:
|
||||
- uses: "actions/checkout@v3"
|
||||
with:
|
||||
ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}"
|
||||
- uses: "DeterminateSystems/nix-installer-action@main"
|
||||
- uses: "DeterminateSystems/flakehub-push@main"
|
||||
with:
|
||||
visibility: "public"
|
||||
name: "ryantm/agenix"
|
||||
tag: "${{ inputs.tag }}"
|
|
@ -0,0 +1,33 @@
|
|||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
# branches to consider in the event; optional, defaults to all
|
||||
branches:
|
||||
- main
|
||||
# pull_request event is required only for autolabeler
|
||||
pull_request:
|
||||
# Only following types are handled by the action, but one can default to all as well
|
||||
types: [opened, reopened, synchronize]
|
||||
# pull_request_target event is required for autolabeler to support PRs from forks
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
permissions:
|
||||
# write permission is required to create a github release
|
||||
contents: write
|
||||
# write permission is required for autolabeler
|
||||
# otherwise, read permission is required at least
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Drafts your next Release notes as Pull Requests are merged into "main"
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -0,0 +1 @@
|
|||
/result
|
479
README.md
479
README.md
|
@ -1,30 +1,31 @@
|
|||
# agenix - [age](https://github.com/FiloSottile/age)-encrypted secrets for NixOS
|
||||
|
||||
`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.
|
||||
`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:
|
||||
1. An `agenix` commandline app (CLI) to encrypt secrets into secured `.age` files that can be copied into the Nix store.
|
||||
2. An `agenix` NixOS module to conveniently
|
||||
* add those encrypted secrets (`.age` files) into the Nix store so that they can be deployed like any other Nix package using `nixos-rebuild` or similar tools.
|
||||
* automatically decrypt on a target machine using the private SSH keys on that machine
|
||||
* automatically mount these decrypted secrets on a well known path like `/run/agenix/...` to be consumed.
|
||||
|
||||
## 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)
|
||||
* [niv](#install-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)
|
||||
* [Reference](#reference)
|
||||
* [`age` module reference](#age-module-reference)
|
||||
* [agenix CLI reference](#agenix-cli-reference)
|
||||
* [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)
|
||||
* [Contributing](#contributing)
|
||||
* [Acknowledgements](#acknowledgements)
|
||||
|
||||
## Problem and solution
|
||||
|
@ -44,19 +45,17 @@ All files in the Nix store are readable by any system user, so it is not a suita
|
|||
|
||||
## 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.
|
||||
* Password-protected ssh keys: since age does 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.
|
||||
|
||||
## Installation
|
||||
|
||||
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)
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
### Install via [niv](https://github.com/nmattia/niv)
|
||||
|
||||
</summary>
|
||||
|
||||
First add it to niv:
|
||||
|
||||
```ShellSession
|
||||
|
@ -83,8 +82,15 @@ To install the `agenix` binary:
|
|||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
### Install via nix-channel
|
||||
|
||||
</summary>
|
||||
|
||||
As root run:
|
||||
|
||||
```ShellSession
|
||||
|
@ -112,8 +118,15 @@ To install the `agenix` binary:
|
|||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
### Install via fetchTarball
|
||||
|
||||
</summary>
|
||||
|
||||
#### Install module via fetchTarball
|
||||
|
||||
Add the following to your configuration.nix:
|
||||
|
@ -134,8 +147,8 @@ Add the following to your configuration.nix:
|
|||
in [
|
||||
"${builtins.fetchTarball {
|
||||
url = "https://github.com/ryantm/agenix/archive/${commit}.tar.gz";
|
||||
# replace this with an actual hash
|
||||
sha256 = "0000000000000000000000000000000000000000000000000000";
|
||||
# update hash from nix build output
|
||||
sha256 = "";
|
||||
}}/modules/age.nix"
|
||||
];
|
||||
}
|
||||
|
@ -151,8 +164,15 @@ To install the `agenix` binary:
|
|||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
### Install via Flakes
|
||||
|
||||
</summary>
|
||||
|
||||
#### Install module via Flakes
|
||||
|
||||
```nix
|
||||
|
@ -160,6 +180,8 @@ To install the `agenix` binary:
|
|||
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
|
||||
|
@ -168,7 +190,7 @@ To install the `agenix` binary:
|
|||
system = "x86_64-linux";
|
||||
modules = [
|
||||
./configuration.nix
|
||||
agenix.nixosModule
|
||||
agenix.nixosModules.default
|
||||
];
|
||||
};
|
||||
};
|
||||
|
@ -177,20 +199,45 @@ To install the `agenix` binary:
|
|||
|
||||
#### Install CLI via Flakes
|
||||
|
||||
You don't need to install it,
|
||||
You can run the CLI tool ad-hoc without installing it:
|
||||
|
||||
```ShellSession
|
||||
nix run github:ryantm/agenix -- --help
|
||||
```
|
||||
|
||||
but, if you want to (change the system based on your system):
|
||||
But you can also add it permanently into a [NixOS module](https://wiki.nixos.org/wiki/NixOS_modules)
|
||||
(replace system "x86_64-linux" with your system):
|
||||
|
||||
```nix
|
||||
{
|
||||
environment.systemPackages = [ agenix.defaultPackage.x86_64-linux ];
|
||||
environment.systemPackages = [ agenix.packages.x86_64-linux.default ];
|
||||
}
|
||||
```
|
||||
|
||||
e.g. inside your `flake.nix` file:
|
||||
|
||||
```nix
|
||||
{
|
||||
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 ];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Tutorial
|
||||
|
||||
1. The system you want to deploy secrets to should already exist and
|
||||
|
@ -198,13 +245,14 @@ but, if you want to (change the system based on your system):
|
|||
`/etc/ssh/`.
|
||||
|
||||
2. Make a directory to store secrets and `secrets.nix` file for listing secrets and their public keys:
|
||||
|
||||
```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)):
|
||||
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.
|
||||
3. Add public keys to your `secrets.nix` file:
|
||||
```nix
|
||||
let
|
||||
user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH";
|
||||
|
@ -220,24 +268,310 @@ but, if you want to (change the system based on your system):
|
|||
"secret2.age".publicKeys = users ++ systems;
|
||||
}
|
||||
```
|
||||
4. Edit secret files (these instructions assume your SSH private key is in ~/.ssh/):
|
||||
These are the users and systems that will be able to decrypt the `.age` files later with their corresponding private keys.
|
||||
You can obtain the public keys from
|
||||
* your local computer usually in `~/.ssh`, e.g. `~/.ssh/id_ed25519.pub`.
|
||||
* from a running target machine with `ssh-keyscan`:
|
||||
```ShellSession
|
||||
$ ssh-keyscan <ip-address>
|
||||
... ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1
|
||||
...
|
||||
```
|
||||
* from GitHub like https://github.com/ryantm.keys.
|
||||
4. Create a secret file:
|
||||
```ShellSession
|
||||
$ 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.
|
||||
5. Add secret to a NixOS module config:
|
||||
```nix
|
||||
age.secrets.secret1.file = ../secrets/secret1.age;
|
||||
{
|
||||
age.secrets.secret1.file = ../secrets/secret1.age;
|
||||
}
|
||||
```
|
||||
6. NixOS rebuild or use your deployment tool like usual.
|
||||
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.
|
||||
|
||||
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).
|
||||
6. Reference the secrets' mount path in your config:
|
||||
```nix
|
||||
{
|
||||
users.users.user1 = {
|
||||
isNormalUser = true;
|
||||
passwordFile = 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.
|
||||
7. Use `nixos-rebuild` or [another deployment tool](https://nixos.wiki/wiki/Applications#Deployment") of choice as usual.
|
||||
|
||||
## Community and Support
|
||||
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.
|
||||
8. Edit secret files:
|
||||
```ShellSession
|
||||
$ 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.
|
||||
```ShellSession
|
||||
$ agenix -e secret1.age -i ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
Support and development discussion is available here on GitHub and
|
||||
also through [Matrix](https://matrix.to/#/#agenix:nixos.org).
|
||||
## Reference
|
||||
|
||||
## Rekeying
|
||||
### `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, 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:
|
||||
|
||||
```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 `age/bin/age`.
|
||||
|
||||
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 - 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'
|
||||
```
|
||||
|
||||
#### Rekeying
|
||||
|
||||
If you change the public keys in `secrets.nix`, you should rekey your
|
||||
secrets:
|
||||
|
@ -251,44 +585,24 @@ 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.)
|
||||
|
||||
## Don't symlink secret
|
||||
#### Overriding age binary
|
||||
|
||||
If your secret cannot be a symlink, you should set the `symlink` option to `false`:
|
||||
The agenix CLI uses `age` by default as its age implemenation, you
|
||||
can use the `rage` implementation with Flakes like this:
|
||||
|
||||
```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.
|
||||
|
||||
## 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
|
||||
{
|
||||
{pkgs,agenix,...}:{
|
||||
environment.systemPackages = [
|
||||
(agenix.defaultPackage.x86_64-linux.override { ageBin = "${pkgs.age}/bin/age"; })
|
||||
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Community and Support
|
||||
|
||||
Support and development discussion is available here on GitHub and
|
||||
also through [Matrix](https://matrix.to/#/#agenix:nixos.org).
|
||||
|
||||
## Threat model/Warnings
|
||||
|
||||
This project has not been audited by a security professional.
|
||||
|
@ -304,17 +618,34 @@ 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`.
|
||||
|
||||
### builtins.readFile anti-pattern
|
||||
## Contributing
|
||||
|
||||
```nix
|
||||
{
|
||||
# Do not do this!
|
||||
config.password = builtins.readFile config.age.secrets.secret1.path;
|
||||
}
|
||||
* 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
|
||||
```
|
||||
|
||||
This can cause the cleartext to be placed into the world-readable Nix
|
||||
store. Instead, have your services read the cleartext path at runtime.
|
||||
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.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
grep -q "$1" pkgs/agenix.nix || (echo "Couldn't find version $1 in pkgs/agenix.nix" && exit 1)
|
||||
sed -i "s/$1/$2/g" pkgs/agenix.nix
|
||||
git add pkgs/agenix.nix
|
||||
git commit -m "version $2"
|
||||
exit 0
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#! nix-shell -p python3 -i python
|
||||
|
||||
# based off of https://git.sr.ht/~sircmpwn/dotfiles/tree/master/item/bin/semver
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
if subprocess.run(["git", "branch", "--show-current"], stdout=subprocess.PIPE
|
||||
).stdout.decode().strip() != "main":
|
||||
print("WARNING! Not on the main branch.")
|
||||
|
||||
subprocess.run(["git", "pull", "--rebase"])
|
||||
p = subprocess.run(["git", "describe", "--abbrev=0"], stdout=subprocess.PIPE)
|
||||
describe = p.stdout.decode().strip()
|
||||
old_version = describe.split("-")[0].split(".")
|
||||
if len(old_version) == 2:
|
||||
[major, minor] = old_version
|
||||
[major, minor] = map(int, [major, minor])
|
||||
patch = 0
|
||||
else:
|
||||
[major, minor, patch] = old_version
|
||||
[major, minor, patch] = map(int, [major, minor, patch])
|
||||
|
||||
p = subprocess.run(["git", "shortlog", "--no-merges", f"{describe}..HEAD"],
|
||||
stdout=subprocess.PIPE)
|
||||
shortlog = p.stdout.decode()
|
||||
|
||||
new_version = None
|
||||
|
||||
if sys.argv[1] == "patch":
|
||||
patch += 1
|
||||
elif sys.argv[1] == "minor":
|
||||
minor += 1
|
||||
patch = 0
|
||||
elif sys.argv[1] == "major":
|
||||
major += 1
|
||||
minor = patch = 0
|
||||
else:
|
||||
new_version = sys.argv[1]
|
||||
|
||||
if new_version is None:
|
||||
if len(old_version) == 2 and patch == 0:
|
||||
new_version = f"{major}.{minor}"
|
||||
else:
|
||||
new_version = f"{major}.{minor}.{patch}"
|
||||
|
||||
p = None
|
||||
if os.path.exists("contrib/_incr_version"):
|
||||
p = subprocess.run(["contrib/_incr_version", describe, new_version])
|
||||
else:
|
||||
print("Warning: no _incr_version script. " +
|
||||
"Does this project have any specific release requirements?")
|
||||
|
||||
if p and p.returncode != 0:
|
||||
print("Error: _incr_version returned nonzero exit code")
|
||||
sys.exit(1)
|
||||
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
basename = os.path.basename(os.getcwd())
|
||||
f.write(f"{basename} {new_version}\n\n".encode())
|
||||
f.write(shortlog.encode())
|
||||
f.flush()
|
||||
subprocess.run(["git", "tag", "-e", "-F", f.name, "-a", new_version])
|
||||
print(new_version)
|
|
@ -1,4 +1,3 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
{
|
||||
agenix = pkgs.callPackage ./pkgs/agenix.nix {};
|
||||
{pkgs ? import <nixpkgs> {}}: {
|
||||
agenix = pkgs.callPackage ./pkgs/agenix.nix {};
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -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).
|
|
@ -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.
|
|
@ -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
|
|
@ -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" {}) ];
|
||||
}
|
||||
```
|
|
@ -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 ];
|
||||
}
|
||||
```
|
|
@ -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" {}) ];
|
||||
}
|
||||
```
|
|
@ -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> {}) ];
|
||||
}
|
||||
```
|
|
@ -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.
|
|
@ -0,0 +1,3 @@
|
|||
# Notices {#notices}
|
||||
|
||||
* Password-protected ssh keys: since age does 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.
|
|
@ -0,0 +1,12 @@
|
|||
# Overriding age binary {#overriding-age-binary}
|
||||
|
||||
The agenix CLI uses `age` by default as its age implemenation, you
|
||||
can use the `rage` implementation with Flakes like this:
|
||||
|
||||
```nix
|
||||
{pkgs,agenix,...}:{
|
||||
environment.systemPackages = [
|
||||
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
|
||||
];
|
||||
}
|
||||
```
|
|
@ -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.
|
|
@ -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 `age/bin/age`.
|
||||
|
||||
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'
|
|
@ -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.)
|
|
@ -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`.
|
|
@ -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)
|
|
@ -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).
|
|
@ -1,9 +1,8 @@
|
|||
let
|
||||
user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH";
|
||||
system1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPJDyIr/FSz1cJdcoW69R+NrWzwGK/+3gJpqD1t8L2zE";
|
||||
in
|
||||
{
|
||||
"secret1.age".publicKeys = [ user1 system1 ];
|
||||
"secret2.age".publicKeys = [ user1 ];
|
||||
"passwordfile-user1.age".publicKeys = [ user1 system1 ];
|
||||
in {
|
||||
"secret1.age".publicKeys = [user1 system1];
|
||||
"secret2.age".publicKeys = [user1];
|
||||
"passwordfile-user1.age".publicKeys = [user1 system1];
|
||||
}
|
||||
|
|
77
flake.lock
77
flake.lock
|
@ -1,24 +1,83 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"darwin": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1638587357,
|
||||
"narHash": "sha256-2ySMW3QARG8BsRPmwe7clTbdCuaObromOKewykP+UJc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e34c5379866833f41e2a36f309912fa675d687c7",
|
||||
"lastModified": 1700795494,
|
||||
"narHash": "sha256-gzGLZSiOhf155FW7262kdHo2YDeugp3VuIFb4/GGng0=",
|
||||
"owner": "lnl7",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "4b9b83d5a92e8c1fbfd8eb27eda375908c11ec4d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-21.11",
|
||||
"owner": "lnl7",
|
||||
"ref": "master",
|
||||
"repo": "nix-darwin",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703113217,
|
||||
"narHash": "sha256-7ulcXOk63TIT2lVDSExj7XzFx09LpdSAPtvgtM7yQPE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "3bfaacf46133c037bb356193bd2f1765d9dc82c1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1703013332,
|
||||
"narHash": "sha256-+tFNwMvlXLbJZXiMHqYq77z/RfmpfpiI3yjL6o/Zo9M=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "54aac082a4d9bb5bbc5c4e899603abfb76a3f6d6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
"darwin": "darwin",
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
111
flake.nix
111
flake.nix
|
@ -2,37 +2,88 @@
|
|||
description = "Secret management with age";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-21.11";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {};
|
||||
in {
|
||||
|
||||
nixosModules.age = import ./modules/age.nix;
|
||||
nixosModule = self.nixosModules.age;
|
||||
|
||||
overlay = import ./overlay.nix;
|
||||
|
||||
packages."aarch64-linux".agenix = agenix "aarch64-linux";
|
||||
defaultPackage."aarch64-linux" = self.packages."aarch64-linux".agenix;
|
||||
|
||||
packages."i686-linux".agenix = agenix "i686-linux";
|
||||
defaultPackage."i686-linux" = self.packages."i686-linux".agenix;
|
||||
|
||||
packages."x86_64-darwin".agenix = agenix "x86_64-darwin";
|
||||
defaultPackage."x86_64-darwin" = self.packages."x86_64-darwin".agenix;
|
||||
|
||||
packages."aarch64-darwin".agenix = agenix "aarch64-darwin";
|
||||
defaultPackage."aarch64-darwin" = self.packages."aarch64-darwin".agenix;
|
||||
|
||||
packages."x86_64-linux".agenix = agenix "x86_64-linux";
|
||||
defaultPackage."x86_64-linux" = self.packages."x86_64-linux".agenix;
|
||||
checks."x86_64-linux".integration = import ./test/integration.nix {
|
||||
inherit nixpkgs; pkgs = nixpkgs.legacyPackages."x86_64-linux"; system = "x86_64-linux";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
darwin = {
|
||||
url = "github:lnl7/nix-darwin/master";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
systems.url = "github:nix-systems/default";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
darwin,
|
||||
home-manager,
|
||||
systems,
|
||||
}: let
|
||||
eachSystem = nixpkgs.lib.genAttrs (import systems);
|
||||
in {
|
||||
nixosModules.age = import ./modules/age.nix;
|
||||
nixosModules.default = self.nixosModules.age;
|
||||
|
||||
darwinModules.age = import ./modules/age.nix;
|
||||
darwinModules.default = self.darwinModules.age;
|
||||
|
||||
homeManagerModules.age = import ./modules/age-home.nix;
|
||||
homeManagerModules.default = self.homeManagerModules.age;
|
||||
|
||||
overlays.default = import ./overlay.nix;
|
||||
|
||||
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
|
||||
|
||||
packages = eachSystem (system: {
|
||||
agenix = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {};
|
||||
doc = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/doc.nix {inherit self;};
|
||||
default = self.packages.${system}.agenix;
|
||||
});
|
||||
|
||||
checks =
|
||||
nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: {
|
||||
integration =
|
||||
(darwin.lib.darwinSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
./test/integration_darwin.nix
|
||||
|
||||
# Allow new-style nix commands in CI
|
||||
{nix.extraOptions = "experimental-features = nix-command flakes";}
|
||||
|
||||
home-manager.darwinModules.home-manager
|
||||
{
|
||||
home-manager = {
|
||||
verbose = true;
|
||||
useGlobalPkgs = true;
|
||||
useUserPackages = true;
|
||||
backupFileExtension = "hmbak";
|
||||
users.runner = ./test/integration_hm_darwin.nix;
|
||||
};
|
||||
}
|
||||
];
|
||||
})
|
||||
.system;
|
||||
})
|
||||
// {
|
||||
x86_64-linux.integration = import ./test/integration.nix {
|
||||
inherit nixpkgs home-manager;
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
};
|
||||
|
||||
darwinConfigurations.integration-x86_64.system = self.checks.x86_64-darwin.integration;
|
||||
darwinConfigurations.integration-aarch64.system = self.checks.aarch64-darwin.integration;
|
||||
|
||||
# Work-around for https://github.com/nix-community/home-manager/issues/3075
|
||||
legacyPackages = nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: {
|
||||
homeConfigurations.integration-darwin = home-manager.lib.homeManagerConfiguration {
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
modules = [./test/integration_hm_darwin.nix];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
{
|
||||
config,
|
||||
options,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.age;
|
||||
|
||||
ageBin = lib.getExe config.age.package;
|
||||
|
||||
newGeneration = ''
|
||||
_agenix_generation="$(basename "$(readlink "${cfg.secretsDir}")" || echo 0)"
|
||||
(( ++_agenix_generation ))
|
||||
echo "[agenix] creating new generation in ${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
mkdir -p "${cfg.secretsMountPoint}"
|
||||
chmod 0751 "${cfg.secretsMountPoint}"
|
||||
mkdir -p "${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
'';
|
||||
|
||||
setTruePath = secretType: ''
|
||||
${
|
||||
if secretType.symlink
|
||||
then ''
|
||||
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
|
||||
''
|
||||
else ''
|
||||
_truePath="${secretType.path}"
|
||||
''
|
||||
}
|
||||
'';
|
||||
|
||||
installSecret = secretType: ''
|
||||
${setTruePath secretType}
|
||||
echo "decrypting '${secretType.file}' to '$_truePath'..."
|
||||
TMP_FILE="$_truePath.tmp"
|
||||
|
||||
IDENTITIES=()
|
||||
# shellcheck disable=2043
|
||||
for identity in ${toString cfg.identityPaths}; do
|
||||
test -r "$identity" || continue
|
||||
IDENTITIES+=(-i)
|
||||
IDENTITIES+=("$identity")
|
||||
done
|
||||
|
||||
test "''${#IDENTITIES[@]}" -eq 0 && echo "[agenix] WARNING: no readable identities found!"
|
||||
|
||||
mkdir -p "$(dirname "$_truePath")"
|
||||
# shellcheck disable=SC2193,SC2050
|
||||
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && mkdir -p "$(dirname "${secretType.path}")"
|
||||
(
|
||||
umask u=r,g=,o=
|
||||
test -f "${secretType.file}" || echo '[agenix] WARNING: encrypted file ${secretType.file} does not exist!'
|
||||
test -d "$(dirname "$TMP_FILE")" || echo "[agenix] WARNING: $(dirname "$TMP_FILE") does not exist!"
|
||||
LANG=${config.i18n.defaultLocale or "C"} ${ageBin} --decrypt "''${IDENTITIES[@]}" -o "$TMP_FILE" "${secretType.file}"
|
||||
)
|
||||
chmod ${secretType.mode} "$TMP_FILE"
|
||||
mv -f "$TMP_FILE" "$_truePath"
|
||||
|
||||
${optionalString secretType.symlink ''
|
||||
# shellcheck disable=SC2193,SC2050
|
||||
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfn "${cfg.secretsDir}/${secretType.name}" "${secretType.path}"
|
||||
''}
|
||||
'';
|
||||
|
||||
testIdentities =
|
||||
map
|
||||
(path: ''
|
||||
test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!'
|
||||
'')
|
||||
cfg.identityPaths;
|
||||
|
||||
cleanupAndLink = ''
|
||||
_agenix_generation="$(basename "$(readlink "${cfg.secretsDir}")" || echo 0)"
|
||||
(( ++_agenix_generation ))
|
||||
echo "[agenix] symlinking new secrets to ${cfg.secretsDir} (generation $_agenix_generation)..."
|
||||
ln -sfn "${cfg.secretsMountPoint}/$_agenix_generation" "${cfg.secretsDir}"
|
||||
|
||||
(( _agenix_generation > 1 )) && {
|
||||
echo "[agenix] removing old secrets (generation $(( _agenix_generation - 1 )))..."
|
||||
rm -rf "${cfg.secretsMountPoint}/$(( _agenix_generation - 1 ))"
|
||||
}
|
||||
'';
|
||||
|
||||
installSecrets = builtins.concatStringsSep "\n" (
|
||||
["echo '[agenix] decrypting secrets...'"]
|
||||
++ testIdentities
|
||||
++ (map installSecret (builtins.attrValues cfg.secrets))
|
||||
++ [cleanupAndLink]
|
||||
);
|
||||
|
||||
secretType = types.submodule ({
|
||||
config,
|
||||
name,
|
||||
...
|
||||
}: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
description = ''
|
||||
Name of the file used in ''${cfg.secretsDir}
|
||||
'';
|
||||
};
|
||||
file = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
Age file the secret is loaded from.
|
||||
'';
|
||||
};
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "${cfg.secretsDir}/${config.name}";
|
||||
description = ''
|
||||
Path where the decrypted secret is installed.
|
||||
'';
|
||||
};
|
||||
mode = mkOption {
|
||||
type = types.str;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the decrypted secret in a format understood by chmod.
|
||||
'';
|
||||
};
|
||||
symlink = mkEnableOption "symlinking secrets to their destination" // {default = true;};
|
||||
};
|
||||
});
|
||||
|
||||
mountingScript = let
|
||||
app = pkgs.writeShellApplication {
|
||||
name = "agenix-home-manager-mount-secrets";
|
||||
runtimeInputs = with pkgs; [coreutils];
|
||||
text = ''
|
||||
${newGeneration}
|
||||
${installSecrets}
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
in
|
||||
lib.getExe app;
|
||||
|
||||
userDirectory = dir: let
|
||||
inherit (pkgs.stdenv.hostPlatform) isDarwin;
|
||||
baseDir =
|
||||
if isDarwin
|
||||
then "$(getconf DARWIN_USER_TEMP_DIR)"
|
||||
else "$XDG_RUNTIME_DIR";
|
||||
in "${baseDir}/${dir}";
|
||||
|
||||
userDirectoryDescription = dir:
|
||||
literalExpression ''
|
||||
"$XDG_RUNTIME_DIR"/${dir} on linux or "$(getconf DARWIN_USER_TEMP_DIR)"/${dir} on darwin.
|
||||
'';
|
||||
in {
|
||||
options.age = {
|
||||
package = mkPackageOption pkgs "age" {};
|
||||
|
||||
secrets = mkOption {
|
||||
type = types.attrsOf secretType;
|
||||
default = {};
|
||||
description = ''
|
||||
Attrset of secrets.
|
||||
'';
|
||||
};
|
||||
|
||||
identityPaths = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [
|
||||
"${config.home.homeDirectory}/.ssh/id_ed25519"
|
||||
"${config.home.homeDirectory}/.ssh/id_rsa"
|
||||
];
|
||||
defaultText = literalExpression ''
|
||||
[
|
||||
"''${config.home.homeDirectory}/.ssh/id_ed25519"
|
||||
"''${config.home.homeDirectory}/.ssh/id_rsa"
|
||||
]
|
||||
'';
|
||||
description = ''
|
||||
Path to SSH keys to be used as identities in age decryption.
|
||||
'';
|
||||
};
|
||||
|
||||
secretsDir = mkOption {
|
||||
type = types.str;
|
||||
default = userDirectory "agenix";
|
||||
defaultText = userDirectoryDescription "agenix";
|
||||
description = ''
|
||||
Folder where secrets are symlinked to
|
||||
'';
|
||||
};
|
||||
|
||||
secretsMountPoint = mkOption {
|
||||
default = userDirectory "agenix.d";
|
||||
defaultText = userDirectoryDescription "agenix.d";
|
||||
description = ''
|
||||
Where secrets are created before they are symlinked to ''${cfg.secretsDir}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (cfg.secrets != {}) {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.identityPaths != [];
|
||||
message = "age.identityPaths must be set.";
|
||||
}
|
||||
];
|
||||
|
||||
systemd.user.services.agenix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
|
||||
Unit = {
|
||||
Description = "agenix activation";
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = mountingScript;
|
||||
};
|
||||
Install.WantedBy = ["default.target"];
|
||||
};
|
||||
|
||||
launchd.agents.activate-agenix = {
|
||||
enable = true;
|
||||
config = {
|
||||
ProgramArguments = [mountingScript];
|
||||
KeepAlive = {
|
||||
Crashed = false;
|
||||
SuccessfulExit = false;
|
||||
};
|
||||
RunAtLoad = true;
|
||||
ProcessType = "Background";
|
||||
StandardOutPath = "${config.home.homeDirectory}/Library/Logs/agenix/stdout";
|
||||
StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/agenix/stderr";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
308
modules/age.nix
308
modules/age.nix
|
@ -1,38 +1,90 @@
|
|||
{ config, options, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
{
|
||||
config,
|
||||
options,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.age;
|
||||
|
||||
# we need at least rage 0.5.0 to support ssh keys
|
||||
rage =
|
||||
if lib.versionOlder pkgs.rage.version "0.5.0"
|
||||
then pkgs.callPackage ../pkgs/rage.nix { }
|
||||
else pkgs.rage;
|
||||
isDarwin = lib.attrsets.hasAttrByPath ["environment" "darwinConfig"] options;
|
||||
|
||||
ageBin = config.age.ageBin;
|
||||
|
||||
users = config.users.users;
|
||||
|
||||
identities = builtins.concatStringsSep " " (map (path: "-i ${path}") cfg.identityPaths);
|
||||
mountCommand =
|
||||
if isDarwin
|
||||
then ''
|
||||
if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then
|
||||
num_sectors=1048576
|
||||
dev=$(hdiutil attach -nomount ram://"$num_sectors" | sed 's/[[:space:]]*$//')
|
||||
newfs_hfs -v agenix "$dev"
|
||||
mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}"
|
||||
fi
|
||||
''
|
||||
else ''
|
||||
grep -q "${cfg.secretsMountPoint} ramfs" /proc/mounts ||
|
||||
mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0751
|
||||
'';
|
||||
newGeneration = ''
|
||||
_agenix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)"
|
||||
(( ++_agenix_generation ))
|
||||
echo "[agenix] creating new generation in ${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
mkdir -p "${cfg.secretsMountPoint}"
|
||||
chmod 0751 "${cfg.secretsMountPoint}"
|
||||
${mountCommand}
|
||||
mkdir -p "${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
'';
|
||||
|
||||
chownGroup =
|
||||
if isDarwin
|
||||
then "admin"
|
||||
else "keys";
|
||||
# chown the secrets mountpoint and the current generation to the keys group
|
||||
# instead of leaving it root:root.
|
||||
chownMountPoint = ''
|
||||
chown :${chownGroup} "${cfg.secretsMountPoint}" "${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
'';
|
||||
|
||||
setTruePath = secretType: ''
|
||||
${
|
||||
if secretType.symlink
|
||||
then ''
|
||||
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
|
||||
''
|
||||
else ''
|
||||
_truePath="${secretType.path}"
|
||||
''
|
||||
}
|
||||
'';
|
||||
|
||||
installSecret = secretType: ''
|
||||
${if secretType.symlink then ''
|
||||
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
|
||||
'' else ''
|
||||
_truePath="${secretType.path}"
|
||||
''}
|
||||
${setTruePath secretType}
|
||||
echo "decrypting '${secretType.file}' to '$_truePath'..."
|
||||
TMP_FILE="$_truePath.tmp"
|
||||
|
||||
IDENTITIES=()
|
||||
for identity in ${toString cfg.identityPaths}; do
|
||||
test -r "$identity" || continue
|
||||
test -s "$identity" || continue
|
||||
IDENTITIES+=(-i)
|
||||
IDENTITIES+=("$identity")
|
||||
done
|
||||
|
||||
test "''${#IDENTITIES[@]}" -eq 0 && echo "[agenix] WARNING: no readable identities found!"
|
||||
|
||||
mkdir -p "$(dirname "$_truePath")"
|
||||
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && mkdir -p "$(dirname "${secretType.path}")"
|
||||
(
|
||||
umask u=r,g=,o=
|
||||
test -f "${secretType.file}" || echo '[agenix] WARNING: encrypted file ${secretType.file} does not exist!'
|
||||
test -d "$(dirname "$TMP_FILE")" || echo "[agenix] WARNING: $(dirname "$TMP_FILE") does not exist!"
|
||||
LANG=${config.i18n.defaultLocale} ${ageBin} --decrypt ${identities} -o "$TMP_FILE" "${secretType.file}"
|
||||
LANG=${config.i18n.defaultLocale or "C"} ${ageBin} --decrypt "''${IDENTITIES[@]}" -o "$TMP_FILE" "${secretType.file}"
|
||||
)
|
||||
chmod ${secretType.mode} "$TMP_FILE"
|
||||
chown ${secretType.owner}:${secretType.group} "$TMP_FILE"
|
||||
mv -f "$TMP_FILE" "$_truePath"
|
||||
|
||||
${optionalString secretType.symlink ''
|
||||
|
@ -40,26 +92,51 @@ let
|
|||
''}
|
||||
'';
|
||||
|
||||
testIdentities = map (path: ''
|
||||
test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!'
|
||||
'') cfg.identityPaths;
|
||||
testIdentities =
|
||||
map
|
||||
(path: ''
|
||||
test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!'
|
||||
'')
|
||||
cfg.identityPaths;
|
||||
|
||||
isRootSecret = st: (st.owner == "root" || st.owner == "0") && (st.group == "root" || st.group == "0");
|
||||
isNotRootSecret = st: !(isRootSecret st);
|
||||
cleanupAndLink = ''
|
||||
_agenix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)"
|
||||
(( ++_agenix_generation ))
|
||||
echo "[agenix] symlinking new secrets to ${cfg.secretsDir} (generation $_agenix_generation)..."
|
||||
ln -sfn "${cfg.secretsMountPoint}/$_agenix_generation" ${cfg.secretsDir}
|
||||
|
||||
rootOwnedSecrets = builtins.filter isRootSecret (builtins.attrValues cfg.secrets);
|
||||
installRootOwnedSecrets = builtins.concatStringsSep "\n" ([ "echo '[agenix] decrypting root secrets...'" ] ++ testIdentities ++ (map installSecret rootOwnedSecrets));
|
||||
(( _agenix_generation > 1 )) && {
|
||||
echo "[agenix] removing old secrets (generation $(( _agenix_generation - 1 )))..."
|
||||
rm -rf "${cfg.secretsMountPoint}/$(( _agenix_generation - 1 ))"
|
||||
}
|
||||
'';
|
||||
|
||||
nonRootSecrets = builtins.filter isNotRootSecret (builtins.attrValues cfg.secrets);
|
||||
installNonRootSecrets = builtins.concatStringsSep "\n" ([ "echo '[agenix] decrypting non-root secrets...'" ] ++ (map installSecret nonRootSecrets));
|
||||
installSecrets = builtins.concatStringsSep "\n" (
|
||||
["echo '[agenix] decrypting secrets...'"]
|
||||
++ testIdentities
|
||||
++ (map installSecret (builtins.attrValues cfg.secrets))
|
||||
++ [cleanupAndLink]
|
||||
);
|
||||
|
||||
secretType = types.submodule ({ config, ... }: {
|
||||
chownSecret = secretType: ''
|
||||
${setTruePath secretType}
|
||||
chown ${secretType.owner}:${secretType.group} "$_truePath"
|
||||
'';
|
||||
|
||||
chownSecrets = builtins.concatStringsSep "\n" (
|
||||
["echo '[agenix] chowning...'"]
|
||||
++ [chownMountPoint]
|
||||
++ (map chownSecret (builtins.attrValues cfg.secrets))
|
||||
);
|
||||
|
||||
secretType = types.submodule ({config, ...}: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = config._module.args.name;
|
||||
defaultText = literalExpression "config._module.args.name";
|
||||
description = ''
|
||||
Name of the file used in ''${cfg.secretsDir}
|
||||
Name of the file used in {option}`age.secretsDir`
|
||||
'';
|
||||
};
|
||||
file = mkOption {
|
||||
|
@ -71,6 +148,9 @@ let
|
|||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "${cfg.secretsDir}/${config.name}";
|
||||
defaultText = literalExpression ''
|
||||
"''${cfg.secretsDir}/''${config.name}"
|
||||
'';
|
||||
description = ''
|
||||
Path where the decrypted secret is installed.
|
||||
'';
|
||||
|
@ -92,31 +172,35 @@ let
|
|||
group = mkOption {
|
||||
type = types.str;
|
||||
default = users.${config.owner}.group or "0";
|
||||
defaultText = literalExpression ''
|
||||
users.''${config.owner}.group or "0"
|
||||
'';
|
||||
description = ''
|
||||
Group of the decrypted secret.
|
||||
'';
|
||||
};
|
||||
symlink = mkEnableOption "symlinking secrets to their destination" // { default = true; };
|
||||
symlink = mkEnableOption "symlinking secrets to their destination" // {default = true;};
|
||||
};
|
||||
});
|
||||
in
|
||||
{
|
||||
|
||||
in {
|
||||
imports = [
|
||||
(mkRenamedOptionModule [ "age" "sshKeyPaths" ] [ "age" "identityPaths" ])
|
||||
(mkRenamedOptionModule ["age" "sshKeyPaths"] ["age" "identityPaths"])
|
||||
];
|
||||
|
||||
options.age = {
|
||||
ageBin = mkOption {
|
||||
type = types.str;
|
||||
default = "${rage}/bin/rage";
|
||||
default = "${pkgs.age}/bin/age";
|
||||
defaultText = literalExpression ''
|
||||
"''${pkgs.age}/bin/age"
|
||||
'';
|
||||
description = ''
|
||||
The age executable to use.
|
||||
'';
|
||||
};
|
||||
secrets = mkOption {
|
||||
type = types.attrsOf secretType;
|
||||
default = { };
|
||||
default = {};
|
||||
description = ''
|
||||
Attrset of secrets.
|
||||
'';
|
||||
|
@ -129,92 +213,108 @@ in
|
|||
'';
|
||||
};
|
||||
secretsMountPoint = mkOption {
|
||||
type = types.addCheck types.str
|
||||
type =
|
||||
types.addCheck types.str
|
||||
(s:
|
||||
(builtins.match "[ \t\n]*" s) == null # non-empty
|
||||
&& (builtins.match ".+/" s) == null) # without trailing slash
|
||||
// { description = "${types.str.description} (with check: non-empty without trailing slash)"; };
|
||||
(builtins.match "[ \t\n]*" s)
|
||||
== null # non-empty
|
||||
&& (builtins.match ".+/" s) == null) # without trailing slash
|
||||
// {description = "${types.str.description} (with check: non-empty without trailing slash)";};
|
||||
default = "/run/agenix.d";
|
||||
defaultText = "/run/agenix.d";
|
||||
description = ''
|
||||
Where secrets are created before they are symlinked to ''${cfg.secretsDir}
|
||||
Where secrets are created before they are symlinked to {option}`age.secretsDir`
|
||||
'';
|
||||
};
|
||||
identityPaths = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default =
|
||||
if config.services.openssh.enable then
|
||||
map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys)
|
||||
else [ ];
|
||||
if (config.services.openssh.enable or false)
|
||||
then map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys)
|
||||
else if isDarwin
|
||||
then [
|
||||
"/etc/ssh/ssh_host_ed25519_key"
|
||||
"/etc/ssh/ssh_host_rsa_key"
|
||||
]
|
||||
else [];
|
||||
defaultText = literalExpression ''
|
||||
if (config.services.openssh.enable or false)
|
||||
then map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys)
|
||||
else if isDarwin
|
||||
then [
|
||||
"/etc/ssh/ssh_host_ed25519_key"
|
||||
"/etc/ssh/ssh_host_rsa_key"
|
||||
]
|
||||
else [];
|
||||
'';
|
||||
description = ''
|
||||
Path to SSH keys to be used as identities in age decryption.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (cfg.secrets != { }) {
|
||||
assertions = [{
|
||||
assertion = cfg.identityPaths != [ ];
|
||||
message = "age.identityPaths must be set.";
|
||||
}];
|
||||
|
||||
# Create a new directory full of secrets for symlinking (this helps
|
||||
# ensure removed secrets are actually removed, or at least become
|
||||
# invalid symlinks).
|
||||
system.activationScripts.agenixMountSecrets = {
|
||||
text = ''
|
||||
_agenix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)"
|
||||
(( ++_agenix_generation ))
|
||||
echo "[agenix] symlinking new secrets to ${cfg.secretsDir} (generation $_agenix_generation)..."
|
||||
mkdir -p "${cfg.secretsMountPoint}"
|
||||
chmod 0751 "${cfg.secretsMountPoint}"
|
||||
grep -q "${cfg.secretsMountPoint} ramfs" /proc/mounts || mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0751
|
||||
mkdir -p "${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
ln -sfn "${cfg.secretsMountPoint}/$_agenix_generation" ${cfg.secretsDir}
|
||||
|
||||
(( _agenix_generation > 1 )) && {
|
||||
echo "[agenix] removing old secrets (generation $(( _agenix_generation - 1 )))..."
|
||||
rm -rf "${cfg.secretsMountPoint}/$(( _agenix_generation - 1 ))"
|
||||
config = mkIf (cfg.secrets != {}) (mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.identityPaths != [];
|
||||
message = "age.identityPaths must be set.";
|
||||
}
|
||||
'';
|
||||
deps = [
|
||||
"specialfs"
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
# Secrets with root owner and group can be installed before users
|
||||
# exist. This allows user password files to be encrypted.
|
||||
system.activationScripts.agenixRoot = {
|
||||
text = installRootOwnedSecrets;
|
||||
deps = [ "agenixMountSecrets" "specialfs" ];
|
||||
};
|
||||
system.activationScripts.users.deps = [ "agenixRoot" ];
|
||||
(optionalAttrs (!isDarwin) {
|
||||
# Create a new directory full of secrets for symlinking (this helps
|
||||
# ensure removed secrets are actually removed, or at least become
|
||||
# invalid symlinks).
|
||||
system.activationScripts.agenixNewGeneration = {
|
||||
text = newGeneration;
|
||||
deps = [
|
||||
"specialfs"
|
||||
];
|
||||
};
|
||||
|
||||
# chown the secrets mountpoint and the current generation to the keys group
|
||||
# instead of leaving it root:root.
|
||||
system.activationScripts.agenixChownKeys = {
|
||||
text = ''
|
||||
chown :keys "${cfg.secretsMountPoint}" "${cfg.secretsMountPoint}/$_agenix_generation"
|
||||
'';
|
||||
deps = [
|
||||
"users"
|
||||
"groups"
|
||||
"agenixMountSecrets"
|
||||
];
|
||||
};
|
||||
system.activationScripts.agenixInstall = {
|
||||
text = installSecrets;
|
||||
deps = [
|
||||
"agenixNewGeneration"
|
||||
"specialfs"
|
||||
];
|
||||
};
|
||||
|
||||
# Other secrets need to wait for users and groups to exist.
|
||||
system.activationScripts.agenix = {
|
||||
text = installNonRootSecrets;
|
||||
deps = [
|
||||
"users"
|
||||
"groups"
|
||||
"specialfs"
|
||||
"agenixMountSecrets"
|
||||
"agenixChownKeys"
|
||||
];
|
||||
};
|
||||
};
|
||||
# So user passwords can be encrypted.
|
||||
system.activationScripts.users.deps = ["agenixInstall"];
|
||||
|
||||
# Change ownership and group after users and groups are made.
|
||||
system.activationScripts.agenixChown = {
|
||||
text = chownSecrets;
|
||||
deps = [
|
||||
"users"
|
||||
"groups"
|
||||
];
|
||||
};
|
||||
|
||||
# So other activation scripts can depend on agenix being done.
|
||||
system.activationScripts.agenix = {
|
||||
text = "";
|
||||
deps = ["agenixChown"];
|
||||
};
|
||||
})
|
||||
(optionalAttrs isDarwin {
|
||||
launchd.daemons.activate-agenix = {
|
||||
script = ''
|
||||
set -e
|
||||
set -o pipefail
|
||||
export PATH="${pkgs.gnugrep}/bin:${pkgs.coreutils}/bin:@out@/sw/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
${newGeneration}
|
||||
${installSecrets}
|
||||
${chownSecrets}
|
||||
exit 0
|
||||
'';
|
||||
serviceConfig = {
|
||||
RunAtLoad = true;
|
||||
KeepAlive.SuccessfulExit = false;
|
||||
};
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
final: prev:
|
||||
{
|
||||
agenix = prev.callPackage ./pkgs/agenix.nix { };
|
||||
final: prev: {
|
||||
agenix = prev.callPackage ./pkgs/agenix.nix {};
|
||||
}
|
||||
|
|
228
pkgs/agenix.nix
228
pkgs/agenix.nix
|
@ -1,188 +1,62 @@
|
|||
{
|
||||
lib,
|
||||
writeShellScriptBin,
|
||||
runtimeShell,
|
||||
callPackage,
|
||||
rage,
|
||||
gnused,
|
||||
stdenv,
|
||||
age,
|
||||
jq,
|
||||
nix,
|
||||
mktemp,
|
||||
diffutils,
|
||||
ageBin ? "${
|
||||
# we need at least rage 0.5.0 to support ssh keys
|
||||
if rage.version < "0.5.0"
|
||||
then callPackage ./rage.nix {}
|
||||
else rage
|
||||
}/bin/rage"
|
||||
} :
|
||||
let
|
||||
sedBin = "${gnused}/bin/sed";
|
||||
nixInstantiate = "${nix}/bin/nix-instantiate";
|
||||
mktempBin = "${mktemp}/bin/mktemp";
|
||||
diffBin = "${diffutils}/bin/diff";
|
||||
substituteAll,
|
||||
ageBin ? "${age}/bin/age",
|
||||
shellcheck,
|
||||
}: let
|
||||
bin = "${placeholder "out"}/bin/agenix";
|
||||
in
|
||||
lib.recursiveUpdate (writeShellScriptBin "agenix" ''
|
||||
set -Eeuo pipefail
|
||||
stdenv.mkDerivation rec {
|
||||
pname = "agenix";
|
||||
version = "0.15.0";
|
||||
src = substituteAll {
|
||||
inherit ageBin version;
|
||||
jqBin = "${jq}/bin/jq";
|
||||
nixInstantiate = "${nix}/bin/nix-instantiate";
|
||||
mktempBin = "${mktemp}/bin/mktemp";
|
||||
diffBin = "${diffutils}/bin/diff";
|
||||
src = ./agenix.sh;
|
||||
};
|
||||
dontUnpack = true;
|
||||
doInstallCheck = true;
|
||||
installCheckInputs = [shellcheck];
|
||||
postInstallCheck = ''
|
||||
shellcheck ${bin}
|
||||
${bin} -h | grep ${version}
|
||||
|
||||
PACKAGE="agenix"
|
||||
HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||
function cleanup {
|
||||
rm -rf $HOME
|
||||
}
|
||||
trap "cleanup" 0 2 3 15
|
||||
|
||||
function show_help () {
|
||||
echo "$PACKAGE - edit and rekey age secret files"
|
||||
echo " "
|
||||
echo "$PACKAGE -e FILE [-i PRIVATE_KEY]"
|
||||
echo "$PACKAGE -r [-i PRIVATE_KEY]"
|
||||
echo ' '
|
||||
echo 'options:'
|
||||
echo '-h, --help show help'
|
||||
echo '-e, --edit FILE edits FILE using $EDITOR'
|
||||
echo '-r, --rekey re-encrypts all secrets with specified recipients'
|
||||
echo '-i, --identity identity to use when decrypting'
|
||||
echo '-v, --verbose verbose output'
|
||||
echo ' '
|
||||
echo 'FILE an age-encrypted file'
|
||||
echo ' '
|
||||
echo 'PRIVATE_KEY a path to a private SSH key used to decrypt file'
|
||||
echo ' '
|
||||
echo 'EDITOR environment variable of editor to use when editing FILE'
|
||||
echo ' '
|
||||
echo 'RULES environment variable with path to Nix file specifying recipient public keys.'
|
||||
echo "Defaults to './secrets.nix'"
|
||||
echo ' '
|
||||
echo "age binary path: ${ageBin}"
|
||||
echo "age version: $(${ageBin} --version)"
|
||||
}
|
||||
mkdir -p $HOME/.ssh
|
||||
cp -r "${../example}" $HOME/secrets
|
||||
chmod -R u+rw $HOME/secrets
|
||||
(
|
||||
umask u=rw,g=r,o=r
|
||||
cp ${../example_keys/user1.pub} $HOME/.ssh/id_ed25519.pub
|
||||
chown $UID $HOME/.ssh/id_ed25519.pub
|
||||
)
|
||||
(
|
||||
umask u=rw,g=,o=
|
||||
cp ${../example_keys/user1} $HOME/.ssh/id_ed25519
|
||||
chown $UID $HOME/.ssh/id_ed25519
|
||||
)
|
||||
|
||||
test $# -eq 0 && (show_help && exit 1)
|
||||
cd $HOME/secrets
|
||||
test $(${bin} -d secret1.age) = "hello"
|
||||
'';
|
||||
|
||||
REKEY=0
|
||||
DEFAULT_DECRYPT=(--decrypt)
|
||||
installPhase = ''
|
||||
install -D $src ${bin}
|
||||
'';
|
||||
|
||||
while test $# -gt 0; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-e|--edit)
|
||||
shift
|
||||
if test $# -gt 0; then
|
||||
export FILE=$1
|
||||
else
|
||||
echo "no FILE specified"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-i|--identity)
|
||||
shift
|
||||
if test $# -gt 0; then
|
||||
DEFAULT_DECRYPT+=(--identity "$1")
|
||||
else
|
||||
echo "no PRIVATE_KEY specified"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-r|--rekey)
|
||||
shift
|
||||
REKEY=1
|
||||
;;
|
||||
-v|--verbose)
|
||||
shift
|
||||
set -x
|
||||
;;
|
||||
*)
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
RULES=''${RULES:-./secrets.nix}
|
||||
|
||||
function cleanup {
|
||||
if [ ! -z ''${CLEARTEXT_DIR+x} ]
|
||||
then
|
||||
rm -rf "$CLEARTEXT_DIR"
|
||||
fi
|
||||
if [ ! -z ''${REENCRYPTED_DIR+x} ]
|
||||
then
|
||||
rm -rf "$REENCRYPTED_DIR"
|
||||
fi
|
||||
}
|
||||
trap "cleanup" 0 2 3 15
|
||||
|
||||
function edit {
|
||||
FILE=$1
|
||||
KEYS=$((${nixInstantiate} --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" rules.\"$FILE\".publicKeys)" | ${sedBin} 's/"//g' | ${sedBin} 's/\\n/\n/g') | ${sedBin} '/^$/d' || exit 1)
|
||||
|
||||
if [ -z "$KEYS" ]
|
||||
then
|
||||
>&2 echo "There is no rule for $FILE in $RULES."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CLEARTEXT_DIR=$(${mktempBin} -d)
|
||||
CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")"
|
||||
|
||||
if [ -f "$FILE" ]
|
||||
then
|
||||
DECRYPT=("''${DEFAULT_DECRYPT[@]}")
|
||||
if [ -f "$HOME/.ssh/id_rsa" ]; then
|
||||
DECRYPT+=(--identity "$HOME/.ssh/id_rsa")
|
||||
fi
|
||||
if [ -f "$HOME/.ssh/id_ed25519" ]; then
|
||||
DECRYPT+=(--identity "$HOME/.ssh/id_ed25519")
|
||||
fi
|
||||
if [[ "''${DECRYPT[*]}" != *"--identity"* ]]; then
|
||||
echo "No identity found to decrypt $FILE. Try adding an SSH key at $HOME/.ssh/id_rsa or $HOME/.ssh/id_ed25519 or using the --identity flag to specify a file."
|
||||
exit 1
|
||||
fi
|
||||
DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE")
|
||||
${ageBin} "''${DECRYPT[@]}" || exit 1
|
||||
cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before"
|
||||
fi
|
||||
|
||||
$EDITOR "$CLEARTEXT_FILE"
|
||||
|
||||
if [ ! -f "$CLEARTEXT_FILE" ]
|
||||
then
|
||||
echo "$FILE wasn't created."
|
||||
return
|
||||
fi
|
||||
[ -f "$FILE" ] && [ "$EDITOR" != ":" ] && ${diffBin} "$CLEARTEXT_FILE.before" "$CLEARTEXT_FILE" 1>/dev/null && echo "$FILE wasn't changed, skipping re-encryption." && return
|
||||
|
||||
ENCRYPT=()
|
||||
while IFS= read -r key
|
||||
do
|
||||
ENCRYPT+=(--recipient "$key")
|
||||
done <<< "$KEYS"
|
||||
|
||||
REENCRYPTED_DIR=$(${mktempBin} -d)
|
||||
REENCRYPTED_FILE="$REENCRYPTED_DIR/$(basename "$FILE")"
|
||||
|
||||
ENCRYPT+=(-o "$REENCRYPTED_FILE")
|
||||
|
||||
${ageBin} "''${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1
|
||||
|
||||
mv -f "$REENCRYPTED_FILE" "$1"
|
||||
}
|
||||
|
||||
function rekey {
|
||||
FILES=$((${nixInstantiate} --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" (builtins.attrNames rules))" | ${sedBin} 's/"//g' | ${sedBin} 's/\\n/\n/g') || exit 1)
|
||||
|
||||
for FILE in $FILES
|
||||
do
|
||||
echo "rekeying $FILE..."
|
||||
EDITOR=: edit "$FILE"
|
||||
cleanup
|
||||
done
|
||||
}
|
||||
|
||||
[ $REKEY -eq 1 ] && rekey && exit 0
|
||||
edit "$FILE" && cleanup && exit 0
|
||||
'')
|
||||
|
||||
{
|
||||
meta.description = "age-encrypted secrets for NixOS";
|
||||
}
|
||||
meta.description = "age-encrypted secrets for NixOS";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
PACKAGE="agenix"
|
||||
|
||||
function show_help () {
|
||||
echo "$PACKAGE - edit and rekey age secret files"
|
||||
echo " "
|
||||
echo "$PACKAGE -e FILE [-i PRIVATE_KEY]"
|
||||
echo "$PACKAGE -r [-i PRIVATE_KEY]"
|
||||
echo ' '
|
||||
echo 'options:'
|
||||
echo '-h, --help show help'
|
||||
# shellcheck disable=SC2016
|
||||
echo '-e, --edit FILE edits FILE using $EDITOR'
|
||||
echo '-r, --rekey re-encrypts all secrets with specified recipients'
|
||||
echo '-d, --decrypt FILE decrypts FILE to STDOUT'
|
||||
echo '-i, --identity identity to use when decrypting'
|
||||
echo '-v, --verbose verbose output'
|
||||
echo ' '
|
||||
echo 'FILE an age-encrypted file'
|
||||
echo ' '
|
||||
echo 'PRIVATE_KEY a path to a private SSH key used to decrypt file'
|
||||
echo ' '
|
||||
echo 'EDITOR environment variable of editor to use when editing FILE'
|
||||
echo ' '
|
||||
echo 'If STDIN is not interactive, EDITOR will be set to "cp /dev/stdin"'
|
||||
echo ' '
|
||||
echo 'RULES environment variable with path to Nix file specifying recipient public keys.'
|
||||
echo "Defaults to './secrets.nix'"
|
||||
echo ' '
|
||||
echo "agenix version: @version@"
|
||||
echo "age binary path: @ageBin@"
|
||||
echo "age version: $(@ageBin@ --version)"
|
||||
}
|
||||
|
||||
function warn() {
|
||||
printf '%s\n' "$*" >&2
|
||||
}
|
||||
|
||||
function err() {
|
||||
warn "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
test $# -eq 0 && (show_help && exit 1)
|
||||
|
||||
REKEY=0
|
||||
DECRYPT_ONLY=0
|
||||
DEFAULT_DECRYPT=(--decrypt)
|
||||
|
||||
while test $# -gt 0; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-e|--edit)
|
||||
shift
|
||||
if test $# -gt 0; then
|
||||
export FILE=$1
|
||||
else
|
||||
echo "no FILE specified"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-i|--identity)
|
||||
shift
|
||||
if test $# -gt 0; then
|
||||
DEFAULT_DECRYPT+=(--identity "$1")
|
||||
else
|
||||
echo "no PRIVATE_KEY specified"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-r|--rekey)
|
||||
shift
|
||||
REKEY=1
|
||||
;;
|
||||
-d|--decrypt)
|
||||
shift
|
||||
DECRYPT_ONLY=1
|
||||
if test $# -gt 0; then
|
||||
export FILE=$1
|
||||
else
|
||||
echo "no FILE specified"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-v|--verbose)
|
||||
shift
|
||||
set -x
|
||||
;;
|
||||
*)
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
RULES=${RULES:-./secrets.nix}
|
||||
function cleanup {
|
||||
if [ -n "${CLEARTEXT_DIR+x}" ]
|
||||
then
|
||||
rm -rf "$CLEARTEXT_DIR"
|
||||
fi
|
||||
if [ -n "${REENCRYPTED_DIR+x}" ]
|
||||
then
|
||||
rm -rf "$REENCRYPTED_DIR"
|
||||
fi
|
||||
}
|
||||
trap "cleanup" 0 2 3 15
|
||||
|
||||
function keys {
|
||||
(@nixInstantiate@ --json --eval --strict -E "(let rules = import $RULES; in rules.\"$1\".publicKeys)" | @jqBin@ -r .[]) || exit 1
|
||||
}
|
||||
|
||||
function decrypt {
|
||||
FILE=$1
|
||||
KEYS=$2
|
||||
if [ -z "$KEYS" ]
|
||||
then
|
||||
err "There is no rule for $FILE in $RULES."
|
||||
fi
|
||||
|
||||
if [ -f "$FILE" ]
|
||||
then
|
||||
DECRYPT=("${DEFAULT_DECRYPT[@]}")
|
||||
if [[ "${DECRYPT[*]}" != *"--identity"* ]]; then
|
||||
if [ -f "$HOME/.ssh/id_rsa" ]; then
|
||||
DECRYPT+=(--identity "$HOME/.ssh/id_rsa")
|
||||
fi
|
||||
if [ -f "$HOME/.ssh/id_ed25519" ]; then
|
||||
DECRYPT+=(--identity "$HOME/.ssh/id_ed25519")
|
||||
fi
|
||||
fi
|
||||
if [[ "${DECRYPT[*]}" != *"--identity"* ]]; then
|
||||
err "No identity found to decrypt $FILE. Try adding an SSH key at $HOME/.ssh/id_rsa or $HOME/.ssh/id_ed25519 or using the --identity flag to specify a file."
|
||||
fi
|
||||
|
||||
@ageBin@ "${DECRYPT[@]}" "$FILE" || exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function edit {
|
||||
FILE=$1
|
||||
KEYS=$(keys "$FILE") || exit 1
|
||||
|
||||
CLEARTEXT_DIR=$(@mktempBin@ -d)
|
||||
CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")"
|
||||
DEFAULT_DECRYPT+=(-o "$CLEARTEXT_FILE")
|
||||
|
||||
decrypt "$FILE" "$KEYS" || exit 1
|
||||
|
||||
[ ! -f "$CLEARTEXT_FILE" ] || cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before"
|
||||
|
||||
[ -t 0 ] || EDITOR='cp /dev/stdin'
|
||||
|
||||
$EDITOR "$CLEARTEXT_FILE"
|
||||
|
||||
if [ ! -f "$CLEARTEXT_FILE" ]
|
||||
then
|
||||
warn "$FILE wasn't created."
|
||||
return
|
||||
fi
|
||||
[ -f "$FILE" ] && [ "$EDITOR" != ":" ] && @diffBin@ -q "$CLEARTEXT_FILE.before" "$CLEARTEXT_FILE" && warn "$FILE wasn't changed, skipping re-encryption." && return
|
||||
|
||||
ENCRYPT=()
|
||||
while IFS= read -r key
|
||||
do
|
||||
if [ -n "$key" ]; then
|
||||
ENCRYPT+=(--recipient "$key")
|
||||
fi
|
||||
done <<< "$KEYS"
|
||||
|
||||
REENCRYPTED_DIR=$(@mktempBin@ -d)
|
||||
REENCRYPTED_FILE="$REENCRYPTED_DIR/$(basename "$FILE")"
|
||||
|
||||
ENCRYPT+=(-o "$REENCRYPTED_FILE")
|
||||
|
||||
@ageBin@ "${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1
|
||||
|
||||
mkdir -p "$(dirname "$FILE")"
|
||||
|
||||
mv -f "$REENCRYPTED_FILE" "$FILE"
|
||||
}
|
||||
|
||||
function rekey {
|
||||
FILES=$( (@nixInstantiate@ --json --eval -E "(let rules = import $RULES; in builtins.attrNames rules)" | @jqBin@ -r .[]) || exit 1)
|
||||
|
||||
for FILE in $FILES
|
||||
do
|
||||
warn "rekeying $FILE..."
|
||||
EDITOR=: edit "$FILE"
|
||||
cleanup
|
||||
done
|
||||
}
|
||||
|
||||
[ $REKEY -eq 1 ] && rekey && exit 0
|
||||
[ $DECRYPT_ONLY -eq 1 ] && DEFAULT_DECRYPT+=("-o" "-") && decrypt "${FILE}" "$(keys "$FILE")" && exit 0
|
||||
edit "$FILE" && cleanup && exit 0
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
stdenvNoCC,
|
||||
mmdoc,
|
||||
self,
|
||||
}:
|
||||
stdenvNoCC.mkDerivation rec {
|
||||
name = "agenix-doc";
|
||||
src = ../doc;
|
||||
phases = ["mmdocPhase"];
|
||||
mmdocPhase = "${mmdoc}/bin/mmdoc agenix $src $out";
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
{ stdenv, rustPlatform, fetchFromGitHub, installShellFiles, darwin }:
|
||||
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "rage";
|
||||
version = "0.5.0";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "str4d";
|
||||
repo = pname;
|
||||
rev = "v${version}";
|
||||
sha256 = "sha256-XSDfAsXfwSoe5JMdJtZlC324Sra+4fVJhE3/k2TthEc=";
|
||||
};
|
||||
|
||||
cargoSha256 = "sha256-GPr5zxeODAjD+ynp/nned9gZUiReYcdzosuEbLIKZSs=";
|
||||
|
||||
nativeBuildInputs = [ installShellFiles ];
|
||||
|
||||
buildInputs = with darwin.apple_sdk.frameworks; stdenv.lib.optionals stdenv.isDarwin [
|
||||
Security
|
||||
Foundation
|
||||
];
|
||||
|
||||
# cargo test has an x86-only dependency
|
||||
doCheck = stdenv.hostPlatform.isx86;
|
||||
|
||||
postBuild = ''
|
||||
cargo run --example generate-docs
|
||||
cargo run --example generate-completions
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
installManPage target/manpages/*
|
||||
installShellCompletion target/completions/*.{bash,fish,zsh}
|
||||
'';
|
||||
|
||||
meta = with stdenv.lib; {
|
||||
description = "A simple, secure and modern encryption tool with small explicit keys, no config options, and UNIX-style composability";
|
||||
homepage = "https://github.com/str4d/rage";
|
||||
changelog = "https://github.com/str4d/rage/releases/tag/v${version}";
|
||||
license = with licenses; [ asl20 mit ]; # either at your option
|
||||
maintainers = with maintainers; [ marsam ryantm ];
|
||||
};
|
||||
}
|
|
@ -1,15 +1,28 @@
|
|||
# Do not copy this! It is insecure. This is only okay because we are testing.
|
||||
{
|
||||
system.activationScripts.agenixRoot.deps = [ "installSSHHostKeys" ];
|
||||
{config, ...}: {
|
||||
system.activationScripts.agenixInstall.deps = ["installSSHHostKeys"];
|
||||
|
||||
system.activationScripts.installSSHHostKeys.text = ''
|
||||
mkdir -p /etc/ssh
|
||||
(umask u=rw,g=r,o=r; cp ${../example_keys/system1.pub} /etc/ssh/ssh_host_ed25519_key.pub)
|
||||
USER1_UID="${toString config.users.users.user1.uid}"
|
||||
USERS_GID="${toString config.users.groups.users.gid}"
|
||||
|
||||
mkdir -p /etc/ssh /home/user1/.ssh
|
||||
chown $USER1_UID:$USERS_GID /home/user1/.ssh
|
||||
(
|
||||
umask u=rw,g=r,o=r
|
||||
cp ${../example_keys/system1.pub} /etc/ssh/ssh_host_ed25519_key.pub
|
||||
cp ${../example_keys/user1.pub} /home/user1/.ssh/id_ed25519.pub
|
||||
chown $USER1_UID:$USERS_GID /home/user1/.ssh/id_ed25519.pub
|
||||
)
|
||||
(
|
||||
umask u=rw,g=,o=
|
||||
cp ${../example_keys/system1} /etc/ssh/ssh_host_ed25519_key
|
||||
cp ${../example_keys/user1} /home/user1/.ssh/id_ed25519
|
||||
chown $USER1_UID:$USERS_GID /home/user1/.ssh/id_ed25519
|
||||
touch /etc/ssh/ssh_host_rsa_key
|
||||
)
|
||||
|
||||
cp -r "${../example}" /tmp/secrets
|
||||
chmod -R u+rw /tmp/secrets
|
||||
chown -R $USER1_UID:$USERS_GID /tmp/secrets
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Do not copy this! It is insecure. This is only okay because we are testing.
|
||||
{
|
||||
system.activationScripts.extraUserActivation.text = ''
|
||||
echo "Installing system SSH host key"
|
||||
sudo cp ${../example_keys/system1.pub} /etc/ssh/ssh_host_ed25519_key.pub
|
||||
sudo cp ${../example_keys/system1} /etc/ssh/ssh_host_ed25519_key
|
||||
sudo chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
|
||||
sudo chmod 600 /etc/ssh/ssh_host_ed25519_key
|
||||
|
||||
echo "Installing user SSH host key"
|
||||
mkdir -p $HOME/.ssh
|
||||
cp ${../example_keys/user1.pub} $HOME/.ssh/id_ed25519.pub
|
||||
cp ${../example_keys/user1} $HOME/.ssh/id_ed25519
|
||||
chmod 644 $HOME/.ssh/id_ed25519.pub
|
||||
chmod 600 $HOME/.ssh/id_ed25519
|
||||
'';
|
||||
}
|
|
@ -1,17 +1,25 @@
|
|||
{
|
||||
nixpkgs ? <nixpkgs>,
|
||||
pkgs ? import <nixpkgs> { inherit system; config = {}; },
|
||||
system ? builtins.currentSystem
|
||||
} @args:
|
||||
|
||||
import "${nixpkgs}/nixos/tests/make-test-python.nix" ({ pkgs, ...}: {
|
||||
nixpkgs ? <nixpkgs>,
|
||||
pkgs ?
|
||||
import <nixpkgs> {
|
||||
inherit system;
|
||||
config = {};
|
||||
},
|
||||
system ? builtins.currentSystem,
|
||||
home-manager ? <home-manager>,
|
||||
}:
|
||||
pkgs.nixosTest {
|
||||
name = "agenix-integration";
|
||||
|
||||
nodes.system1 = { config, lib, ... }: {
|
||||
|
||||
nodes.system1 = {
|
||||
config,
|
||||
pkgs,
|
||||
options,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
../modules/age.nix
|
||||
./install_ssh_host_keys.nix
|
||||
"${home-manager}/nixos"
|
||||
];
|
||||
|
||||
services.openssh.enable = true;
|
||||
|
@ -20,6 +28,12 @@ import "${nixpkgs}/nixos/tests/make-test-python.nix" ({ pkgs, ...}: {
|
|||
file = ../example/passwordfile-user1.age;
|
||||
};
|
||||
|
||||
age.identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
|
||||
|
||||
environment.systemPackages = [
|
||||
(pkgs.callPackage ../pkgs/agenix.nix {})
|
||||
];
|
||||
|
||||
users = {
|
||||
mutableUsers = false;
|
||||
|
||||
|
@ -27,16 +41,36 @@ import "${nixpkgs}/nixos/tests/make-test-python.nix" ({ pkgs, ...}: {
|
|||
user1 = {
|
||||
isNormalUser = true;
|
||||
passwordFile = config.age.secrets.passwordfile-user1.path;
|
||||
uid = 1000;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.user1 = {options, ...}: {
|
||||
imports = [
|
||||
../modules/age-home.nix
|
||||
];
|
||||
|
||||
home.stateVersion = pkgs.lib.trivial.release;
|
||||
|
||||
age = {
|
||||
identityPaths = options.age.identityPaths.default ++ ["/home/user1/.ssh/this_key_wont_exist"];
|
||||
secrets.secret2 = {
|
||||
# Only decryptable by user1's key
|
||||
file = ../example/secret2.age;
|
||||
};
|
||||
secrets.secret2Path = {
|
||||
file = ../example/secret2.age;
|
||||
path = "/home/user1/secret2";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
let
|
||||
testScript = let
|
||||
user = "user1";
|
||||
password = "password1234";
|
||||
secret2 = "world!";
|
||||
in ''
|
||||
system1.wait_for_unit("multi-user.target")
|
||||
system1.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
|
||||
|
@ -45,14 +79,48 @@ import "${nixpkgs}/nixos/tests/make-test-python.nix" ({ pkgs, ...}: {
|
|||
system1.wait_until_succeeds("[ $(fgconsole) = 2 ]")
|
||||
system1.wait_for_unit("getty@tty2.service")
|
||||
system1.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
|
||||
system1.wait_until_tty_matches(2, "login: ")
|
||||
system1.wait_until_tty_matches("2", "login: ")
|
||||
system1.send_chars("${user}\n")
|
||||
system1.wait_until_tty_matches(2, "login: ${user}")
|
||||
system1.wait_until_tty_matches("2", "login: ${user}")
|
||||
system1.wait_until_succeeds("pgrep login")
|
||||
system1.sleep(2)
|
||||
system1.send_chars("${password}\n")
|
||||
system1.send_chars("whoami > /tmp/1\n")
|
||||
system1.wait_for_file("/tmp/1")
|
||||
assert "${user}" in system1.succeed("cat /tmp/1")
|
||||
system1.send_chars("cat /run/user/$(id -u)/agenix/secret2 > /tmp/2\n")
|
||||
system1.wait_for_file("/tmp/2")
|
||||
assert "${secret2}" in system1.succeed("cat /tmp/2")
|
||||
|
||||
userDo = lambda input : f"sudo -u user1 -- bash -c 'set -eou pipefail; cd /tmp/secrets; {input}'"
|
||||
|
||||
before_hash = system1.succeed(userDo('sha256sum passwordfile-user1.age')).split()
|
||||
print(system1.succeed(userDo('agenix -r -i /home/user1/.ssh/id_ed25519')))
|
||||
after_hash = system1.succeed(userDo('sha256sum passwordfile-user1.age')).split()
|
||||
|
||||
# Ensure we actually have hashes
|
||||
for h in [before_hash, after_hash]:
|
||||
assert len(h) == 2, "hash should be [hash, filename]"
|
||||
assert h[1] == "passwordfile-user1.age", "filename is incorrect"
|
||||
assert len(h[0].strip()) == 64, "hash length is incorrect"
|
||||
assert before_hash[0] != after_hash[0], "hash did not change with rekeying"
|
||||
|
||||
# user1 can edit passwordfile-user1.age
|
||||
system1.succeed(userDo("EDITOR=cat agenix -e passwordfile-user1.age"))
|
||||
|
||||
# user1 can edit even if bogus id_rsa present
|
||||
system1.succeed(userDo("echo bogus > ~/.ssh/id_rsa"))
|
||||
system1.fail(userDo("EDITOR=cat agenix -e passwordfile-user1.age"))
|
||||
system1.succeed(userDo("EDITOR=cat agenix -e passwordfile-user1.age -i /home/user1/.ssh/id_ed25519"))
|
||||
system1.succeed(userDo("rm ~/.ssh/id_rsa"))
|
||||
|
||||
# user1 can edit a secret by piping in contents
|
||||
system1.succeed(userDo("echo 'secret1234' | agenix -e passwordfile-user1.age"))
|
||||
|
||||
# and get it back out via --decrypt
|
||||
assert "secret1234" in system1.succeed(userDo("agenix -d passwordfile-user1.age"))
|
||||
|
||||
# finally, the plain text should not linger around anywhere in the filesystem.
|
||||
system1.fail("grep -r secret1234 /tmp")
|
||||
'';
|
||||
}) args
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
config,
|
||||
pkgs,
|
||||
options,
|
||||
...
|
||||
}: let
|
||||
secret = "hello";
|
||||
testScript = pkgs.writeShellApplication {
|
||||
name = "agenix-integration";
|
||||
text = ''
|
||||
grep "${secret}" "${config.age.secrets.system-secret.path}"
|
||||
'';
|
||||
};
|
||||
in {
|
||||
imports = [
|
||||
./install_ssh_host_keys_darwin.nix
|
||||
../modules/age.nix
|
||||
];
|
||||
|
||||
services.nix-daemon.enable = true;
|
||||
|
||||
age = {
|
||||
identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
|
||||
secrets.system-secret.file = ../example/secret1.age;
|
||||
};
|
||||
|
||||
environment.systemPackages = [testScript];
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
pkgs,
|
||||
config,
|
||||
options,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
imports = [../modules/age-home.nix];
|
||||
|
||||
age = {
|
||||
identityPaths = options.age.identityPaths.default ++ ["/Users/user1/.ssh/this_key_wont_exist"];
|
||||
secrets.user-secret.file = ../example/secret2.age;
|
||||
};
|
||||
|
||||
home = rec {
|
||||
username = "runner";
|
||||
homeDirectory = lib.mkForce "/Users/${username}";
|
||||
stateVersion = lib.trivial.release;
|
||||
};
|
||||
|
||||
home.file = let
|
||||
name = "agenix-home-integration";
|
||||
in {
|
||||
${name}.source = pkgs.writeShellApplication {
|
||||
inherit name;
|
||||
text = let
|
||||
secret = "world!";
|
||||
in ''
|
||||
diff -q "${config.age.secrets.user-secret.path}" <(printf '${secret}\n')
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue