Access NixOS SOPS Secret via Home Manager

Description

I’ve setup sops-nix in my flakes NixOS configuration. I can access secrets from my NixOS configuration files. How do I access a secret from Home Manager without having to pass in the --impure option?

Accessing SOPS Secrets via Home Manager

The Home Manager file that needs access to a secret:

{ config, osConfig, pkgs, ... }: {
  home = {
    packages = with pkgs; [ element-desktop ];
    file."${config.xdg.userDirs.documents}/Secrets/Element/security-key.txt".source = osConfig.sops.secrets."app/element-desktop/message-key".path;
  };
}

Current version of the above file here.

This works when I rebuild my system with sudo nixos-rebuild switch --flake ./ --impure, however, I’d like to pass in the value(s) in sops.secrets... into Home Manager via my flake.nix file so Nix knows that it’s an external dependency (and presumably will not require the use of --impure).

Stripped Down Version of My flake.nix

{
  inputs = {
    ...
    sops-nix.url = "github:Mic92/sops-nix";
  };

  outputs = inputs@{ self, home-manager, nixpkgs, sops-nix, ... }: {
    nixosConfigurations.<hostName> = inputs.nixpkgs.lib.nixosSystem {
      specialArgs = { inherit inputs pkgs; };
      modules = [
        home-manager.nixosModules.home-manager
        <pathToNixOsConfig>      <----- This would be the NixOS file that
        {                         \---- configures sops-nix and currently
          home-manager = {         \--- accessed secrets w/o the --impure option.
            extraSpecialArgs = { inherit inputs pkgs; };
            users.<userName>.imports = [
              <pathToUserHomeManagerConfig>    <----- This would be the Home
            ];                                  \---- Manager file that is
          };                                     \--- attempting to access
        }                                         \-- the sops secret.
      ];
    };
  };
}

Real flake.nix file here.

Note(s)

Here is my full NixOS + Home Manager configuration.

In your home-manager flake user imports add inputs.sops-nix.homeManagerModule.

Then in your HM configuration file add:

{
  sops-nix,
  ...
}:{
  sops = {
    age.keyFile = ...
    defaultSopsFile = ...
  };
  <config>
}

Hopefully that works for you!

The Good

It imported sops-nix and config.sops.secrets.<bla> exist.

The Bad

The path that’s returned is incorrect. Specificly, it’s %r/secrets/app/element-desktop/message-key when it should be /run/user/1000/secrets/app/element-desktop/message-key (because it’s a Home Manager modules rather than a NixOS one).

The Ugly

There exists no /run/user/1000/secrets/ directory, so the path is wrong, but even if it was correct, it doesn’t exist. It also doesn’t exist in the location it would be at if it was configured as a NixOS module (/run/secrets/app/element-desktop/message-key).

New Home Manager File

{ config, pkgs, sops-nix, user, ... }: {
  sops = {
    defaultSopsFile = <pathToSecretsYamlFile>;
    defaultSopsFormat = "yaml";
    age.keyFile = <pathToSopsAgeKeysFile>;
    secrets."app/element-desktop/message-key" = { };
  };
  home = {
    packages = with pkgs; [ element-desktop ];
    file."${config.xdg.userDirs.documents}/Secrets/Element/security-key.txt".source = config.sops.secrets."app/element-desktop/message-key".path;
  };
}

Note

When I change .source to .text it writes this %r/secrets/app/element-desktop/message-key to the file and /run/user/1000/secrets/ still don’t exist.

Error Output

error:
       … while calling the 'head' builtin

         at /nix/store/5hwz775f3grzikafj1sbwx4lqkjwqswb-source/lib/attrsets.nix:922:11:

          921|         || pred here (elemAt values 1) (head values) then
          922|           head values
             |           ^
          923|         else

       … while evaluating the attribute 'value'

         at /nix/store/5hwz775f3grzikafj1sbwx4lqkjwqswb-source/lib/modules.nix:807:9:

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

       (stack trace truncated; use '--show-trace' to show the full trace)

       error: A definition for option `home-manager.users.reedclanton.home.file."/home/reedclanton/Documents/Secrets/Element/security-key.txt".source' is not of type `path'. Definition values:
       - In `/nix/store/z1ggqi8k3xhgcwc32702m2glhkr4k7qm-source/users/reedclanton/home/modules/applications/gui/element-desktop.nix': "%r/secrets/app/element-desktop/message-key"

I think it needs to be something like:

sops.secrets.element-message-key = {};
home = {
  file."${config......" = config.sops.secrets.element-message-key.path;
};

And then your file contents in the secrets file set something like:

element-message-key: |
  abcdefgh12345

Unfortunately, no luck. The value changes, but only to mach the new name (i.e. %r/secrets/app-element-desktop-message-key rather than %s/secrets/app/element-desktop/message-key).

Also, the same value I had works when I use --impure (as described in the original post), so I don’t think it’s an issue associated with how the secrets key is formatted. Plus, that secrets file has multiple other secrets in it that are currently functioning in the NixOS portion of the configuration.

Sorry, I’m running up against the end of my knowledge! I don’t see why you’re not getting the files created in /run/user/1000/secrets.

Don’t be sorry! Thanks for the help. I learned some about importing modules from your responses.

Perhaps the issue relate to how sops-nix is imported. Maybe importing both NixOS and Home Manager modules system wide (i.e. from the flake) causes a conflict.

Still haven’t solved this. Suggestions welcome.

FWIW I’m having the ame issue - did you ever solve this?

Unfortunately no. I’ve just been using the NixOS module in Home Manager. This requires that --impure be passed in (and is ugly and I hate it).

Hey, I actually was able to work around it. There’s a github issue for this from a while back, and it seems the solution is to append the following options to your sops config:

sops = {
  # ... all the other stuff ...
  defaultSymlinkPath = "/run/user/1000/secrets";
  defaultSecretsMountPoint = "/run/user/1000/secrets.d";
};

Reason being that sops-nix apparently has no way of knowing your UUID at build time. (I’m not really happy with this solution because I would rather not hardcode the UUID, if you happen to know a way around that, please do tell!)

Hope this is helpful!

There’s a github issue for this…

Which GitHub issue is it? Are there any updates on this issue?

My home.nix file:

{ config, pkgs, ... }:

let
  sops-path = builtins.toString inputs.nix-secrets;
  uid = let
    uidScript = pkgs.writeScript "get-uid" ''
      #!${pkgs.runtimeShell}
      id -u
    '';
  in pkgs.lib.strings.toIntBase10 (builtins.readFile
      (pkgs.runCommand "get-uid-result" { } ''
        ${uidScript} >$out
      '')
    );
in
{
  sops = {
    defaultSymlinkPath = "/run/user/${builtins.toString uid}/secrets";
    defaultSecretsMountPoint = "/run/user/${builtins.toString uid}/secrets.d";
    secrets = {
      # ...
    };
  };

  # ... other config
}

There really should be a way to get your user id from the home manager configuration, but this was the best I could figure out so far. Perhaps there is an issue filed within the home-manager repository or this could become a feature request? If anyone finds an issue, it would be great to link it here!

And this of course evaluates in pure mode, so no need for an --impure flag either!

Am I misunderstanding what you are doing, or is this

  1. Import from derivation and
  2. Getting the UID of the builder, not the user running home-manager?

There is no pure and reliable way to get the users UID, unless you hardcode it.

You know, I’m not sure if its getting the UID of the builder or the home manager. I would assume its getting the UID of the home manager user since I build as root and its returning 1000 in my derivation. A simple two user test should reveal what it’s doing. It all depends on who pkgs.runCommand is run as.

Also I would like to note that from this thread: Home Manager Symlinking Directories - #3 by user2358, you shouldn’t readFile your secret since it would put it in the nix store visible to everyone. Instead you could symlink the file so that permission are retained.

If I’m reading home-manager/nixos/default.nix at 1e8c62c651242fc685b10efc4a48ab777635fb7f · nix-community/home-manager · GitHub correctly, home-manager sets up a systemd service per user that is run as that user. Perhaps pkgs.runCommand is run during this stage? I’m not super familiar with the build order of nix to know when and who the command is run as. If the command is run as root, then perhaps we can pass in the username to the command? The config.username, or home.username option seems to exist, home-manager/modules/home-environment.nix at 1e8c62c651242fc685b10efc4a48ab777635fb7f · nix-community/home-manager · GitHub. Then run id -u ${username}?

No, it isn’t. runCommand causes a derivation.

I am really curious why this is producing a 1000 for you, I really wouldn’t expect that.

I am even more confused. I ran a simple nix repl and issued the same command, commands below if someone wants to try it. I ran the nix repl both as myself and as root using sudo su root. Both returned 1000, which is not correct, but also not what I was expecting. So even though I’m issuing the command as sudo nixos-rebuild switch --flake /etc/nixos, it returns 1000 as my uid. I would also assume that this does not work for two users, as it will return 1000 no matter what.

nix-repl> pkgs = import <nixpkgs> {}
nix-repl> let
          uid = let
              uidScript = pkgs.writeScript "get-uid" ''
                #!${pkgs.runtimeShell}
                id -u
              '';
            in pkgs.lib.strings.toIntBase10 (builtins.readFile
                (pkgs.runCommand "get-uid-result" { } ''
                  ${uidScript} >$out
                '')
              );
          in {
          uid = uid;
          }

{ uid = 1000; }

I think a better way to not hard-code the UID’s is to somehow use arguments passed to the home manager through where you declare the home.nix file. From Home Manager Manual, near the bottom of the section:

Home Manager will pass osConfig as a module argument to any modules you create. This contains the system’s NixOS configuration.

{ lib, pkgs, osConfig, ... }:

I then used the osConfig to get the user id.

{ config, pkgs, osConfig, inputs, ... }:

let
  sops-path = builtins.toString inputs.nix-secrets;
  uid = osConfig.users.users.${config.home.username}.uid;
in
{
  sops = { # ... };
}

In order for this to work, you also need to set a uid in your configuration.nix for the main OS. This is because the uid can be null, and if so, is only picked upon activation, nixpkgs/nixos/modules/config/users-groups.nix at 3e362ce63e16b9572d8c2297c04f7c19ab6725a5 · NixOS/nixpkgs · GitHub.

The account UID. If the UID is null, a free UID is picked on activation.