Working NixOS port: all core services operational
- Fix Caddy cfProxy helper for cloudflared http:// vhosts (X-Forwarded-Proto) - Fix Authelia LDAP bind (readonly user ACL + password sync) - Add gitea-admin-setup oneshot service to survive rebuilds - Update Authelia forward_auth with header_up X-Forwarded-Proto https - Update TODO.org with completed tasks and LDAP config details - Remove old Helm/k8s artifacts (Chart.yaml, templates/, values/, scripts) - Add result to .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
#+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=
|
||||
Reference in New Issue
Block a user