Giter Site home page Giter Site logo

ryantm / agenix Goto Github PK

View Code? Open in Web Editor NEW
1.4K 19.0 113.0 222 KB

age-encrypted secrets for NixOS and Home manager

Home Page: https://matrix.to/#/#agenix:nixos.org

License: Creative Commons Zero v1.0 Universal

Nix 84.64% Shell 15.36%
nixos encryption

agenix's Introduction

agenix - 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.

Contents

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

  • Password-protected ssh keys: since age does not support ssh-agent, password-protected ssh keys do not work well. For example, if you need to rekey 20 secrets you will have to enter your password 20 times.

Installation

Install via niv

First add it to niv:

$ niv add ryantm/agenix

Install module via niv

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

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

Install CLI via niv

To install the agenix binary:

{
  environment.systemPackages = [ (pkgs.callPackage "${(import ./nix/sources.nix).agenix}/pkgs/agenix.nix" {}) ];
}

Install via nix-channel

As root run:

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

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

Install CLI via nix-channel

To install the agenix binary:

{
  environment.systemPackages = [ (pkgs.callPackage <agenix/pkgs/agenix.nix> {}) ];
}

Install via fetchTarball

Install module via fetchTarball

Add the following to your configuration.nix:

{
  imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.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";
      # update hash from nix build output
      sha256 = "";
    }}/modules/age.nix"
  ];
}

Install CLI via fetchTarball

To install the agenix binary:

{
  environment.systemPackages = [ (pkgs.callPackage "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/pkgs/agenix.nix" {}) ];
}

Install via Flakes

Install module via Flakes

{
  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
    nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
      # change to your system:
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
        agenix.nixosModules.default
      ];
    };
  };
}

Install CLI via Flakes

You can run the CLI tool ad-hoc without installing it:

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

But you can also add it permanently into a NixOS module (replace system "x86_64-linux" with your system):

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

e.g. inside your flake.nix file:

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

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

    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:

    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;
    }

    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:
      $ ssh-keyscan <ip-address>
      ... ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1
      ...
    • from GitHub like https://github.com/ryantm.keys.
  4. Create a secret file:

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

    {
      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:

    {
      users.users.user1 = {
        isNormalUser = true;
        passwordFile = config.age.secrets.secret1.path;
      };
    }

    You can reference the mount path to the (later) unencrypted secret already in your other configuration. So config.age.secrets.secret1.path will contain the path /run/agenix/secret1 by default.

  7. Use nixos-rebuild or another deployment tool of choice as 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:

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

    $ agenix -e secret1.age -i ~/.ssh/id_ed25519

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:

{
  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:

{
  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:

{
  users.users.ryantm = {
    isNormalUser = true;
    passwordFile = config.age.secrets.passwordfile-ryantm.path;
  };
}
builtins.readFile anti-pattern
{
  # 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:

{
  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:

{
  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:

{
  age.secrets.nginx-htpasswd = {
    file = ../secrets/nginx.htpasswd.age;
    mode = "770";
    owner = "nginx";
    group = "nginx";
  };
}

age.secrets.<name>.symlink

age.secrets.<name>.symlink is a boolean. If true (the default), secrets are symlinked to age.secrets.<name>.path. If false, secrets are copied to age.secrets.<name>.path. Usually, you want to keep this as true, because it secure cleanup of secrets no longer used. (The symlink will still be there, but it will be broken.) If false, you are responsible for cleaning up your own secrets after you stop using them.

Some programs do not like following symlinks (for example Java programs like Elasticsearch).

Example:

{
  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:

{
  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:

{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:

{
    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:

{
    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:

{
    age.secretsMountPoint = "/run/secret-generations";
}

agenix CLI reference

agenix - edit and rekey age secret files

agenix -e FILE [-i PRIVATE_KEY]
agenix -r [-i PRIVATE_KEY]

options:
-h, --help                show help
-e, --edit FILE           edits FILE using $EDITOR
-r, --rekey               re-encrypts all secrets with specified recipients
-d, --decrypt FILE        decrypts FILE to STDOUT
-i, --identity            identity to use when decrypting
-v, --verbose             verbose output

FILE an age-encrypted file

PRIVATE_KEY a path to a private SSH key used to decrypt file

EDITOR environment variable of editor to use when editing FILE

If STDIN is not interactive, EDITOR will be set to "cp /dev/stdin"

RULES environment variable with path to Nix file specifying recipient public keys.
Defaults to './secrets.nix'

Rekeying

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

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

Overriding age binary

The agenix CLI uses age by default as its age implemenation, you can use the rage implementation with Flakes like this:

{pkgs,agenix,...}:{
  environment.systemPackages = [
    (agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
  ];
}

Community and Support

Support and development discussion is available here on GitHub and also through Matrix.

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 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 is as of 19th June 2024 NOT Post-Quantum Safe 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 to store your keys now for later decryption including the case where a major vulnerability is found that would expose the secrets. See FiloSottile/age#578 for details.

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

nix flake check

You can run the integration tests in interactive mode like this:

nix run .#checks.x86_64-linux.integration.driverInteractive

After it starts, enter run_tests() to run the tests.

Acknowledgements

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

agenix's People

Contributors

aluisioasg avatar ambroisie avatar chuangzhu avatar cobaltcause avatar cole-h avatar edrex avatar felixscheinost avatar gabysbrain avatar hansemschnokeloch avatar jian-lin avatar jtojnar avatar kraem avatar kreyren avatar malteneuss avatar montchr avatar n8henrie avatar nixinator avatar oddlama avatar oluceps avatar rien avatar ryantm avatar samuelefacenda avatar scrumplex avatar shivak avatar timhae avatar veehaitch avatar whentze avatar willpower3309 avatar winny- avatar ymarkus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

agenix's Issues

TODO: add README with threat-model

People unfamiliar with age might be surprised that secrets are not authenticated.
This means that every attacker that has write access to the repository can modify secrets because public keys are exposed.
This seems not a problem on the first glance because changing configuration itself could
expose secrets easily. However it is easier to review configuration changes rather than random secrets i.e. 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.

system.activationScripts.agenixRoot.deps is undefined

Using agenix at c27b633, nixpkgs at 773ef5ae, trying to build a NixOS configuration from flake.nix. Running into the following error:

$ nixos-rebuild build --flake '.#'                                                                                                                                                           
building the system configuration...                                                                                                                                                                                                 
error: --- EvalError --------------------------------------------------------------------------------------------------------------- nix
at: (61:28) in file: /nix/store/vkn2kvjvgy5dh93i0ra5f9828h0c2s79-source/lib/strings-with-deps.nix                                       
                                                                                                                                        
    60|           if isAttrs entry then                                                                                                 
    61|             let x = f done entry.deps;                                                                                          
      |                            ^                                                                                                    
    62|                 y = f x.done (tail todo);                                                                                       
                                                                                                                                        
attribute 'deps' missing                                                                                                                
(use '--show-trace' to show detailed location information)

Downgrading to 204bd95d makes the error go away.

I suspect the source is ecee2c7, as adding

system.activationScripts.agenixRoot.deps = []

to my NixOS configuration also makes the error go away.

Thank you!

[ERROR i18n_embed::requester] Unable to parse your locale: ParseError(InvalidLanguage)

Encountered on 20.09, resulting in secrets not being decrypted.

dmesg | grep stage-2-
# [ERROR i18n_embed::requester] Unable to parse your locale: ParseError(InvalidLanguage)

The fix is:

{config, pkgs, lib, ...}:
{
  system.activationScripts.agenixFixLocale = lib.stringAfter [ "stdio" ] ''
    export LANG=${config.i18n.defaultLocale} \
           LOCALE_ARCHIVE=${config.i18n.glibcLocales}/lib/locale/locale-archive
  '';
}

Likely, locale should be set for stage-2 in here, since other things might depend on it as well, but having experienced no side-effects without agenix, can't be certain. Maybe it's just a rust thing, no idea.

Activation of system should fail if secret file is not present

Hey, very cool project :)

I played around with it and noticed that the system will be activated even though the file configured as secret file is not present. Because other services depend on the decrypted file being present, it should fail the activation and print an error message to the user.

Feature: restart systemd service when relevant secret updated

If a systemd service depends on a secret, and agenix is managing that secret, it would be nice to restart the service on nixos-rebuild switch. Now, at the moment agenix has no way of knowing which service depends on what secret, so we could maybe add a field to specify that per-secret. Alternatively, a more general solution which is what morph does, is to just add a field for a command (or maybe script) to be run when the secret is deployed / changes.

The way I am currently managing this is via systemd paths:

{
    systemd.paths."secret-watcher" = {
      wantedBy = [ "multi-user.target" ];
      pathConfig = {
        PathModified = config.agenix."secret".path;
      };
    };
    systemd.services."secret-watcher" = {
      serviceConfig = {
        Type = "oneshot";
        ExecStart = "systemctl restart foo.service";
      };
    };
}

I'm not sure if there is a better nix-internal way to do this, but it shouldn't be too hard to translate this solution into an agenix-internal one. I'm happy to implement it if people like the idea.

Support for ssh-keys with passphrases

If I run agenix -e secret1.age and hit CTRL+C when it's asking for my passphrase. Then if I try to type stuff in my prompt, I don't see what I type.

[bbigras@desktop:~/nix-config]$ agenix -e secret1.age
Type passphrase for OpenSSH key '/home/bbigras/.ssh/id_ed25519':

Maybe it's a problem with the askpass thing or whatever.

Peek 18-12-2020 14-54

Feel free to rename the issue's title.

Don't decrypt if secrets haven't changed from previous activation

Currently, agenix decrypts secrets every time the system is activated, even if the secrets haven't changed. Ideally, if the secrets haven't changed, the activation script would detect this and skip decrypting that secret. I can't really think of how this could be implemented though, hence the opening of this issue.

Can't get `agenix` into `nix shell`

I have the following shell.nix and nix shell fails because of a hash mismatch of rage-unstable. Am I doing something wrong?

I'm tracking unstable and nix-shell -p rage works great and rage --version prints 0.6.0 within that shell.

shell.nix

with (import <nixpkgs> {});
let
  imports =
    let agenixCommit = "204bd95";
    in {
        agenix = (callPackage "${builtins.fetchTarball {
                url = "https://github.com/ryantm/agenix/archive/${agenixCommit}.tar.gz";
                sha256 = "0xpfq3mnpnj7ygl4yphm8mvlhbx5a87p0nh3cjgnd2v1l4m0044g";
        }}" {}).agenix;
  };
in mkShell {
  buildInput = [
        imports.agenix
  ];
}

tail of nix-shell output

   Vendoring zeroize_derive v1.0.0 (/build/source/cargo-home.MaY/registry/src/github.com-1ecc6299db9ec823/zeroize_derive-1.0.0) to rage-unstable-2020-09-05-vendor.tar.gz/zeroize_derive
   Vendoring zip v0.5.6 (/build/source/cargo-home.MaY/registry/src/github.com-1ecc6299db9ec823/zip-0.5.6) to rage-unstable-2020-09-05-vendor.tar.gz/zip
To use vendored sources, add this to your .cargo/config.toml for this project:

installing
hash mismatch in fixed-output derivation '/nix/store/wbr2864sjaqfd9zgyj4hg71b7jzfwajj-rage-unstable-2020-09-05-vendor.tar.gz':
  wanted: sha256:0r3n5x89d4r5lmz3lxn8yrs52vkbdgag1qbnnffjdcpin6kns0s3
  got:    sha256:149wg4xnzn1fk8vwhgz4aqmqm2rqbl5yhpkjqlqsha6k9nqfmhw1
cannot build derivation '/nix/store/2wa4107rhbnaxpm8lndcrbfr42ir7lfz-rage-unstable-2020-09-05.drv': 1 dependencies couldn't be built
cannot build derivation '/nix/store/iqwbqlnqvmi6hqn6v46rbbx5nippjhzi-agenix.drv': 1 dependencies couldn't be built
error: build of '/nix/store/iqwbqlnqvmi6hqn6v46rbbx5nippjhzi-agenix.drv' failed

need to hard code user key paths

I have a few keys which are only encrypted with my user ssh keys to keep them consistent across systems. On a nixos-rebuild switch I was getting an error:

decrypting /nix/store/ddsxinl7ixlmfhlay848zy8qg916f1ks-google-vdirsyncer.age to /run/secrets/google-vdirsyncer...
Error: No matching keys found

[ Did rage not do what you expected? Could an error be more useful? ]
[ Tell us: https://str4d.xyz/rage/report                            ]
chmod: cannot access '/run/secrets/google-vdirsyncer.tmp': No such file or directory
chown: cannot access '/run/secrets/google-vdirsyncer.tmp': No such file or directory
mv: cannot stat '/run/secrets/google-vdirsyncer.tmp': No such file or directory

I traced this down to the fact that age,sshKeyPaths defaults to only the system rsa and ed25519 keys but does not try and look in user directories. Is this intentional? If so, I can update the documentation to reflect this but otherwise this will need some changes to the nixos module.

Thanks for an awesome package! This has made things so much easier!

Support for aarch64

I am trying to setup agenix for a raspberry pi 3 (aarch64) and unfortunately after several hours of compiling it errors because criterion-cycles-per-byte only supports x86 platforms.

Since this dependency is only used as part of the benchmarks, this dependency may actually not be needed.

It is marked as supported on aarch64-linux in nixpkgs, but I cannot find any build of it in hydra.

Until this issue is resolved, I would mention that this project does not support ARM in the README.

attribute 'agenixRoot' missing

After a recent update of nixpkgs unstable, the system rebuild fails with this error message "attribute 'agenixRoot' missing".

I found this pull request NixOS/nixpkgs#136605 has changes to users/groups and system activation scripts, but sadly I can't quite understand how does the activation process of NixOS work.

Hope you can have a look of it :)

Incompatible with setting GRUB password(s)

Agenix should decrypt secrets before boot.loader.grub.users.«name».passwordFile is required, so that a password for GRUB can be set.

(Currently, a workaround is to run nixos-rebuild test before nixos-rebuild switch.)

One-way encryption, ad-hoc encryption for users, without private keys

The CLI instructions show an edit of an existing secret and having access to private keys necessary to do so. I may want to set up a situation where the ssh private key is unrecoverable by the user who needs to encrypt secrets. In such case, a one-way encryption workflow is necessary. The ad-hoc pathway to get encrypted secrets into the store, if there is one, is not documented on the README and needs to be pulled out of code. A user who can re-encrypt secrets can always control the contents of secrets without needing to access them, a blind edit.

I'm looking into the management of decryption keys still.

This issue is motivated by dotfiles use cases with multiple users, not the root user case.

Feature: templates based on secrets

Motivation

It has come up a couple of times that I've wanted a file to mostly not be encrypted, but have a few secrets in it (for instance, a config file that has a password in it). Of course, one can just encrypt the whole file, but it's less elegant, makes it awkward to share a common secret between files that depend on it, and makes it so only those with permissions to decrypt the secret can change other things in the file.

Proposal

The solution that I thought of is having another kind of secret that is specified by a template, and a list of secrets it depends on. Agenix would then splice in the secrets at activation time. I'm working on an implementation and will make a PR when done, but I wanted to see if people liked this idea in the meantime.

Problems

The downside is it adds some complexity to the code. We would need logic to say "if it's a regular secret, do X, if it's a template secret do Y". This is maybe okay in this case, but it worries me that this starts to be complexity creep and things become harder to maintain. I guess the question comes down to: is it worth it in this case? Is this a common enough use-case to include? It is for me, but I don't know whether other people use agenix in this way a lot.

Alternatives

In theory, this doesn't need to be managed by agenix. I could create an activationScript that runs after agenix and pieces the secrets together. However, that requires some amount of repetition of code, and I never need to do this except when managing secrets. Usually I would paste things together at build time.

Error: Header is invalid

I got this error but I have no idea how to resolve. I have a secrets.nix like this:

let
  macbook2017 =
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIESRdLYXkaqruy2cq7LFiwuYpkz+BIEGoaDz5pl58o+5 [email protected]";
in { "telegram-bot.age".publicKeys = [ macbook2017 ]; }

And I run it like this:

touch telegram-bot.age
nix run github:ryantm/agenix -- -e telegram-bot.age

And I got this:

warning: unknown setting 'experimental-features'
Error: Header is invalid

[ Did rage not do what you expected? Could an error be more useful? ]
[ Tell us: https://str4d.xyz/rage/report                            ]

Is this an upstream issue?

agenix has no meta description

so it doesn't explain itself, for example in a devshell:

[secrets]

  agenix
  ssh-show-ed25519     - ssh-show-ed25519 <user@hostName> | Show target host's SSH ed25519 key

I'll give it a shot

umask is inverted

pennae ryantm: since we see you, i think you inverted the umask in agenix by accident
ryantm pennae: Are you talking about

(umask 0400; LANG=${config.i18n.defaultLocale} ${ageBin} --decrypt ${identities} -o "$TMP_FILE" "${secretType.file}")
?
pennae yeah
ryantm pennae: Could you be more specific what you mean about "inverted"?
pennae as it stands it'll clear the owner-read bit and leave all others untouched either we can't brain right now or you probably meant 0177
ryantm pennae: I think you're right; thanks for letting me know. I'll look into fixing it.
pennae also there might be a bug with a root secret "something.tmp" and a non-root secret "something" that'll delete the root secret? not sure about that tho

Asking me for my RSA key password

secrets.nix:

let
  rocky = "ssh-ed25519";
  elaine = "ssh-ed25519";
in
{
  "secret1.age".publicKeys = [ rocky elaine ];
}

Command that fails only after second entry:

agenix -e secret1.age

Thank you in advance!

Workflow for new builds - General Help - Question

I am relatively new to nix, so I am having a hard time determining what the workflow would be for a new build. Meaning we do not know the public key of the host, for we have yet to build the system.

RFC: agenix-rs tool

Over the past few weeks, I've been working on a replacement for the shell script agenix tool, written in Rust. I've published it at https://github.com/cole-h/agenix-rs/.

I really like how sops-nix has the sops tool to edit binaries and then encrypt them to the proper recipients based on path globs specified in a config file, and I really dislike how the agenix script required a secrets.nix file for the identities.

I'm hoping for any and all feedback on it, and in the future it would be nice to replace the agenix shell script with agenix-rs.

Darwin: Undefined variable `Foundation`

Hi!

I recently added Foundation to the buildInputs on darwin. (PR #20)

But unfortunately PR #19 broke support on my machine again.

The build complains about a missing attribute pkgs.darwin.Foundation while pkgs.darwin.apple_sdk.frameworks.Foundation does work for me.

My PR added darwin.apple_sdk.frameworks.Foundation while the other PR changed it to darwin.Foundation.

I am not sure if this is the result of merge gone wrong or whether somethings wrong with my setup.

Thanks!
Felix

Incompatible with dry activation of users/groups

nixpkgs PR 136605 adds a feature to perform a dry-run to show which users/groups will be added or removed without switching to the configuration. The PR enables this feature for the system.activationScripts.users activation script by default which Agenix also uses.

This breaks nixos-rebuild because the agenixRoot attribute is not available during the dry-run. Disabling dryRun for the system.activationScripts.users script resolves the issue.

system.activationScripts.users.supportsDryActivation = lib.mkForce false;

It looks like agenix can check if the NIXOS_ACTION environment variable equals "dry-activate" to alter behavior during a dry-run. I'm not sure what all is required to make the agenixRoot attribute available.

Here's the full stack trace:

   error: attribute 'agenixRoot' missing

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/strings-with-deps.nix:72:71:

       71|           else if done ? ${entry} then f done (tail todo)
       72|           else f (done // listToAttrs [{name = entry; value = 1;}]) ([predefined.${entry}] ++ tail todo);
         |                                                                       ^
       73|     in (f {} arg).result;

   … while evaluating 'f'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/strings-with-deps.nix:61:17:

       60|     let
       61|       f = done: todo:
         |                 ^
       62|         if todo == [] then {result = []; inherit done;}

   … from call site

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/strings-with-deps.nix:72:16:

       71|           else if done ? ${entry} then f done (tail todo)
       72|           else f (done // listToAttrs [{name = entry; value = 1;}]) ([predefined.${entry}] ++ tail todo);
         |                ^
       73|     in (f {} arg).result;

   … while evaluating 'f'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/strings-with-deps.nix:61:17:

       60|     let
       61|       f = done: todo:
         |                 ^
       62|         if todo == [] then {result = []; inherit done;}

   … from call site

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/strings-with-deps.nix:66:21:

       65|           if isAttrs entry then
       66|             let x = f done entry.deps;
         |                     ^
       67|                 y = f x.done (tail todo);

   … while evaluating the attribute 'result'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/strings-with-deps.nix:68:18:

       67|                 y = f x.done (tail todo);
       68|             in { result = x.result ++ [entry.text] ++ y.result;
         |                  ^
       69|                  done = y.done;

   … while evaluating 'textClosureList'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/strings-with-deps.nix:59:33:

       58|
       59|   textClosureList = predefined: arg:
         |                                 ^
       60|     let

   … from call site

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/strings-with-deps.nix:76:35:

       75|   textClosureMap = f: predefined: names:
       76|     concatStringsSep "\n" (map f (textClosureList predefined names));
         |                                   ^
       77|

   … while evaluating 'textClosureMap'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/strings-with-deps.nix:75:35:

       74|
       75|   textClosureMap = f: predefined: names:
         |                                   ^
       76|     concatStringsSep "\n" (map f (textClosureList predefined names));

   … from call site

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/nixos/modules/system/activation/activation-script.nix:40:9:

       39|
       40|       ${textClosureMap id (withHeadlines) (attrNames withHeadlines)}
         |         ^
       41|

   … while evaluating 'systemActivationScript'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/nixos/modules/system/activation/activation-script.nix:20:33:

       19|
       20|   systemActivationScript = set: onlyDry: let
         |                                 ^
       21|     set' = filterAttrs (_: v: onlyDry -> v.supportsDryActivation) (mapAttrs (_: v: if isString v then (noDepEntry v) // { supportsDryActivation = false; } else v) set);

   … from call site

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/nixos/modules/system/activation/activation-script.nix:135:17:

      134|       internal = true;
      135|       default = systemActivationScript (removeAttrs config.system.activationScripts [ "script" ]) true;
         |                 ^
      136|     };

   … while evaluating the attribute 'default'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/nixos/modules/system/activation/activation-script.nix:135:7:

      134|       internal = true;
      135|       default = systemActivationScript (removeAttrs config.system.activationScripts [ "script" ]) true;
         |       ^
      136|     };

   … while evaluating the attribute 'value.content'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/modules.nix:708:14:

      707|     { _type = "override";
      708|       inherit priority content;
         |              ^
      709|     };

   … while evaluating the attribute 'value._type'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/modules.nix:648:73:

      647|       highestPrio = foldl' (prio: def: min (getPrio def) prio) 9999 defs;
      648|       strip = def: if def.value._type or "" == "override" then def // { value = def.value.content; } else def;
         |                                                                         ^
      649|     in {

   … while evaluating anonymous lambda

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/modules.nix:547:19:

      546|           # Avoid sorting if we don't have to.
      547|           if any (def: def.value._type or "" == "order") defs''.values
         |                   ^
      548|           then sortProperties defs''.values

   … from call site

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/modules.nix:547:14:

      546|           # Avoid sorting if we don't have to.
      547|           if any (def: def.value._type or "" == "order") defs''.values
         |              ^
      548|           then sortProperties defs''.values

   … while evaluating the attribute 'values'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/modules.nix:551:9:

      550|       in {
      551|         values = defs''';
         |         ^
      552|         inherit (defs'') highestPrio;

   … while evaluating the attribute 'mergedValue'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/modules.nix:557:5:

      556|     # Type-check the remaining definitions, and merge them. Or throw if no definitions.
      557|     mergedValue =
         |     ^
      558|       if isDefined then

   … while evaluating the option `system.dryActivationScript':

   … while evaluating the attribute 'value'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/modules.nix:525:9:

      524|     in warnDeprecation opt //
      525|       { value = builtins.addErrorContext "while evaluating the option `${showOption loc}':" value;
         |         ^
      526|         inherit (res.defsFinal') highestPrio;

   … while evaluating anonymous lambda

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/modules.nix:140:72:

      139|           # For definitions that have an associated option
      140|           declaredConfig = mapAttrsRecursiveCond (v: ! isOption v) (_: v: v.value) options;
         |                                                                        ^
      141|

   … from call site

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/attrsets.nix:304:20:

      303|               then recurse (path ++ [name]) value
      304|               else f (path ++ [name]) value;
         |                    ^
      305|         in mapAttrs g set;

   … while evaluating 'g'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/lib/attrsets.nix:301:19:

      300|           g =
      301|             name: value:
         |                   ^
      302|             if isAttrs value && cond value

   … from call site

   … while evaluating the attribute 'system.dryActivationScript'

   … while evaluating the attribute 'dryActivationScript' of the derivation 'nixos-system-infinitejest-21.11.20210908.09cd65b'

   at /nix/store/6dcqil119qr3sad2lp9ykkkc852ppmqm-source/pkgs/stdenv/generic/make-derivation.nix:205:7:

      204|     // (lib.optionalAttrs (attrs ? name || (attrs ? pname && attrs ? version)) {
      205|       name =
         |       ^
      206|         let

Confusing behaviour with age encryption/decryption

I've managed to set up agenix from unstable to a point where it appears to be working during the nixos-rebuild process. I've been using ssh host keys (located under /etc/ssh/) as outlined in:

map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys)


This is my secrets.nix file I have:

let
  key =
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPWMxDpxfrlXhAyln0+MKZs7q3i1VimlHhGgUxVVaeYY Oxygen";
in {
  "ngrokConfig.age" = {
    publicKeys = [ key ];
  };
}

The issue I have arises when I use the agenix tool by command agenix -e ngrokConfig.age. When I first initialise the file it works, opening a blank file and jumping into the neovim process I have set as my editor. However the second time I try to do it, it gives back an error: No identity found to decrypt ngrokConfig.age. Try adding an SSH key at /home/padraic/.ssh/id_rsa or /home/padraic/.ssh/id_ed25519 or using the --identity flag to specify a file.

Using the -v option:

hosts/Oxygen/secrets on  main [⇡]
❯ agenix -e ngrokConfig.age -v
+ test 0 -gt 0
+ RULES=./secrets.nix
+ trap cleanup 0 2 3 15
+ '[' 0 -eq 1 ']'
+ edit ngrokConfig.age
+ FILE=ngrokConfig.age
++ /nix/store/iyw8r3bfd2jgvdp0hwsn97h7a6ps1zxx-nix-2.3.11/bin/nix-instantiate --eval -E '(let rules = import ./secrets.nix; in builtins.concatStringsSep "\n" rules."ngrokConfig.age".publicKeys)'
++ /nix/store/g34ldykl1cal5b9ir3xinnq70m52fcnq-gnused-4.8/bin/sed 's/\\n/\n/g'
++ /nix/store/g34ldykl1cal5b9ir3xinnq70m52fcnq-gnused-4.8/bin/sed 's/"//g'
warning: unknown setting 'experimental-features'
+ KEYS='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPWMxDpxfrlXhAyln0+MKZs7q3i1VimlHhGgUxVVaeYY Oxygen'
+ '[' -z 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPWMxDpxfrlXhAyln0+MKZs7q3i1VimlHhGgUxVVaeYY Oxygen' ']'
++ /nix/store/761kp8ybw3yq5hiws56h0vxnjmyx9as0-mktemp-1.7/bin/mktemp -d
+ CLEARTEXT_DIR=/tmp/tmp.6J8hPwoNbz
++ basename ngrokConfig.age
+ CLEARTEXT_FILE=/tmp/tmp.6J8hPwoNbz/ngrokConfig.age
+ '[' -f ngrokConfig.age ']'
+ DECRYPT=("${DEFAULT_DECRYPT[@]}")
+ '[' -f /home/padraic/.ssh/id_rsa ']'
+ '[' -f /home/padraic/.ssh/id_ed25519 ']'
+ [[ --decrypt != *\-\-\i\d\e\n\t\i\t\y* ]]
+ echo 'No identity found to decrypt ngrokConfig.age. Try adding an SSH key at /home/padraic/.ssh/id_rsa or /home/padraic/.ssh/id_ed25519 or using the --identity flag to specify a file.'
No identity found to decrypt ngrokConfig.age. Try adding an SSH key at /home/padraic/.ssh/id_rsa or /home/padraic/.ssh/id_ed25519 or using the --identity flag to specify a file.
+ exit 1
+ cleanup
+ '[' '!' -z x ']'
+ rm -rf /tmp/tmp.6J8hPwoNbz
+ '[' '!' -z ']'

<br/>

If I try doing so directly with the host key using the `--identity` flag I get:

```bashhosts/Oxygen/secrets on  main [⇡]
❯ agenix -e ngrokConfig.age -i /etc/ssh/ssh_host_ed25519_key
Error: Permission denied (os error 13)

[ Did rage not do what you expected? Could an error be more useful? ]
[ Tell us: https://str4d.xyz/rage/report                            ]

And using running the same command as root user through sudo:

❯ EDITOR=nvim sudo agenix -e ngrokConfig.age -i /etc/ssh/ssh_host_ed25519_key -v
+ test 0 -gt 0
+ RULES=./secrets.nix
+ trap cleanup 0 2 3 15
+ '[' 0 -eq 1 ']'
+ edit ngrokConfig.age
+ FILE=ngrokConfig.age
++ /nix/store/iyw8r3bfd2jgvdp0hwsn97h7a6ps1zxx-nix-2.3.11/bin/nix-instantiate --eval -E '(let rules = import ./secrets.nix; in builtins.concatStringsSep "\n" rules."ngrokConfig.age".publicKeys)'
++ /nix/store/g34ldykl1cal5b9ir3xinnq70m52fcnq-gnused-4.8/bin/sed 's/"//g'
++ /nix/store/g34ldykl1cal5b9ir3xinnq70m52fcnq-gnused-4.8/bin/sed 's/\\n/\n/g'
warning: unknown setting 'experimental-features'
+ KEYS='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPWMxDpxfrlXhAyln0+MKZs7q3i1VimlHhGgUxVVaeYY Oxygen'
+ '[' -z 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPWMxDpxfrlXhAyln0+MKZs7q3i1VimlHhGgUxVVaeYY Oxygen' ']'
++ /nix/store/761kp8ybw3yq5hiws56h0vxnjmyx9as0-mktemp-1.7/bin/mktemp -d
+ CLEARTEXT_DIR=/tmp/tmp.yL3LXJNTNO
++ basename ngrokConfig.age
+ CLEARTEXT_FILE=/tmp/tmp.yL3LXJNTNO/ngrokConfig.age
+ '[' -f ngrokConfig.age ']'
+ DECRYPT=("${DEFAULT_DECRYPT[@]}")
+ '[' -f /root/.ssh/id_rsa ']'
+ '[' -f /root/.ssh/id_ed25519 ']'
+ [[ --decrypt --identity /etc/ssh/ssh_host_ed25519_key != *\-\-\i\d\e\n\t\i\t\y* ]]
+ DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE")
+ /nix/store/rc7g2395vw74cd625drh32rsvglallhi-rage-0.6.0/bin/rage --decrypt --identity /etc/ssh/ssh_host_ed25519_key -o /tmp/tmp.yL3LXJNTNO/ngrokConfig.age ngrokConfig.age
+ cp /tmp/tmp.yL3LXJNTNO/ngrokConfig.age /tmp/tmp.yL3LXJNTNO/ngrokConfig.age.before
/run/current-system/sw/bin/agenix: line 123: EDITOR: unbound variable
++ cleanup
++ '[' '!' -z x ']'
++ rm -rf /tmp/tmp.yL3LXJNTNO
++ '[' '!' -z ']'

~I'm inclined to think that I may be going about the encryption process incorrectly and it was only intended to be a one-time edit of the file. I can get by well enough by just deleting the file and recreating it again, however it is a bit tedious and would be preferred if I can make incremental changes to secret configs and the such. ~

All above aside, great tool btw

Update:

On further reading of the agenix.nix file, I believe I understand the issue better. I have on my machine two ssh keys, one under /etc/ssh, the "host" key and then a personal ssh key under ~/.ssh. The script will automatically add the ~/.ssh key as a recipient if it exists but does not appear to search under /etc/hosts/ for the decryption, only ~/ssh.
I think it would be a good default to lookup host ssh keys in the event of running as root (as they could only be accessed by root).

home-manager integration?

Is it possible to use agenix from inside a home-manager module? I don't see anything talking about this, so I'm guessing it's not possible, but I figured I'd check.

I realize I can use an agenix secret from inside a home-manager module by just accessing the /run/secrets/blah file, but I'd like to keep the file generation (i.e. age.secrets.blah.file = "${self}/secrets/blah";) close to the point of use, rather than off in it's own NixOS module.

error unsupported ssh identity

Here is what I did
agenix -e postgresPassword.age -i ~/.ssh/hetzner
edit the file and then quit

when I try to edit again though I get the following error. It seems it's not trying to use the key I provided

❯ agenix -e postgresPassword.age -i ~/.ssh/hetzner
Error: Unsupported SSH identity: ⁨/Users/raphael/.ssh/id_rsa

Here are the contents of my secrets.nix

  raphael = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGyQSeQ0CV/qhZPre37+Nd0E9eW+soGs+up6a/bwggoP [email protected]";
  hetzner-AX41 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAi5EeQLa6AwhHI2QO4QGtOArRBzmz3q7/pGmiiR0pV5";
in
{
  "postgresPassword.age".publicKeys = [ raphael hetzner-AX41 ];
}

I have verified that the content of the ~/.ssh/hetzner.pub key match that of what I put in the raphael variable in my secrets.nix.

Maybe I am missed something somewhere?

pipe (generated) key to agenix

I just got agenix set up today. Yay!

I'd like to be able to generate machine keys without a lot of manual steps. It's a small, useful thing and I'd be happy to submit and impl if we can settle on a spec.

Rather than integrating a particular password gen how about adding support for taking the cleartext from stdin?

Either overload the existing -e mode:

cat /dev/urandom | head -c 128 | agenix -e mysecret.age

or add a new mode like -i

wdyt?

Bootstrap / recreate secrets

In a situation where an operator want's to bootstrap a prefabricated environment, or in a situation where an operator has to "break the glass" and cycle the root secrets,

it would be useful to store create instructions alongside the encryption definition in secrets.nix.


Example bootstrap/recreate scripts could be:

encrypt="$(nomad operator keygen)"
echo '{}' | jq --arg encrypt "$encrypt" '.server.encrypt = $encrypt'
export PATH="${lib.makeBinPath (with pkgs; [ xkcdpass ])}"
xkcdpass -n 24

Without further research, I would assume agenix cli contracting output on stdout for subsequent encryption would be good enough.

/cc @veehaitch if @ryantm is interested, I'd probably implement this for ragenix asap.

Restrict folder permissions

Hello, I'm trying out agenix as a replacement for my current secret management scheme (krops + pass). Is there a reason why the /run/secrets/ directory has so open permissions of 755? I don't think others should be able to list the directory at all.

I think it would provide additional security at virtually no cost to set it to something like 0751 like sops-nix does or even to 440 so that only root can read them. If changing this in general is a problem, it would be nice to at least have the option to change them myself.

Secrets owned by non-root users are no longer readable by that user

Since 0.10 / #27

Relevant namei -l output:

drwxr-xr-x root root /
drwxr-xr-x root root run
lrwxrwxrwx root root agenix -> /run/agenix.d/2
drwxr-xr-x root root   /
drwxr-xr-x root root   run
drwxr-x--- root keys   agenix.d
                       2 - Permission denied

Even if I set symlink = false this doesn't work, since even if the secrets are decrypted to /run/agenix, they are actually decrypted to /run/agenix.d/2, since /run/agenix is already a symlink to /run/agenix.d/2 at that point. (Which kind of defeats the point of symlink = false IIUC. Maybe this should be a separate issue?)

I can add the relevant user(s) to the keys group to fix this behaviour, but this is not documented anywhere.

Symlink secrets like sops-nix does?

I think I would prefer if agenix would ln -sf /run/secrets/${name} ${path} files to their desired locations rather than mv -f them. When path is outside of /run/secrets, this means the files will not be cleaned up automatically (e.g. on reboot).

Is there a reason agenix decided not to go this route?

Removing old secrets

I noticed that the current logic, which relies on activationScripts, leaves old secrets around.

E.g., if I rename a secret in my secrets.nix, the new and the old secret will both be present on the machine.

Have you considered using systemd oneshot units? AFAICT, those are garbage-collected on system switch.

the cli is broken when using agenix via Flakes

In the readme is stated that you don't need to install agenix to use the cli via Flakes.
But that doesn't seem to work properly since the paths get messed up completely.

e.g when running nix run github:ryantm/agenix -- -e test.age you first get the message that there is no ./secrets.nix (even if its there). And if you workarround it by setting the absolute path via RULES env var. You get the editor to write down the secret, but the secret file is written to somewere!? (at least not to the directory where you would expect it).

when agenix is installed it works just fine as expected.

`secrets/` folder best practices

It seems that the standard way to do things is to put secrets.nix as well as any secrets in a special secrets/ folder. To me it seems more natural to have the secrets.nix file at top-level, and have the actual secrets spread out through the repository (close to the code that uses them).

Is there a reason the former style is preferred?

Needs better key detection logic

done <<<"$((find ~/.ssh -maxdepth 1 -type f -not -name "*pub" -not -name "config" -not -name "authorized_keys" -not -name "known_hosts") || exit 1)"

My ~/.ssh is a symbolic link into a dotfiles repository, and find will skip over it unless passed the -H or -L flags.

Even with that fixed, I see it's going to pass to rage files that are not SSH keys, such as known_hosts.old that is automatically generated by ssh-keygen.

I think it'd be better to be conservative here and only auto-detect the standard id_ed25519/id_rsa key files, and have the user rely on --identity for non-standard setups.

Decryption fails when no SSH key files are found

When no files match the find command used to auto-detect identity files (see #5), agenix ends up passing an empty filename identity to rage:

done <<<"$((find ~/.ssh -maxdepth 1 -type f -not -name "*pub" -not -name "config" -not -name "authorized_keys" -not -name "known_hosts") || exit 1)"

+ /nix/store/wqy46cyqqh9hkr542n6kyghqzh0s4v07-rage-unstable-2020-09-05/bin/rage --decrypt --identity /home/aasg/.ssh/id_rsa --identity '' -o /tmp/tmp.IlafYcjgOA/somesecret somesecret
Error: No such file or directory (os error 2)

Support rules from flake output (nixosConfigurations)

Copy-pasting here from yaxitech/ragenix#52

Just an idea I wanted to propose:

Would it be possible to support reading rules from another flake's output instead of a separate secrets.nix file?
E.g. If I have a flake defining my systems like this:

{
  outputs = { self, nixpkgs }: {

    nixosConfigurations = {
      system1 = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ({ pkgs, ... }:
            {
              # Config ...
              age.option-to-set-key = "ssh-ed25519 AAAAAAA...";
              age.secrets.secret1.file = ./secrets/secret1.age;
              age.secrets.secret2.file = ./secrets/secret2.age;
            })
        ];
      };

      system2 = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ({ pkgs, ... }:
            {
              # Config
              age.option-to-set-key = "ssh-ed25519 AAAABBB...";
              age.secrets.secret2.file = ./secrets/secret2.age;
            })
        ];
      };
    };
  };
}

nixosConfigurations could be used directly resulting in a rule set equivalent to:

let
  system1 = "ssh-ed25519 AAAAAAA..";
  system2 = "ssh-ed25519 AAAABBB..";
in
{
  "secret1.age".publicKeys = [ system1 ];
  "secret2.age".publicKeys = [ system1 system2];
}

Since you plan on supporting flakes it seems like an extra step to have to write a secrets.nix file, as the information is already present in an organized form.

aarch64-darwin support

Is it planned to support aarch64-darwin?
Currently we need to use nix-packages in Rosetta mode for this.

Gitlab - permission denied on password

Hi, I have the following

# secrets.nix
let angelisland-root = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC/XI9D42X8djPVrmRblIa6RjcN9rMoIJn8Fw8UF08K5cbzeKRhLGwMw97kur7Onr13+3QtVimhkW732YyMwL7PwdEJVzLyOCoF5EW9sHxbPMj3dKNi5c/EOruCP2Q3TT+NR8nMPmWr2ByOXUqWYiP8DMbGAZqT1h/l/7owLT+ygxn24UMrEUbeKYyVHNk7Mind7H2TuCdGoN6LXwAlSNeXldGWtytbtK0UVaiw5cYsxCcNb84GLP4XKhJvfx3z7blQWzGFIcj+BAVb0ErTDaaocNZIV0thrdfDv9JuOgJSU+4GcaLeeq/9ePek08TWfsgaT4YXS7j7gm1w9SfZksESgy3nQQ1ub+TeQTEYhp8OoWBA5ohLpYv13z4aVDy9GJdGa1aQqCXSS3Lu+g6Gas5kEAMrQxFdlext7XQAbMYe3Je29x039k2iX4MuH8R8XuDf8RnmVB0CbRbIasSlxXZ/UOlxuPgyaGepxGB7b09PFjUr8kIqh8ZJqWH6NVhy5B1b8UZXDo/5P7jS1FpPP+tTIzdHDzz4XLQdLZsJQA5c9Y6qfy1a7W7JnRHDQuddBmHnEkmQa6dAAlQ7R6MtcY2NT+QlYlgqGYAHeW9hZAOkankc7Aae0vjy1yeRq6Kk5xLkv2CtzRGVdIEW0aEvKKI05mftp1rNlOfDllCoW3waSw==";
in
{
  "angelisland.secrets.db.age".publicKeys = [ angelisland-root ];
  "angelisland.secrets.jws.age".publicKeys = [ angelisland-root ];
  "angelisland.secrets.otp.age".publicKeys = [ angelisland-root ];
  "angelisland.secrets.secret.age".publicKeys = [ angelisland-root ];
  "angelisland.passwords.db.age".publicKeys = [ angelisland-root ];
  "angelisland.passwords.root.age".publicKeys = [ angelisland-root ];
}
#gitlab-module
{config, pkgs, lib, ...}: {
 
  age.secrets."secrets.db".file = ../secrets/angelisland.secrets.db.age;
  age.secrets."secrets.jws".file = ../secrets/angelisland.secrets.jws.age;
  age.secrets."secrets.otp".file = ../secrets/angelisland.secrets.otp.age;
  age.secrets."secrets.secret".file = ../secrets/angelisland.secrets.secret.age;
  age.secrets."passwords.db".file = ../secrets/angelisland.passwords.db.age;
  age.secrets."passwords.root".file = ../secrets/angelisland.passwords.root.age;

  services.gitlab = {
    enable = true;
    https = true;
    databasePasswordFile = config.age.secrets."passwords.db".path;
    initialRootPasswordFile = config.age.secrets."passwords.root".path;
    host = "<redacted>";
    port = 443;
    user = "git";
    databaseUsername = "git";
    group = "git";
    smtp = {
      enable = true;
      address = "localhost";
      port = 25;
    };
    secrets = {
      dbFile = config.age.secrets."secrets.db".path;
      jwsFile = config.age.secrets."secrets.jws".path;
      secretFile = config.age.secrets."secrets.secret".path;
      otpFile = config.age.secrets."secrets.otp".path;
    };

};

This results in:

Dec 06 15:11:51 angelisland cnwcsrz6kxcb4ax02dfswff1443f6yhj-gitlab-config[15746]: mkdir -p /var/gitlab/state/home/.ssh: OK
Dec 06 15:11:51 angelisland cnwcsrz6kxcb4ax02dfswff1443f6yhj-gitlab-config[15746]: chmod 700 /var/gitlab/state/home/.ssh: OK
Dec 06 15:11:51 angelisland cnwcsrz6kxcb4ax02dfswff1443f6yhj-gitlab-config[15753]: /nix/store/cnwcsrz6kxcb4ax02dfswff1443f6yhj-gitlab-config: line 27: /run/agenix/passwords.db: Permission denied
Dec 06 15:11:51 angelisland cnwcsrz6kxcb4ax02dfswff1443f6yhj-gitlab-config[15750]: Database password was an empty string!
Dec 06 15:11:51 angelisland systemd[1]: gitlab-config.service: Main process exited, code=exited, status=1/FAILURE
Dec 06 15:11:51 angelisland systemd[1]: gitlab-config.service: Failed with result 'exit-code'.
Dec 06 15:11:51 angelisland systemd[1]: Failed to start gitlab-config.service.

Understanding secrets workflow

Hi. I'm currently converting my systems into nixos, and going with the flakes design already. I'm almost there, but I have a thing with agenix I cannot seem to get working.

I have a few things in my setup I do not want to push to the git repo. Secrets, that can easily stay unencrypted in my computer's memory, but that might not be such a great idea to push to the github.

So I have a nix file with my secrets, but what I can get from agenix is only the path to the unencrypted file, not the file content itself. Now let's say I have a file home.age, that has encrypted the following nix script:

{
  key = "secret";
}

Now, I'd import the data in my configuration with:

let
  home-secrets = import "${config.age.secrets.home.path}";
in {
  ...
}

But, this is not possible, and will lead to an error:

❯ sudo nixos-rebuild switch
warning: Git tree '/home/pimeys/.config/nixpkgs' is dirty
building the system configuration...
warning: Git tree '/home/pimeys/.config/nixpkgs' is dirty
error: --- RestrictedPathError ----------------------------------------------------------------------- nix
access to path '/run/secrets/home' is forbidden in restricted mode
(use '--show-trace' to show detailed location information)

I'm asking, is this even possible with agenix and flakes? What I want is to get the data out from the secrets, set them to system env vars and use them when my system boots up. If agenix is not the right tool for this, is there another way I could utilize a few tokens in nixos without needing to push them to a git repository?

default secret path broken after a reboot

Not sure how I did not notice it sooner but the secret files no longer exist in the default path /run/agenix/${name} after reboot – it is just an empty directory.

$ sudo ls -la /run/agenix
total 0
drwxr-xr-x  2 root root  40 Jan  6 21:28 .
drwxr-xr-x 23 root root 560 Jan  6 21:29 ..
Log of activation after reboot
Jan 06 21:28:11 azazel unknown: booting system configuration /nix/store/d8ndfx49x56ldl73vl9zzr8iiwh58qz6-nixos-system-azazel-22.05.20220103.78cd22c
Jan 06 21:28:11 azazel stage-2-init: running activation script...
Jan 06 21:28:11 azazel stage-2-init: + ((  _localstatus > 0  ))
Jan 06 21:28:11 azazel stage-2-init: + _localstatus=0
Jan 06 21:28:11 azazel stage-2-init: +++ readlink /run/agenix
Jan 06 21:28:11 azazel stage-2-init: ++ basename /run/agenix.d/4
Jan 06 21:28:11 azazel stage-2-init: + _agenix_generation=4
Jan 06 21:28:11 azazel stage-2-init: + ((  ++_agenix_generation  ))
Jan 06 21:28:11 azazel stage-2-init: + echo '[agenix] symlinking new secrets to /run/agenix (generation 5)...'
Jan 06 21:28:11 azazel stage-2-init: [agenix] symlinking new secrets to /run/agenix (generation 5)...
Jan 06 21:28:11 azazel stage-2-init: + mkdir -p /run/agenix.d
Jan 06 21:28:11 azazel stage-2-init: + chmod 0751 /run/agenix.d
Jan 06 21:28:11 azazel stage-2-init: + grep -q '/run/agenix.d ramfs' /proc/mounts
Jan 06 21:28:11 azazel stage-2-init: + mount -t ramfs none /run/agenix.d -o nodev,nosuid,mode=0751
Jan 06 21:28:11 azazel stage-2-init: + mkdir -p /run/agenix.d/5
Jan 06 21:28:11 azazel stage-2-init: + chmod 0751 /run/agenix.d/5
Jan 06 21:28:11 azazel stage-2-init: + ln -sfn /run/agenix.d/5 /run/agenix
Jan 06 21:28:11 azazel stage-2-init: + ((  _agenix_generation > 1  ))
Jan 06 21:28:11 azazel stage-2-init: + echo '[agenix] removing old secrets (generation 4)...'
Jan 06 21:28:11 azazel stage-2-init: [agenix] removing old secrets (generation 4)...
Jan 06 21:28:11 azazel stage-2-init: + rm -rf /run/agenix.d/4
[…]
Jan 06 21:28:11 azazel stage-2-init: + echo '[agenix] decrypting root secrets...'
Jan 06 21:28:11 azazel stage-2-init: [agenix] decrypting root secrets...
Jan 06 21:28:11 azazel stage-2-init: + _truePath=/run/agenix.d/5/bag.ogion.cz-secret
Jan 06 21:28:11 azazel stage-2-init: + echo 'decrypting '\''/nix/store/ga41w0dn9dhk12w1w2zcv7pr4w9g36v2-bag.ogion.cz-secret.age'\'' to '\''/run/agenix.d/5/bag.ogion.cz-secret'\''...'
Jan 06 21:28:11 azazel stage-2-init: decrypting '/nix/store/ga41w0dn9dhk12w1w2zcv7pr4w9g36v2-bag.ogion.cz-secret.age' to '/run/agenix.d/5/bag.ogion.cz-secret'...
Jan 06 21:28:11 azazel stage-2-init: + TMP_FILE=/run/agenix.d/5/bag.ogion.cz-secret.tmp
Jan 06 21:28:11 azazel stage-2-init: ++ dirname /run/agenix.d/5/bag.ogion.cz-secret
Jan 06 21:28:11 azazel stage-2-init: + mkdir -p /run/agenix.d/5
Jan 06 21:28:11 azazel stage-2-init: ++ dirname /run/agenix/bag.ogion.cz-secret
Jan 06 21:28:11 azazel stage-2-init: + mkdir -p /run/agenix
Jan 06 21:28:11 azazel stage-2-init: + umask u=r,g=,o=
Jan 06 21:28:11 azazel stage-2-init: + LANG=en_US.UTF-8
Jan 06 21:28:11 azazel stage-2-init: + /nix/store/b7dz2sz5mh4h3xbav9q15j7547qjhdyl-rage-0.7.1/bin/rage --decrypt -i /etc/ssh/ssh_host_rsa_key -i /etc/ssh/ssh_host_ed25519_key -o /run/agenix.d/5/bag.ogion.cz-secret.tmp /nix/store/ga41w0dn9dhk12w1w2zcv7pr4w9g36v2-bag.ogion.cz-secret.age
Jan 06 21:28:11 azazel stage-2-init: + chmod 0400 /run/agenix.d/5/bag.ogion.cz-secret.tmp
Jan 06 21:28:11 azazel stage-2-init: + chown 0:0 /run/agenix.d/5/bag.ogion.cz-secret.tmp
Jan 06 21:28:11 azazel stage-2-init: + mv -f /run/agenix.d/5/bag.ogion.cz-secret.tmp /run/agenix.d/5/bag.ogion.cz-secret
Jan 06 21:28:11 azazel stage-2-init: + '[' /run/agenix/bag.ogion.cz-secret '!=' /run/agenix/bag.ogion.cz-secret ']'
Jan 06 21:28:11 azazel stage-2-init: + _truePath=/run/agenix.d/5/blackfire-agent-server-id
Jan 06 21:28:11 azazel stage-2-init: + echo 'decrypting '\''/nix/store/89qy5kr20hb5m88sqsii2q6v5xwpibg0-blackfire-agent-server-id.age'\'' to '\''/run/agenix.d/5/blackfire-agent-server-id'\''...'
Jan 06 21:28:11 azazel stage-2-init: decrypting '/nix/store/89qy5kr20hb5m88sqsii2q6v5xwpibg0-blackfire-agent-server-id.age' to '/run/agenix.d/5/blackfire-agent-server-id'...
Jan 06 21:28:11 azazel stage-2-init: + TMP_FILE=/run/agenix.d/5/blackfire-agent-server-id.tmp
Jan 06 21:28:11 azazel stage-2-init: ++ dirname /run/agenix.d/5/blackfire-agent-server-id
Jan 06 21:28:11 azazel stage-2-init: + mkdir -p /run/agenix.d/5
Jan 06 21:28:11 azazel stage-2-init: ++ dirname /run/agenix/blackfire-agent-server-id
Jan 06 21:28:11 azazel stage-2-init: + mkdir -p /run/agenix
Jan 06 21:28:11 azazel stage-2-init: + umask u=r,g=,o=
Jan 06 21:28:11 azazel stage-2-init: + LANG=en_US.UTF-8
Jan 06 21:28:11 azazel stage-2-init: + /nix/store/b7dz2sz5mh4h3xbav9q15j7547qjhdyl-rage-0.7.1/bin/rage --decrypt -i /etc/ssh/ssh_host_rsa_key -i /etc/ssh/ssh_host_ed25519_key -o /run/agenix.d/5/blackfire-agent-server-id.tmp /nix/store/89qy5kr20hb5m88sqsii2q6v5xwpibg0-blackfire-agent-server-id.age
Jan 06 21:28:11 azazel stage-2-init: + chmod 0400 /run/agenix.d/5/blackfire-agent-server-id.tmp
Jan 06 21:28:11 azazel stage-2-init: + chown 0:0 /run/agenix.d/5/blackfire-agent-server-id.tmp
Jan 06 21:28:11 azazel stage-2-init: + mv -f /run/agenix.d/5/blackfire-agent-server-id.tmp /run/agenix.d/5/blackfire-agent-server-id
Jan 06 21:28:11 azazel stage-2-init: + '[' /run/agenix/blackfire-agent-server-id '!=' /run/agenix/blackfire-agent-server-id ']'
Jan 06 21:28:11 azazel stage-2-init: + _truePath=/run/agenix.d/5/blackfire-agent-server-token
Jan 06 21:28:11 azazel stage-2-init: + echo 'decrypting '\''/nix/store/bxxyhwbw784f95vgxj5kn3bvg0dfd5xy-blackfire-agent-server-token.age'\'' to '\''/run/agenix.d/5/blackfire-agent-server-token'\''...'
Jan 06 21:28:11 azazel stage-2-init: decrypting '/nix/store/bxxyhwbw784f95vgxj5kn3bvg0dfd5xy-blackfire-agent-server-token.age' to '/run/agenix.d/5/blackfire-agent-server-token'...
Jan 06 21:28:11 azazel stage-2-init: + TMP_FILE=/run/agenix.d/5/blackfire-agent-server-token.tmp
Jan 06 21:28:11 azazel stage-2-init: ++ dirname /run/agenix.d/5/blackfire-agent-server-token
Jan 06 21:28:11 azazel stage-2-init: + mkdir -p /run/agenix.d/5
Jan 06 21:28:11 azazel stage-2-init: ++ dirname /run/agenix/blackfire-agent-server-token
Jan 06 21:28:11 azazel stage-2-init: + mkdir -p /run/agenix
Jan 06 21:28:11 azazel stage-2-init: + umask u=r,g=,o=
Jan 06 21:28:11 azazel stage-2-init: + LANG=en_US.UTF-8
Jan 06 21:28:11 azazel stage-2-init: + /nix/store/b7dz2sz5mh4h3xbav9q15j7547qjhdyl-rage-0.7.1/bin/rage --decrypt -i /etc/ssh/ssh_host_rsa_key -i /etc/ssh/ssh_host_ed25519_key -o /run/agenix.d/5/blackfire-agent-server-token.tmp /nix/store/bxxyhwbw784f95vgxj5kn3bvg0dfd5xy-blackfire-agent-server-token.age
Jan 06 21:28:11 azazel stage-2-init: + chmod 0400 /run/agenix.d/5/blackfire-agent-server-token.tmp
Jan 06 21:28:11 azazel stage-2-init: + chown 0:0 /run/agenix.d/5/blackfire-agent-server-token.tmp
Jan 06 21:28:11 azazel stage-2-init: + mv -f /run/agenix.d/5/blackfire-agent-server-token.tmp /run/agenix.d/5/blackfire-agent-server-token
Jan 06 21:28:11 azazel stage-2-init: + '[' /run/agenix/blackfire-agent-server-token '!=' /run/agenix/blackfire-agent-server-token ']'
Jan 06 21:28:11 azazel stage-2-init: + ((  _localstatus > 0  ))
Jan 06 21:28:11 azazel stage-2-init: + _localstatus=0
Jan 06 21:28:11 azazel stage-2-init: + install -m 0700 -d /root
Jan 06 21:28:11 azazel stage-2-init: + install -m 0755 -d /home
Jan 06 21:28:11 azazel stage-2-init: + /nix/store/i1hy2bjj3dmmn3qg61y87rci7xfv37dk-perl-5.34.0-env/bin/perl -w /nix/store/8smw5zaclai395bpr5gp5inzdgbkn43h-update-users-groups.pl /nix/store/s9nldar80mkgvnw760i2j2p2yy5nj95a-users-groups.json
Jan 06 21:28:11 azazel stage-2-init: + ((  _localstatus > 0  ))
Jan 06 21:28:11 azazel stage-2-init: + _localstatus=0
Jan 06 21:28:11 azazel stage-2-init: + ((  _localstatus > 0  ))
Jan 06 21:28:11 azazel stage-2-init: + _localstatus=0
Jan 06 21:28:11 azazel stage-2-init: + chown :keys /run/agenix.d /run/agenix.d/5
Jan 06 21:28:11 azazel stage-2-init: + ((  _localstatus > 0  ))
Jan 06 21:28:11 azazel stage-2-init: + _localstatus=0
Jan 06 21:28:11 azazel stage-2-init: + echo '[agenix] decrypting non-root secrets...'
Jan 06 21:28:11 azazel stage-2-init: [agenix] decrypting non-root secrets...
Jan 06 21:28:11 azazel stage-2-init: + ((  _localstatus > 0  ))
Jan 06 21:28:11 azazel stage-2-init: + _localstatus=0
Jan 06 21:28:11 azazel stage-2-init: + echo 'setting up /etc...'
Jan 06 21:28:11 azazel stage-2-init: setting up /etc...
Jan 06 21:28:11 azazel stage-2-init: + /nix/store/p36s6mbwqh4czbh107jyzc4d0hc345ck-perl-5.34.0-env/bin/perl /nix/store/cz6na7w751iv7z78fb9ms8hhvnsd0l8z-setup-etc.pl /nix/store/71ib0qisds7zvq58fr8qy3wn92ai6pcp-etc/etc
Jan 06 21:28:11 azazel stage-2-init: + ((  _localstatus > 0  ))
Jan 06 21:28:11 azazel stage-2-init: + _localstatus=0
Jan 06 21:28:11 azazel stage-2-init: ++ cat /run/agenix/bag.ogion.cz-secret
Jan 06 21:28:11 azazel stage-2-init: cat: /run/agenix/bag.ogion.cz-secret: No such file or directory
Jan 06 21:28:11 azazel stage-2-init: + secret=

If I delete the directory and switch, the symlink is created correctly:

$ sudo ls -la /run/agenix
lrwxrwxrwx 1 root root 15 Jan  6 21:34 /run/agenix -> /run/agenix.d/1
Log of first activation after deleting the directory
activating the configuration...
+ ((  _localstatus > 0  ))
+ _localstatus=0
+++ readlink /run/agenix
++ basename ''
+ _agenix_generation=
+ ((  ++_agenix_generation  ))
+ echo '[agenix] symlinking new secrets to /run/agenix (generation 1)...'
[agenix] symlinking new secrets to /run/agenix (generation 1)...
+ mkdir -p /run/agenix.d
+ chmod 0751 /run/agenix.d
+ grep -q '/run/agenix.d ramfs' /proc/mounts
+ mkdir -p /run/agenix.d/1
+ chmod 0751 /run/agenix.d/1
+ ln -sfn /run/agenix.d/1 /run/agenix
+ ((  _agenix_generation > 1  ))
[…]
[agenix] decrypting root secrets...
+ echo '[agenix] decrypting root secrets...'
+ _truePath=/run/agenix.d/1/bag.ogion.cz-secret
+ echo 'decrypting '\''/nix/store/ga41w0dn9dhk12w1w2zcv7pr4w9g36v2-bag.ogion.cz-secret.age'\'' to '\''/run/agenix.d/1/bag.ogion.cz-secret'\''...'
decrypting '/nix/store/ga41w0dn9dhk12w1w2zcv7pr4w9g36v2-bag.ogion.cz-secret.age' to '/run/agenix.d/1/bag.ogion.cz-secret'...
+ TMP_FILE=/run/agenix.d/1/bag.ogion.cz-secret.tmp
++ dirname /run/agenix.d/1/bag.ogion.cz-secret
+ mkdir -p /run/agenix.d/1
++ dirname /run/agenix/bag.ogion.cz-secret
+ mkdir -p /run/agenix
+ umask u=r,g=,o=
+ LANG=en_US.UTF-8
+ /nix/store/b7dz2sz5mh4h3xbav9q15j7547qjhdyl-rage-0.7.1/bin/rage --decrypt -i /etc/ssh/ssh_host_rsa_key -i /etc/ssh/ssh_host_ed25519_key -o /run/agenix.d/1/bag.ogion.cz-secret.tmp /nix/store/ga41w0dn9dhk12w1w2zcv7pr4w9g36v2-bag.ogion.cz-secret.age
+ chmod 0400 /run/agenix.d/1/bag.ogion.cz-secret.tmp
+ chown 0:0 /run/agenix.d/1/bag.ogion.cz-secret.tmp
+ mv -f /run/agenix.d/1/bag.ogion.cz-secret.tmp /run/agenix.d/1/bag.ogion.cz-secret
+ '[' /run/agenix/bag.ogion.cz-secret '!=' /run/agenix/bag.ogion.cz-secret ']'
decrypting '/nix/store/89qy5kr20hb5m88sqsii2q6v5xwpibg0-blackfire-agent-server-id.age' to '/run/agenix.d/1/blackfire-agent-server-id'...
+ _truePath=/run/agenix.d/1/blackfire-agent-server-id
+ echo 'decrypting '\''/nix/store/89qy5kr20hb5m88sqsii2q6v5xwpibg0-blackfire-agent-server-id.age'\'' to '\''/run/agenix.d/1/blackfire-agent-server-id'\''...'
+ TMP_FILE=/run/agenix.d/1/blackfire-agent-server-id.tmp
++ dirname /run/agenix.d/1/blackfire-agent-server-id
+ mkdir -p /run/agenix.d/1
++ dirname /run/agenix/blackfire-agent-server-id
+ mkdir -p /run/agenix
+ umask u=r,g=,o=
+ LANG=en_US.UTF-8
+ /nix/store/b7dz2sz5mh4h3xbav9q15j7547qjhdyl-rage-0.7.1/bin/rage --decrypt -i /etc/ssh/ssh_host_rsa_key -i /etc/ssh/ssh_host_ed25519_key -o /run/agenix.d/1/blackfire-agent-server-id.tmp /nix/store/89qy5kr20hb5m88sqsii2q6v5xwpibg0-blackfire-agent-server-id.age
+ chmod 0400 /run/agenix.d/1/blackfire-agent-server-id.tmp
+ chown 0:0 /run/agenix.d/1/blackfire-agent-server-id.tmp
+ mv -f /run/agenix.d/1/blackfire-agent-server-id.tmp /run/agenix.d/1/blackfire-agent-server-id
+ '[' /run/agenix/blackfire-agent-server-id '!=' /run/agenix/blackfire-agent-server-id ']'
+ _truePath=/run/agenix.d/1/blackfire-agent-server-token
+ echo 'decrypting '\''/nix/store/bxxyhwbw784f95vgxj5kn3bvg0dfd5xy-blackfire-agent-server-token.age'\'' to '\''/run/agenix.d/1/blackfire-agent-server-token'\''...'
decrypting '/nix/store/bxxyhwbw784f95vgxj5kn3bvg0dfd5xy-blackfire-agent-server-token.age' to '/run/agenix.d/1/blackfire-agent-server-token'...
+ TMP_FILE=/run/agenix.d/1/blackfire-agent-server-token.tmp
++ dirname /run/agenix.d/1/blackfire-agent-server-token
+ mkdir -p /run/agenix.d/1
++ dirname /run/agenix/blackfire-agent-server-token
+ mkdir -p /run/agenix
+ umask u=r,g=,o=
+ LANG=en_US.UTF-8
+ /nix/store/b7dz2sz5mh4h3xbav9q15j7547qjhdyl-rage-0.7.1/bin/rage --decrypt -i /etc/ssh/ssh_host_rsa_key -i /etc/ssh/ssh_host_ed25519_key -o /run/agenix.d/1/blackfire-agent-server-token.tmp /nix/store/bxxyhwbw784f95vgxj5kn3bvg0dfd5xy-blackfire-agent-server-token.age
+ chmod 0400 /run/agenix.d/1/blackfire-agent-server-token.tmp
+ chown 0:0 /run/agenix.d/1/blackfire-agent-server-token.tmp
+ mv -f /run/agenix.d/1/blackfire-agent-server-token.tmp /run/agenix.d/1/blackfire-agent-server-token
+ '[' /run/agenix/blackfire-agent-server-token '!=' /run/agenix/blackfire-agent-server-token ']'
+ ((  _localstatus > 0  ))
+ _localstatus=0
+ install -m 0700 -d /root
+ install -m 0755 -d /home
+ /nix/store/i1hy2bjj3dmmn3qg61y87rci7xfv37dk-perl-5.34.0-env/bin/perl -w /nix/store/8smw5zaclai395bpr5gp5inzdgbkn43h-update-users-groups.pl /nix/store/s9nldar80mkgvnw760i2j2p2yy5nj95a-users-groups.json
+ ((  _localstatus > 0  ))
+ _localstatus=0
+ ((  _localstatus > 0  ))
+ _localstatus=0
+ chown :keys /run/agenix.d /run/agenix.d/1
+ ((  _localstatus > 0  ))
+ _localstatus=0
[agenix] decrypting non-root secrets...
setting up /etc...
+ echo '[agenix] decrypting non-root secrets...'

and same on successive activations:

$ sudo ls -la /run/agenix
lrwxrwxrwx 1 root root 15 Jan  6 21:35 /run/agenix -> /run/agenix.d/2
Log of second activation after deleting the directory
+++ readlink /run/agenix
++ basename /run/agenix.d/1
+ _agenix_generation=1
+ ((  ++_agenix_generation  ))
[agenix] symlinking new secrets to /run/agenix (generation 2)...
+ echo '[agenix] symlinking new secrets to /run/agenix (generation 2)...'
+ mkdir -p /run/agenix.d
+ chmod 0751 /run/agenix.d
+ grep -q '/run/agenix.d ramfs' /proc/mounts
+ mkdir -p /run/agenix.d/2
+ chmod 0751 /run/agenix.d/2
+ ln -sfn /run/agenix.d/2 /run/agenix
+ ((  _agenix_generation > 1  ))
[agenix] removing old secrets (generation 1)...
+ echo '[agenix] removing old secrets (generation 1)...'
+ rm -rf /run/agenix.d/1
[…]
[agenix] decrypting root secrets...
decrypting '/nix/store/ga41w0dn9dhk12w1w2zcv7pr4w9g36v2-bag.ogion.cz-secret.age' to '/run/agenix.d/2/bag.ogion.cz-secret'...
+ echo '[agenix] decrypting root secrets...'
+ _truePath=/run/agenix.d/2/bag.ogion.cz-secret
+ echo 'decrypting '\''/nix/store/ga41w0dn9dhk12w1w2zcv7pr4w9g36v2-bag.ogion.cz-secret.age'\'' to '\''/run/agenix.d/2/bag.ogion.cz-secret'\''...'
+ TMP_FILE=/run/agenix.d/2/bag.ogion.cz-secret.tmp
++ dirname /run/agenix.d/2/bag.ogion.cz-secret
+ mkdir -p /run/agenix.d/2
++ dirname /run/agenix/bag.ogion.cz-secret
+ mkdir -p /run/agenix
+ umask u=r,g=,o=
+ LANG=en_US.UTF-8
+ /nix/store/b7dz2sz5mh4h3xbav9q15j7547qjhdyl-rage-0.7.1/bin/rage --decrypt -i /etc/ssh/ssh_host_rsa_key -i /etc/ssh/ssh_host_ed25519_key -o /run/agenix.d/2/bag.ogion.cz-secret.tmp /nix/store/ga41w0dn9dhk12w1w2zcv7pr4w9g36v2-bag.ogion.cz-secret.age
+ chmod 0400 /run/agenix.d/2/bag.ogion.cz-secret.tmp
+ chown 0:0 /run/agenix.d/2/bag.ogion.cz-secret.tmp
+ mv -f /run/agenix.d/2/bag.ogion.cz-secret.tmp /run/agenix.d/2/bag.ogion.cz-secret
+ '[' /run/agenix/bag.ogion.cz-secret '!=' /run/agenix/bag.ogion.cz-secret ']'
+ _truePath=/run/agenix.d/2/blackfire-agent-server-id
decrypting '/nix/store/89qy5kr20hb5m88sqsii2q6v5xwpibg0-blackfire-agent-server-id.age' to '/run/agenix.d/2/blackfire-agent-server-id'...
+ echo 'decrypting '\''/nix/store/89qy5kr20hb5m88sqsii2q6v5xwpibg0-blackfire-agent-server-id.age'\'' to '\''/run/agenix.d/2/blackfire-agent-server-id'\''...'
+ TMP_FILE=/run/agenix.d/2/blackfire-agent-server-id.tmp
++ dirname /run/agenix.d/2/blackfire-agent-server-id
+ mkdir -p /run/agenix.d/2
++ dirname /run/agenix/blackfire-agent-server-id
+ mkdir -p /run/agenix
+ umask u=r,g=,o=
+ LANG=en_US.UTF-8
+ /nix/store/b7dz2sz5mh4h3xbav9q15j7547qjhdyl-rage-0.7.1/bin/rage --decrypt -i /etc/ssh/ssh_host_rsa_key -i /etc/ssh/ssh_host_ed25519_key -o /run/agenix.d/2/blackfire-agent-server-id.tmp /nix/store/89qy5kr20hb5m88sqsii2q6v5xwpibg0-blackfire-agent-server-id.age
+ chmod 0400 /run/agenix.d/2/blackfire-agent-server-id.tmp
+ chown 0:0 /run/agenix.d/2/blackfire-agent-server-id.tmp
+ mv -f /run/agenix.d/2/blackfire-agent-server-id.tmp /run/agenix.d/2/blackfire-agent-server-id
+ '[' /run/agenix/blackfire-agent-server-id '!=' /run/agenix/blackfire-agent-server-id ']'
+ _truePath=/run/agenix.d/2/blackfire-agent-server-token
decrypting '/nix/store/bxxyhwbw784f95vgxj5kn3bvg0dfd5xy-blackfire-agent-server-token.age' to '/run/agenix.d/2/blackfire-agent-server-token'...
+ echo 'decrypting '\''/nix/store/bxxyhwbw784f95vgxj5kn3bvg0dfd5xy-blackfire-agent-server-token.age'\'' to '\''/run/agenix.d/2/blackfire-agent-server-token'\''...'
+ TMP_FILE=/run/agenix.d/2/blackfire-agent-server-token.tmp
++ dirname /run/agenix.d/2/blackfire-agent-server-token
+ mkdir -p /run/agenix.d/2
++ dirname /run/agenix/blackfire-agent-server-token
+ mkdir -p /run/agenix
+ umask u=r,g=,o=
+ LANG=en_US.UTF-8
+ /nix/store/b7dz2sz5mh4h3xbav9q15j7547qjhdyl-rage-0.7.1/bin/rage --decrypt -i /etc/ssh/ssh_host_rsa_key -i /etc/ssh/ssh_host_ed25519_key -o /run/agenix.d/2/blackfire-agent-server-token.tmp /nix/store/bxxyhwbw784f95vgxj5kn3bvg0dfd5xy-blackfire-agent-server-token.age
+ chmod 0400 /run/agenix.d/2/blackfire-agent-server-token.tmp
+ chown 0:0 /run/agenix.d/2/blackfire-agent-server-token.tmp
+ mv -f /run/agenix.d/2/blackfire-agent-server-token.tmp /run/agenix.d/2/blackfire-agent-server-token
+ '[' /run/agenix/blackfire-agent-server-token '!=' /run/agenix/blackfire-agent-server-token ']'
+ ((  _localstatus > 0  ))
+ _localstatus=0
+ install -m 0700 -d /root
+ install -m 0755 -d /home
+ /nix/store/i1hy2bjj3dmmn3qg61y87rci7xfv37dk-perl-5.34.0-env/bin/perl -w /nix/store/8smw5zaclai395bpr5gp5inzdgbkn43h-update-users-groups.pl /nix/store/s9nldar80mkgvnw760i2j2p2yy5nj95a-users-groups.json
+ ((  _localstatus > 0  ))
+ _localstatus=0
+ ((  _localstatus > 0  ))
+ _localstatus=0
+ chown :keys /run/agenix.d /run/agenix.d/2
[agenix] decrypting non-root secrets...
+ ((  _localstatus > 0  ))
+ _localstatus=0
+ echo '[agenix] decrypting non-root secrets...'

But it breaks again after a reboot.

If I run activation after reboot, basename /run/agenix returns empty string since it is a directory so the symlink will be crated in it:

$ sudo ls -la /run/agenix
total 0
drwxr-xr-x  2 root root  60 Jan  6 21:47 .
drwxr-xr-x 24 root root 580 Jan  6 21:47 ..
lrwxrwxrwx  1 root root  15 Jan  6 21:47 1 -> /run/agenix.d/1

I have no idea what is creating the directory.

mkdir -p "$(dirname "${secretType.path}")"

is superfluous but it only runs after agenixMountSecrets.

darwin build failure: `system.activationScripts.users.deps` does not exist

I'm using Nix (with nix-darwin and flakes support) on macOS 11.6 (though this happened on previous versions of macOS too). I've configured agenix as suggested in the README, but nix build fails with the following error:

error: The option `system.activationScripts.users.deps' does not exist. Definition values:
       - In `<unknown-file>':
           [
             "agenixRoot"
           ]

I think this is the relevant file in nix-darwin that configures system.activationScripts.users, and deps is not one of its attributes.

Bear with me, as I'm new-ish to Nix and haven't yet started working with NixOS: judging by the declaration of deps as a required(?) attribute for activation scripts in NixOS here, I'm guessing it would make more sense for nix-darwin to add support for this rather than have agenix accommodate nix-darwin? If so, I'm not sure about the level of effort there, so I'd welcome any advice about a temporary workaround for the time being.

no secrets on boot

There seems to be an issue after upgrading to latest nixos-unstable where the secrets don't get created properly on boot. Not sure what the root cause is yet, but I can tell you a simple nixos-rebuild switch does successfully make the secrets so it's only a boot time issue.

Strangely the files don't seem to exist on boot as the boot log is full of these message:

May 24 17:39:59 serval-ws stage-2-init: decrypting /nix/store/mxhq2zldxmbqabzhsayqjmnv7hm3r6mj-source/secrets/nrd.age to /run/secrets/nrd...
May 24 17:39:59 serval-ws stage-2-init: Error: No such file or directory (os error 2)
May 24 17:39:59 serval-ws stage-2-init: [ Did rage not do what you expected? Could an error be more useful? ]
May 24 17:39:59 serval-ws stage-2-init: [ Tell us: https://str4d.xyz/rage/report                            ]
May 24 17:39:59 serval-ws stage-2-init: chmod: cannot access '/run/secrets/nrd.tmp': No such file or directory
May 24 17:39:59 serval-ws stage-2-init: chown: cannot access '/run/secrets/nrd.tmp': No such file or directory
May 24 17:39:59 serval-ws stage-2-init: mv: cannot stat '/run/secrets/nrd.tmp': No such file or directory
May 24 17:39:59 serval-ws stage-2-init: decrypting /nix/store/mxhq2zldxmbqabzhsayqjmnv7hm3r6mj-source/secrets/root.age to /run/secrets/root...
May 24 17:39:59 serval-ws stage-2-init: Error: No such file or directory (os error 2)
May 24 17:39:59 serval-ws stage-2-init: [ Did rage not do what you expected? Could an error be more useful? ]
May 24 17:39:59 serval-ws stage-2-init: [ Tell us: https://str4d.xyz/rage/report                            ]
May 24 17:39:59 serval-ws stage-2-init: chmod: cannot access '/run/secrets/root.tmp': No such file or directory
May 24 17:39:59 serval-ws stage-2-init: chown: cannot access '/run/secrets/root.tmp': No such file or directory
May 24 17:39:59 serval-ws stage-2-init: mv: cannot stat '/run/secrets/root.tmp': No such file or directory
May 24 17:39:59 serval-ws stage-2-init: decrypting /nix/store/mxhq2zldxmbqabzhsayqjmnv7hm3r6mj-source/secrets/wireguard.age to /run/secrets/wireguard...
May 24 17:39:59 serval-ws stage-2-init: Error: No such file or directory (os error 2)
May 24 17:39:59 serval-ws stage-2-init: [ Did rage not do what you expected? Could an error be more useful? ]
May 24 17:39:59 serval-ws stage-2-init: [ Tell us: https://str4d.xyz/rage/report                            ]
May 24 17:39:59 serval-ws stage-2-init: chmod: cannot access '/run/secrets/wireguard.tmp': No such file or directory
May 24 17:39:59 serval-ws stage-2-init: chown: cannot access '/run/secrets/wireguard.tmp': No such file or directory
May 24 17:39:59 serval-ws stage-2-init: mv: cannot stat '/run/secrets/wireguard.tmp': No such file or directory
May 24 17:39:59 serval-ws stage-2-init: Activation script snippet 'agenixRoot' failed (1)
May 24 17:39:59 serval-ws stage-2-init: warning: password file ‘/run/secrets/nrd’ does not exist
May 24 17:39:59 serval-ws stage-2-init: warning: password file ‘/run/secrets/root’ does not exist
May 24 17:39:59 serval-ws stage-2-init: [agenix] decrypting non-root secrets...
May 24 17:39:59 serval-ws stage-2-init: decrypting /nix/store/mxhq2zldxmbqabzhsayqjmnv7hm3r6mj-source/secrets/aws.age to /run/secrets/aws...
May 24 17:39:59 serval-ws stage-2-init: Error: No such file or directory (os error 2)
May 24 17:39:59 serval-ws stage-2-init: [ Did rage not do what you expected? Could an error be more useful? ]
...

These files do in fact exist in the nix store and, as I said, all my secrets are properly created with a simple nixos-rebuild switch. My only guess so far is that stage-2-init doesn't have proper access to the nix/store somehow, but I'm not sure why.

Option to use minisign to verify secrets

As called out in https://github.com/ryantm/agenix#threat-modelwarnings, age does not (currently?) support authentication of secrets. However, if it were paired with minisign, it may be possible to at least sidestep this limitation.

I imagine the API just being an additional 2 options: minisignFile and verifySignature. If verifySignature is true and minisignFile is unset, try ${file}.minisig; if verifySignature is true and minisignFile is set, use that file; if verifySignature is false, don't verify.

Could also change minisignFile to signatureFile / signaturePath. This would allow extensibility if we ever want to implement support for a different signing program, and could just add a signatureType enum.

If the secret is validated, proceed. Otherwise, error loudly that the secret may have been tampered with. (Provide escape hatch to deploy / switch / whatever in spite of this?)

Thoughts? Would this be accepted as a PR?

EDIT: Of course, the signing key would need to be accessible; probably as another option.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.