289 lines
12 KiB
Org Mode
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.
|
|
|
|
** 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: =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=
|
|
|
|
** 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: =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
|
|
|
|
** 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=
|