Enabling secret service in `nixosTest`

Hello, I’m trying to test some software I wrote that uses the system keyring, but I’m getting an error showing that it’s unavailable. I have set services.gnome.gnome-keyring.enable = true;, but same error. I’m using the startx display manager.

How can I get the keyring up and running in a nixosTest?

Here is the full node config:

{ config, pkgs, ... }: {
  imports = args.imports or [ ];

  users.users.${user} = {
    isNormalUser = true;
    home = "/home/${user}";
    createHome = true;
    extraGroups = [ "wheel" ];
    password = user;
  };
  users.users.root = {
    hashedPassword = "";
    hashedPasswordFile = null;
  };
  services.openssh = {
    enable = true;
    settings.PermitRootLogin = "yes";
    settings.PermitEmptyPasswords = "yes";
  };
  services.gnome.gnome-keyring.enable = true;

  services.xserver.enable = true;
  services.xserver.displayManager.startx.enable = true;
  services.getty.autologinUser = user;

  environment.systemPackages = with pkgs; [
    xdotool
    reaper
    reaper_lib_cloud_reaper
    reacloudd
    cargo-reaper
  ]
  ++ (args.packages or [ ]);

  systemd.services.reacloudd.environment = {
    LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${ with pkgs; lib.makeLibraryPath [
      libx11
      libxi
      libxcursor
      libxrandr
      libxkbcommon
    ] }";
  };
}

Not clear if you’re also setting up a D-Bus session and connecting to it properly. Can you post your full test?

I’ve since figured out that the keyring iss working properly but my app running in a systemd service isn’t able to access it

Again, I suspect the D-Bus connection (in the absence of any concrete error messages). Post the full test or good luck!

Sorry I was having to copy things over from several files:

# in my flake checks
          single-user-with-updates =
            let
              server = "reacloud";
              nodes.${server} = mkServerNode {
                imports = [
                  modules.stitches
                  ../configurations
                ];
              };
              user = "buster";
              nodes.${user} = mkUserNode {
                inherit user;
                imports = [
                  ({ config, lib, pkgs, ... }: {
                    imports = [ modules.reacloudd ];
                    config.services.reacloudd = {
                      enable = true;
                      server-url = "http://${server}:8080";
                      headless = true;
                      log-level = "debug"; # we need debug logs to check user id
                      # debug = true;
                    };
                  })
                ];
              };
              testScript = builtins.readFile (testScripts.single-user-with-updates {
                inherit server user;
              });
            in
            pkgs.testers.nixosTest {
              inherit nodes testScript;
              name = "single-user-with-updates";
            };

# the service that cannot use the keyring
  config = lib.mkIf cfg.enable {
    systemd.services.reacloudd = {
      description = "reacloudd";
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        Type = "simple";
        ExecStart =
          let
            command = (builtins.concatStringsSep " \\\n  "
              (lib.optionals cfg.headless [
                "${pkgs.xvfb-run}/bin/xvfb-run -a"
              ]
              ++ [
                "${cfg.package}/bin/reacloudd"
                "--addr ${cfg.extension-address}"
                "--port ${toString cfg.extension-port}"
                "--url ${cfg.server-url}"
              ]));
          in
          if cfg.debug then
            builtins.trace "ExecStart:\n${command}" command
          else
            command
        ;
        Restart = "always";
        Environment =
          let
            env = [
              "RUST_LOG=${cfg.log-level}"
            ];
          in
          if cfg.debug then
            builtins.trace "Environment:\n${builtins.concatStringsSep "\n" env}" env
          else
            env
        ;
      };
    };
  };
# the test script
{ writeText
, cargo-reaper
, reaper_lib_cloud_reaper
, reacloudctl
, server
, user
}:
writeText "single-user-with-updates.py" ''
  start_all();
  ${server}.wait_for_unit("stitches.service");
  ${user}.wait_for_unit("reacloudd.service");

  # Launch REAPER once to initialize `~/.config/REAPER/UserPlugins`
  ${user}.succeed("su - ${user} -c '${cargo-reaper}/bin/cargo-reaper run --no-build --headless --timeout 5s --stdout null --stderr null'");

  # Install extension plugin
  ${user}.succeed("su - ${user} -c '${cargo-reaper}/bin/cargo-reaper link ${reaper_lib_cloud_reaper}/lib/reaper_lib_cloud_reaper.*'");
  ${user}.succeed("test -e /home/${user}/.config/REAPER/UserPlugins/reaper_lib_cloud_reaper.*");

  # Copy project onto host machine
  ${user}.succeed("mkdir -p /home/${user}/Documents/REAPER\ Media/midi_with_render/Media");
  ${user}.copy_from_host("${../../../../.sim/midi_with_render/midi_with_render.RPP}", "/home/${user}/Documents/REAPER Media/midi_with_render/midi_with_render.RPP");
  ${user}.copy_from_host("${../../../../.sim/midi_with_render/Media/midi_render.wav}", "/home/${user}/Documents/REAPER Media/midi_with_render/Media/midi_render.wav");

  # Login to ReaCloud for user
  ${user}.succeed("echo Password1@ | su - ${user} -c '${reacloudctl}/bin/reacloudctl login bsavage.hyneman@mythbusters.com'");
  ${user}.sleep(2); # the daemon needs time to process the login request

  # Check debug log for user login
  ${user}.succeed("journalctl -u reacloudd.service | grep 37502984-d06f-469d-963f-8f0e949465eb");

  # TODO: Make a change to the session and trigger a sync event
''
# the error from the app running as a systemd service
vm-test-run-single-user-with-updates> buster # [   13.078463] xvfb-run[849]:   2026-02-28T19:18:05.195525Z  INFO reacloudd::login::login_manager: creating new keyring entry and adding credentials for current user
vm-test-run-single-user-with-updates> buster # [   13.079945] xvfb-run[849]:     at src/reacloudd/src/login/login_manager.rs:191
vm-test-run-single-user-with-updates> buster # [   13.080882] xvfb-run[849]:   2026-02-28T19:18:05.195549Z  INFO reacloudd::login::login_manager: adding new credentials to login manager
vm-test-run-single-user-with-updates> buster # [   13.082084] xvfb-run[849]:     at src/reacloudd/src/login/login_manager.rs:219
vm-test-run-single-user-with-updates> buster # [   13.082947] xvfb-run[849]:   2026-02-28T19:18:05.199107Z ERROR reacloudd: login manager failed to process login response: PlatformFailure(Unavailable): Err(NotPresent)

The Err(NotPreset) is from std::env::var("DBUS_SESSION_BUS_ADDRESS").

The PlatformFailure(Unavailable) is from the rust keyring crate’s keyring::Entry::new.

Wait, so reacloudd runs as a system-wide service, and you expect it to integrate with the keyring, which is a per-user daemon? How’s that supposed to work?

That was my own misunderstanding about the keyring and system services vs. user services. I just updated it to be a user service, and now I get this error instead (yay progress!):

buster # [   82.300599] xvfb-run[976]:   2026-02-28T19:54:24.101372Z ERROR reacloudd: login manager failed to process login response: NoStorageAccess(NoResult): Ok("unix:path=/run/user/1000/bus")

Here’s the updated user service:

  config = lib.mkIf cfg.enable {
    systemd.user.services.reacloudd = {
      description = "reacloudd";
      wantedBy = [ "default.target" ];
      serviceConfig = {
        Type = "simple";
        ExecStart =
          let
            command = (builtins.concatStringsSep " \\\n  "
              (lib.optionals cfg.headless [
                "${pkgs.xvfb-run}/bin/xvfb-run -a"
              ]
              ++ [
                "${cfg.package}/bin/reacloudd"
                "--addr ${cfg.extension-address}"
                "--port ${toString cfg.extension-port}"
                "--url ${cfg.server-url}"
              ]));
          in
          if cfg.debug then
            builtins.trace "ExecStart:\n${command}" command
          else
            command
        ;
        Restart = "always";
        Environment =
          let
            env = [
              "RUST_LOG=${cfg.log-level}"
            ];
          in
          if cfg.debug then
            builtins.trace "Environment:\n${builtins.concatStringsSep "\n" env}" env
          else
            env
        ;
      };
    };
  };

That error might mean that you haven’t unlocked the keyring. AIUI, if you use services.getty.autologinUser, the keyring won’t unlock automatically when the user is logged in, because the PAM module that would do that needs a password to be entered. Try removing autologinUser and writing your test to log the user in with send_chars.

1 Like

After unlocking the keyring for the user, I get this error when trying to create a new entry:

ERROR reacloudd: login manager failed to process login response: PlatformFailure(Zbus(MethodError(OwnedErrorName("org.freedesktop.DBus.Error.UnknownMethod"), Some("Object does not exist at path “/org/freedesktop/secrets/collection/login”"), Msg { type: Error, serial: 12, sender: UniqueName(":1.2"), reply-serial: 11, body: Signature("s"), fds: [] }))): Ok("unix:path=/run/user/1000/bus")

I’ll have a repro ready here in just a moment.

Okay, well, the first problem is you didn’t take my advice and disable autologinUser. Your way of unlocking the keyring doesn’t work without manually specifying the D-Bus session address.

The second problem is that there are a lot of… choices made in the config that I think are contributing to the problems. I would start with the below and only add back in what’s necessary to make something work—and if you do that incrementally, you’ll know what caused any breaks you encounter. (You’ll have to translate back into flake-ese on your own but it shouldn’t be a challenge.)

{
  lib,
  rustPlatform,
  testers,
}:
let

  app = rustPlatform.buildRustPackage {
    pname = "nixos-test-keyring";
    version = "0.1.0";
    src = lib.cleanSourceWith {
      filter = path: _type: !lib.hasSuffix ".nix" path;
      src = lib.cleanSource ./.;
    };
    cargoLock.lockFile = ./Cargo.lock;
  };

  module.systemd.user.services.nixos-test-keyring = {
    description = "nixos-test-keyring";
    wantedBy = [ "default.target" ];
    serviceConfig = {
      Type = "simple";
      Restart = "always";
      ExecStart = "${app}/bin/nixos-test-keyring";
      Environment = [ "RUST_LOG=debug" ];
    };
  };

in
testers.nixosTest {
  name = "nixos-test-keyring";
  nodes.machine = {
    imports = [ module ];
    users.users.machine = {
      isNormalUser = true;
      password = "machine";
    };
    services.gnome.gnome-keyring.enable = true;
  };
  testScript = ''
    machine.start();
    machine.wait_until_tty_matches("1", "login: ")
    machine.send_chars("machine\n")
    machine.wait_until_tty_matches("1", "Password: ")
    machine.send_chars("machine\n")
    machine.wait_for_unit("user@1000.service")
    machine.wait_for_unit("nixos-test-keyring.service", user="machine")
    machine.sleep(4) # wait for logs to be written
    logs = machine.succeed("journalctl _SYSTEMD_USER_UNIT=nixos-test-keyring.service")
    assert "ERROR" not in logs
    assert "successfully set password" in logs
    assert "successfully got password: password" in logs
  '';
}
1 Like

Yeah some of this stuff is extra, but I doubt was causing the issue. I removed them since some of them weren’t even useful, they were there for debugging something a long time ago. You were right about the auto login though, I didn’t know what you meant by send_chars so I just assumed I needed to login to the keyring and went that way since I knew how to do it that way.

Sometimes moving fast results in messy code… Thanks for the help.

I have a private project that needs something similar. I use pass to provide the secret service without pulling in too many things.

Here’s the basic setup, you’ll probably need to massage this a bit for it to work as I copy pasted just the relevant parts:

{
  nodes.machine = {
    # Provide a lightweight libsecret Secret Service
    services.dbus.enable = true;
    services.passSecretService.enable = true;

    users.users.alice.linger = true;
  };

  testScript =
    let
      key = ./data/${user.name}-gpg.key;
      keyidSh = ''
        ${lib.getExe pkgs.gnupg} --show-keys --with-colons ${key} \
          | grep ^fpr: \
          | sed -e '1!d' -Ee 's/.+:([^:]+):$/\1/'
      '';

      user.name = "alice";
      su = command: toPythonStr "su -l ${user.name} -c ${lib.escapeShellArg command}";
      toPythonStr = str: ''"${lib.replaceStrings [ ''"'' "\n" ''\'' ] [ ''\"'' ''\n'' ''\\'' ] str}"'';
    in
    # python
    ''
      ${machine}.systemctl("start dbus.service", "${user.name}")
      ${machine}.wait_for_unit("dbus.service", "${user.name}")

      ${machine}.succeed(
        ${su "${lib.getExe pkgs.gnupg} --import ${./data/${user.name}-gpg.key}"},
        ${su ''${lib.getExe pkgs.gnupg} --import-ownertrust <<< "$(${keyidSh})":6:''},

        ${su "${lib.getExe pkgs.pass} init ${user.name}@test.invalid"},
      )
    '';
}

1 Like