Files
2026-05-10 11:30:43 +03:00

289 lines
12 KiB
Org Mode

#+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.
** DONE 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
** DONE 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
** DONE 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
** DONE 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
** DONE 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: =openldap=, Port: =389=, Security: Unencrypted
(containers talk via the =homey= podman network — use container name, not =127.0.0.1=)
- 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=
** 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 the =homey= network — not =127.0.0.1=)
- 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
** 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.):
#+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=