diff --git a/examples/README.md b/examples/README.md index d7c9d39e..e693d0be 100644 --- a/examples/README.md +++ b/examples/README.md @@ -38,6 +38,12 @@ By default, [`configuration.nix`](configuration.nix) enables `bitcoind` and `cli nix-bitcoin configuration to it using [krops](https://github.com/krebs/krops).\ Requires: [Nix](https://nixos.org/nix/), Linux +- [`./flakes-agenix/deploy.sh`](./flakes-agenix/deploy.sh) shows how to deploy a + nix-bitcoin node flake using [agenix](https://github.com/ryantm/agenix) secrets encryption.\ + agenix allows repo-defined secrets that can be deployed with any deployment scheme.\ + The node is deployed in a container.\ + Requires: [Nix](https://nixos.org/), a systemd-based Linux distro and root privileges + - [`./deploy-container-minimal.sh`](deploy-container-minimal.sh) creates a container defined by [importable-configuration.nix](importable-configuration.nix).\ You can copy and import this file to use nix-bitcoin in an existing NixOS configuration.\ @@ -63,3 +69,5 @@ The commands in `shell.nix` allow you to locally run the node in a VM or contain Flakes make it easy to include `nix-bitcoin` in an existing NixOS config. The [flakes example](./flakes/flake.nix) shows how to use `nix-bitcoin` as an input to a system flake. + +To use [agenix](https://github.com/ryantm/agenix), which allows committing secrets to the node repo, see [`./flakes-agenix`](./flakes-agenix). diff --git a/examples/flakes-agenix/deploy.sh b/examples/flakes-agenix/deploy.sh new file mode 100755 index 00000000..bc855aea --- /dev/null +++ b/examples/flakes-agenix/deploy.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Strategy: +# 1. Copy the node flake (./flake.nix) to a temporary dir and create a Git repo. +# 2. Generate age-encrypted secrets by running a package defined by the node flake. +# Commit the secrets. +# 3. Start the node in a container and run test commands. +# The container is destroyed afterwards. +# +# Run this script with arg `-i` or `--interactive` to start an +# interactive shell in the container. + +# You can use ./flake.nix as a template for a real deployment. + +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# 0. Prelude +interactive= +case "${1:-}" in + -i|--interactive) + interactive=1 + ;; +esac + +cd "${BASH_SOURCE[0]%/*}" +scriptDir=$PWD +nixBitcoin="$scriptDir/../.." + +tmpDir=$(mktemp -d /tmp/nix-bitcoin-agenix.XXX) +trap 'rm -rf $tmpDir' EXIT + +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# 1. Create a flake repo in a tmp dir +rsync -a ./ --exclude deploy.sh ./ "$tmpDir" + +cd "$tmpDir" + +git init +git add . +git commit -a -m init + +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# 2. Generate age-encrypted secrets + +# Use this nix-bitcoin repo as the flake input when generating secrets, +# so that this script can be used for automated testing. +nix run --override-input nix-bitcoin "$nixBitcoin" .#generateAgeSecrets +# +# In a real deployment, you can simply run the following from the deployment repo root: +# nix run .#generateAgeSecrets +# +# or, if you don't define the `generateAgeSecrets` helper package: +# nix run .#nixosConfigurations.demo-node.config.nix-bitcoin.age.generateSecretsScript +# +# Show help +# nix run .#generateAgeSecrets --help + +echo +echo "Encrypted secrets:" +ls -al secrets +echo + +# Commit age-encrypted secrets +git add ./secrets +git commit -a -m 'add secrets' + +# Success! +# This node flake can now be deployed with any deployment method. + +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# 3. Run node in container + +if [[ $interactive ]]; then + # Start interactive container shell + runCmd=() +else + runCmd=( + --run c bash -c ' + echo + echo "Unencrypted secrets in /run/agenix:" + ls -alH /run/agenix + echo + systemctl status bitcoind + ' + ) +fi + +runContainer=( + nix run --override-input nix-bitcoin "$nixBitcoin" .#container -- "${runCmd[@]}" +) +nix shell --inputs-from "$nixBitcoin" extra-container -c "${runContainer[@]}" + +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# Debug helper: Build flake outputs +# nix build --no-link -L --print-out-paths --override-input nix-bitcoin "$nixBitcoin" .#container +# nix build --no-link -L --print-out-paths --override-input nix-bitcoin "$nixBitcoin" .#generateAgeSecrets diff --git a/examples/flakes-agenix/flake.lock b/examples/flakes-agenix/flake.lock new file mode 100644 index 00000000..c1363060 --- /dev/null +++ b/examples/flakes-agenix/flake.lock @@ -0,0 +1,32 @@ +{ + "nodes": { + "agenix": { + "inputs": { + "nixpkgs": [ + "nix-bitcoin", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1665870395, + "narHash": "sha256-Tsbqb27LDNxOoPLh0gw2hIb6L/6Ow/6lIBvqcHzEKBI=", + "owner": "ryantm", + "repo": "agenix", + "rev": "a630400067c6d03c9b3e0455347dc8559db14288", + "type": "github" + }, + "original": { + "owner": "ryantm", + "repo": "agenix", + "type": "github" + } + }, + "root": { + "inputs": { + "agenix": "agenix" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/examples/flakes-agenix/flake.nix b/examples/flakes-agenix/flake.nix new file mode 100644 index 00000000..cf8f51cb --- /dev/null +++ b/examples/flakes-agenix/flake.nix @@ -0,0 +1,108 @@ +# When using this file as the base for a real deployment, +# make sure to check all lines marked by 'FIXME:' + +# This file is used by ./deploy.sh to deploy a container with +# age-encrypted secrets. + +{ + inputs.nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release"; + inputs.agenix.url = "github:ryantm/agenix"; + inputs.agenix.inputs.nixpkgs.follows = "nix-bitcoin/nixpkgs"; + + inputs.flake-utils.follows = "nix-bitcoin/flake-utils"; + + outputs = { self, nix-bitcoin, agenix, flake-utils }: { + modules = { + demoNode = { config, lib, ... }: { + imports = [ + # TODO-EXTERNAL: + # Set this to `agenix.nixosModules.default` when + # https://github.com/ryantm/agenix/pull/126 is merged + agenix.nixosModules.age + nix-bitcoin.nixosModules.default + (nix-bitcoin + "/modules/secrets/age.nix") + ]; + + # Use age-encrypted secrets + nix-bitcoin.age = { + enable = true; + + # The local secrets dir and its contents can be created with the + # `generateAgeSecrets` flake package (defined below). + # Use it like so: + # nix run .#generateAgeSecrets + # and commit the newly created ./secrets dir afterwards. + # + # This script must be rerun when adding node services that + # require new secrets. + # + # For a real-life example, see ./deploy.sh + secretsSourceDir = ./secrets; + + # FIXME: + # Set this to a public SSH host key of your node (preferably key type `ed25519`). + # You can query host keys with command `ssh-keyscan `. + # The keys defined here are used to age-encrypt the secrets. + publicKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDoAaEMk8jMbg5MnvKDApWC6EpUHRJTzavy/wU2EtgtU" + ]; + }; + + # Enable services. + # See ../configuration.nix for all available features. + services.bitcoind.enable = true; + # + # See ../flakes/flake.nix for more settings useful for production nodes. + + + # WARNING: + # FIXME: + # Remove the following `age.identityPaths` setting in a real deployment. + # This copies a private key to the (publicly readable) Nix store, + # which allows ./deploy.sh to start a age-based container in + # a single deployment step. + # + # In a real deployment, just leave `age.identityPaths` undefined. + # In this case, agenix uses the auto-generated SSH host key. + age.identityPaths = [ ./host-key ]; + }; + }; + + nixosConfigurations.demoNode = nix-bitcoin.inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ self.modules.demoNode ]; + }; + } + // (nix-bitcoin.inputs.nixpkgs.lib.recursiveUpdate + + # Allow runnning this node as a container, used by ./deploy.sh + (flake-utils.lib.eachSystem nix-bitcoin.lib.supportedSystems (system: { + packages = { + container = nix-bitcoin.inputs.extra-container.lib.buildContainers { + inherit system; + config.containers.nb-agenix = { + privateNetwork = true; + config.imports = [ self.modules.demoNode ]; + }; + # Set this when running on a NixOS container host with `system.stateVersion` <22.05 + # legacyInstallDirs = true; + }; + }; + })) + + # This allows generating age-encrypted secrets on systems + # that differ from the target node. + # E.g. manage a `x86_64-linux` node from macOS (`aarch64-darwin`) + (flake-utils.lib.eachDefaultSystem (system: { + packages = { + generateAgeSecrets = let + nodeSystem = nix-bitcoin.inputs.nixpkgs.lib.nixosSystem { + inherit system; + modules = [ self.modules.demoNode ]; + }; + in + nodeSystem.config.nix-bitcoin.age.generateSecretsScript; + }; + })) + ); +} diff --git a/examples/flakes-agenix/host-key b/examples/flakes-agenix/host-key new file mode 100644 index 00000000..ad136d3f --- /dev/null +++ b/examples/flakes-agenix/host-key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACA6AGhDJPIzG4OTJ7ygwKVguhKVB0SU82r8v8FNhLYLVAAAAIhEbEOERGxD +hAAAAAtzc2gtZWQyNTUxOQAAACA6AGhDJPIzG4OTJ7ygwKVguhKVB0SU82r8v8FNhLYLVA +AAAECBS5tCD9AcaOcNzPYlreA4BVrsy2f0FaGgEoJfBQzMqzoAaEMk8jMbg5MnvKDApWC6 +EpUHRJTzavy/wU2EtgtUAAAABG5vbmUB +-----END OPENSSH PRIVATE KEY----- diff --git a/helper/makeShell.nix b/helper/makeShell.nix index 2ab42df8..3dcf204a 100644 --- a/helper/makeShell.nix +++ b/helper/makeShell.nix @@ -85,7 +85,7 @@ pkgs.stdenv.mkDerivation { config="${cfgDir}/configuration.nix" fi genSecrets=$(nix-build --no-out-link -I nixos-config="$config" \ - '' -A config.nix-bitcoin.generateSecretsScript) + '' -A config.nix-bitcoin.generateSecretsScriptImpl) mkdir -p "${cfgDir}/secrets" (cd "${cfgDir}/secrets"; $genSecrets) )} diff --git a/modules/secrets/age.nix b/modules/secrets/age.nix new file mode 100644 index 00000000..c96193f2 --- /dev/null +++ b/modules/secrets/age.nix @@ -0,0 +1,93 @@ +{ config, pkgs, lib, ... }: + +with lib; +let + options.nix-bitcoin.age = { + enable = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + Enable age-encrypted secrets. + + This requires that the [agenix](https://github.com/ryantm/agenix) + module is included in your config. + ''; + }; + + secretsSourceDir = mkOption { + type = types.path; + example = literalExpression "./secrets"; + description = mdDoc '' + The directory where age-encrypted secrets are stored. + ''; + }; + + publicKeys = mkOption { + type = with types; listOf str; + example = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH" ]; + description = mdDoc '' + Public keys for encrypting the secrets. + + A sensible default is to set this to the ed25519 public SSH host key of your node. + You can query host keys with command {command}`ssh-keyscan `. + + When not using a SSH host key, make sure to set the corresponding agenix + option {option}`age.identityPaths`. + ''; + }; + + generateSecretsScript = mkOption { + readOnly = true; + + description = mdDoc secretsScriptLib.scriptHelp; + + default = pkgs.writers.writeBashBin "generate-secrets" '' + ${secretsScriptLib.gotoDestDir} + + tmpSecretsDir=$(mktemp -d) + trap 'rm -rf $tmpSecretsDir' EXIT + + # 1. Generate secrets to a tmp dir + pushd "$tmpSecretsDir" >/dev/null + ${config.nix-bitcoin.generateSecretsScriptImpl} + popd >/dev/null + + # 2. Age-encrypt each secret to $PWD/$name.age + encrypt() { + ${getExe pkgs.rage} "$tmpSecretsDir/$1" -o "$1.age" ${ + concatMapStringsSep " " (pubkey: + "--recipient ${escapeShellArg pubkey}" + ) cfg.publicKeys + } + } + ${ + concatMapStrings (name: '' + encrypt "${name}" + '') (builtins.attrNames config.nix-bitcoin.secrets) + } + ''; + }; + }; + + cfg = config.nix-bitcoin.age; + inherit (config.nix-bitcoin) secretsScriptLib; +in { + inherit options; + + config = { + nix-bitcoin.secretsSetupMethod = "age"; + + nix-bitcoin.secretsDir = config.age.secretsDir; + + # The `nix-bitcoin-secrets` target has no dependencies, + # because agenix runs via the activation script, before systemd. + systemd.targets.nix-bitcoin-secrets = {}; + + age.secrets = mapAttrs (name: value: { + owner = value.user; + group = value.group; + mode = value.permissions; + file = (cfg.secretsSourceDir + "/${name}.age"); + }) config.nix-bitcoin.secrets; + }; +} diff --git a/modules/secrets/secrets.nix b/modules/secrets/secrets.nix index be3e7b42..2e4e6ed9 100644 --- a/modules/secrets/secrets.nix +++ b/modules/secrets/secrets.nix @@ -71,6 +71,67 @@ let }; generateSecretsScript = mkOption { + readOnly = true; + + description = mdDoc cfg.secretsScriptLib.scriptHelp; + + default = pkgs.writers.writeBashBin "generate-secrets" '' + ${cfg.secretsScriptLib.gotoDestDir} + ${cfg.generateSecretsScriptImpl} + ''; + defaultText = "(See source)"; + }; + + # Snippets for assembling generate secrets scripts + secretsScriptLib = mkOption { + internal = true; + readOnly = true; + default = { + scriptHelp = '' + Script to generate secrets. + + Usage: + generate-secrets + + Writes secrets to ./secrets, if dir ./.git exists. + Writes secrets to the working directory, otherwise. + + generate-secrets + + Writes secrets to + ''; + gotoDestDir = '' + set -euo pipefail + + case ''${1:-} in + -h|--help) + echo '${cfg.secretsScriptLib.scriptHelp}' + exit 0 + ;; + esac + + destDir=''${1:-} + + if [[ ! $destDir ]]; then + if [[ -d .git ]]; then + destDir=./secrets + else + destDir=. + fi + fi + + echo "Writing secrets to $destDir" >&2 + + if [[ $destDir != . ]]; then + ${pkgs.coreutils}/bin/mkdir -p "$destDir" + cd "$destDir" + fi + ''; + }; + }; + + # Writes secrets to PWD + generateSecretsScriptImpl = mkOption { internal = true; default = let rpcauthSrc = pkgs.fetchurl { @@ -182,7 +243,7 @@ in { cd "${cfg.secretsDir}" chown root: . chmod 0700 . - ${cfg.generateSecretsScript} + ${cfg.generateSecretsScriptImpl} ''} setupSecret() { diff --git a/test/run-tests.sh b/test/run-tests.sh index 778d3aa4..075b3606 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -292,6 +292,7 @@ examples() { runExample deploy-container-minimal.sh runExample deploy-qemu-vm.sh runExample deploy-krops.sh + runExample flakes-agenix/deploy.sh ' (cd "$scriptDir/../examples" && nix-shell --run "$script") }