🧶
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

sops-nix setup on NixOS (Raspberry Pi 4 from macOS)#

Migrate a plain configuration.nix to Nix Flakes and encrypt secrets using sops-nix, deploying from macOS to a Raspberry Pi 4.

1. Overview#

1.1 Goal#

Reproducible NixOS config for the Pi with secrets kept encrypted in git and deployed remotely from the laptop.

1.2 Stack#

  • sops — encrypts secrets in git
  • age — encryption backend (derived from existing ED25519 SSH keys via ssh-to-age)
  • sops-nix — NixOS module that decrypts secrets at boot
  • deploy-rs — deploys from macOS to the Pi over SSH with automatic rollback

1.3 Hosts#

Alias Role Machine
Sauce Laptop (admin) macOS (aarch64-darwin)
Mikan Target Raspberry Pi 4 (aarch64-linux)

2. Initial setup#

2.1 Sauce — derive age identity from SSH key#

mkdir -p $HOME/.config/sops/age/
read -s SSH_TO_AGE_PASSPHRASE; export SSH_TO_AGE_PASSPHRASE
nix run nixpkgs#ssh-to-age -- -private-key -i $HOME/.ssh/id_ed25519 -o $HOME/.config/sops/age/keys.txt

Get the age public key:

nix shell nixpkgs#age --command age-keygen -y $HOME/.config/sops/age/keys.txt

2.2 Mikan — get the host age public key#

cat /etc/ssh/ssh_host_ed25519_key.pub | nix run nixpkgs#ssh-to-age

2.3 Sauce — create .sops.yaml#

keys:
  - &admin_you    age1...   # from 2.1
  - &raspberry    age1...   # from 2.2
creation_rules:
  - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$
    key_groups:
    - age:
      - *admin_you
      - *raspberry

2.4 Sauce — create and edit secrets#

mkdir -p secrets
nix run nixpkgs#sops secrets/raspberry.yaml

3. Consuming secrets in configuration.nix#

Sops secrets are files on disk at runtime, not Nix strings. They cannot be interpolated with ${} at build time. Each type of secret needs a different approach:

Secret How to consume it
User password hashedPasswordFile = config.sops.secrets.X.path
WiFi credentials networking.wireless.secretsFile + @VARIABLE@ placeholders
SSH public key Plain text in config — it's public, no need to encrypt

3.1 User password#

The secret must have neededForUsers = true so it is available before user activation runs:

sops.secrets.USER_PASSWORD = { neededForUsers = true; };

users.users.alice = {
  hashedPasswordFile = config.sops.secrets.USER_PASSWORD.path;
};

3.2 WiFi credentials#

WiFi credentials need a single file containing both values:

# secrets/raspberry.yaml
WIFI_CREDENTIALS: |
    WIFI_SSID=YourNetwork
    WIFI_PASSWORD=YourPassword
networking.wireless = {
  enable = true;
  secretsFile = config.sops.secrets.WIFI_CREDENTIALS.path;
  networks."@WIFI_SSID@".psk = "@WIFI_PASSWORD@";
};

3.3 SSH public keys#

Public keys are not sensitive — inline them in the config instead of routing through sops (see also 4.2).

openssh.authorizedKeys.keys = [
  "ssh-ed25519 AAAA... you@sauce"
];

4. Troubleshooting#

4.1 sops — missing age key file on decrypt#

Did not find keys in locations 'SOPS_AGE_KEY_FILE', '/Users/.../.ssh/id_rsa'

Cause: sops looks for id_rsa by default and doesn't find keys.txt.

Fix: export the variable pointing to the age keys file and add it to the shell config:

export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"

4.2 Nix — /run is forbidden in pure evaluation mode#

error: access to absolute path '/run' is forbidden in pure evaluation mode

Cause: openssh.authorizedKeys.keyFiles was set to config.sops.secrets.SSH_PUBLIC_KEY.path, which resolves to /run/secrets/.... Nix evaluates this path at build time, but /run only exists at runtime.

Fix: SSH public keys are not sensitive. Remove SSH_PUBLIC_KEY from sops and put the key directly in the config (see 3.3).

4.3 deploy-rs — target architecture mismatch#

error: Cannot build '/nix/store/lc2x2l1wvzwlv48sh2vdp4khynvldlmf-builder.pl.drv'.
       Reason: required system or feature not available
       Required system: 'aarch64-linux' with features {}
       Current system: 'aarch64-darwin' with features {apple-virt, benchmark, big-parallel, nixos-test}

Cause: deploy-rs was trying to build a aarch64-linux target on the aarch64-darwin laptop (Macbook M1).

Fix: Enable remote builds so the Pi builds its own closure.

outputs.deploy.nodes.raspberry = {
  remoteBuild = true;
}

5. Clarifications#

5.1 age.sshKeyPaths points to a file on the Pi — do I need it locally?#

age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];

No. This path is read at activation time on the target (Mikan), not during local evaluation. sops-nix converts the host's SSH ed25519 key into an age key and uses it to decrypt /run/secrets/* when the system boots.

Two different keys are at play:

Where Key Purpose
Sauce ~/.config/sops/age/keys.txt&sauce Encrypt/edit secrets locally with sops
Mikan /etc/ssh/ssh_host_ed25519_key&mikan Decrypt secrets at boot via sops-nix

Both public keys are listed in .sops.yaml so the encrypted file carries two recipients. Missing the file locally is fine; missing it on the Pi would fail activation with "no age key found".

5.2 How does deploy.nodes.mikan.profiles.system.path work?#

path = deploy-rs.lib.aarch64-linux.activate.nixos
  self.nixosConfigurations.mikan;
  • deploy-rs.lib.aarch64-linux.activate.nixos is a function from the deploy-rs flake, specific to the target architecture. It wraps a NixOS configuration into an activation script that swaps the system profile, runs switch-to-configuration, and rolls back on failure.
  • self.nixosConfigurations.mikan is the argument — self refers to this flake, so it picks up the mikan config defined above.
  • In Nix, f x (juxtaposition) is function application. The two lines are a single call spread across lines.
  • The architecture aarch64-linux must match nixosSystem.system, otherwise deploy-rs will try to activate a closure built for the wrong platform.