Compare commits
1 commit
main
...
python-tes
Author | SHA1 | Date | |
---|---|---|---|
|
9d8f0f5e5c |
37 changed files with 345 additions and 1395 deletions
35
.github/release-drafter.yml
vendored
35
.github/release-drafter.yml
vendored
|
@ -1,35 +0,0 @@
|
|||
|
||||
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
|
41
.github/workflows/ci.yaml
vendored
41
.github/workflows/ci.yaml
vendored
|
@ -7,45 +7,24 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: cachix/install-nix-action@v18
|
||||
with:
|
||||
extra_nix_config: |
|
||||
system-features = nixos-test recursive-nix benchmark big-parallel kvm
|
||||
extra-experimental-features = recursive-nix nix-command flakes
|
||||
extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm"
|
||||
- run: nix build
|
||||
- run: nix build .#doc
|
||||
- run: nix fmt . -- --check
|
||||
- run: nix flake check
|
||||
tests-darwin:
|
||||
runs-on: macos-12
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v24
|
||||
- uses: cachix/install-nix-action@v18
|
||||
with:
|
||||
extra_nix_config: |
|
||||
system-features = nixos-test recursive-nix benchmark big-parallel kvm
|
||||
extra-experimental-features = recursive-nix nix-command flakes
|
||||
extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm"
|
||||
- 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
|
||||
- run: |
|
||||
system=$(nix build --no-link --print-out-paths .#checks.x86_64-darwin.integration)
|
||||
${system}/activate-user
|
||||
sudo ${system}/activate
|
||||
- run: sudo /run/current-system/sw/bin/agenix-integration
|
||||
|
|
41
.github/workflows/doc.yml
vendored
41
.github/workflows/doc.yml
vendored
|
@ -1,41 +0,0 @@
|
|||
# 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
|
27
.github/workflows/flakehub-publish-tagged.yml
vendored
27
.github/workflows/flakehub-publish-tagged.yml
vendored
|
@ -1,27 +0,0 @@
|
|||
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 }}"
|
33
.github/workflows/release-drafter.yml
vendored
33
.github/workflows/release-drafter.yml
vendored
|
@ -1,33 +0,0 @@
|
|||
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 }}
|
118
README.md
118
README.md
|
@ -1,14 +1,6 @@
|
|||
# agenix - [age](https://github.com/FiloSottile/age)-encrypted secrets for NixOS
|
||||
|
||||
`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.
|
||||
`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.
|
||||
|
||||
## Contents
|
||||
|
||||
|
@ -45,7 +37,7 @@ All files in the Nix store are readable by any system user, so it is not a suita
|
|||
|
||||
## 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.
|
||||
* 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.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -180,8 +172,6 @@ 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
|
||||
|
@ -199,14 +189,13 @@ To install the `agenix` binary:
|
|||
|
||||
#### Install CLI via Flakes
|
||||
|
||||
You can run the CLI tool ad-hoc without installing it:
|
||||
You don't need to install it,
|
||||
|
||||
```ShellSession
|
||||
nix run github:ryantm/agenix -- --help
|
||||
```
|
||||
|
||||
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):
|
||||
but, if you want to (change the system based on your system):
|
||||
|
||||
```nix
|
||||
{
|
||||
|
@ -214,28 +203,6 @@ But you can also add it permanently into a [NixOS module](https://wiki.nixos.org
|
|||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
@ -244,15 +211,14 @@ e.g. inside your `flake.nix` file:
|
|||
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:
|
||||
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
|
||||
```
|
||||
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:
|
||||
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";
|
||||
|
@ -268,32 +234,17 @@ e.g. inside your `flake.nix` file:
|
|||
"secret2.age".publicKeys = users ++ systems;
|
||||
}
|
||||
```
|
||||
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:
|
||||
4. Edit secret files (these instructions assume your SSH private key is in ~/.ssh/):
|
||||
```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;
|
||||
}
|
||||
```
|
||||
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.
|
||||
|
||||
6. Reference the secrets' mount path in your config:
|
||||
6. Use the secret in your config:
|
||||
```nix
|
||||
{
|
||||
users.users.user1 = {
|
||||
|
@ -302,22 +253,9 @@ e.g. inside your `flake.nix` file:
|
|||
};
|
||||
}
|
||||
```
|
||||
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.
|
||||
7. NixOS rebuild or use your deployment tool like usual.
|
||||
|
||||
The `secret1.age` file will be copied over to the target machine like any other Nix package.
|
||||
Then it will be decrypted and mounted as described before.
|
||||
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
|
||||
```
|
||||
The secret will be decrypted to the value of `config.age.secrets.secret1.path` (`/run/agenix/secret1` by default).
|
||||
|
||||
## Reference
|
||||
|
||||
|
@ -445,7 +383,7 @@ Example:
|
|||
#### `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
|
||||
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
|
||||
|
@ -487,7 +425,7 @@ Example of a secret with a name different from its attrpath:
|
|||
#### `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`.
|
||||
don't need to change this. Defaults to `rage/bin/rage`.
|
||||
|
||||
Overriding `age.ageBin` example:
|
||||
|
||||
|
@ -499,14 +437,13 @@ Overriding `age.ageBin` example:
|
|||
|
||||
#### `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:
|
||||
`age.identityPaths` is a list of paths to recipient keys to try to use
|
||||
to decrypt the secrets. All of the file paths must be present, but
|
||||
only one needs to be able to decrypt the secret. Usually, you don't
|
||||
need to change this. By default, this is the `rsa` and `ed25519` keys
|
||||
in `config.services.openssh.hostKeys`.
|
||||
|
||||
Overriding `age.identityPaths` example:
|
||||
|
||||
```nix
|
||||
{
|
||||
|
@ -517,7 +454,7 @@ secret in question. Overriding `age.identityPaths` example:
|
|||
#### `age.secretsDir`
|
||||
|
||||
`age.secretsDir` is the directory where secrets are symlinked to by
|
||||
default. Usually, you don't need to change this. Defaults to
|
||||
default.Usually, you don't need to change this. Defaults to
|
||||
`/run/agenix`.
|
||||
|
||||
Overriding `age.secretsDir` example:
|
||||
|
@ -546,8 +483,6 @@ Overriding `age.secretsMountPoint` example:
|
|||
### agenix CLI reference
|
||||
|
||||
```
|
||||
agenix - edit and rekey age secret files
|
||||
|
||||
agenix -e FILE [-i PRIVATE_KEY]
|
||||
agenix -r [-i PRIVATE_KEY]
|
||||
|
||||
|
@ -555,7 +490,6 @@ 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
|
||||
|
||||
|
@ -565,8 +499,6 @@ 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'
|
||||
```
|
||||
|
@ -587,13 +519,13 @@ improved upon by reading the identities from the age file.)
|
|||
|
||||
#### Overriding age binary
|
||||
|
||||
The agenix CLI uses `age` by default as its age implemenation, you
|
||||
can use the `rage` implementation with Flakes like this:
|
||||
The agenix CLI uses `rage` by default as its age implemenation, you
|
||||
can use the reference implementation `age` with Flakes like this:
|
||||
|
||||
```nix
|
||||
{pkgs,agenix,...}:{
|
||||
environment.systemPackages = [
|
||||
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
|
||||
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.age}/bin/age"; })
|
||||
];
|
||||
}
|
||||
```
|
||||
|
@ -618,8 +550,6 @@ 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`.
|
||||
|
||||
Additionally you should only encrypt secrets that you are able to make useless in the event that they are decrypted in the future and be ready to rotate them periodically as [age](https://github.com/FiloSottile/age) is [as of 19th June 2024 NOT Post-Quantum Safe](https://github.com/FiloSottile/age/discussions/231#discussioncomment-3092773) and so in case the threat actor can access your encrypted keys e.g. via their use in a public repository then they can utilize the strategy of [Harvest Now, Decrypt Later](https://en.wikipedia.org/wiki/Harvest_now,_decrypt_later) to store your keys now for later decryption including the case where a major vulnerability is found that would expose the secrets. See https://github.com/FiloSottile/age/issues/578 for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
* The main branch is protected against direct pushes
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# 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.
|
|
@ -1,4 +0,0 @@
|
|||
# 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).
|
|
@ -1,28 +0,0 @@
|
|||
# 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.
|
|
@ -1,8 +0,0 @@
|
|||
# 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
|
|
@ -1,38 +0,0 @@
|
|||
# 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" {}) ];
|
||||
}
|
||||
```
|
|
@ -1,39 +0,0 @@
|
|||
# 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 ];
|
||||
}
|
||||
```
|
|
@ -1,27 +0,0 @@
|
|||
# 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" {}) ];
|
||||
}
|
||||
```
|
|
@ -1,28 +0,0 @@
|
|||
# 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> {}) ];
|
||||
}
|
||||
```
|
|
@ -1,3 +0,0 @@
|
|||
# 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.
|
|
@ -1,3 +0,0 @@
|
|||
# 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.
|
|
@ -1,12 +0,0 @@
|
|||
# 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"; })
|
||||
];
|
||||
}
|
||||
```
|
|
@ -1,5 +0,0 @@
|
|||
# Problem and solution {#problem-and-solution}
|
||||
|
||||
All files in the Nix store are readable by any system user, so it is not a suitable place for including cleartext secrets. Many existing tools (like NixOps deployment.keys) deploy secrets separately from `nixos-rebuild`, making deployment, caching, and auditing more difficult. Out-of-band secret management is also less reproducible.
|
||||
|
||||
`agenix` solves these issues by using your pre-existing SSH key infrastructure and `age` to encrypt secrets into the Nix store. Secrets are decrypted using an SSH host private key during NixOS system activation.
|
250
doc/reference.md
250
doc/reference.md
|
@ -1,250 +0,0 @@
|
|||
# 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'
|
|
@ -1,13 +0,0 @@
|
|||
# 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.)
|
|
@ -1,14 +0,0 @@
|
|||
# Threat model/Warnings {#threat-model-warnings}
|
||||
|
||||
This project has not been audited by a security professional.
|
||||
|
||||
People unfamiliar with `age` might be surprised that secrets are not
|
||||
authenticated. This means that every attacker that has write access to
|
||||
the secret files can modify secrets because public keys are exposed.
|
||||
This seems like not a problem on the first glance because changing the
|
||||
configuration itself could expose secrets easily. However, reviewing
|
||||
configuration changes is easier than reviewing random secrets (for
|
||||
example, 4096-bit rsa keys). This would be solved by having a message
|
||||
authentication code (MAC) like other implementations like GPG or
|
||||
[sops](https://github.com/Mic92/sops-nix) have, however this was left
|
||||
out for simplicity in `age`.
|
18
doc/toc.md
18
doc/toc.md
|
@ -1,18 +0,0 @@
|
|||
# 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)
|
|
@ -1,51 +0,0 @@
|
|||
# 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).
|
51
flake.lock
51
flake.lock
|
@ -7,11 +7,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1700795494,
|
||||
"narHash": "sha256-gzGLZSiOhf155FW7262kdHo2YDeugp3VuIFb4/GGng0=",
|
||||
"lastModified": 1673295039,
|
||||
"narHash": "sha256-AsdYgE8/GPwcelGgrntlijMg4t3hLFJFCRF3tL5WVjA=",
|
||||
"owner": "lnl7",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "4b9b83d5a92e8c1fbfd8eb27eda375908c11ec4d",
|
||||
"rev": "87b9d090ad39b25b2400029c64825fc2a8868943",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -21,33 +21,13 @@
|
|||
"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=",
|
||||
"lastModified": 1674641431,
|
||||
"narHash": "sha256-qfo19qVZBP4qn5M5gXc/h1MDgAtPA5VxJm9s8RUAkVk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "54aac082a4d9bb5bbc5c4e899603abfb76a3f6d6",
|
||||
"rev": "9b97ad7b4330aacda9b2343396eb3df8a853b4fc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -60,24 +40,7 @@
|
|||
"root": {
|
||||
"inputs": {
|
||||
"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"
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
93
flake.nix
93
flake.nix
|
@ -7,83 +7,60 @@
|
|||
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);
|
||||
agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {};
|
||||
in {
|
||||
nixosModules.age = ./modules/age.nix;
|
||||
nixosModules.age = import ./modules/age.nix;
|
||||
nixosModules.default = self.nixosModules.age;
|
||||
|
||||
darwinModules.age = ./modules/age.nix;
|
||||
darwinModules.age = import ./modules/age.nix;
|
||||
darwinModules.default = self.darwinModules.age;
|
||||
|
||||
homeManagerModules.age = ./modules/age-home.nix;
|
||||
homeManagerModules.default = self.homeManagerModules.age;
|
||||
|
||||
overlays.default = import ./overlay.nix;
|
||||
|
||||
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
|
||||
formatter.x86_64-darwin = nixpkgs.legacyPackages.x86_64-darwin.alejandra;
|
||||
packages.x86_64-darwin.agenix = agenix "x86_64-darwin";
|
||||
packages.x86_64-darwin.default = self.packages.x86_64-darwin.agenix;
|
||||
|
||||
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;
|
||||
});
|
||||
formatter.aarch64-darwin = nixpkgs.legacyPackages.aarch64-darwin.alejandra;
|
||||
packages.aarch64-darwin.agenix = agenix "aarch64-darwin";
|
||||
packages.aarch64-darwin.default = self.packages.aarch64-darwin.agenix;
|
||||
|
||||
checks =
|
||||
nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: {
|
||||
integration =
|
||||
(darwin.lib.darwinSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
./test/integration_darwin.nix
|
||||
formatter.aarch64-linux = nixpkgs.legacyPackages.aarch64-linux.alejandra;
|
||||
packages.aarch64-linux.agenix = agenix "aarch64-linux";
|
||||
packages.aarch64-linux.default = self.packages.aarch64-linux.agenix;
|
||||
|
||||
# Allow new-style nix commands in CI
|
||||
{nix.extraOptions = "experimental-features = nix-command flakes";}
|
||||
formatter.i686-linux = nixpkgs.legacyPackages.i686-linux.alejandra;
|
||||
packages.i686-linux.agenix = agenix "i686-linux";
|
||||
packages.i686-linux.default = self.packages.i686-linux.agenix;
|
||||
|
||||
home-manager.darwinModules.home-manager
|
||||
{
|
||||
home-manager = {
|
||||
verbose = true;
|
||||
useGlobalPkgs = true;
|
||||
useUserPackages = true;
|
||||
backupFileExtension = "hmbak";
|
||||
users.runner = ./test/integration_hm_darwin.nix;
|
||||
};
|
||||
}
|
||||
];
|
||||
})
|
||||
.system;
|
||||
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.alejandra;
|
||||
packages.x86_64-linux.agenix = agenix "x86_64-linux";
|
||||
packages.x86_64-linux.default = 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";
|
||||
};
|
||||
checks."aarch64-darwin".integration =
|
||||
(darwin.lib.darwinSystem {
|
||||
system = "aarch64-darwin";
|
||||
modules = [./test/integration_darwin.nix "${darwin.outPath}/pkgs/darwin-installer/installer.nix"];
|
||||
})
|
||||
// {
|
||||
x86_64-linux.integration = import ./test/integration.nix {
|
||||
inherit nixpkgs home-manager;
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
};
|
||||
.system;
|
||||
checks."x86_64-darwin".integration =
|
||||
(darwin.lib.darwinSystem {
|
||||
system = "x86_64-darwin";
|
||||
modules = [./test/integration_darwin.nix "${darwin.outPath}/pkgs/darwin-installer/installer.nix"];
|
||||
})
|
||||
.system;
|
||||
|
||||
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];
|
||||
};
|
||||
});
|
||||
darwinConfigurations.integration.system = self.checks."x86_64-darwin".integration;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,237 +0,0 @@
|
|||
{
|
||||
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 -sfT "${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 -sfT "${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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -10,6 +10,11 @@ with lib; let
|
|||
|
||||
isDarwin = lib.attrsets.hasAttrByPath ["environment" "darwinConfig"] options;
|
||||
|
||||
# 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;
|
||||
ageBin = config.age.ageBin;
|
||||
|
||||
users = config.users.users;
|
||||
|
@ -69,7 +74,6 @@ with lib; let
|
|||
IDENTITIES=()
|
||||
for identity in ${toString cfg.identityPaths}; do
|
||||
test -r "$identity" || continue
|
||||
test -s "$identity" || continue
|
||||
IDENTITIES+=(-i)
|
||||
IDENTITIES+=("$identity")
|
||||
done
|
||||
|
@ -88,7 +92,7 @@ with lib; let
|
|||
mv -f "$TMP_FILE" "$_truePath"
|
||||
|
||||
${optionalString secretType.symlink ''
|
||||
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfT "${cfg.secretsDir}/${secretType.name}" "${secretType.path}"
|
||||
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfn "${cfg.secretsDir}/${secretType.name}" "${secretType.path}"
|
||||
''}
|
||||
'';
|
||||
|
||||
|
@ -103,7 +107,7 @@ with lib; let
|
|||
_agenix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)"
|
||||
(( ++_agenix_generation ))
|
||||
echo "[agenix] symlinking new secrets to ${cfg.secretsDir} (generation $_agenix_generation)..."
|
||||
ln -sfT "${cfg.secretsMountPoint}/$_agenix_generation" ${cfg.secretsDir}
|
||||
ln -sfn "${cfg.secretsMountPoint}/$_agenix_generation" ${cfg.secretsDir}
|
||||
|
||||
(( _agenix_generation > 1 )) && {
|
||||
echo "[agenix] removing old secrets (generation $(( _agenix_generation - 1 )))..."
|
||||
|
@ -134,9 +138,8 @@ with lib; let
|
|||
name = mkOption {
|
||||
type = types.str;
|
||||
default = config._module.args.name;
|
||||
defaultText = literalExpression "config._module.args.name";
|
||||
description = ''
|
||||
Name of the file used in {option}`age.secretsDir`
|
||||
Name of the file used in ''${cfg.secretsDir}
|
||||
'';
|
||||
};
|
||||
file = mkOption {
|
||||
|
@ -148,9 +151,6 @@ with lib; 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.
|
||||
'';
|
||||
|
@ -172,9 +172,6 @@ with lib; 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.
|
||||
'';
|
||||
|
@ -190,10 +187,7 @@ in {
|
|||
options.age = {
|
||||
ageBin = mkOption {
|
||||
type = types.str;
|
||||
default = "${pkgs.age}/bin/age";
|
||||
defaultText = literalExpression ''
|
||||
"''${pkgs.age}/bin/age"
|
||||
'';
|
||||
default = "${rage}/bin/rage";
|
||||
description = ''
|
||||
The age executable to use.
|
||||
'';
|
||||
|
@ -221,8 +215,9 @@ in {
|
|||
&& (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 {option}`age.secretsDir`
|
||||
Where secrets are created before they are symlinked to ''${cfg.secretsDir}
|
||||
'';
|
||||
};
|
||||
identityPaths = mkOption {
|
||||
|
@ -236,16 +231,6 @@ in {
|
|||
"/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.
|
||||
'';
|
||||
|
|
|
@ -1,66 +1,37 @@
|
|||
{
|
||||
lib,
|
||||
stdenv,
|
||||
age,
|
||||
jq,
|
||||
rage,
|
||||
gnused,
|
||||
nix,
|
||||
mktemp,
|
||||
diffutils,
|
||||
substituteAll,
|
||||
ageBin ? "${age}/bin/age",
|
||||
ageBin ? "${rage}/bin/rage",
|
||||
shellcheck,
|
||||
}: let
|
||||
bin = "${placeholder "out"}/bin/agenix";
|
||||
in
|
||||
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}
|
||||
}:
|
||||
stdenv.mkDerivation rec {
|
||||
pname = "agenix";
|
||||
version = "0.13.0";
|
||||
src = substituteAll {
|
||||
inherit ageBin version;
|
||||
sedBin = "${gnused}/bin/sed";
|
||||
nixInstantiate = "${nix}/bin/nix-instantiate";
|
||||
mktempBin = "${mktemp}/bin/mktemp";
|
||||
diffBin = "${diffutils}/bin/diff";
|
||||
src = ./agenix.sh;
|
||||
};
|
||||
dontUnpack = true;
|
||||
|
||||
test_tmp=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||
export HOME="$test_tmp/home"
|
||||
export NIX_STORE_DIR="$test_tmp/nix/store"
|
||||
export NIX_STATE_DIR="$test_tmp/nix/var"
|
||||
mkdir -p "$HOME" "$NIX_STORE_DIR" "$NIX_STATE_DIR"
|
||||
function cleanup {
|
||||
rm -rf "$test_tmp"
|
||||
}
|
||||
trap "cleanup" 0 2 3 15
|
||||
doCheck = true;
|
||||
checkInputs = [shellcheck];
|
||||
postCheck = ''
|
||||
shellcheck $src
|
||||
'';
|
||||
|
||||
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
|
||||
)
|
||||
installPhase = ''
|
||||
install -D $src ${placeholder "out"}/bin/agenix
|
||||
'';
|
||||
|
||||
cd $HOME/secrets
|
||||
test $(${bin} -d secret1.age) = "hello"
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
install -D $src ${bin}
|
||||
'';
|
||||
|
||||
meta.description = "age-encrypted secrets for NixOS";
|
||||
}
|
||||
meta.description = "age-encrypted secrets for NixOS";
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ function 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 ' '
|
||||
|
@ -46,7 +45,6 @@ function err() {
|
|||
test $# -eq 0 && (show_help && exit 1)
|
||||
|
||||
REKEY=0
|
||||
DECRYPT_ONLY=0
|
||||
DEFAULT_DECRYPT=(--decrypt)
|
||||
|
||||
while test $# -gt 0; do
|
||||
|
@ -79,17 +77,6 @@ while test $# -gt 0; do
|
|||
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
|
||||
|
@ -102,6 +89,7 @@ while test $# -gt 0; do
|
|||
done
|
||||
|
||||
RULES=${RULES:-./secrets.nix}
|
||||
|
||||
function cleanup {
|
||||
if [ -n "${CLEARTEXT_DIR+x}" ]
|
||||
then
|
||||
|
@ -114,18 +102,18 @@ function cleanup {
|
|||
}
|
||||
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 {
|
||||
function edit {
|
||||
FILE=$1
|
||||
KEYS=$2
|
||||
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
|
||||
err "There is no rule for $FILE in $RULES."
|
||||
fi
|
||||
|
||||
CLEARTEXT_DIR=$(@mktempBin@ -d)
|
||||
CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")"
|
||||
|
||||
if [ -f "$FILE" ]
|
||||
then
|
||||
DECRYPT=("${DEFAULT_DECRYPT[@]}")
|
||||
|
@ -140,22 +128,10 @@ function decrypt {
|
|||
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
|
||||
DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE")
|
||||
@ageBin@ "${DECRYPT[@]}" || exit 1
|
||||
cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before"
|
||||
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'
|
||||
|
||||
|
@ -171,9 +147,7 @@ function edit {
|
|||
ENCRYPT=()
|
||||
while IFS= read -r key
|
||||
do
|
||||
if [ -n "$key" ]; then
|
||||
ENCRYPT+=(--recipient "$key")
|
||||
fi
|
||||
ENCRYPT+=(--recipient "$key")
|
||||
done <<< "$KEYS"
|
||||
|
||||
REENCRYPTED_DIR=$(@mktempBin@ -d)
|
||||
|
@ -183,13 +157,11 @@ function edit {
|
|||
|
||||
@ageBin@ "${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1
|
||||
|
||||
mkdir -p "$(dirname "$FILE")"
|
||||
|
||||
mv -f "$REENCRYPTED_FILE" "$FILE"
|
||||
mv -f "$REENCRYPTED_FILE" "$1"
|
||||
}
|
||||
|
||||
function rekey {
|
||||
FILES=$( (@nixInstantiate@ --json --eval -E "(let rules = import $RULES; in builtins.attrNames rules)" | @jqBin@ -r .[]) || exit 1)
|
||||
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
|
||||
|
@ -200,5 +172,4 @@ function rekey {
|
|||
}
|
||||
|
||||
[ $REKEY -eq 1 ] && rekey && exit 0
|
||||
[ $DECRYPT_ONLY -eq 1 ] && DEFAULT_DECRYPT+=("-o" "-") && decrypt "${FILE}" "$(keys "$FILE")" && exit 0
|
||||
edit "$FILE" && cleanup && exit 0
|
||||
|
|
11
pkgs/doc.nix
11
pkgs/doc.nix
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
stdenvNoCC,
|
||||
mmdoc,
|
||||
self,
|
||||
}:
|
||||
stdenvNoCC.mkDerivation rec {
|
||||
name = "agenix-doc";
|
||||
src = ../doc;
|
||||
phases = ["mmdocPhase"];
|
||||
mmdocPhase = "${mmdoc}/bin/mmdoc agenix $src $out";
|
||||
}
|
49
pkgs/rage.nix
Normal file
49
pkgs/rage.nix
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
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];
|
||||
};
|
||||
}
|
130
test/agenix_testing.py
Normal file
130
test/agenix_testing.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
"""Provide a class and helper methods for agenix integration tests."""
|
||||
|
||||
|
||||
import typing as t
|
||||
|
||||
T = t.TypeVar("T", str, list[str])
|
||||
|
||||
|
||||
class AgenixTester:
|
||||
"""Provide a class to help reduce repetition in setup."""
|
||||
|
||||
def __init__(self, system, user, password) -> None:
|
||||
"""Necessary setup can be put here."""
|
||||
self.system = system
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.setup()
|
||||
|
||||
def login(self) -> None:
|
||||
self.system.wait_for_unit("multi-user.target")
|
||||
self.system.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
|
||||
self.system.sleep(2)
|
||||
self.system.send_key("alt-f2")
|
||||
self.system.wait_until_succeeds("[ $(fgconsole) = 2 ]")
|
||||
self.system.wait_for_unit("getty@tty2.service")
|
||||
self.system.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
|
||||
self.system.wait_until_tty_matches("2", "login: ")
|
||||
self.system.send_chars(f"{self.user}\n")
|
||||
self.system.wait_until_tty_matches("2", f"login: {self.user}")
|
||||
self.system.wait_until_succeeds("pgrep login")
|
||||
self.system.sleep(2)
|
||||
self.system.send_chars(f"{self.password}\n")
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Run common setup code."""
|
||||
self.login()
|
||||
|
||||
def user_succeed(
|
||||
self,
|
||||
cmds: T,
|
||||
directory: str | None = None,
|
||||
debug: bool = False,
|
||||
) -> T:
|
||||
"""Run cmds as `self.user`, optionally in a specified directory.
|
||||
|
||||
For convenience, if cmds is a sequence, returns output as a list of
|
||||
outputs corresponding with each line in cmds. if cmds is a string,
|
||||
returns output as a string.
|
||||
"""
|
||||
|
||||
context: list[str] = [
|
||||
"set -Eeu -o pipefail",
|
||||
"shopt -s inherit_errexit",
|
||||
]
|
||||
if debug:
|
||||
context.append("set -x")
|
||||
|
||||
if directory:
|
||||
context.append(f"cd {directory}")
|
||||
|
||||
if isinstance(cmds, str):
|
||||
commands_str = "\n".join([*context, cmds])
|
||||
final_command = f"sudo -u {self.user} -- bash -c '{commands_str}'"
|
||||
return self.system.succeed(final_command)
|
||||
|
||||
results: list[str] = []
|
||||
for cmd in cmds:
|
||||
commands_str = "\n".join([*context, cmd])
|
||||
final_command = f"sudo -u {self.user} -- bash -c '{commands_str}'"
|
||||
result = self.system.succeed(final_command)
|
||||
results.append(result.strip())
|
||||
return t.cast(T, results)
|
||||
|
||||
def run_all(self) -> None:
|
||||
self.test_rekeying()
|
||||
self.test_user_edit()
|
||||
|
||||
def test_rekeying(self) -> None:
|
||||
"""Ensure we can rekey a file and its hash changes."""
|
||||
|
||||
before_hash, _, after_hash = self.user_succeed(
|
||||
[
|
||||
"sha256sum passwordfile-user1.age",
|
||||
f"agenix -r -i /home/{self.user}/.ssh/id_ed25519",
|
||||
"sha256sum passwordfile-user1.age",
|
||||
],
|
||||
directory="/tmp/secrets",
|
||||
)
|
||||
|
||||
# Ensure we actually have hashes
|
||||
for line in [before_hash, after_hash]:
|
||||
h = line.split()
|
||||
assert len(h) == 2, f"hash should be [hash, filename], got {h}"
|
||||
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"
|
||||
|
||||
def test_user_edit(self):
|
||||
"""Ensure user1 can edit passwordfile-user1.age."""
|
||||
self.user_succeed(
|
||||
"EDITOR=cat agenix -e passwordfile-user1.age",
|
||||
directory="/tmp/secrets",
|
||||
)
|
||||
|
||||
self.user_succeed("echo bogus > ~/.ssh/id_rsa")
|
||||
|
||||
# Cannot edit with bogus default id_rsa
|
||||
self.system.fail(
|
||||
f"sudo -u {self.user} -- bash -c '"
|
||||
"cd /tmp/secrets; "
|
||||
"EDITOR=cat agenix -e /tmp/secrets/passwordfile-user1.age; "
|
||||
"'"
|
||||
)
|
||||
|
||||
# user1 can still edit if good identity specified
|
||||
*_, pw = self.user_succeed(
|
||||
[
|
||||
(
|
||||
"EDITOR=cat agenix -e passwordfile-user1.age "
|
||||
"-i /home/user1/.ssh/id_ed25519"
|
||||
),
|
||||
"rm ~/.ssh/id_rsa",
|
||||
"echo 'secret1234' | agenix -e passwordfile-user1.age",
|
||||
"EDITOR=cat agenix -e passwordfile-user1.age",
|
||||
],
|
||||
directory="/tmp/secrets",
|
||||
)
|
||||
assert pw == "secret1234", f"password didn't match, got '{pw}'"
|
|
@ -1,17 +1,10 @@
|
|||
# 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"
|
||||
echo "Installing 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
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -6,10 +6,27 @@
|
|||
config = {};
|
||||
},
|
||||
system ? builtins.currentSystem,
|
||||
home-manager ? <home-manager>,
|
||||
}:
|
||||
pkgs.nixosTest {
|
||||
name = "agenix-integration";
|
||||
extraPythonPackages = ps: let
|
||||
agenixTesting = let
|
||||
version = (pkgs.callPackage ../pkgs/agenix.nix {}).version;
|
||||
in
|
||||
ps.buildPythonPackage rec {
|
||||
inherit version;
|
||||
pname = "agenix_testing";
|
||||
src = ./.;
|
||||
format = "pyproject";
|
||||
propagatedBuildInputs = [ps.setuptools];
|
||||
postPatch = ''
|
||||
# Keep a default version makes for easy installation outside of
|
||||
# nix for debugging
|
||||
substituteInPlace pyproject.toml \
|
||||
--replace 'version = "0.1.0"' 'version = "${version}"'
|
||||
'';
|
||||
};
|
||||
in [agenixTesting];
|
||||
nodes.system1 = {
|
||||
config,
|
||||
pkgs,
|
||||
|
@ -19,7 +36,6 @@ pkgs.nixosTest {
|
|||
imports = [
|
||||
../modules/age.nix
|
||||
./install_ssh_host_keys.nix
|
||||
"${home-manager}/nixos"
|
||||
];
|
||||
|
||||
services.openssh.enable = true;
|
||||
|
@ -45,82 +61,23 @@ pkgs.nixosTest {
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
user = "user1";
|
||||
password = "password1234";
|
||||
secret2 = "world!";
|
||||
in ''
|
||||
system1.wait_for_unit("multi-user.target")
|
||||
system1.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
|
||||
system1.sleep(2)
|
||||
system1.send_key("alt-f2")
|
||||
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.send_chars("${user}\n")
|
||||
system1.wait_until_tty_matches("2", "login: ${user}")
|
||||
system1.wait_until_succeeds("pgrep login")
|
||||
system1.sleep(2)
|
||||
system1.send_chars("${password}\n")
|
||||
# Skipping analyzing "agenix_testing": module is installed, but missing
|
||||
# library stubs or py.typed marker
|
||||
from agenix_testing import AgenixTester # type: ignore
|
||||
tester = AgenixTester(system=system1, user="${user}", password="${password}")
|
||||
|
||||
# Can still be used as before
|
||||
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")
|
||||
# Or from `tester.system`
|
||||
tester.system.wait_for_file("/tmp/1")
|
||||
assert "${user}" in tester.system.succeed("cat /tmp/1")
|
||||
|
||||
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")
|
||||
tester.run_all()
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
testScript = pkgs.writeShellApplication {
|
||||
name = "agenix-integration";
|
||||
text = ''
|
||||
grep "${secret}" "${config.age.secrets.system-secret.path}"
|
||||
grep ${secret} ${config.age.secrets.secret1.path}
|
||||
'';
|
||||
};
|
||||
in {
|
||||
|
@ -19,10 +19,9 @@ in {
|
|||
|
||||
services.nix-daemon.enable = true;
|
||||
|
||||
age = {
|
||||
identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
|
||||
secrets.system-secret.file = ../example/secret1.age;
|
||||
};
|
||||
age.identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
|
||||
|
||||
age.secrets.secret1.file = ../example/secret1.age;
|
||||
|
||||
environment.systemPackages = [testScript];
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
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')
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
7
test/pyproject.toml
Normal file
7
test/pyproject.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "agenix_testing"
|
||||
version = "0.1.0"
|
Loading…
Reference in a new issue