Skip to content

darwin: sops-install-secrets fails with "cannot create gpg home in '/run/secrets.d': mkdir ... permission denied" during deploy-rs activation, caused by stale launchd daemon with old manifest #974

Description

@xbpk3t

Summary

On macOS (aarch64-darwin, nix-darwin) with deploy-rs, sops-install-secrets fails during activation with:

sops-install-secrets: Imported /etc/ssh/ssh_host_rsa_key as GPG key with fingerprint c501a8ed...274554
⭐ ❌ Nix activation script command resulted in a bad exit status: exit status: 1

Running the same sops-install-secrets binary + same manifest as root manually succeeds silently (exit 0). The failure only happens during deploy-rs activate, and the true error (seen when running as non-root) is:

sops-install-secrets: error setting up gpg keyring: cannot create gpg home in '/run/secrets.d': mkdir /run/secrets.d/gpg1698314092: permission denied

Root cause: stale launchd daemon keeps the old manifest (with GPG import enabled)

The nix-darwin activation script (activate, around line 1146+) re-installs /Library/LaunchDaemons/org.nixos.sops-install-secrets.plist and calls launchctl load -w when the plist changes. But the launchd daemon process itself is not reliably re-loaded after cp -f: the previous daemon registration (from the previous generation) keeps running with:

  • the old manifest (e.g. gsr78izq...-manifest.json with sshKeyPaths = ["/etc/ssh/ssh_host_rsa_key"]),
  • the old SOPS_GPG_EXEC=.../gpg env var.

So even when the new generation's manifest has sshKeyPaths = [] (GPG import disabled — see our config below), the stale daemon still runs sops-install-secrets with the old manifest, imports /etc/ssh/ssh_host_rsa_key as a GPG key, and then tries to create its gpg keyring home inside the secrets mount point /run/secrets.d/ (which on macOS is the mounted secretfs at /dev/disk5), hitting mkdir /run/secrets.d/gpg<timestamp>: permission denied.

Evidence

  1. Active successful generation (e.g. gen 504, 26.11.57a3171) after a nix flake rollback → its activate script references manifest gsr8izq1... which has sshKeyPaths = ["/etc/ssh/ssh_host_rsa_key"] → and its launchctl daemon's ProgramArguments contains export SOPS_GPG_EXEC=.../gpg && sops-install-secrets <that manifest>.
  2. Next deploy (new generation 26.11.15abb8c) → activation reloads the plist (new manifest sshKeyPaths=[]), but the daemon still runs with the old manifest + GPG → failure inside /run/secrets.d.
  3. Manual re-run as root with the exact same (new) manifest + binary → silent success (exit 0), because by then secrets are already written and gpg keyring not needed... (but during the actual failed activate, the stale daemon still uses the old manifest.)
  4. Between successful gen and failed gen: sops-install-secrets store path differs (jffi34i10... vs zbawbql73...) but the binary content is identical (same arch, strings same; md5 differs only due to build meta). So it's not a binary regression — it's the daemon reload race.

My configuration (secrets/default.nix)

sops = {
  age.keyFile = "/Users/luck/Library/Application Support/sops/age/keys.txt";
  age.sshKeyPaths = [ ];   # Disable SSH key import
  gnupg.home = null;       # Disable GPG key import
  # NOTE: manifest still includes sshKeyPaths = ["/etc/ssh/ssh_host_rsa_key"]
  # even with gnupg.home = null — this is what revived the stale daemon's GPG import.
  # Setting gnupg.sshKeyPaths = [] makes the new manifest sshKeyPaths = [] and
  # sops-install-secrets silent (no GPG import), but the stale daemon still uses
  # the old one during switch.
};

Repro

  1. Have a nix-darwin system with sops.age.sshKeyPaths empty but sops.gnupg.home = null (GPG import still on by default).
  2. Deploy with succeeded generation (e.g. after nix flake rollback).
  3. Then deploy a new generation that changes the manifest (any secret/template change).
  4. Activation fails at Setting up secrets... with Imported /etc/ssh/ssh_host_rsa_key as GPG key then exit 1.

Expected

  • nix-darwin should fully restart (or re-bootstrap) the org.nixos.sops-install-secrets launchd daemon after replacing its plist, and/or
  • sops.gnupg.sshKeyPaths = [] (or age.sshKeyPaths = [] + gnupg.home=null) should fully suppress the SSH→GPG import so it never touches /run/secrets.d.

Related (but diff)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions