How to pass a sensitive value to a nixos options that expects a string?

Hi,

I’m struggling to find a way to securely pass a secret value located in /run/secrets/my-secret (generated using sops-nix) to a nixos option that expects a string. It seems simple but I can’t figure it out.

{ config, pkgs, ... }: {
  # omitted for brevity
  sops = {
    defaultSopsFile = ./default.yaml;
    defaultSopsFormat = "yaml";
    age.keyFile = "/home/my-user/.config/sops/age/keys.txt";
    secrets.lldap-admin-password = {};
  };

  services.guacamole-client = {
    enable = true;
    settings = {
      ldap-hostname = "domain-controller.example.com";
      ldap-port = 636;
      ldap-encryption-method = "ssl";
      ldap-search-bind-dn = "cn=admin,dc=example,dc=com";
      ldap-search-bind-password = "admin-password";
    };
  };
}

Generally there would be a service option that allows the use of a file that contains the sensitive data like “services.guacamole-client.settings.ldap-search-bind-passwordFile” but it doesn’t exists for this service. Does anyone have any ideas how I would go about passing the value in “/run/secrets/my-secret” to the ldap-search-bind-password option?

P.S - I’m running flakes so I can’t use builtins.readFile unfortunately…

If the module isn’t written in a way that would allow reading the secret from a file, then you have to dig the modules implementation if you can work around it by other means, and in the worst case you have to completely set up the service by hand, taking appropriate measures to get the secret where its needed by reading at runtime or using templates.

There is no “one-fits-all” solution, sadly.

Any pointers on how I would go about doing it for nixpkgs/nixos/modules/services/web-apps/guacamole-client.nix at b43c397f6c213918d6cfe6e3550abfe79b5d1c51 · NixOS/nixpkgs · GitHub?

I’ve tried doing something like this…

{ config,  pkgs, ... }: let
  original-config = config.environment.etc."guacamole/guacamole.properties".source;

  final-config = pkgs.runCommand "final-config" {} ''
    cat ${original-config} > $out

    cat ${config.sops.templates."guacamole.properties".path} >> $out
  '';
in {
  sops = {
    defaultSopsFile = ../../../sops/default.yaml;
    defaultSopsFormat = "yaml";
    age.keyFile = "/home/${vars.adminUser}/.config/sops/age/keys.txt";
    secrets.lldap-admin-password = {};

    templates."guacamole.properties" = {
      content = ''
        ldap-search-bind-password = "${config.sops.placeholder.lldap-admin-password}";
      '';
    };
  };

  environment.etc."guacamole/guacamole-properties".source = lib.mkForce final-config;
}

In an effort to take the generated guacamole.properties file that the nixos option produces and append "ldap-search-bind-password = “secret-value” to the end of it but the final-config derivation complains that it can’t find the secrets path since it’s isolated in the sandbox environment.

That whole approach is an invalid way to handle secrets since it would put them in the nix store if it had succeeded. Everything in the nix store is world-readable. Secrets must be handled at activation/run time, not build time, in order to avoid this.

As far as I can tell though there isn’t any way to pass that value during runtime since the /etc/guacamole/guacamole.properties file is generated during the build time and I can’t modify it afterwards.

So if you can not inject the secret in a way that satisfies the he module, you have to drop the module and wire up the service on your own.

Alternatively, contribute changes to the current module that make both ways work.

You could set environment.etc."guacamole/guacamole-properties".enable = false; and then create the file at runtime with an activation snippet or a systemd oneshot service. The script that creates it can directly access the (now-unused) config.environment.etc."guacamole/guacamole-properties".source file as an input making that script the only actual reference to that file in your config, and grab the secret from wherever sops puts it, splicing it in and placing the result in /etc.

Hey Tejing,

I ended up coming up with a solution that an existing nixos module implemented (nixpkgs/nixos/modules/config/mysql.nix at b43c397f6c213918d6cfe6e3550abfe79b5d1c51 · NixOS/nixpkgs · GitHub).

It creates a copy of the final guacamole.properties file during runtime and appends the contents of the password file and places the new config back to the original location.

This was the config that achieved what I desired:

{config, pkgs, ... }: {
  systemd.services.insert-ldap-password = {
    wantedBy = [
      "multi-user.target"
    ];

    serviceConfig = {
      Type = "oneshot";
      User = "root";
      Group = "root";
      ExecStartPost = "${pkgs.systemdUkify}/bin/systemctl restart tomcat.service";
    };

    script = ''
      if [[ -r ${cfg.admin-ldap-password-file} ]]; then
        umask 0077
        temp_conf="$(mktemp)"
        cp /etc/guacamole/guacamole.properties $temp_conf
        printf 'ldap-search-bind-password = %s\n' "$(cat ${cfg.admin-ldap-password-file})" >> $temp_conf
        mv -fT "$temp_conf" /etc/guacamole/guacamole.properties
        chown root:root /etc/guacamole/guacamole.properties
        chmod 750 /etc/guacamole/guacamole.properties
      fi
    '';
  };
}

Appreciate the help nonetheless! :slight_smile:

That turned out pretty close to what I was suggesting, except that:

  • It’s possible for the secret-less config file to appear in /etc depending on race conditions.
  • It’s possible to add the secret multiple times… which may or may not be a problem.
  • The etc activation snippet and your oneshot service are fighting for control of the same file.

I would still set environment.etc."guacamole/guacamole-properties".enable = false; (you may need to lib.mkForce that, not sure) to stop the etc activation script from actually adding the file, and change

cp /etc/guacamole/guacamole.properties $temp_conf

to

cp ${config.environment.etc."guacamole/guacamole-properties".source} $temp_conf

So the oneshot service pulls the pristine config file the module created directly from the nix store each time. Less stateful and more robust, overall.

Regardless, your solution as it is properly secures the secret, which is the main criteria.

Hmm if I set environment.etc."guacamole/guacamole-properties".enable = false; wouldn’t that make the service fail since it would be trying to copy over a non-existent file?

I did end up setting cp ${config.environment.etc."guacamole/guacamole-properties".source} $temp_conf though I agree it keeps things more stateless which is always nice.

I don’t see how. The service would be the thing responsible for creating the file. The value of config.environment.etc."guacamole/guacamole-properties".source would be unaffected by setting the enable = false;, if that’s what you mean. The normal /etc management machinery would ignore all the settings in environment.etc."guacamole/guacamole-properties", but they would still have their values.

Hmm yeah you’re right adding that line didn’t seem to have any effect on the one-shot service.

My understanding seemed to be incorrect and everything seems to be working as expected now with the added benefits of no race conditions or multiple processes fighting for control over the guacamole.properties file.

Thanks heaps for the help man appreciate it!

1 Like