age-encrypted secrets for NixOS
Go to file
Ryan Mulligan 4fefd7cfff
Merge pull request #71 from ryantm/fix-non-root-secrets
fix: make non-root secrets accessible again
2021-11-20 12:23:07 -08:00
.github/workflows ci: split linux and macos 2021-11-20 11:39:24 -08:00
example dev: add integration test 2021-05-09 14:22:48 -07:00
example_keys add README and examples 2020-09-03 13:16:44 -07:00
modules fix: make non-root secrets accessible again 2021-11-20 12:19:52 -08:00
pkgs Make 'diff' an explicit dependency 2021-07-22 13:58:29 +02:00
test dev: add integration test 2021-05-09 14:22:48 -07:00
LICENSE initial prototype 2020-08-31 21:37:26 -07:00
README.md modules/age: add option to disable symlinking 2021-11-15 21:39:32 -08:00
default.nix add flake and default .nix files; add agenix command 2020-09-03 11:24:33 -07:00
flake.lock fix: flake show and flake check 2021-05-09 15:36:04 -07:00
flake.nix fix: flake show and flake check 2021-05-09 15:36:04 -07:00
overlay.nix Update overlay.nix 2020-12-30 16:18:38 -05:00

README.md

agenix - age-encrypted secrets for NixOS

agenix is a commandline tool for managing secrets encrypted with your existing SSH keys. This project also includes the NixOS module age for adding encrypted secrets into the Nix store and decrypting them.

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.

Features

  • Secrets are encrypted with SSH 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

Notices

  • The age module will only work if you use NixOS with commit e6b8587 which is included in the latest nixos-20.09 or nixos-unstable releases.
  • 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

Choose one of the following methods:

niv (Current recommendation)

First add it to niv:

$ niv add ryantm/agenix

Module

Then add the following to your configuration.nix in the imports list:

{
  imports = [ "${(import ./nix/sources.nix).agenix}/modules/age" ];
}

nix-channel

As root run:

$ nix-channel --add https://github.com/ryantm/agenix/archive/master.tar.gz agenix
$ nix-channel --update

Then add the following to your configuration.nix in the imports list:

{
  imports = [ <agenix/modules/age.nix> ];
}

CLI

To install the agenix binary:

{
  environment.systemPackages = [ import <agenix> {}.agenix ];
}

fetchTarball

Add the following to your configuration.nix:

{
  imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/master.tar.gz"}/modules/age.nix" ];
}

or with pinning:

{
  imports = let
    # replace this with an actual commit id or tag
    commit = "298b235f664f925b433614dc33380f0662adfc3f";
  in [
    "${builtins.fetchTarball {
      url = "https://github.com/ryantm/agenix/archive/${commit}.tar.gz";
      # replace this with an actual hash
      sha256 = "0000000000000000000000000000000000000000000000000000";
    }}/modules/age"
  ];
}

Flakes

Module

{
  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.age
      ];
    };
  };
}

CLI

You don't need to install it,

nix run github:ryantm/agenix -- --help

but, if you want to (change the system based on your system):

{
  environment.systemPackages = [ agenix.defaultPackage.x86_64-linux ];
}

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:

    $ 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)):

    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/):

    $ agenix -e secret1.age
    
  5. Add secret to a NixOS module config:

    age.secrets.secret1.file = ../secrets/secret1.age;
    
  6. NixOS rebuild or use your deployment tool like usual.

    The secret will be decrypted to the value of age.secrets.secret1.path (/run/agenix/secret1 by default). For per-secret options controlling ownership etc, see modules/age.nix.

Rekeying

If you change the public keys in secrets.nix, you should rekey your secrets:

$ 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.)

If your secret cannot be a symlink, you should set the symlink option to false:

{
  age.secrets.some-secret = {
    file = ./secret;
    path = "/var/lib/some-service/some-secret";
    symlink = false;
  };
}

Instead of first decrypting the secret to /run/agenix and then symlinking to its path, the secret will instead be forcibly moved to its path. Please note that, currently, there are no cleanup mechanisms for secrets that are not symlinked by agenix.

Threat model/Warnings

This project has not be 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 have, however this was left out for simplicity in age.

Acknowledgements

This project is based off of sops-nix created Mic92. Thank you to Mic92 for inspiration and advice.