#+TITLE: Homey NixOS Port — Outstanding Tasks #+DATE: 2026-04-15 #+OPTIONS: toc:nil * 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: #+begin_src bash # 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 #+end_src ** 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= - [X] =users.users.admin.openssh.authorizedKeys.keys= — SSH public key added - [X] =homey.storage.device= — =/dev/disk/by-id/usb-WD_Ext_HDD_1021_5743415A4146313531393031-0:0-part1= - [X] =homey.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: #+begin_src bash lsblk -o NAME,LABEL #+end_src 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 1. Go to Cloudflare Zero Trust dashboard → Networks → Tunnels → Create tunnel 2. Name it (e.g. =homey=) 3. Copy the tunnel token into =secrets/secrets.yaml= under =cloudflare/tunnel_token= 4. Configure public hostnames for each service (see service/URL table in AGENTS.md) ** DONE Create Cloudflare DNS API token 1. Cloudflare dashboard → My Profile → API Tokens → Create Token 2. Use the "Edit zone DNS" template, scope to =zakobar.com= 3. Copy into =secrets/secrets.yaml= under =cloudflare/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): #+begin_src bash # 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 #+end_src Flash to SD card (replace =/dev/sdX= with your card's device): #+begin_src bash # 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 #+end_src 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): #+begin_src bash ssh admin@192.168.1.100 #+end_src ** TODO Phase 2 — Generate age key and add it to sops On the Pi (over SSH): #+begin_src bash 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 #+end_src On the workstation — edit =secrets/.sops.yaml=, uncomment the age section and replace the placeholder with the public key you just copied: #+begin_src yaml key_groups: - pgp: - 076AA297579A0064 age: - age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # paste here #+end_src Re-encrypt =secrets/secrets.yaml= so the Pi's age key can decrypt it: #+begin_src bash sops updatekeys secrets/secrets.yaml #+end_src Commit and push: #+begin_src bash git add secrets/.sops.yaml secrets/secrets.yaml git commit -m "add Pi age key to sops recipients" #+end_src ** 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: #+begin_src bash nix build .#nixosConfigurations.pi-main.config.system.build.toplevel \ --accept-flake-config 2>&1 | grep 'got:' #+end_src Copy the hash from the error message and replace =lib.fakeHash= in =modules/caddy.nix=, then commit: #+begin_src bash git add modules/caddy.nix git commit -m "fix caddy vendorHash" #+end_src Deploy to the Pi: #+begin_src bash # 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 #+end_src After a successful switch, subsequent deploys can use the hostname: #+begin_src bash nixos-rebuild switch --flake .#pi-main --target-host admin@pi-main --use-remote-sudo #+end_src * Post-Deployment Manual Steps ** DONE Configure Gitea LDAP authentication Admin → Site Administration → Authentication Sources → Add LDAP (via BindDN): - Host: =127.0.0.1=, Port: =389=, Security: Unencrypted - Bind DN: =cn=readonly,dc=zakobar,dc=com= - Bind Password: see =openldap/ro_password= in 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= ** TODO 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: =127.0.0.1=, Port: =389= - Bind DN: =cn=readonly,dc=zakobar,dc=com= - Bind Password: see =openldap/ro_password= in sops - Base DN: =dc=zakobar,dc=com= - Users: filter =objectClass=inetOrgPerson=, search base =ou=users= - Login attribute: =uid= - Email attribute: =mail= ** TODO (Optional) Enable Jellyfin and Transmission When ready, in =hosts/pi-main/default.nix=: #+begin_src nix homey.jellyfin.enable = true; homey.transmission.enable = true; #+end_src * Backup Strategy ** TODO 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.): #+begin_src nix 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"; #+end_src Add the S3 credentials to =secrets/secrets.yaml=: #+begin_src yaml restic/s3_access_key_id: "YOUR_KEY_ID" restic/s3_secret_access_key: "YOUR_SECRET_KEY" #+end_src 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 =--target= argument: a local path to the mounted disk (e.g. =/media/aner/backup-disk= or =/mnt/usb=) - Uses =restic copy= to clone snapshots from the S3 repo into a local restic repo on the target disk (deduplication is preserved, no double storage) - Alternatively can use =rsync= for 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: #+begin_src bash # 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 #+end_src This script does not exist yet — needs to be written. * Future ** TODO Add second machine (=pi-secondary=) When ready: 1. Create =hosts/pi-secondary/= directory with =default.nix= and =hardware.nix= 2. Uncomment the =pi-secondary= entry in =flake.nix= 3. Services communicating cross-machine should reference the primary Pi's LAN IP instead of =127.0.0.1=