From 6f5fe8036b513be8a4c90150c2cd1516aef554c0 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Mon, 30 Mar 2026 15:49:09 +0200 Subject: [PATCH] sops-install-secrets: call systemctl directly when run as a systemd unit When useSystemdActivation is enabled, sops-install-secrets.service runs ordered Before=sysinit-reactivation.target, which switch-to-configuration restarts *after* it has already consumed /run/nixos/activation-*-list. Writing to those files from the service therefore does nothing on the current switch and leaks into the next one. NixOS 26.05 also deprecates the activation-list mechanism, printing a warning whenever the files exist, with removal planned for 26.11. Detect systemd invocation via INVOCATION_ID and call systemctl directly (try-restart / try-reload-or-restart, --no-block to avoid deadlocking the sysinit transaction). The legacy activation-script path keeps writing the list files for backward compatibility. --- modules/sops/default.nix | 2 +- modules/sops/secrets-for-users/default.nix | 2 +- pkgs/sops-install-secrets/main.go | 105 ++++++++++++++++----- 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/modules/sops/default.nix b/modules/sops/default.nix index 9f19ab6..08d1723 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -472,7 +472,7 @@ in before = [ "sysinit-reactivation.target" ]; environment = cfg.environment; unitConfig.DefaultDependencies = "no"; - path = cfg.age.plugins; + path = cfg.age.plugins ++ [ config.systemd.package ]; serviceConfig = { Type = "oneshot"; diff --git a/modules/sops/secrets-for-users/default.nix b/modules/sops/secrets-for-users/default.nix index 841eb13..5fe756c 100644 --- a/modules/sops/secrets-for-users/default.nix +++ b/modules/sops/secrets-for-users/default.nix @@ -37,7 +37,7 @@ in before = [ "systemd-sysusers.service" ]; environment = cfg.environment; unitConfig.DefaultDependencies = "no"; - path = cfg.age.plugins; + path = cfg.age.plugins ++ [ config.systemd.package ]; serviceConfig = { Type = "oneshot"; diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 05e6cfc..1e216b5 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -8,6 +8,7 @@ import ( "flag" "fmt" "os" + "os/exec" "os/user" "path" "path/filepath" @@ -1020,38 +1021,90 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s } } - writeLines := func(list []string, file string) error { - if len(list) != 0 { - if _, err := os.Stat(filepath.Dir(file)); err != nil { - if os.IsNotExist(err) { - return nil - } + // Decide how to propagate restart/reload requests. + // + // When we run as a systemd service (useSystemdActivation = true), the + // unit is ordered Before=sysinit-reactivation.target, which + // switch-to-configuration restarts *after* it has already consumed + // /run/nixos/activation-{restart,reload}-list. Writing to those files + // from here therefore does nothing on the current switch and leaks + // into the next one. On top of that, NixOS 26.05 deprecates the + // activation-list mechanism entirely. + // + // Detect systemd invocation via INVOCATION_ID and call systemctl + // directly in that case. For the legacy activation-script path (no + // INVOCATION_ID), keep writing the list files so that + // switch-to-configuration picks them up as before. + if _, runningUnderSystemd := os.LookupEnv("INVOCATION_ID"); runningUnderSystemd { + systemctl := func(verb string, units []string) error { + if len(units) == 0 { + return nil + } + // --no-block: we are ordered before sysinit-reactivation.target + // with DefaultDependencies=no. Blocking on a normal service + // (which has After=sysinit.target) would deadlock the + // transaction. + args := append([]string{"--no-block", verb}, units...) + cmd := exec.Command("systemctl", args...) + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("systemctl %s %s: %w", verb, strings.Join(units, " "), err) + } + return nil + } + if isDry { + for _, u := range restart { + fmt.Fprintf(os.Stderr, "would restart %s\n", u) + } + for _, u := range reload { + fmt.Fprintf(os.Stderr, "would reload %s\n", u) + } + } else { + // try-restart: only act on units that are already running. + // On first activation the unit starts fresh with the new + // secret anyway, so a no-op is correct. + if err := systemctl("try-restart", restart); err != nil { return err } - f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) - if err != nil { + if err := systemctl("try-reload-or-restart", reload); err != nil { return err } - defer func() { _ = f.Close() }() - for _, unit := range list { - if _, err = f.WriteString(unit + "\n"); err != nil { - return err - } - } } - return nil - } - var dryPrefix string - if isDry { - dryPrefix = "/run/nixos/dry-activation" } else { - dryPrefix = "/run/nixos/activation" - } - if err := writeLines(restart, dryPrefix+"-restart-list"); err != nil { - return err - } - if err := writeLines(reload, dryPrefix+"-reload-list"); err != nil { - return err + writeLines := func(list []string, file string) error { + if len(list) != 0 { + if _, err := os.Stat(filepath.Dir(file)); err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + for _, unit := range list { + if _, err = f.WriteString(unit + "\n"); err != nil { + return err + } + } + } + return nil + } + var dryPrefix string + if isDry { + dryPrefix = "/run/nixos/dry-activation" + } else { + dryPrefix = "/run/nixos/activation" + } + if err := writeLines(restart, dryPrefix+"-restart-list"); err != nil { + return err + } + if err := writeLines(reload, dryPrefix+"-reload-list"); err != nil { + return err + } } // Do not output changes if not requested