···11# (ノ◕ヮ◕)ノ*:・゚✧dotfiles(⌒_⌒;) ♥
2233-## Targets:
33+Reproducible NixOS config for the Pi with secrets kept encrypted in git and
44+deployed remotely from my Macbook.
4555-- Mikan → Raspberry Pi 4B66+| Alias | Role | Machine |
77+|---|---|---|
88+| Sauce | Laptop (admin) | macOS (aarch64-darwin) |
99+| Mikan | Target | Raspberry Pi 4 (aarch64-linux) |
+1-3
configuration.nix
···2323 };
24242525 sops = {
2626- # TODO: Explain why this works even having the file located in the Pi's folder
2726 age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
2827 defaultSopsFile = ./secrets/mikan.yaml;
2928 secrets = {
···4544 services = {
4645 openssh = {
4746 enable = true;
4848- ports = [ 2200 ];
4747+ ports = [ 22 2200 ];
4948 settings = {
5050- # TODO: Don't forget to turn this off after testing the config
5149 PasswordAuthentication = true;
5250 PermitRootLogin = "no";
5351 };
+97-39
docs/notes.md
···11# sops-nix setup on NixOS (Raspberry Pi 4 from macOS)
2233-## Goal
44-53Migrate a plain `configuration.nix` to Nix Flakes and encrypt secrets using
64`sops-nix`, deploying from macOS to a Raspberry Pi 4.
7588-## Stack
66+## 1. Overview
77+88+### 1.1 Goal
99+1010+Reproducible NixOS config for the Pi with secrets kept encrypted in git and
1111+deployed remotely from the laptop.
1212+1313+### 1.2 Stack
9141015- **sops** — encrypts secrets in git
1116- **age** — encryption backend (derived from existing ED25519 SSH keys via `ssh-to-age`)
1217- **sops-nix** — NixOS module that decrypts secrets at boot
1318- **deploy-rs** — deploys from macOS to the Pi over SSH with automatic rollback
14192020+### 1.3 Hosts
2121+2222+| Alias | Role | Machine |
2323+|---|---|---|
2424+| Sauce | Laptop (admin) | macOS (aarch64-darwin) |
2525+| Mikan | Target | Raspberry Pi 4 (aarch64-linux) |
2626+1527---
16281717-## Setup steps
2929+## 2. Initial setup
18301919-### 1. Desktop — derive age identity from your SSH key
3131+### 2.1 Sauce — derive age identity from SSH key
20322133```bash
2234mkdir -p $HOME/.config/sops/age/
···2436nix run nixpkgs#ssh-to-age -- -private-key -i $HOME/.ssh/id_ed25519 -o $HOME/.config/sops/age/keys.txt
2537```
26382727-Get your age public key:
3939+Get the age public key:
28402941```bash
3042nix shell nixpkgs#age --command age-keygen -y $HOME/.config/sops/age/keys.txt
3143```
32443333-### 2. Raspberry Pi — get the host age public key
4545+### 2.2 Mikan — get the host age public key
34463547```bash
3648cat /etc/ssh/ssh_host_ed25519_key.pub | nix run nixpkgs#ssh-to-age
3749```
38503939-### 3. Desktop — create `.sops.yaml`
5151+### 2.3 Sauce — create `.sops.yaml`
40524153```yaml
4254keys:
4343- - &admin_you age1... # from step 1
4444- - &raspberry age1... # from step 2
5555+ - &admin_you age1... # from 2.1
5656+ - &raspberry age1... # from 2.2
4557creation_rules:
4658 - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$
4759 key_groups:
···5062 - *raspberry
5163```
52645353-### 4. Desktop — create and edit secrets
6565+### 2.4 Sauce — create and edit secrets
54665567```bash
5668mkdir -p secrets
···59716072---
61736262-## Secrets in `configuration.nix`
7474+## 3. Consuming secrets in `configuration.nix`
63756476Sops secrets are **files on disk at runtime**, not Nix strings. They cannot be
6577interpolated with `${}` at build time. Each type of secret needs a different
···7183| WiFi credentials | `networking.wireless.secretsFile` + `@VARIABLE@` placeholders |
7284| SSH public key | Plain text in config — it's public, no need to encrypt |
73857474-### WiFi
8686+### 3.1 User password
8787+8888+The secret must have `neededForUsers = true` so it is available before user
8989+activation runs:
9090+9191+```nix
9292+sops.secrets.USER_PASSWORD = { neededForUsers = true; };
9393+9494+users.users.alice = {
9595+ hashedPasswordFile = config.sops.secrets.USER_PASSWORD.path;
9696+};
9797+```
9898+9999+### 3.2 WiFi credentials
7510076101WiFi credentials need a single file containing both values:
77102···90115};
91116```
921179393-### User password
118118+### 3.3 SSH public keys
941199595-The secret must have `neededForUsers = true` so it is available before user
9696-activation runs:
120120+Public keys are not sensitive — inline them in the config instead of routing
121121+through sops (see also [4.2](#42-nix--run-is-forbidden-in-pure-evaluation-mode)).
9712298123```nix
9999-sops.secrets.USER_PASSWORD = { neededForUsers = true; };
100100-101101-users.users.alice = {
102102- hashedPasswordFile = config.sops.secrets.USER_PASSWORD.path;
103103-};
124124+openssh.authorizedKeys.keys = [
125125+ "ssh-ed25519 AAAA... you@sauce"
126126+];
104127```
105128106129---
107130108108-## Errors encountered and fixes
131131+## 4. Troubleshooting
109132110110-### sops can't decrypt the file on second open
133133+### 4.1 sops — missing age key file on decrypt
111134112135```
113136Did not find keys in locations 'SOPS_AGE_KEY_FILE', '/Users/.../.ssh/id_rsa'
···115138116139**Cause:** sops looks for `id_rsa` by default and doesn't find `keys.txt`.
117140118118-**Fix:** export the variable pointing to the age keys file and add it to your shell config:
141141+**Fix:** export the variable pointing to the age keys file and add it to the
142142+shell config:
119143120144```bash
121145export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
122146```
123147124124----
125125-126126-### `/run` is forbidden in pure evaluation mode
148148+### 4.2 Nix — `/run` is forbidden in pure evaluation mode
127149128150```
129151error: access to absolute path '/run' is forbidden in pure evaluation mode
···134156Nix evaluates this path at build time, but `/run` only exists at runtime.
135157136158**Fix:** SSH public keys are not sensitive. Remove `SSH_PUBLIC_KEY` from sops
137137-and put the key directly in the config:
138138-139139-```nix
140140-openssh.authorizedKeys.keys = [
141141- "ssh-ed25519 AAAA... you@desktop"
142142-];
143143-```
144144-145145----
159159+and put the key directly in the config (see [3.3](#33-ssh-public-keys)).
146160147147-### current system differs from required
161161+### 4.3 deploy-rs — target architecture mismatch
148162149163```
150164error: Cannot build '/nix/store/lc2x2l1wvzwlv48sh2vdp4khynvldlmf-builder.pl.drv'.
···153167 Current system: 'aarch64-darwin' with features {apple-virt, benchmark, big-parallel, nixos-test}
154168```
155169156156-**Cause:** `deploy-rs` was trying to build in my `aarch64-darwin` system (Macbook M1) a `aarch64-linux` target.
170170+**Cause:** `deploy-rs` was trying to build a `aarch64-linux` target on the
171171+`aarch64-darwin` laptop (Macbook M1).
157172158158-**Fix:** Enable remote builds to run them on the Raspberry instead.
173173+**Fix:** Enable remote builds so the Pi builds its own closure.
159174160175```nix
161176outputs.deploy.nodes.raspberry = {
162177 remoteBuild = true;
163178}
164179```
180180+181181+---
182182+183183+## 5. Clarifications
184184+185185+### 5.1 `age.sshKeyPaths` points to a file on the Pi — do I need it locally?
186186+187187+```nix
188188+age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
189189+```
190190+191191+**No.** This path is read at **activation time on the target** (Mikan), not
192192+during local evaluation. `sops-nix` converts the host's SSH ed25519 key into an
193193+age key and uses it to decrypt `/run/secrets/*` when the system boots.
194194+195195+Two different keys are at play:
196196+197197+| Where | Key | Purpose |
198198+|---|---|---|
199199+| Sauce | `~/.config/sops/age/keys.txt` → `&sauce` | Encrypt/edit secrets locally with `sops` |
200200+| Mikan | `/etc/ssh/ssh_host_ed25519_key` → `&mikan` | Decrypt secrets at boot via `sops-nix` |
201201+202202+Both public keys are listed in `.sops.yaml` so the encrypted file carries two
203203+recipients. Missing the file locally is fine; missing it on the Pi would fail
204204+activation with "no age key found".
205205+206206+### 5.2 How does `deploy.nodes.mikan.profiles.system.path` work?
207207+208208+```nix
209209+path = deploy-rs.lib.aarch64-linux.activate.nixos
210210+ self.nixosConfigurations.mikan;
211211+```
212212+213213+- `deploy-rs.lib.aarch64-linux.activate.nixos` is a function from the deploy-rs
214214+ flake, specific to the target architecture. It wraps a NixOS configuration
215215+ into an activation script that swaps the system profile, runs
216216+ `switch-to-configuration`, and rolls back on failure.
217217+- `self.nixosConfigurations.mikan` is the argument — `self` refers to this
218218+ flake, so it picks up the `mikan` config defined above.
219219+- In Nix, `f x` (juxtaposition) is function application. The two lines are a
220220+ single call spread across lines.
221221+- The architecture `aarch64-linux` must match `nixosSystem.system`, otherwise
222222+ deploy-rs will try to activate a closure built for the wrong platform.