Idiomatic use of replace-secret and agenix to create configuration files with passwords/secrets

Scenario

I am currently setting up a server with Postfix and need to configure several Postfix lookup tables which are defined in configuration files such as

user = db_user                                                                                                                                           
password = my-secret-db-user-password                                                                                                                                                                                  
hosts = 127.0.0.1                                                                                                                                                          
dbname = mail_server                                                                                                                                                                             
query = SELECT * FROM domains WHERE domain='%s'

As you can see, the files contain a secret password and thus shouldn’t be stored in the nix store. Thus, normal NixOS methods can not be used here.

I know of (and use) agenix and replace-secret but there seems to be no reference solution on how to exactly use them together idiomatically.

From how both tools work, it is clear that

  • agenix will provide the password in a file to be read
  • the configuration file will have be stored somewhere with the password replaced by placeholder such as @secret-db-password@
  • since most of NixOS paths are read-only, the configuration file will have be copied someplace where it can be modified
  • replace-secret will take this stored configuration and replace the placeholder with the real password
  • the final file will need to end up in a location where the service Postfix will read it

Open Questions

  1. How are configuration files stored and written?
    a. an actual template file next to configuration.nix which gets copied to the final destination?
    b. pkgs.writeText?
    c. etc.environment.text? (probably not, because the template file is useless by itself)
  2. What mechanisms to use to get this file into its final destination?
    a. cp?
    b. install?
    c. something built-in into NixOS?
  3. Where to store the final configuration file?
    a. in /etc/postfix/myfile.cf?
    b. in /var/lib/postfix/conf/myfile.cf?

Current Solution (excerpt)

I came up with the following solution so far:

# mail_server.nix
{ config, pkgs, lib, ... }:    
let                                                          
  postfix-map1-file = pkgs.writeText "postfix-map1-file" ''
    user = db_user                  
    password = @DB_USER_PASSWORD@                           
    hosts = 127.0.0.1    
    dbname = mail_server
    query = SELECT * FROM domains WHERE domain='%s'
  '';
  postfix-map2-file = pkgs.writeText "postfix-map2-file" ''
    user = db_user                  
    password = @DB_USER_PASSWORD@                           
    hosts = 127.0.0.1    
    dbname = mail_server
    query = ANOTHER SQL QUERY HERE'
  '';
in
  # ...
  # Other services' configuration
  # ...

  # POSTFIX
  services.postfix =
  {
    enable = true;
    # main.cf
    config = {
      map1 = "pgsql:/var/lib/postfix/conf/postfix-map1-file.cf";
      map2 = "pgsql:/var/lib/postfix/conf/postfix-map1-file.cf";
    };
  };

  systemd.services.postfix = {
    preStart = ''
      install --owner postfix --mode 400 ${postfix-map1-file} /var/lib/postfix/conf/postfix-map1-file.cf
      install --owner postfix --mode 400 ${postfix-map2-file} /var/lib/postfix/conf/postfix-map1-file.cf
      ${pkgs.replace-secret}/bin/replace-secret @MAIL_USER_PASSWORD@ ${config.age.secrets.db-user-password.path} /var/lib/postfix/conf/postfix-map1-file.cf
      ${pkgs.replace-secret}/bin/replace-secret @MAIL_USER_PASSWORD@ ${config.age.secrets.db-user-password.path} /var/lib/postfix/conf/postfix-map2-file.cf
    '';
    path = with pkgs; [ replace-secret ];
  };

Can something be improved here? Is this the intended/idiomatic use? I can only think of making a loop for the map files to not have write install and replace-sercret twice.


Related thread where I did something similar for setting users password in PostgreSQL.

If you encrypt the whole postfix config file instead of just the password you just need to copy the file from agenix.
I do this with systemd tmpfile rule.

Something like this should work in your case:

  systemd.tmpfiles.rules = [
      "C /var/lib/postfix/conf/postfix-map1-file.cf - - - - ${config.age.secrets.postfix-map1-file.path}"
  ] ;

Systemd tmpfile doc available here: tmpfiles.d

Yeah, this crossed my mind, too. For the short lookup table files this would work but I don’t like that now a part of the configuration is stored somewhere entirely else and needs to be edited using agenix -e instead of just an editor.

Therefore, I would like to find a generic best-practice method that would also work with large config files and keeps all configuration in one place.

In case anyone is having the same problem:
I ended up configuring PostgreSQL to accept connections over sockets (which is also faster) and use PostgreSQL’s ident mappings (NixOS option services.postgresql.identMap) to map the postfix system user to the SQL user db_user. This way, I don’t need to store a password in a configuration file anymore.

I am keeping this thread open since a generic solution might be useful for other services that don’t support sockets or are on another host.

1 Like

You shouldn’t need to use tmpfiles to do this; agenix can either link or copy files into other locations if needed, using age.secrets.<name>.path and age.secrets.<name>.symlink

See GitHub - ryantm/agenix: age-encrypted secrets for NixOS and Home manager

1 Like