Using a custom shell script in a service (services.postfixadmin)

Alright, this is all very specific so I’m gonna light a candle for this one. Hope someone can help me out.

I’m migrating away from a multi-system Funtoo setup to a properly managed NixOS infrastructure. One of the systems I am not yet ready to migrate to NixOS, is my email server. That one is still running on an older Funtoo LXC container, and for now it is okay to keep it that way. However, postfixadmin used to run on the old Funtoo host, and that part does have to be migrated to the new NixOS host.

When the host was still running Funtoo, I had postfixadmin running in a way that wasn’t ideal, but worked. It had postgresql and had nginx for postfixadmin’s front end, it used certain tools to ensure it would be able to add and remove virtual email accounts that in turn would be accessible by the lxc mail server guest. One of those tools was a dovecotpw.sh wrapper script that would call dovecotadm pw in a certain way. The script “Rebuilds dovecotpw’s original command line options”:

/usr/bin/doveadm pw ${list}${plaintext}${scheme}${user}${verify}

I don’t know how to include this script in a way that can function. In my postfixadmin flake, I have a line under services.postfixadmin.extraConfig as follows:

$CONF['dovecotpw']   = '${./postfixadmin/dovecotpw.sh}';

Grasping for straws I tried to edit this script so now it reads the following, please bear with me:

  #!/usr/bin/env nix-shell
  #!nix-shell -i bash -p dovecot

  # MPeXnetworks - Lars Braeuer 11/2011

  # Rebuild dovecotpw's original command line options, which are:
  #usage: dovecotpw [-l] [-p plaintext] [-s scheme] [-u user] [-V]
  # -l List known password schemes
  # -p plaintext New password
  # -s scheme Password scheme
  # -u user Username (if scheme uses it)
  # -V Internally verify the hash

  while getopts ":l:p:s:u:V:" opt; do
          case "$opt" in
                  l) list=" -l" ;;
                  p) plaintext=" -p $OPTARG" ;;
                  s) scheme=" -s $OPTARG" ;;
                  u) user=" -u $OPTARG" ;;
                  V) verify=" -V" ;;
          esac
  done

  logger "doveadm args '$*' pw ${list}${plaintext}${scheme}${user}${verify}"
  /usr/bin/env doveadm pw ${list}${plaintext}${scheme}${user}${verify}
  exit $?

The postfixadmin setup.php file says:

Password Hashing - attempted to use configured encrypt backend (dovecot:CRAM-MD5) triggered an error: /nix/store/5idpzz8nsbzgy461algqyfy9j2pb39i8-dovecotpw.sh failed, see error log for details`

In the errorlog (syslog) we can see (anonymized a bit):

nginx[2217282]: 2023/06/15 23:45:31 [error] 2217282#2217282: *140374 FastCGI sent in stderr: "PHP message: Failed to read password from /nix/store/5idpzz8nsbzgy461algqyfy9j2pb39i8-dovecotpw.sh ... stderr: error:
file 'nixpkgs' was not found in the Nix search path (add it using $NIX_PATH or -I)
nginx[2217282]:        at «string»:1:25:
nginx[2217282]:             1| {...}@args: with import <nixpkgs> args; (pkgs.runCommandCC or pkgs.runCommand) "shell" { buildInputs = [ (dovecot) ]; } ""
nginx[2217282]:              |                         ^
nginx[2217282]: (use '--show-trace' to show detailed location information)
nginx[2217282]: , password:" while reading upstream, client: <MYIP>, server: postfixadmin.example.com, request: "GET /setup.php HTTP/2.0", upstream: "fastcgi://unix:/run/phpfpm/postfixadmin.sock:", host:
"postfixadmin.example.com"
nginx[2217282]: 2023/06/15 23:45:43 [error] 2217282#2217282: *140374 FastCGI sent in stderr: "PHP message: Failed to read password from /nix/store/5idpzz8nsbzgy461algqyfy9j2pb39i8-dovecotpw.sh ... stderr: error:
file 'nixpkgs' was not found in the Nix search path (add it using $NIX_PATH or -I)
nginx[2217282]:        at «string»:1:25:
nginx[2217282]:             1| {...}@args: with import <nixpkgs> args; (pkgs.runCommandCC or pkgs.runCommand) "shell" { buildInputs = [ (dovecot) ]; } ""
nginx[2217282]:              |                         ^
nginx[2217282]: (use '--show-trace' to show detailed location information)
nginx[2217282]: , password:" while reading response header from upstream, client: <MYIP>, server: postfixadmin.example.com, request: "POST /setup.php HTTP/2.0", upstream:
"fastcgi://unix:/run/phpfpm/postfixadmin.sock:", host: "postfixadmin.example.com", referrer: "https://postfixadmin.example.com/setup.php"

I’m also fairly certain that doveadm needs a working dovecot.conf, but, dovecot is not running on the host but on the guest. So how should I deal with this?

This story can be continued - to create / destroy mailboxes, it needs to run commands on the lxc guest. I plan to use ssh for that but that’s for a later time. First I’d really like to get this working and all insights are very much appreciated. Thanks. If I get any breakthrough myself, I’ll be sure to add it to this thread as well.

You’re getting the error because nix-shell doesn’t know where to look for NIXPKGS.

But using nix-shell as the interpreter is for situations where you don’t control the environment in which it is going to run. Since you are deploying this script on your host, you should instead use resholve.writeShellScript (or writeShellScriptBin if you want to manually manage dependencies).

Hi Peter, thanks for your response, it helped me understand why I got the error. But the solution… I’m not sure.

It just got a bit more complicated too, because apparently, the doveadm command needs a working dovecot.conf, which I don’t even have on this host.

But ok, first things first, let’s first try to figure out how and where to use resholve.writeShellScript.

I’ve read Shell Scripts - NixOS Wiki and nixpkgs/pkgs/development/misc/resholve/README.md at 7beaf651c847cd311c9fbd7991c52611db2a4eaa · NixOS/nixpkgs · GitHub but I don’t even know where to start in using this.

Currently, in my flake, I am including the below file:

{config, settings, pkgs, ...}: {
  age.secrets.pfadminpw = {
    file = ../../secrets/host/pfadminpw.age;
    owner = "nginx";
    group = "nginx";
  };

  age.secrets.pfadmindbpw = {
    file = ../../secrets/host/pfadmindbpw.age;
    owner = "nginx";
    group = "nginx";
  };

  users.users.postfixadmin = {
    isSystemUser = true;
    group = "postfixadmin";
    home = "/var/www/postfixadmin";
    shell = pkgs.zsh;
  };

  users.groups.postfixadmin = {};

  services.postfixadmin = {
    enable = true;
    adminEmail = "postfixadmin@example.com";
    setupPasswordFile = config.age.secrets.pfadminpw.path;
    hostName = "postfixadmin.example.com";
    database = {
      host     = "postgresql.lan";
      dbname   = "postfixadmin";
      username = "postfixadmin";  # Looks like this is broken - only used for a local install
      passwordFile = config.age.secrets.pfadmindbpw.path;
    };
    extraConfig = ''
      # bunch of config options
      $CONF['database_password'] = trim($CONF['database_password']);
      $CONF['setup_password'] = trim($CONF['setup_password']);
      $CONF['database_user'] = '${config.services.postfixadmin.database.username}';
      $CONF['dovecotpw']   = '${./postfixadmin/dovecotpw.sh}'; # This is the currently broken implementation
      $CONF['mailbox_postcreation_script']='/usr/bin/env ssh -p22 -o BatchMode=yes user@mail.lan /usr/local/bin/postfixadmin-mailbox-postcreation.sh';
      # ...etc...
      '';
  };
}

I’m going sad panda mode for a bit but it’s just too difficult and involved for me at this stage. I’ve decided to install postfixadmin on the LXC mail guest on Funtoo, and I can use the NixOS host’s nginx to proxy it through. As this will not require me to use ssh and dovecotpw on the host, it’s a bit easier and less convoluted.

:panda_face: