From 07bd5766284927f1283c4bcd7b617b4815134d37 Mon Sep 17 00:00:00 2001 From: Abel Luck Date: Thu, 26 Feb 2026 19:11:53 +0100 Subject: [PATCH 1/7] add initial nixos modules --- flake.nix | 32 +++++++++++++++++++++++++++ nixos-module-server.nix | 49 +++++++++++++++++++++++++++++++++++++++++ nixos-module.nix | 42 +++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 nixos-module-server.nix create mode 100644 nixos-module.nix diff --git a/flake.nix b/flake.nix index e1c0ef9..a5a5919 100644 --- a/flake.nix +++ b/flake.nix @@ -44,5 +44,37 @@ ]; }; }); + + nixosModules = { + # Workstation: systemd user timer+service running `nix-cache-login refresh` + default = + { + config, + lib, + pkgs, + ... + }: + { + imports = [ ./nixos-module.nix ]; + services.nix-cache-login.package = + lib.mkDefault + self.packages.${pkgs.stdenv.hostPlatform.system}.default; + }; + + # Server: system-level timer+service running `nix-cache-login service-account` + server = + { + config, + lib, + pkgs, + ... + }: + { + imports = [ ./nixos-module-server.nix ]; + services.nix-cache-login-server.package = + lib.mkDefault + self.packages.${pkgs.stdenv.hostPlatform.system}.default; + }; + }; }; } diff --git a/nixos-module-server.nix b/nixos-module-server.nix new file mode 100644 index 0000000..74bac7f --- /dev/null +++ b/nixos-module-server.nix @@ -0,0 +1,49 @@ +{ config, lib, ... }: +let + cfg = config.services.nix-cache-login-server; +in +{ + options.services.nix-cache-login-server = { + enable = lib.mkEnableOption "nix-cache-login service-account token refresh"; + package = lib.mkOption { + type = lib.types.package; + description = "The nix-cache-login package to use."; + }; + configFile = lib.mkOption { + type = lib.types.path; + description = '' + Path to the nix-cache-login config.toml file. Must include + client_secret_file pointing to a readable credentials file. + ''; + example = "/etc/nix-cache-login/config.toml"; + }; + refreshInterval = lib.mkOption { + type = lib.types.str; + default = "15min"; + description = '' + Interval between token refresh attempts, as a systemd time span. + On failure the service logs an error and the timer retries on schedule. + ''; + example = "1h"; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.nix-cache-login = { + description = "Nix cache login - service account token refresh"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${cfg.package}/bin/nix-cache-login --config ${cfg.configFile} service-account"; + }; + }; + + systemd.timers.nix-cache-login = { + description = "Nix cache login - periodic service account token refresh"; + timerConfig = { + OnBootSec = "2min"; + OnUnitActiveSec = cfg.refreshInterval; + }; + wantedBy = [ "timers.target" ]; + }; + }; +} diff --git a/nixos-module.nix b/nixos-module.nix new file mode 100644 index 0000000..ab0e12a --- /dev/null +++ b/nixos-module.nix @@ -0,0 +1,42 @@ +{ config, lib, ... }: +let + cfg = config.services.nix-cache-login; +in +{ + options.services.nix-cache-login = { + enable = lib.mkEnableOption "nix-cache-login automatic token refresh"; + package = lib.mkOption { + type = lib.types.package; + description = "The nix-cache-login package to use."; + }; + refreshInterval = lib.mkOption { + type = lib.types.str; + default = "15min"; + description = '' + Interval between token refresh attempts, as a systemd time span. + If no valid session exists, the service logs an error and the timer + retries on the next interval. Run nix-cache-login to log in. + ''; + example = "1h"; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.user.services.nix-cache-login = { + description = "Nix cache login - refresh access token"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${cfg.package}/bin/nix-cache-login refresh"; + }; + }; + + systemd.user.timers.nix-cache-login = { + description = "Nix cache login - periodic token refresh"; + timerConfig = { + OnBootSec = "2min"; + OnUnitActiveSec = cfg.refreshInterval; + }; + wantedBy = [ "timers.target" ]; + }; + }; +} From 7b49665ee53b812eb13fff69d0c0305a23165861 Mon Sep 17 00:00:00 2001 From: Abel Luck Date: Fri, 27 Feb 2026 08:36:10 +0100 Subject: [PATCH 2/7] lower min go version --- go.mod | 2 +- package.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 78ab0a1..423298f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module guardianproject.dev/ops/nix-cache-login -go 1.25.7 +go 1.25.0 require ( github.com/adrg/xdg v0.5.3 diff --git a/package.nix b/package.nix index c5344f1..cb25f28 100644 --- a/package.nix +++ b/package.nix @@ -6,7 +6,7 @@ buildGoModule { pname = "nix-cache-login"; - version = "0.1.0"; + version = "0.1.1"; src = ./.; # src = fetchgit { # url = "https://guardianproject.dev/ops/nix-cache-login.git"; From eeb8a69740548795317e79ad3db180d5f59c264e Mon Sep 17 00:00:00 2001 From: Abel Luck Date: Fri, 27 Feb 2026 08:39:18 +0100 Subject: [PATCH 3/7] add package to PATH --- nixos-module-server.nix | 1 + nixos-module.nix | 1 + 2 files changed, 2 insertions(+) diff --git a/nixos-module-server.nix b/nixos-module-server.nix index 74bac7f..85300b9 100644 --- a/nixos-module-server.nix +++ b/nixos-module-server.nix @@ -29,6 +29,7 @@ in }; config = lib.mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; systemd.services.nix-cache-login = { description = "Nix cache login - service account token refresh"; serviceConfig = { diff --git a/nixos-module.nix b/nixos-module.nix index ab0e12a..d2ad4e7 100644 --- a/nixos-module.nix +++ b/nixos-module.nix @@ -22,6 +22,7 @@ in }; config = lib.mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; systemd.user.services.nix-cache-login = { description = "Nix cache login - refresh access token"; serviceConfig = { From ba34ac0d6729c8df5e05eed7aa04920cc48ea570 Mon Sep 17 00:00:00 2001 From: Abel Luck Date: Fri, 27 Feb 2026 09:27:34 +0100 Subject: [PATCH 4/7] add nixos and darwin-nix modules --- CHANGELOG.md | 2 ++ flake.nix | 48 ++++++++++++++++++++++++++++---------- home-module.nix | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ nixos-test.nix | 29 +++++++++++++++++++++++ 4 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 home-module.nix create mode 100644 nixos-test.nix diff --git a/CHANGELOG.md b/CHANGELOG.md index b728640..4deead8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes yet to be released are documented here. +- Add nixos, home-manager, and darwin-nix modules + ## v0.1.0 Initial release. diff --git a/flake.nix b/flake.nix index a5a5919..72df013 100644 --- a/flake.nix +++ b/flake.nix @@ -6,6 +6,7 @@ let systems = [ "x86_64-linux" + "aarch64-linux" "aarch64-darwin" ]; forAllSystems = fn: nixpkgs.lib.genAttrs systems (system: fn nixpkgs.legacyPackages.${system}); @@ -23,18 +24,24 @@ }; }); - checks = forAllSystems (pkgs: { - tests = self.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs (_: { - pname = "nix-cache-login-tests"; - checkPhase = '' - runHook preCheck - go test ./... - runHook postCheck - ''; - doCheck = true; - }); - devShell = self.devShells.${pkgs.stdenv.hostPlatform.system}.default; - }); + checks = forAllSystems ( + pkgs: + { + tests = self.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs (_: { + pname = "nix-cache-login-tests"; + checkPhase = '' + runHook preCheck + go test ./... + runHook postCheck + ''; + doCheck = true; + }); + devShell = self.devShells.${pkgs.stdenv.hostPlatform.system}.default; + } + // pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { + nixos-module = pkgs.testers.runNixOSTest (import ./nixos-test.nix self); + } + ); devShells = forAllSystems (pkgs: { default = pkgs.mkShell { @@ -45,6 +52,23 @@ }; }); + homeModules = { + # Workstation (Linux + macOS): home-manager module running `nix-cache-login refresh` + default = + { + config, + lib, + pkgs, + ... + }: + { + imports = [ ./home-module.nix ]; + services.nix-cache-login.package = + lib.mkDefault + self.packages.${pkgs.stdenv.hostPlatform.system}.default; + }; + }; + nixosModules = { # Workstation: systemd user timer+service running `nix-cache-login refresh` default = diff --git a/home-module.nix b/home-module.nix new file mode 100644 index 0000000..90e80ab --- /dev/null +++ b/home-module.nix @@ -0,0 +1,62 @@ +{ + config, + lib, + ... +}: +let + cfg = config.services.nix-cache-login; +in +{ + options.services.nix-cache-login = { + enable = lib.mkEnableOption "nix-cache-login automatic token refresh"; + package = lib.mkOption { + type = lib.types.package; + description = "The nix-cache-login package to use."; + }; + refreshInterval = lib.mkOption { + type = lib.types.ints.positive; + default = 900; + description = '' + How often to attempt token refresh, in seconds. + If no valid session exists, the service logs an error and retries on + the next interval. Run {command}`nix-cache-login` to log in. + ''; + example = 1800; + }; + }; + + config = lib.mkIf cfg.enable { + home.packages = [ cfg.package ]; + systemd.user.services.nix-cache-login = { + Unit.Description = "Nix cache login - refresh access token"; + Service = { + Type = "oneshot"; + ExecStart = "${cfg.package}/bin/nix-cache-login refresh"; + }; + }; + + systemd.user.timers.nix-cache-login = { + Unit.Description = "Nix cache login - periodic token refresh"; + Timer = { + OnBootSec = "2min"; + OnUnitActiveSec = "${toString cfg.refreshInterval}s"; + }; + Install.WantedBy = [ "timers.target" ]; + }; + + launchd.agents.nix-cache-login = { + enable = true; + config = { + ProgramArguments = [ + "${cfg.package}/bin/nix-cache-login" + "refresh" + ]; + StartInterval = cfg.refreshInterval; + RunAtLoad = true; + ProcessType = "Background"; + StandardOutPath = "${config.home.homeDirectory}/Library/Logs/nix-cache-login.log"; + StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/nix-cache-login.log"; + }; + }; + }; +} diff --git a/nixos-test.nix b/nixos-test.nix new file mode 100644 index 0000000..9c5840f --- /dev/null +++ b/nixos-test.nix @@ -0,0 +1,29 @@ +self: { + name = "nix-cache-login-nixos-module"; + + nodes.machine = + { ... }: + { + imports = [ self.nixosModules.default ]; + services.nix-cache-login.enable = true; + }; + + testScript = '' + machine.wait_for_unit("multi-user.target") + + # The module should install timer and service unit files for all users + machine.succeed("test -f /etc/systemd/user/nix-cache-login.timer") + machine.succeed("test -f /etc/systemd/user/nix-cache-login.service") + + # wantedBy = ["timers.target"] should create this symlink + machine.succeed( + "test -L /etc/systemd/user/timers.target.wants/nix-cache-login.timer" + ) + + # Service unit should reference the correct subcommand + unit = machine.succeed("cat /etc/systemd/user/nix-cache-login.service") + assert "nix-cache-login refresh" in unit, ( + f"ExecStart not found in service unit:\n{unit}" + ) + ''; +} From 29fe7f76c122934ca37ef6edae628c8387a4298f Mon Sep 17 00:00:00 2001 From: Abel Luck Date: Fri, 27 Feb 2026 09:27:49 +0100 Subject: [PATCH 5/7] expand the path to support working in systemd service --- CHANGELOG.md | 1 + internal/config/config.go | 27 +++++++++++++++++++++++++-- package.nix | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4deead8..7731311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changes yet to be released are documented here. +- Fix path expansion when running in systemd - Add nixos, home-manager, and darwin-nix modules ## v0.1.0 diff --git a/internal/config/config.go b/internal/config/config.go index 77faaa1..cc37736 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -37,8 +37,8 @@ func Load(path string) (*Config, error) { return nil, fmt.Errorf("parsing config file: %w", err) } - cfg.NetrcPath = os.ExpandEnv(cfg.NetrcPath) - cfg.ClientSecretFile = os.ExpandEnv(cfg.ClientSecretFile) + cfg.NetrcPath = expandPath(cfg.NetrcPath) + cfg.ClientSecretFile = expandPath(cfg.ClientSecretFile) if cfg.ClientSecretFile != "" { secret, err := os.ReadFile(cfg.ClientSecretFile) @@ -71,6 +71,29 @@ func (c *Config) validate() error { return nil } +// expandPath expands environment variables in a path. XDG base directory +// variables are resolved using the xdg library, which applies the XDG spec +// fallbacks (e.g. $HOME/.config when $XDG_CONFIG_HOME is unset). This ensures +// correct behaviour in systemd user services, which do not set XDG variables. +func expandPath(s string) string { + return os.Expand(s, func(key string) string { + switch key { + case "XDG_CONFIG_HOME": + return xdg.ConfigHome + case "XDG_DATA_HOME": + return xdg.DataHome + case "XDG_CACHE_HOME": + return xdg.CacheHome + case "XDG_STATE_HOME": + return xdg.StateHome + case "XDG_RUNTIME_DIR": + return xdg.RuntimeDir + default: + return os.Getenv(key) + } + }) +} + // RefreshTokenPath returns the path to the stored refresh token. func RefreshTokenPath() string { return filepath.Join(xdg.ConfigHome, "nix-cache-login", "refresh-token") diff --git a/package.nix b/package.nix index cb25f28..e85a434 100644 --- a/package.nix +++ b/package.nix @@ -6,7 +6,7 @@ buildGoModule { pname = "nix-cache-login"; - version = "0.1.1"; + version = "0.1.2"; src = ./.; # src = fetchgit { # url = "https://guardianproject.dev/ops/nix-cache-login.git"; From b0959fcf38946589299b63da1f0ad26fdf7c14fd Mon Sep 17 00:00:00 2001 From: Abel Luck Date: Fri, 27 Feb 2026 09:53:24 +0100 Subject: [PATCH 6/7] Fix netrc writing bug --- CHANGELOG.md | 1 + internal/netrc/netrc.go | 9 +++++++-- internal/netrc/netrc_test.go | 10 +++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7731311..4c0ba04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changes yet to be released are documented here. - Fix path expansion when running in systemd +- Fix bug in netrc writing - Add nixos, home-manager, and darwin-nix modules ## v0.1.0 diff --git a/internal/netrc/netrc.go b/internal/netrc/netrc.go index f020a4d..6eaae73 100644 --- a/internal/netrc/netrc.go +++ b/internal/netrc/netrc.go @@ -25,7 +25,7 @@ func Upsert(path, machine, password string) error { } } if !found { - entries = append(entries, entry{machine: machine, password: password}) + entries = append(entries, entry{machine: machine, login: "dummy", password: password}) } return write(path, entries) @@ -71,6 +71,7 @@ func GetPassword(path, machine string) (string, error) { type entry struct { machine string + login string password string } @@ -102,6 +103,10 @@ func parse(path string) ([]entry, error) { entries = append(entries, *current) } current = &entry{machine: fields[1]} + case "login": + if current != nil { + current.login = fields[1] + } case "password": if current != nil { current.password = fields[1] @@ -129,7 +134,7 @@ func write(path string, entries []entry) error { if i > 0 { b.WriteString("\n") } - fmt.Fprintf(&b, "machine %s\npassword %s\n", e.machine, e.password) + fmt.Fprintf(&b, "machine %s\nlogin %s\npassword %s\n", e.machine, e.login, e.password) } if err := os.WriteFile(path, []byte(b.String()), 0600); err != nil { diff --git a/internal/netrc/netrc_test.go b/internal/netrc/netrc_test.go index 611c8c4..e8a0b51 100644 --- a/internal/netrc/netrc_test.go +++ b/internal/netrc/netrc_test.go @@ -27,7 +27,7 @@ func TestUpsertUpdateExisting(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "netrc") - initial := "machine other.host\npassword otherpass\n\nmachine cache.example.com\npassword oldtoken\n" + initial := "machine other.host\nlogin dummy\npassword otherpass\n\nmachine cache.example.com\nlogin dummy\npassword oldtoken\n" if err := os.WriteFile(path, []byte(initial), 0600); err != nil { t.Fatal(err) } @@ -59,7 +59,7 @@ func TestUpsertAppend(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "netrc") - initial := "machine existing.host\npassword existingpass\n" + initial := "machine existing.host\nlogin dummy\npassword existingpass\n" if err := os.WriteFile(path, []byte(initial), 0600); err != nil { t.Fatal(err) } @@ -89,7 +89,7 @@ func TestRemove(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "netrc") - initial := "machine keep.host\npassword keeppass\n\nmachine remove.host\npassword removepass\n" + initial := "machine keep.host\nlogin dummy\npassword keeppass\n\nmachine remove.host\nlogin dummy\npassword removepass\n" if err := os.WriteFile(path, []byte(initial), 0600); err != nil { t.Fatal(err) } @@ -141,7 +141,7 @@ func TestGetPasswordNotFound(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "netrc") - content := "machine other.host\npassword otherpass\n" + content := "machine other.host\nlogin dummy\npassword otherpass\n" if err := os.WriteFile(path, []byte(content), 0600); err != nil { t.Fatal(err) } @@ -179,7 +179,7 @@ func TestFilePermissionsCorrected(t *testing.T) { path := filepath.Join(dir, "netrc") // Create file with overly permissive mode - if err := os.WriteFile(path, []byte("machine old.host\npassword oldpass\n"), 0644); err != nil { + if err := os.WriteFile(path, []byte("machine old.host\nlogin dummy\npassword oldpass\n"), 0644); err != nil { t.Fatal(err) } From b6309a9e12cfc787e1e559a935f6a54bb7f60a9b Mon Sep 17 00:00:00 2001 From: Abel Luck Date: Fri, 27 Feb 2026 09:53:35 +0100 Subject: [PATCH 7/7] Update hm module to use the user's netrc --- home-module.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/home-module.nix b/home-module.nix index 90e80ab..572366e 100644 --- a/home-module.nix +++ b/home-module.nix @@ -26,6 +26,7 @@ in }; config = lib.mkIf cfg.enable { + nix.settings.netrc-file = "${config.xdg.configHome}/nix/netrc"; home.packages = [ cfg.package ]; systemd.user.services.nix-cache-login = { Unit.Description = "Nix cache login - refresh access token";