12 KiB
Homey NixOS Port — Outstanding Tasks
Secrets Setup
DONE
Configure sops recipients in secrets/.sops.yaml
GPG encryption subkey 076AA297579A0064 is already configured in
.sops.yaml. The Pi's age key will be added post-boot (see Deployment section).
DONE
Populate secrets/secrets.yaml with real values
Every key in secrets/secrets.yaml needs a real value. Fill in each row below.
* Recovered from k8s backup
| Key | Source k8s secret | Value |
|---|---|---|
| openldap/admin_password | openldap-admin | lfQWQgBZporyJT4xFSJTnu4vQMC7UevW |
| openldap/config_password | openldap-config | ZxlWbDAeHLdHi5lgdxmyZOWzsG3qDgrT |
| openldap/ro_password | openldap-ro | CZ7JLn23vSzhVjNW7UHGZ2YLFJPDLGsF |
| gitea/admin_password | gitea-admin-pass | y5kCPeCP1e1sCzahd7QmLyJqdQvd37ek |
| nextcloud/postgres_password | nextcloud-postgres-pass | hEq4zt1B1VKYtVAoiKYDmswcUmTbknSP |
| authelia/jwt_secret | jwt-secret | YJZBnQCD4OmhJkgdr6kksmMCatrKLCl3 |
| authelia/storage_encryption_key | fek-secret | KYWRYApCWWIN60gpSi7jhLuj1Wcm5z9Q |
* Needs generation or manual creation
| Key | Action |
|---|---|
| authelia/session_secret | Generate fresh (64 random chars) |
| gitea/lfs_jwt_secret | Generate fresh (43-char base64url) |
| gitea/oauth2_jwt_secret | Generate fresh (43-char base64url) |
| gitea/internal_token | Generate fresh (100-char alphanumeric) |
| restic/password | Generate fresh (passphrase) |
| nextcloud/admin_password | NOT in k8s backup; try old value or reset later |
| cloudflare/api_token | Create DNS Edit token in Cloudflare dashboard |
| cloudflare/tunnel_token | Create tunnel in Cloudflare Zero Trust dashboard |
| restic/s3_access_key_id | Needs S3 provider credentials |
| restic/s3_secret_access_key | Needs S3 provider credentials |
Generate random secrets with:
# 64-char hex string
openssl rand -hex 32
# base64url (for gitea tokens)
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
# 100-char alphanumeric (gitea internal token)
openssl rand -base64 75 | tr -dc 'A-Za-z0-9' | head -c 100
DONE
Encrypt secrets/secrets.yaml with sops
Encrypted with PGP key 076AA297579A0064. Safe to commit.
Pi Hardware Setup
DONE
Fill in real values in hosts/pi-main/default.nix
users.users.admin.openssh.authorizedKeys.keys— SSH public key addedhomey.storage.device—/dev/disk/by-id/usb-WD_Ext_HDD_1021_5743415A4146313531393031-0:0-part1homey.backup.repository—s3:https://s3.us-east-005.backblazeb2.com/zakobar-home-backup
DONE Integrate nixos-raspberrypi flake
Replaced nixos-hardware with nixos-raspberrypi for vendor kernel, firmware,
u-boot bootloader, and binary cache. Both pi-main and pi-main-bootstrap
now use nixos-raspberrypi.lib.nixosSystem and raspberry-pi-4.base.
nix flake check passes.
TODO
Verify SD card partition labels in hosts/pi-main/hardware.nix
The config assumes labels NIXOS_SD (root) and FIRMWARE (boot).
After flashing, check with:
lsblk -o NAME,LABEL
Update fileSystems entries in hosts/pi-main/hardware.nix if they differ.
Caddy Build
TODO
Fix vendorHash in modules/caddy.nix
The Caddy build with the Cloudflare DNS plugin currently uses lib.fakeHash
as a placeholder. After the first nix build attempt it will fail with the
correct hash in the error message. Replace lib.fakeHash with that value.
Cloudflare Setup
DONE Create Cloudflare Tunnel
- Go to Cloudflare Zero Trust dashboard → Networks → Tunnels → Create tunnel
- Name it (e.g.
homey) - Copy the tunnel token into
secrets/secrets.yamlundercloudflare/tunnel_token - Configure public hostnames for each service (see service/URL table in AGENTS.md)
DONE Create Cloudflare DNS API token
- Cloudflare dashboard → My Profile → API Tokens → Create Token
- Use the "Edit zone DNS" template, scope to
zakobar.com - Copy into
secrets/secrets.yamlundercloudflare/api_token
Deployment
TODO Phase 1 — Build and flash bootstrap SD card image
The bootstrap image is a minimal NixOS with SSH + WiFi only (no sops, no services). Its sole purpose is to boot the Pi so you can generate the age key and then deploy the full config remotely.
Build on workstation (cross-compiles for aarch64):
# Accept the nixos-raspberrypi cache config so pre-built kernel/firmware
# are fetched instead of compiled. First build still takes ~10-20 min.
nix build .#nixosConfigurations.pi-main-bootstrap.config.system.build.sdImage \
--accept-flake-config
Flash to SD card (replace /dev/sdX with your card's device):
# Decompress and write in one step — avoids storing the raw image on disk
zstdcat result/sd-image/*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
sudo sync
Insert the SD card into the Pi and power it on.
It will connect to WiFi (Zakobar) with static IP 192.168.1.100.
Verify SSH access (wait ~60 s for first boot):
ssh admin@192.168.1.100
TODO Phase 2 — Generate age key and add it to sops
On the Pi (over SSH):
sudo mkdir -p /var/lib/sops-nix
sudo age-keygen -o /var/lib/sops-nix/key.txt
# Print the public key — copy this output to your workstation clipboard
sudo age-keygen -y /var/lib/sops-nix/key.txt
On the workstation — edit secrets/.sops.yaml, uncomment the age section
and replace the placeholder with the public key you just copied:
key_groups:
- pgp:
- 076AA297579A0064
age:
- age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # paste here
Re-encrypt secrets/secrets.yaml so the Pi's age key can decrypt it:
sops updatekeys secrets/secrets.yaml
Commit and push:
git add secrets/.sops.yaml secrets/secrets.yaml
git commit -m "add Pi age key to sops recipients"
TODO Phase 3 — Fix Caddy vendorHash, then deploy full config
The full pi-main config includes Caddy built with the Cloudflare DNS
plugin. The first build will fail with the correct hash in the error output.
Attempt the build to get the hash:
nix build .#nixosConfigurations.pi-main.config.system.build.toplevel \
--accept-flake-config 2>&1 | grep 'got:'
Copy the hash from the error message and replace lib.fakeHash in
modules/caddy.nix, then commit:
git add modules/caddy.nix
git commit -m "fix caddy vendorHash"
Deploy to the Pi:
# Dry-run first — shows what will change without applying
nixos-rebuild dry-activate \
--flake .#pi-main \
--target-host admin@192.168.1.100 \
--use-remote-sudo \
--accept-flake-config
# Apply when happy with the diff
nixos-rebuild switch \
--flake .#pi-main \
--target-host admin@192.168.1.100 \
--use-remote-sudo \
--accept-flake-config
After a successful switch, subsequent deploys can use the hostname:
nixos-rebuild switch --flake .#pi-main --target-host admin@pi-main --use-remote-sudo
Post-Deployment Manual Steps
DONE Configure Gitea LDAP authentication
Admin → Site Administration → Authentication Sources → Add LDAP (via BindDN):
- Host:
openldap, Port:389, Security: Unencrypted (containers talk via thehomeypodman network — use container name, not127.0.0.1) - Bind DN:
cn=readonly,dc=zakobar,dc=com - Bind Password: see
openldap/ro_passwordin sops - User Search Base:
ou=users,dc=zakobar,dc=com - User Filter:
(&(objectClass=inetOrgPerson)(uid=%s)) - Username attribute:
uid - First name attribute:
cn - Surname attribute:
sn - Email attribute:
mail
DONE Verify Nextcloud LDAP app configuration
After restoring the Nextcloud volume, check: Admin → LDAP/AD Integration — confirm the LDAP Users and Contacts app is configured. If reconfiguring from scratch, use the same settings as Gitea above but with Nextcloud's LDAP wizard:
- Server:
openldap, Port:389(container name on thehomeynetwork — not127.0.0.1) - Bind DN:
cn=readonly,dc=zakobar,dc=com - Bind Password: see
openldap/ro_passwordin sops - Base DN:
dc=zakobar,dc=com - Users: filter
objectClass=inetOrgPerson, search baseou=users - Login attribute:
uid - Email attribute:
mail
TODO (Optional) Enable Jellyfin and Transmission
When ready, in hosts/pi-main/default.nix:
homey.jellyfin.enable = true;
homey.transmission.enable = true;
Backup Strategy
DONE Configure S3-compatible automatic backup target
Update homey.backup.repository in hosts/pi-main/default.nix to point at
your S3-compatible bucket (Backblaze B2, Wasabi, AWS S3, etc.):
homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket-name";
# or for AWS:
# homey.backup.repository = "s3:s3.amazonaws.com/your-bucket-name";
Add the S3 credentials to secrets/secrets.yaml:
restic/s3_access_key_id: "YOUR_KEY_ID"
restic/s3_secret_access_key: "YOUR_SECRET_KEY"
Then wire them into modules/backup.nix via environment variables:
AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (restic reads these natively).
The existing daily schedule + prune retention in modules/backup.nix will
handle the rest automatically.
TODO
Write manual offload script (scripts/offload-backup.sh)
A standalone script for copying backup data to an external disk — either plugged directly into the Pi or mounted on your workstation.
Design:
- Accepts a
--targetargument: a local path to the mounted disk (e.g./media/aner/backup-diskor/mnt/usb) - Uses
restic copyto clone snapshots from the S3 repo into a local restic repo on the target disk (deduplication is preserved, no double storage) - Alternatively can use
rsyncfor a plain directory copy if restic is not available on the target machine - Should be runnable from either the Pi or a workstation (with the Pi's data disk mounted or accessible over SSH)
Example invocation:
# On the Pi, with USB disk mounted at /mnt/usb:
./scripts/offload-backup.sh --target /mnt/usb/homey-backup
# On workstation, with Pi data disk mounted locally:
./scripts/offload-backup.sh --target /media/aner/backup-disk/homey-backup
This script does not exist yet — needs to be written.
Future
TODO
Add second machine (pi-secondary)
When ready:
- Create
hosts/pi-secondary/directory withdefault.nixandhardware.nix - Uncomment the
pi-secondaryentry inflake.nix - Services communicating cross-machine should reference the primary Pi's LAN IP
instead of
127.0.0.1