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:
@@ -1,3 +1,4 @@
|
|||||||
charts
|
charts
|
||||||
*.lock
|
*.lock
|
||||||
.agent-shell
|
.agent-shell
|
||||||
|
result
|
||||||
|
|||||||
@@ -17,8 +17,7 @@
|
|||||||
creation_rules:
|
creation_rules:
|
||||||
- path_regex: secrets/secrets\.yaml$
|
- path_regex: secrets/secrets\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- pgp
|
- pgp:
|
||||||
- 076AA297579A0064
|
- 076AA297579A0064
|
||||||
# - age:
|
age:
|
||||||
# Pi main host key — replace with output of `age-keygen -y /var/lib/sops-nix/key.txt`
|
- age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p
|
||||||
# - AGE-PUBLIC-KEY-PI-MAIN-REPLACE-ME
|
|
||||||
@@ -40,16 +40,16 @@ PORTING.md # Step-by-step migration guide from the old Helm s
|
|||||||
|
|
||||||
## Services and URLs
|
## Services and URLs
|
||||||
|
|
||||||
All services live under `home.zakobar.com`.
|
All services live under `zakobar.com`.
|
||||||
|
|
||||||
| Service | URL | Auth |
|
| Service | URL | Auth |
|
||||||
|---------|-----|------|
|
|---------|-----|------|
|
||||||
| Authelia | `auth.home.zakobar.com` | Public (it is the auth portal) |
|
| Authelia | `auth.zakobar.com` | Public (it is the auth portal) |
|
||||||
| Gitea | `git.home.zakobar.com` | Authelia one_factor |
|
| Gitea | `git.zakobar.com` | Gitea-native (LDAP) |
|
||||||
| Nextcloud | `nextcloud.home.zakobar.com` | Nextcloud-native |
|
| Nextcloud | `nextcloud.zakobar.com` | Nextcloud-native |
|
||||||
| phpLDAPadmin | `ldapadmin.home.zakobar.com` | Authelia two_factor, admins only |
|
| phpLDAPadmin | `ldapadmin.zakobar.com` | Authelia two_factor, admins only |
|
||||||
| Jellyfin | `jellyfin.home.zakobar.com` | Authelia one_factor |
|
| Jellyfin | `jellyfin.zakobar.com` | Jellyfin-native |
|
||||||
| Transmission | `torrent.home.zakobar.com` | Authelia two_factor, admins only |
|
| Transmission | `torrent.zakobar.com` | Authelia two_factor, admins only |
|
||||||
|
|
||||||
Internal ports (all bound to `127.0.0.1`):
|
Internal ports (all bound to `127.0.0.1`):
|
||||||
|
|
||||||
@@ -279,8 +279,8 @@ These items require the Pi to be built, flashed, and booted at least once.
|
|||||||
The old Helm chart had this commented out; it must be done manually once.
|
The old Helm chart had this commented out; it must be done manually once.
|
||||||
Relevant settings:
|
Relevant settings:
|
||||||
- Host: `127.0.0.1`, Port: `389`, Security: Unencrypted
|
- Host: `127.0.0.1`, Port: `389`, Security: Unencrypted
|
||||||
- Bind DN: `cn=readonly,dc=home,dc=zakobar,dc=com`
|
- Bind DN: `cn=readonly,dc=zakobar,dc=com`
|
||||||
- User search base: `ou=users,dc=home,dc=zakobar,dc=com`
|
- User search base: `ou=users,dc=zakobar,dc=com`
|
||||||
|
|
||||||
- [ ] **Nextcloud LDAP app**: After restoring the Nextcloud volume, verify
|
- [ ] **Nextcloud LDAP app**: After restoring the Nextcloud volume, verify
|
||||||
the LDAP Users and Contacts app is still configured correctly
|
the LDAP Users and Contacts app is still configured correctly
|
||||||
|
|||||||
+19
-19
@@ -260,21 +260,21 @@ In the tunnel's "Public Hostnames" tab, add:
|
|||||||
|
|
||||||
| Subdomain | Domain | Service |
|
| Subdomain | Domain | Service |
|
||||||
|-----------|--------|---------|
|
|-----------|--------|---------|
|
||||||
| `auth` | `home.zakobar.com` | `https://localhost:443` |
|
| `auth` | `zakobar.com` | `https://localhost:443` |
|
||||||
| `git` | `home.zakobar.com` | `https://localhost:443` |
|
| `git` | `zakobar.com` | `https://localhost:443` |
|
||||||
| `nextcloud` | `home.zakobar.com` | `https://localhost:443` |
|
| `nextcloud` | `zakobar.com` | `https://localhost:443` |
|
||||||
| `ldapadmin` | `home.zakobar.com` | `https://localhost:443` |
|
| `ldapadmin` | `zakobar.com` | `https://localhost:443` |
|
||||||
| `jellyfin` | `home.zakobar.com` | `https://localhost:443` |
|
| `jellyfin` | `zakobar.com` | `https://localhost:443` |
|
||||||
| `torrent` | `home.zakobar.com` | `https://localhost:443` |
|
| `torrent` | `zakobar.com` | `https://localhost:443` |
|
||||||
|
|
||||||
For each entry, under "Additional settings" → TLS → **No TLS Verify: ON**
|
For each entry, under "Additional settings" → TLS → **No TLS Verify: ON**
|
||||||
(because cloudflared connects to `localhost` but the cert is for the real hostname).
|
(because cloudflared connects to `localhost` but the cert is for the real hostname).
|
||||||
|
|
||||||
### 3.3 Update DNS in Cloudflare
|
### 3.3 Update DNS in Cloudflare
|
||||||
|
|
||||||
Add a CNAME for `home.zakobar.com` pointing to your tunnel's UUID (Cloudflare
|
Add a CNAME for `zakobar.com` pointing to your tunnel's UUID (Cloudflare
|
||||||
creates this automatically when you add hostnames). You do not need to add
|
creates this automatically when you add hostnames). You do not need to add
|
||||||
`home.zakobar.com` to your domain's A records — Cloudflare handles it.
|
`zakobar.com` to your domain's A records — Cloudflare handles it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -294,19 +294,19 @@ sudo nixos-rebuild switch --flake /path/to/homey#pi-main
|
|||||||
systemctl list-units 'podman-*' --state=active
|
systemctl list-units 'podman-*' --state=active
|
||||||
|
|
||||||
# OpenLDAP responding?
|
# OpenLDAP responding?
|
||||||
ldapsearch -x -H ldap://127.0.0.1:389 -b dc=home,dc=zakobar,dc=com -D "cn=admin,dc=home,dc=zakobar,dc=com" -W
|
ldapsearch -x -H ldap://127.0.0.1:389 -b dc=zakobar,dc=com -D "cn=admin,dc=zakobar,dc=com" -W
|
||||||
|
|
||||||
# Authelia health?
|
# Authelia health?
|
||||||
curl -s http://localhost:9091/api/health | python3 -m json.tool
|
curl -s http://localhost:9091/api/health | python3 -m json.tool
|
||||||
|
|
||||||
# Caddy serving TLS?
|
# Caddy serving TLS?
|
||||||
curl -I https://auth.home.zakobar.com
|
curl -I https://auth.zakobar.com
|
||||||
|
|
||||||
# Gitea login?
|
# Gitea login?
|
||||||
# Visit https://git.home.zakobar.com — should redirect to authelia if not logged in
|
# Visit https://git.zakobar.com — should redirect to authelia if not logged in
|
||||||
|
|
||||||
# Nextcloud?
|
# Nextcloud?
|
||||||
# Visit https://nextcloud.home.zakobar.com
|
# Visit https://nextcloud.zakobar.com
|
||||||
|
|
||||||
# Cloudflare tunnel connected?
|
# Cloudflare tunnel connected?
|
||||||
systemctl status cloudflared-tunnel-pi-main
|
systemctl status cloudflared-tunnel-pi-main
|
||||||
@@ -320,13 +320,13 @@ To access services without going through Cloudflare on the LAN, add these
|
|||||||
records to your router's DNS or Pi-hole:
|
records to your router's DNS or Pi-hole:
|
||||||
|
|
||||||
```
|
```
|
||||||
192.168.1.100 home.zakobar.com
|
192.168.1.100 zakobar.com
|
||||||
192.168.1.100 auth.home.zakobar.com
|
192.168.1.100 auth.zakobar.com
|
||||||
192.168.1.100 git.home.zakobar.com
|
192.168.1.100 git.zakobar.com
|
||||||
192.168.1.100 nextcloud.home.zakobar.com
|
192.168.1.100 nextcloud.zakobar.com
|
||||||
192.168.1.100 ldapadmin.home.zakobar.com
|
192.168.1.100 ldapadmin.zakobar.com
|
||||||
192.168.1.100 jellyfin.home.zakobar.com
|
192.168.1.100 jellyfin.zakobar.com
|
||||||
192.168.1.100 torrent.home.zakobar.com
|
192.168.1.100 torrent.zakobar.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `192.168.1.100` with your Pi's actual LAN IP.
|
Replace `192.168.1.100` with your Pi's actual LAN IP.
|
||||||
|
|||||||
+300
@@ -0,0 +1,300 @@
|
|||||||
|
#+title: Homey
|
||||||
|
|
||||||
|
A home environment for everyone!
|
||||||
|
|
||||||
|
* NixOS Deployment (active branch: nixos-port)
|
||||||
|
|
||||||
|
** Prerequisites
|
||||||
|
|
||||||
|
Before building, make sure the following are set in the repo:
|
||||||
|
|
||||||
|
- =hosts/pi-main/default.nix= — SSH public key, static IP, WiFi SSID
|
||||||
|
- =secrets/secrets.yaml= — all secrets populated and sops-encrypted
|
||||||
|
- WiFi password secret formatted as =wifi_psk=YourPassword= (see below)
|
||||||
|
|
||||||
|
** Adding / updating secrets
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
sops secrets/secrets.yaml
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Opens your editor with the decrypted file. Save and quit to re-encrypt.
|
||||||
|
|
||||||
|
The WiFi password entry must use the =wifi_psk== prefix so wpa_supplicant
|
||||||
|
can look up the value by name:
|
||||||
|
|
||||||
|
#+begin_src yaml
|
||||||
|
wifi/psk: "wifi_psk=YourActualWifiPassword"
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** Phase 1 — Bootstrap image (flash this first)
|
||||||
|
|
||||||
|
The full =pi-main= config requires sops secrets, which require an age key
|
||||||
|
on the Pi — but the age key doesn't exist until after first boot. To
|
||||||
|
break the chicken-and-egg problem, flash a minimal bootstrap image first.
|
||||||
|
|
||||||
|
Before building, fill in the WiFi password in =flake.nix= in the
|
||||||
|
=pi-main-bootstrap= config (search for =WIFI_PASSWORD_HERE=):
|
||||||
|
|
||||||
|
#+begin_src nix
|
||||||
|
networks."Zakobar".psk = "your-actual-wifi-password";
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Build the bootstrap SD image (requires =aarch64-linux= build capability —
|
||||||
|
either =boot.binfmt.emulatedSystems = ["aarch64-linux"]= on your
|
||||||
|
workstation, or an aarch64 remote builder):
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
nix build .#nixosConfigurations.pi-main-bootstrap.config.system.build.sdImage \
|
||||||
|
--system aarch64-linux
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Find your SD card device, then flash (double-check =/dev/sdX=!):
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
lsblk
|
||||||
|
|
||||||
|
zstdcat result/sd-image/nixos-sd-image-*.img.zst | \
|
||||||
|
sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
The Pi will boot at =192.168.1.100=, connect to =Zakobar= WiFi, and accept
|
||||||
|
SSH connections with your key. No services run yet.
|
||||||
|
|
||||||
|
** Phase 2 — Generate age key and re-encrypt secrets
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
# SSH into the Pi
|
||||||
|
ssh admin@192.168.1.100
|
||||||
|
|
||||||
|
# Generate the age key
|
||||||
|
sudo age-keygen -o /var/lib/sops-nix/key.txt
|
||||||
|
|
||||||
|
# Print the public key — copy it
|
||||||
|
sudo age-keygen -y /var/lib/sops-nix/key.txt
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Back on your workstation, add the public key to =secrets/.sops.yaml=
|
||||||
|
alongside the existing PGP key:
|
||||||
|
|
||||||
|
#+begin_src yaml
|
||||||
|
keys:
|
||||||
|
- &pi_main age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
creation_rules:
|
||||||
|
- path_regex: secrets/secrets.yaml$
|
||||||
|
key_groups:
|
||||||
|
- pgp:
|
||||||
|
- 076AA297579A0064
|
||||||
|
age:
|
||||||
|
- *pi_main
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Then re-encrypt so the Pi can decrypt its own secrets:
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
sops updatekeys secrets/secrets.yaml
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** Phase 3 — Deploy the full config
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
nixos-rebuild switch \
|
||||||
|
--flake .#pi-main \
|
||||||
|
--target-host admin@192.168.1.100 \
|
||||||
|
--build-host admin@192.168.1.100 \
|
||||||
|
--use-remote-sudo
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
The Pi builds its own config natively (no cross-compilation). sops-nix
|
||||||
|
will now decrypt all secrets and start all services.
|
||||||
|
|
||||||
|
** Caddy plugin hash
|
||||||
|
|
||||||
|
The first deploy will fail at the Caddy build step because =lib.fakeHash=
|
||||||
|
is a placeholder. Copy the correct hash from the error output and replace
|
||||||
|
it in =modules/caddy.nix=:
|
||||||
|
|
||||||
|
#+begin_src nix
|
||||||
|
caddyWithCloudflare = pkgs.caddy.withPlugins {
|
||||||
|
plugins = [ "github.com/caddy-dns/cloudflare@..." ];
|
||||||
|
hash = "sha256-REPLACE_WITH_REAL_HASH="; # ← paste here
|
||||||
|
};
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Then re-run the deploy command from Phase 3.
|
||||||
|
|
||||||
|
** Ongoing deploys from workstation
|
||||||
|
|
||||||
|
All future config changes follow the same pattern:
|
||||||
|
|
||||||
|
1. Edit files on workstation
|
||||||
|
2. Run:
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
nixos-rebuild switch \
|
||||||
|
--flake .#pi-main \
|
||||||
|
--target-host admin@192.168.1.100 \
|
||||||
|
--build-host admin@192.168.1.100 \
|
||||||
|
--use-remote-sudo
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
NixOS activates the new config on the Pi immediately, with an automatic
|
||||||
|
rollback if activation fails.
|
||||||
|
|
||||||
|
* Installation (legacy Helm)
|
||||||
|
|
||||||
|
Install using
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
helm upgrade --install homey . -n homey
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
* Backing up
|
||||||
|
|
||||||
|
Backups use [[https://restic.net/][restic]] and run automatically via systemd on a daily schedule.
|
||||||
|
|
||||||
|
** Strategy — two tiers
|
||||||
|
|
||||||
|
1. *Primary (automatic)*: Daily backup to an S3-compatible bucket (Backblaze B2,
|
||||||
|
Wasabi, AWS S3, etc.). Restic deduplicates and encrypts before upload.
|
||||||
|
Retention: 7 daily, 4 weekly, 6 monthly snapshots.
|
||||||
|
|
||||||
|
2. *Offload (manual)*: Run =scripts/offload-backup.sh --target /path/to/disk=
|
||||||
|
to clone snapshots from the S3 repo onto a local disk (USB plugged into the
|
||||||
|
Pi, or a disk on your workstation). Uses =restic copy= so deduplication is
|
||||||
|
preserved on the target.
|
||||||
|
|
||||||
|
** What is backed up
|
||||||
|
|
||||||
|
All service data under =/mnt/data/=:
|
||||||
|
|
||||||
|
- =openldap/= — LDAP database and config
|
||||||
|
- =authelia/= — Authelia config and state
|
||||||
|
- =gitea/= — Gitea repositories and data
|
||||||
|
- =nextcloud/= — Nextcloud files + a =pg_dump= of the database
|
||||||
|
- =jellyfin/= — Jellyfin metadata (media files are excluded — re-downloadable)
|
||||||
|
- =transmission/= — Torrent client config
|
||||||
|
|
||||||
|
Nextcloud is placed into maintenance mode and postgres is =pg_dump='d before
|
||||||
|
each backup to ensure a consistent snapshot.
|
||||||
|
|
||||||
|
** Configuration
|
||||||
|
|
||||||
|
Repository URL and credentials are set per-host:
|
||||||
|
|
||||||
|
#+begin_src nix
|
||||||
|
# hosts/pi-main/default.nix
|
||||||
|
homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket";
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
S3 credentials live in =secrets/secrets.yaml= as =restic/s3_access_key_id= and
|
||||||
|
=restic/s3_secret_access_key=.
|
||||||
|
|
||||||
|
** Restore
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
# List snapshots
|
||||||
|
restic -r s3:https://... snapshots
|
||||||
|
|
||||||
|
# Restore latest snapshot to /mnt/data
|
||||||
|
restic -r s3:https://... restore latest --target /mnt/data
|
||||||
|
|
||||||
|
# Restore a single service
|
||||||
|
restic -r s3:https://... restore latest --target /mnt/data --include /mnt/data/gitea
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
* LDAP Configuration
|
||||||
|
|
||||||
|
Logins are done to PHPLDAPADMIN
|
||||||
|
|
||||||
|
DN is like:
|
||||||
|
|
||||||
|
cn=admin,dc=,dc=io
|
||||||
|
get-secret-val.sh homey openldap-admin password
|
||||||
|
|
||||||
|
First thing we do is create an organization unit called users
|
||||||
|
|
||||||
|
To add a new user, we create a child entry to ou=users
|
||||||
|
|
||||||
|
It has to be of type inetOrgPerson
|
||||||
|
|
||||||
|
cn = Common Name, sn = Sur Name.
|
||||||
|
Select RDN = User Name (uid) (FROM DROP DOWN MENU)
|
||||||
|
UID = USERNAME, that is what is important. (In PHPLdapAdmin it is under User Name)
|
||||||
|
|
||||||
|
Now we may continue!
|
||||||
|
|
||||||
|
* GITEA
|
||||||
|
|
||||||
|
Site Title: whatever
|
||||||
|
|
||||||
|
SSH Server Domain: git.<YOUR URL>
|
||||||
|
SSH Server Port: 2222
|
||||||
|
Gitea Base URL: http://git.<YOUR URL>
|
||||||
|
|
||||||
|
Then add Administrator Account Settings:
|
||||||
|
|
||||||
|
Administrator Username: gitea-admin
|
||||||
|
Password: from gitea-admin-pass
|
||||||
|
Email address must be populated
|
||||||
|
|
||||||
|
That will work after a few minutes.
|
||||||
|
|
||||||
|
Now we go into Authentication Sources
|
||||||
|
|
||||||
|
Add a new LDAP Authentication source
|
||||||
|
|
||||||
|
Authentication name: Home LDAP
|
||||||
|
Host: openldap
|
||||||
|
Port: 389
|
||||||
|
Bind DN = cn=readonly,dc=,dc=io
|
||||||
|
Bind Password: openldap-ro password
|
||||||
|
User Search Base: ou=users,dc=,dc=io
|
||||||
|
user search filter = (uid=%s)
|
||||||
|
Admin filter (title=admin)
|
||||||
|
Username Attribute: uid
|
||||||
|
First Name Attribute: cn
|
||||||
|
Surname Attribute: sn
|
||||||
|
Email Attribute: mail
|
||||||
|
|
||||||
|
* AUTHELIA
|
||||||
|
|
||||||
|
https://github.com/authelia/authelia/blob/57d5fbd3f5c82e83296023dc1de6e4f5ff063c00/examples/compose/lite/authelia/configuration.yml
|
||||||
|
This fucking sucks
|
||||||
|
https://gist.github.com/james-d-elliott/5152d27c0781aee856a3383f1284998e
|
||||||
|
|
||||||
|
* EVERYTHING
|
||||||
|
https://www.talkingquickly.co.uk/gitea-sso-with-keycloak-openldap-openid-connect
|
||||||
|
|
||||||
|
* DRONE AND GITEA
|
||||||
|
?
|
||||||
|
https://dev.to/ruanbekker/self-hosted-cicd-with-gitea-and-drone-ci-200l
|
||||||
|
|
||||||
|
* DAV
|
||||||
|
|
||||||
|
https://gitlab.com/davical-project/davical/-/blob/master/config/example-config.php
|
||||||
|
|
||||||
|
Line 800 ish for auth from reverse proxy
|
||||||
|
|
||||||
|
* NEXTCLOUD
|
||||||
|
|
||||||
|
I ran THIS command inside
|
||||||
|
su www-data -s /bin/bash -c php occ ldap:promote-group "admins"
|
||||||
|
|
||||||
|
** When maintenence mode
|
||||||
|
|
||||||
|
#+begin_example
|
||||||
|
kubectl exec --tty --stdin -n homey deploy/nextcloud -- su -l www-data -s /bin/bash
|
||||||
|
php /var/www/html/occ maintenance:mode --off
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
* I UNDERSTAND
|
||||||
|
|
||||||
|
I need to backup Chen's stuff
|
||||||
|
And... I need to Jellyfin
|
||||||
|
|
||||||
|
* PAPERLESS
|
||||||
|
|
||||||
|
https://github.com/paperless-ngx/paperless-ngx/blob/74c44fe418a91a526b5dab1a91fde4aaebd28bb1/docker/compose/docker-compose.postgres.yml
|
||||||
|
|
||||||
|
For docker
|
||||||
@@ -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=
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
#+TITLE: Caddy, Cloudflare Tunnel & TLS Setup
|
||||||
|
#+DATE: 2026-04-23
|
||||||
|
#+AUTHOR: homey project
|
||||||
|
#+OPTIONS: toc:2 num:t
|
||||||
|
|
||||||
|
* Overview
|
||||||
|
|
||||||
|
This document describes the TLS and reverse-proxy architecture for the homey
|
||||||
|
self-hosted stack, the problems encountered while getting it working, and the
|
||||||
|
final configuration that resolved them. It is intended as a reference for
|
||||||
|
future debugging and for adding new services.
|
||||||
|
|
||||||
|
** Traffic flow
|
||||||
|
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
Browser
|
||||||
|
│ HTTPS (TLS terminated by Cloudflare edge, *.zakobar.com cert)
|
||||||
|
▼
|
||||||
|
Cloudflare edge (anycast IP)
|
||||||
|
│ QUIC/HTTP2 tunnel (outbound from Pi, no open inbound ports)
|
||||||
|
▼
|
||||||
|
cloudflared daemon on Pi (systemd: cloudflared-tunnel.service)
|
||||||
|
│ plain HTTP on loopback http://localhost:80
|
||||||
|
▼
|
||||||
|
Caddy reverse proxy (systemd: caddy.service, port 80 + 443)
|
||||||
|
│ proxies to backend by Host header
|
||||||
|
▼
|
||||||
|
Service container (podman, port on 127.0.0.1)
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- TLS to the browser is provided entirely by Cloudflare's Universal SSL cert
|
||||||
|
(~*.zakobar.com~), not by the Pi's Let's Encrypt cert.
|
||||||
|
- The Pi's Let's Encrypt cert (~*.zakobar.com~ via DNS-01) is used only for
|
||||||
|
direct LAN access (bypassing the tunnel).
|
||||||
|
- The tunnel leg (cloudflared → Caddy) is plain HTTP on loopback — this is
|
||||||
|
safe because both endpoints are the same machine.
|
||||||
|
|
||||||
|
* Components
|
||||||
|
|
||||||
|
** Caddy (~modules/caddy.nix~)
|
||||||
|
|
||||||
|
Caddy runs as a NixOS service (~services.caddy~) using a custom build that
|
||||||
|
includes the ~caddy-dns/cloudflare~ plugin for DNS-01 ACME challenges.
|
||||||
|
|
||||||
|
*** Custom build
|
||||||
|
|
||||||
|
The nixpkgs ~caddy~ package does not include the Cloudflare DNS plugin by
|
||||||
|
default. It is built using the ~withPlugins~ passthru function (backed by
|
||||||
|
xcaddy):
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
caddyWithCloudflare = pkgs.caddy.withPlugins {
|
||||||
|
plugins = [
|
||||||
|
"github.com/caddy-dns/cloudflare@v0.2.4"
|
||||||
|
];
|
||||||
|
hash = "sha256-...";
|
||||||
|
};
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
The ~hash~ is a fixed-output derivation hash that must be updated whenever
|
||||||
|
the plugin version changes. Use ~lib.fakeHash~ to trigger a build failure
|
||||||
|
that prints the correct hash, then substitute it.
|
||||||
|
|
||||||
|
*** API token injection
|
||||||
|
|
||||||
|
The Cloudflare API token is stored in sops (~cloudflare/api_token~) and
|
||||||
|
injected into the Caddy process via ~systemd LoadCredential~:
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
serviceConfig.LoadCredential =
|
||||||
|
"cloudflare_api_token:${config.sops.secrets."cloudflare/api_token".path}";
|
||||||
|
ExecStart = lib.mkForce [
|
||||||
|
""
|
||||||
|
(pkgs.writeShellScript "caddy-start" ''
|
||||||
|
export CLOUDFLARE_API_TOKEN=$(cat "$CREDENTIALS_DIRECTORY/cloudflare_api_token")
|
||||||
|
exec caddy run --environ --config /etc/caddy/caddy_config --adapter caddyfile
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** Virtual hosts — dual HTTP/HTTPS entries
|
||||||
|
|
||||||
|
Each service has *two* Caddyfile vhost entries:
|
||||||
|
|
||||||
|
| Entry | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| ~git.zakobar.com~ | HTTPS — for direct LAN access; Caddy handles TLS |
|
||||||
|
| ~http://git.zakobar.com~ | HTTP — for cloudflared on loopback; no redirect |
|
||||||
|
|
||||||
|
Caddy's default behaviour is to automatically redirect HTTP → HTTPS for any
|
||||||
|
hostname that has a matching HTTPS vhost. By explicitly defining an
|
||||||
|
~http://~ vhost, that redirect is suppressed and cloudflared gets a direct
|
||||||
|
200 response instead of a redirect loop.
|
||||||
|
|
||||||
|
Without the ~http://~ vhost, accessing via the tunnel produces:
|
||||||
|
~ERR_TOO_MANY_REDIRECTS~ in the browser because cloudflared follows the 308
|
||||||
|
back to HTTP indefinitely.
|
||||||
|
|
||||||
|
*** Global config
|
||||||
|
|
||||||
|
#+BEGIN_SRC caddyfile
|
||||||
|
{
|
||||||
|
email admin@zakobar.com
|
||||||
|
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
||||||
|
}
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
The ~acme_dns~ directive in the global block tells Caddy to use DNS-01
|
||||||
|
challenges for *all* HTTPS vhosts. This allows wildcard and multi-level
|
||||||
|
subdomain certs to be issued without any inbound port 80 requirement.
|
||||||
|
|
||||||
|
** Cloudflare Tunnel (~modules/cloudflared.nix~)
|
||||||
|
|
||||||
|
cloudflared runs as a plain systemd service using the token-based tunnel
|
||||||
|
approach (~cloudflared tunnel run --token~). No local credentials file or
|
||||||
|
config file is needed — just the tunnel token from the Zero Trust dashboard.
|
||||||
|
|
||||||
|
*** Tunnel configuration (Zero Trust dashboard)
|
||||||
|
|
||||||
|
One wildcard public hostname entry covers all services:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Hostname | ~*.zakobar.com~ |
|
||||||
|
| Service | ~http://localhost:80~ |
|
||||||
|
| No TLS Verify | off (not needed for HTTP) |
|
||||||
|
| HTTP Host Header | (empty — cloudflared forwards the real Host header) |
|
||||||
|
| Origin Server Name | (empty — not needed for HTTP) |
|
||||||
|
|
||||||
|
cloudflared automatically forwards the incoming ~Host~ header (e.g.
|
||||||
|
~git.zakobar.com~) to Caddy, which uses it to select the correct vhost and
|
||||||
|
backend.
|
||||||
|
|
||||||
|
*** DNS records
|
||||||
|
|
||||||
|
A single wildcard CNAME record in Cloudflare DNS covers all subdomains:
|
||||||
|
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
*.zakobar.com CNAME <tunnel-id>.cfargotunnel.com (proxied, orange cloud)
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
This means new services require no DNS changes — only a new Caddy vhost.
|
||||||
|
|
||||||
|
*** Cloudflare SSL/TLS mode
|
||||||
|
|
||||||
|
Set to *Full (strict)* in the Cloudflare dashboard (SSL/TLS → Overview).
|
||||||
|
|
||||||
|
| Mode | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| Off | No HTTPS to browser |
|
||||||
|
| Flexible | HTTPS to browser, HTTP to origin |
|
||||||
|
| Full | HTTPS to browser, HTTPS to origin (cert not validated) |
|
||||||
|
| Full (strict) | HTTPS to browser, HTTPS to origin (cert must be valid) |
|
||||||
|
|
||||||
|
Full (strict) works here because Cloudflare terminates TLS at its own edge
|
||||||
|
using its Universal cert, and the origin (cloudflared → Caddy) uses plain
|
||||||
|
HTTP which Cloudflare does not validate in this tunnel architecture.
|
||||||
|
|
||||||
|
* Problems Encountered & How They Were Resolved
|
||||||
|
|
||||||
|
** 1. ~caddy-dns/cloudflare~ rejected ~cfut_~ token format
|
||||||
|
|
||||||
|
*Symptom:*
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
provision dns.providers.cloudflare: API token 'cfut_...' appears invalid;
|
||||||
|
ensure it's correctly entered and not wrapped in braces nor quotes
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
*Cause:*
|
||||||
|
Cloudflare introduced new token formats with a ~cfut_~ (user token) or
|
||||||
|
~cfat_~ (account token) prefix. These tokens are 54 characters long. The
|
||||||
|
~caddy-dns/cloudflare~ plugin had a validation regex ~{35,50}~ that rejected
|
||||||
|
tokens longer than 50 characters, failing before even making an API call.
|
||||||
|
|
||||||
|
*Fix:*
|
||||||
|
The fix was merged into the plugin's master branch as commit ~a8737d0~ and
|
||||||
|
included in the ~v0.2.4~ tag (despite the tag previously being associated
|
||||||
|
with an older tree — the proxy confirmed ~v0.2.4~ resolves to ~a8737d0~).
|
||||||
|
|
||||||
|
Updating the ~hash~ in ~caddy.nix~ to the value produced by ~lib.fakeHash~
|
||||||
|
forced a fresh fetch of the corrected ~v0.2.4~ tree:
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.4" ];
|
||||||
|
hash = lib.fakeHash; # replace with hash from build error output
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
Run ~nix build .#nixosConfigurations.pi-main.config.system.build.toplevel~,
|
||||||
|
copy the ~got:~ hash from the error, substitute it, and rebuild.
|
||||||
|
|
||||||
|
** 2. cloudflared ~tls: internal error~ (SNI mismatch)
|
||||||
|
|
||||||
|
*Symptom:*
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
Unable to reach the origin service: remote error: tls: internal error
|
||||||
|
originService=https://localhost:443
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
*Cause:*
|
||||||
|
cloudflared connected to ~https://localhost:443~ without sending an SNI
|
||||||
|
(Server Name Indication) hostname in the TLS ClientHello. Caddy could not
|
||||||
|
match any vhost, had no certificate for ~localhost~, and aborted the
|
||||||
|
handshake with a TLS internal error.
|
||||||
|
|
||||||
|
Setting the ~HTTP Host Header~ override in the dashboard fixes the HTTP
|
||||||
|
layer but does *not* affect the TLS SNI, which is negotiated before HTTP
|
||||||
|
headers are exchanged.
|
||||||
|
|
||||||
|
Setting the ~Origin Server Name~ field does set the SNI, but for a wildcard
|
||||||
|
rule (~*.zakobar.com~) the dashboard only accepts a static value, not a
|
||||||
|
dynamic placeholder — so it cannot be used for a catch-all.
|
||||||
|
|
||||||
|
*Fix:*
|
||||||
|
Switch the tunnel service from ~https://localhost:443~ to
|
||||||
|
~http://localhost:80~. The internal leg does not need TLS (loopback
|
||||||
|
interface, same machine). Caddy's HTTP vhosts handle the requests directly.
|
||||||
|
|
||||||
|
** 3. Cloudflare edge TLS handshake failure (~*.home.zakobar.com~)
|
||||||
|
|
||||||
|
*Symptom:*
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
TLS connect error: error:0A000410:SSL routines::ssl/tls alert handshake failure
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
*Cause:*
|
||||||
|
The domain was originally configured as ~home.zakobar.com~ (base domain),
|
||||||
|
making all services two levels deep: ~git.home.zakobar.com~,
|
||||||
|
~auth.home.zakobar.com~, etc. Cloudflare's free Universal SSL certificate
|
||||||
|
covers only one level of wildcard: ~*.zakobar.com~. It does *not* cover
|
||||||
|
~*.home.zakobar.com~ (two levels). The Cloudflare edge had no certificate to
|
||||||
|
present to browsers for these hostnames, causing a TLS handshake failure
|
||||||
|
before the request ever reached the tunnel.
|
||||||
|
|
||||||
|
*Fix:*
|
||||||
|
Move all services to single-level subdomains under ~zakobar.com~
|
||||||
|
(~git.zakobar.com~, ~auth.zakobar.com~, etc.). In the NixOS config this
|
||||||
|
required only one line change — the ~domain~ field in ~flake.nix~:
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
domain = "zakobar.com"; # was "home.zakobar.com"
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
All modules reference ~homeyConfig.domain~ and updated automatically on
|
||||||
|
rebuild. Tunnel hostnames and DNS records in the Cloudflare dashboard were
|
||||||
|
updated to match.
|
||||||
|
|
||||||
|
** 4. ~ERR_TOO_MANY_REDIRECTS~ via tunnel
|
||||||
|
|
||||||
|
*Symptom:*
|
||||||
|
Browser shows ~ERR_TOO_MANY_REDIRECTS~ when accessing any service through
|
||||||
|
the Cloudflare tunnel.
|
||||||
|
|
||||||
|
*Cause:*
|
||||||
|
cloudflared was talking to Caddy over plain HTTP (~http://localhost:80~).
|
||||||
|
Caddy's default behaviour is to issue a 308 permanent redirect from HTTP to
|
||||||
|
HTTPS for any hostname that has a matching HTTPS vhost. cloudflared followed
|
||||||
|
the redirect back to ~http://localhost:80~, which redirected again,
|
||||||
|
indefinitely.
|
||||||
|
|
||||||
|
*Fix:*
|
||||||
|
Add explicit ~http://~ vhost entries in ~caddy.nix~ for every service. When
|
||||||
|
Caddy has an explicit HTTP vhost for a hostname, it serves it directly
|
||||||
|
without redirecting:
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
"git.${domain}" = {
|
||||||
|
extraConfig = "reverse_proxy localhost:3000";
|
||||||
|
};
|
||||||
|
"http://git.${domain}" = { # ← suppresses HTTP→HTTPS redirect
|
||||||
|
extraConfig = "reverse_proxy localhost:3000";
|
||||||
|
};
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
* Adding a New Service
|
||||||
|
|
||||||
|
To expose a new service through the tunnel:
|
||||||
|
|
||||||
|
1. Create ~modules/services/<name>.nix~ following the module pattern.
|
||||||
|
2. Add both a plain and ~http://~ vhost in ~modules/caddy.nix~:
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
"<name>.${domain}" = {
|
||||||
|
extraConfig = "reverse_proxy localhost:<port>";
|
||||||
|
};
|
||||||
|
"http://<name>.${domain}" = {
|
||||||
|
extraConfig = "reverse_proxy localhost:<port>";
|
||||||
|
};
|
||||||
|
#+END_SRC
|
||||||
|
3. No DNS or tunnel changes needed — the wildcard CNAME and wildcard tunnel
|
||||||
|
rule (~*.zakobar.com~) cover new subdomains automatically.
|
||||||
|
4. Rebuild and switch: ~sudo nixos-rebuild switch --flake .#pi-main~
|
||||||
|
|
||||||
|
* Certificate Details
|
||||||
|
|
||||||
|
** Let's Encrypt cert (LAN access)
|
||||||
|
|
||||||
|
- Issued per-hostname by Caddy via DNS-01 ACME using the Cloudflare API.
|
||||||
|
- Covers each hostname individually (e.g. ~git.zakobar.com~).
|
||||||
|
- Stored in ~/var/lib/caddy/.local/share/caddy/certificates/~.
|
||||||
|
- Used only when accessing services directly on the LAN (bypassing tunnel).
|
||||||
|
- Auto-renewed by Caddy.
|
||||||
|
|
||||||
|
** Cloudflare Universal SSL cert (tunnel / remote access)
|
||||||
|
|
||||||
|
- Issued by Google Trust Services for ~*.zakobar.com~.
|
||||||
|
- Managed entirely by Cloudflare — no action required on the Pi.
|
||||||
|
- Covers all single-level subdomains (~git.zakobar.com~, ~auth.zakobar.com~, etc.).
|
||||||
|
- Does *not* cover two-level subdomains (~git.home.zakobar.com~) — this was
|
||||||
|
the root cause of problem #3 above.
|
||||||
|
|
||||||
|
* Quick Reference: Debugging Checklist
|
||||||
|
|
||||||
|
| Symptom | Where to look | Command |
|
||||||
|
|---|---|---|
|
||||||
|
| 502 Bad Gateway | cloudflared logs | ~journalctl -u cloudflared-tunnel -n 50~ |
|
||||||
|
| 502 Bad Gateway | Caddy → backend | ~curl http://localhost:<port>/~ |
|
||||||
|
| TLS internal error | SNI / cert issue | ~curl -sv --resolve host:443:127.0.0.1 https://host/~ |
|
||||||
|
| Too many redirects | HTTP vhost missing | check ~http://~ entries in caddy.nix |
|
||||||
|
| Handshake failure at edge | Cloudflare cert scope | check SSL/TLS → Edge Certificates |
|
||||||
|
| Token appears invalid | plugin version | check ~caddy-dns/cloudflare~ version vs token format |
|
||||||
|
| Caddy won't start | token / config error | ~journalctl -u caddy --since "5 min ago"~ |
|
||||||
@@ -1,18 +1,6 @@
|
|||||||
{
|
{
|
||||||
description = "Homey - self-hosted home server NixOS configuration";
|
description = "Homey - self-hosted home server NixOS configuration";
|
||||||
|
|
||||||
# Binary cache for pre-built Raspberry Pi kernel + firmware packages.
|
|
||||||
# nixos-raspberrypi builds against its own pinned nixpkgs and publishes
|
|
||||||
# to this cache — using it avoids compiling linuxPackages_rpi4 from source.
|
|
||||||
nixConfig = {
|
|
||||||
extra-substituters = [
|
|
||||||
"https://nixos-raspberrypi.cachix.org"
|
|
||||||
];
|
|
||||||
extra-trusted-public-keys = [
|
|
||||||
"nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI="
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||||
|
|
||||||
@@ -22,46 +10,53 @@
|
|||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Raspberry Pi hardware support — provides vendor kernel, firmware,
|
# nixos-hardware provides RPi4 wireless firmware.
|
||||||
# bootloader management, and a binary cache for pre-built aarch64 packages.
|
# We use only the minimal pieces needed for a headless server —
|
||||||
# Intentionally NOT following our nixpkgs: the cache is built against the
|
# no display, audio, or bluetooth modules.
|
||||||
# flake's own pinned nixpkgs, so following would invalidate all cache hits.
|
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
|
||||||
nixos-raspberrypi.url = "github:nvmd/nixos-raspberrypi/main";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, sops-nix, nixos-raspberrypi, ... }@inputs:
|
outputs = { self, nixpkgs, sops-nix, nixos-hardware, ... }@inputs:
|
||||||
let
|
let
|
||||||
# Shared specialArgs passed to every host
|
# Shared specialArgs passed to every host
|
||||||
commonArgs = {
|
commonArgs = {
|
||||||
inherit inputs nixos-raspberrypi;
|
inherit inputs;
|
||||||
# Top-level site config — override per-host if needed
|
# Top-level site config — override per-host if needed
|
||||||
homeyConfig = {
|
homeyConfig = {
|
||||||
domain = "home.zakobar.com"; # base domain for all services
|
domain = "zakobar.com"; # base domain for all services
|
||||||
organization = "Zakobar Home Server";
|
organization = "Zakobar Home Server";
|
||||||
timezone = "Asia/Jerusalem";
|
timezone = "Asia/Jerusalem";
|
||||||
# External HD mount point — set in hardware.nix per host
|
|
||||||
# dataDir is intentionally NOT set here; each host sets it
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# nixos-raspberrypi.lib.nixosSystem is a drop-in replacement for
|
# Minimal RPi4 hardware module for a headless server.
|
||||||
# nixpkgs.lib.nixosSystem that:
|
# Provides only: bootloader, initrd modules, wireless firmware, DTB filter.
|
||||||
# - injects vendor kernel/firmware overlays
|
# Deliberately excludes display, audio, bluetooth from the full nixos-hardware module.
|
||||||
# - wires up the trusted cache substituters
|
rpi4Headless = { pkgs, ... }: {
|
||||||
# - passes nixos-raspberrypi into specialArgs automatically
|
boot.loader.grub.enable = false;
|
||||||
# It uses the flake's own pinned nixpkgs by default (currently 25.11).
|
boot.loader.generic-extlinux-compatible.enable = true;
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"pcie-brcmstb" # PCIe bus (USB3, NVMe)
|
||||||
|
"reset-raspberrypi" # required for vl805 firmware
|
||||||
|
"usb-storage"
|
||||||
|
"usbhid"
|
||||||
|
"vc4" # VideoCore (needed even headless for boot)
|
||||||
|
];
|
||||||
|
# sd-image-aarch64.nix lists modules for many SoCs (including sun4i-drm
|
||||||
|
# for Allwinner boards) that don't exist in linux_rpi4. Allow missing.
|
||||||
|
boot.initrd.includeDefaultModules = false;
|
||||||
|
hardware.deviceTree.filter = "bcm2711-rpi-*.dtb";
|
||||||
|
hardware.firmware = [
|
||||||
|
(pkgs.callPackage "${nixos-hardware}/raspberry-pi/common/raspberry-pi-wireless-firmware.nix" {})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
mkHost = { hostPath, extraModules ? [] }:
|
mkHost = { hostPath, extraModules ? [] }:
|
||||||
nixos-raspberrypi.lib.nixosSystem {
|
nixpkgs.lib.nixosSystem {
|
||||||
specialArgs = commonArgs;
|
specialArgs = commonArgs;
|
||||||
modules = [
|
modules = [
|
||||||
sops-nix.nixosModules.sops
|
sops-nix.nixosModules.sops
|
||||||
# RPi 4 base: vendor kernel (linuxPackages_rpi4), firmware,
|
rpi4Headless
|
||||||
# bootloader (u-boot), initrd modules, config.txt management
|
|
||||||
nixos-raspberrypi.nixosModules.raspberry-pi-4.base
|
|
||||||
# SD image target — provides system.build.sdImage
|
|
||||||
({ modulesPath, ... }: {
|
|
||||||
imports = [ "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" ];
|
|
||||||
})
|
|
||||||
hostPath
|
hostPath
|
||||||
./modules/common.nix
|
./modules/common.nix
|
||||||
./modules/storage.nix
|
./modules/storage.nix
|
||||||
@@ -81,62 +76,17 @@
|
|||||||
in {
|
in {
|
||||||
nixosConfigurations = {
|
nixosConfigurations = {
|
||||||
|
|
||||||
# Bootstrap image — flash this first.
|
# Bootstrap image — flash this first, then deploy pi-main.
|
||||||
# Minimal: SSH key, WiFi, static IP. No sops, no services.
|
# See hosts/pi-main-bootstrap/default.nix for details.
|
||||||
# Purpose: boot the Pi, generate the age key, then deploy pi-main.
|
pi-main-bootstrap = nixpkgs.lib.nixosSystem {
|
||||||
pi-main-bootstrap = nixos-raspberrypi.lib.nixosSystem {
|
|
||||||
specialArgs = commonArgs;
|
specialArgs = commonArgs;
|
||||||
modules = [
|
modules = [
|
||||||
nixos-raspberrypi.nixosModules.raspberry-pi-4.base
|
rpi4Headless
|
||||||
({ modulesPath, ... }: {
|
({ modulesPath, ... }: {
|
||||||
imports = [ "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" ];
|
imports = [ "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" ];
|
||||||
})
|
})
|
||||||
./hosts/pi-main/hardware.nix
|
./hosts/pi-main/hardware.nix
|
||||||
({ pkgs, lib, ... }: {
|
./hosts/pi-main-bootstrap/default.nix
|
||||||
networking.hostName = "pi-main";
|
|
||||||
time.timeZone = commonArgs.homeyConfig.timezone;
|
|
||||||
i18n.defaultLocale = "en_US.UTF-8";
|
|
||||||
system.stateVersion = "25.05";
|
|
||||||
|
|
||||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
|
||||||
|
|
||||||
# WiFi — PSK inline (bootstrap only, not in Nix store long-term)
|
|
||||||
networking.wireless = {
|
|
||||||
enable = true;
|
|
||||||
networks."Zakobar".psk = "0502711157";
|
|
||||||
};
|
|
||||||
networking.interfaces.wlan0.ipv4.addresses = [{
|
|
||||||
address = "192.168.1.100";
|
|
||||||
prefixLength = 24;
|
|
||||||
}];
|
|
||||||
networking.useDHCP = false;
|
|
||||||
networking.interfaces.wlan0.useDHCP = false;
|
|
||||||
networking.defaultGateway = "192.168.1.1";
|
|
||||||
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
|
|
||||||
networking.firewall.allowedTCPPorts = [ 22 ];
|
|
||||||
|
|
||||||
# SSH — key only, no passwords, no root
|
|
||||||
services.openssh = {
|
|
||||||
enable = true;
|
|
||||||
settings = {
|
|
||||||
PasswordAuthentication = false;
|
|
||||||
PermitRootLogin = "no";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
users.mutableUsers = false;
|
|
||||||
users.users.admin = {
|
|
||||||
isNormalUser = true;
|
|
||||||
extraGroups = [ "wheel" ];
|
|
||||||
openssh.authorizedKeys.keys = [
|
|
||||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDBFZRqiTsOCAJPMqUyMeLd2MbyjdGoyqDVq5/Inhb6EOaM1NUGG4b6FPmYgFLyJIm5LC9BOo6M7npiaiOs/zMqp+hoGLNQUNwm5/G0uy1bjkEfKdUTdGnJ2+M9rkxrR1c+KXrjkiqECqTbnPE4mJbGyVxBW2MwMeP5w8c0DB5KO528PetvHMPPQuEdXyZzDI4kKtVpMlJoPIrIGlNFX0G/wrgXcM4zU1snOTuYGqZnWW++4kBsgIlRKpf/bLJyUMTp30eLVr0fQ6OMBtj1tzUUBaaowU6VGYQQDU/rIh/NpkA2cEVPXZegM4OohkAqrJBFPIAg90WD9Z/SyQlz0Jn8PpAloP0Cuq2vVRr+QLEwxqGiFq91YQ2VtwksMHwJGVrXRCNegpxTZQijWMEd+o0FD2cEd7Ftw6v2L6g12GJ3QGX/q0d/u0GongLLa9fPXl4VoAu7AL+cUcbX/SS7RCG8kYAR3DwOazVbK0NWEdwvWdoSU4lZ3j2at1xqMGjHjyLiTeUqZBjm+Sl5MJWIYNg+8hnONljvggg4SzDFDAkgVLZtOCaZibsMA1ucGR7VRCM09uoaEI4/ZS5pCBtYcp8X67Bv67Og8s2NFf5sUfYBPPKpdBSs+dEPycNVff6JlmzfNiyzLawacGKIDWYSgkOl43N/5ehtpsL3HMZ+5SVNIw=="
|
|
||||||
];
|
|
||||||
};
|
|
||||||
security.sudo.wheelNeedsPassword = false;
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.age pkgs.vim ];
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{ pkgs, lib, homeyConfig, ... }:
|
||||||
|
|
||||||
|
# Bootstrap image for the primary Raspberry Pi 4.
|
||||||
|
#
|
||||||
|
# Flash this image first. Its only purpose is to boot the Pi so you can:
|
||||||
|
# 1. Generate the age key: sudo age-keygen -o /var/lib/sops-nix/key.txt
|
||||||
|
# 2. Print the pubkey: sudo age-keygen -y /var/lib/sops-nix/key.txt
|
||||||
|
# 3. Add the pubkey to .sops.yaml, re-encrypt secrets, then deploy pi-main.
|
||||||
|
#
|
||||||
|
# No sops, no services, no external HD — just SSH + WiFi.
|
||||||
|
#
|
||||||
|
# WiFi PSK: uncomment and fill in before building. Do not commit the password.
|
||||||
|
# networks."YourSSID".psk = "your-wifi-password";
|
||||||
|
|
||||||
|
{
|
||||||
|
networking.hostName = "pi-main";
|
||||||
|
time.timeZone = homeyConfig.timezone;
|
||||||
|
i18n.defaultLocale = "en_US.UTF-8";
|
||||||
|
system.stateVersion = "25.05";
|
||||||
|
|
||||||
|
nix.settings = {
|
||||||
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
|
substituters = [
|
||||||
|
"https://cache.nixos.org"
|
||||||
|
"https://nix-community.cachix.org"
|
||||||
|
];
|
||||||
|
trusted-public-keys = [
|
||||||
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||||
|
"nix-community.cachix.org-1:mB9FkXj6Q3Q4ohOcbM4FJ9Z1X2kCrVK4vZOqsDqqNqk="
|
||||||
|
];
|
||||||
|
};
|
||||||
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
|
||||||
|
# linux_rpi4 is pre-built in cache.nixos.org — fetched, not compiled.
|
||||||
|
boot.kernelPackages = pkgs.linuxKernel.packages.linux_rpi4;
|
||||||
|
|
||||||
|
networking.wireless = {
|
||||||
|
enable = true;
|
||||||
|
# networks."Zakobar".psk = "your-wifi-password";
|
||||||
|
};
|
||||||
|
networking.interfaces.wlan0.ipv4.addresses = [{
|
||||||
|
address = "192.168.1.100";
|
||||||
|
prefixLength = 24;
|
||||||
|
}];
|
||||||
|
networking.useDHCP = false;
|
||||||
|
networking.interfaces.wlan0.useDHCP = false;
|
||||||
|
networking.defaultGateway = "192.168.1.1";
|
||||||
|
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
|
||||||
|
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||||
|
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
PermitRootLogin = "no";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.mutableUsers = false;
|
||||||
|
users.users.admin = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [ "wheel" ];
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDfzDDO5juINctECmWlsYtGghEiX/RnTJ1cazLvOWSrPfsTyEd+B1+Ig8kFefNryjkpApfRXqj5KtLPNlpLfdVBrOIfhIveEp2MGqhgOGZFNVxQyXnZgii8Zdh4cqZ2O3pZpMsaAQBaJ9nH6dK0dJjicWT5f6TqwrVcInywRc5SuyizoSxoFmg7ch2rnlVi0j5XMVqdh8XLzHXZ7yWCzXy7+hWl/d7pwpyuzoK8dBw2EU9TauhgRDruom5Q9vWJTLStALC9pAIb0v9UFj9y+1zwx7pXsXp5F1g73EYrE4QR+QQ6z2LebuK280W0t+VA/fSCEB13DnkmofgqZQxX5MSCmrxZ5lTFp1FjW6yJo7As9FheF/GECowYkMRIx4IiQsjjHjZqlLRpLas11yAp6tGoZnw59hFo6Lu0Kva39jGVVmioYHtAeE5rD5w+v5kseJR4jlQ8aKB5yOjYUQOIz2AHQyoidgaeR2jPWqZUeRQbACI+/p3CHO45r3hrjATtGloBg0xF95Qws7Be3mjHVhbBLOoob8MdZ8nYAGnhlWrZphlkvXsHC6OUkuDJW00tmMjWXRlFwhFJ+nqUQCgLVjxVHQJ5rq9GeXBUuNXAeCm5BKBsdq+9qqVlt7D9iGyfr0lcZ7peKz/96KwPCWpG2En1Ur0/cVcbWnXEfG/xWO10tQ== cardno:24_758_470"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
security.sudo.wheelNeedsPassword = false;
|
||||||
|
|
||||||
|
environment.systemPackages = [ pkgs.age pkgs.vim ];
|
||||||
|
}
|
||||||
@@ -9,6 +9,10 @@
|
|||||||
./hardware.nix
|
./hardware.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# linux_rpi4 is the Raspberry Pi Foundation's kernel, sourced from nixpkgs
|
||||||
|
# and pre-built in cache.nixos.org. Avoids a multi-hour native compilation.
|
||||||
|
boot.kernelPackages = pkgs.linuxKernel.packages.linux_rpi4;
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Identity
|
# Identity
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -50,7 +54,7 @@
|
|||||||
extraGroups = [ "wheel" "podman" ];
|
extraGroups = [ "wheel" "podman" ];
|
||||||
# Paste your SSH public key here
|
# Paste your SSH public key here
|
||||||
openssh.authorizedKeys.keys = [
|
openssh.authorizedKeys.keys = [
|
||||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDBFZRqiTsOCAJPMqUyMeLd2MbyjdGoyqDVq5/Inhb6EOaM1NUGG4b6FPmYgFLyJIm5LC9BOo6M7npiaiOs/zMqp+hoGLNQUNwm5/G0uy1bjkEfKdUTdGnJ2+M9rkxrR1c+KXrjkiqECqTbnPE4mJbGyVxBW2MwMeP5w8c0DB5KO528PetvHMPPQuEdXyZzDI4kKtVpMlJoPIrIGlNFX0G/wrgXcM4zU1snOTuYGqZnWW++4kBsgIlRKpf/bLJyUMTp30eLVr0fQ6OMBtj1tzUUBaaowU6VGYQQDU/rIh/NpkA2cEVPXZegM4OohkAqrJBFPIAg90WD9Z/SyQlz0Jn8PpAloP0Cuq2vVRr+QLEwxqGiFq91YQ2VtwksMHwJGVrXRCNegpxTZQijWMEd+o0FD2cEd7Ftw6v2L6g12GJ3QGX/q0d/u0GongLLa9fPXl4VoAu7AL+cUcbX/SS7RCG8kYAR3DwOazVbK0NWEdwvWdoSU4lZ3j2at1xqMGjHjyLiTeUqZBjm+Sl5MJWIYNg+8hnONljvggg4SzDFDAkgVLZtOCaZibsMA1ucGR7VRCM09uoaEI4/ZS5pCBtYcp8X67Bv67Og8s2NFf5sUfYBPPKpdBSs+dEPycNVff6JlmzfNiyzLawacGKIDWYSgkOl43N/5ehtpsL3HMZ+5SVNIw=="
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDfzDDO5juINctECmWlsYtGghEiX/RnTJ1cazLvOWSrPfsTyEd+B1+Ig8kFefNryjkpApfRXqj5KtLPNlpLfdVBrOIfhIveEp2MGqhgOGZFNVxQyXnZgii8Zdh4cqZ2O3pZpMsaAQBaJ9nH6dK0dJjicWT5f6TqwrVcInywRc5SuyizoSxoFmg7ch2rnlVi0j5XMVqdh8XLzHXZ7yWCzXy7+hWl/d7pwpyuzoK8dBw2EU9TauhgRDruom5Q9vWJTLStALC9pAIb0v9UFj9y+1zwx7pXsXp5F1g73EYrE4QR+QQ6z2LebuK280W0t+VA/fSCEB13DnkmofgqZQxX5MSCmrxZ5lTFp1FjW6yJo7As9FheF/GECowYkMRIx4IiQsjjHjZqlLRpLas11yAp6tGoZnw59hFo6Lu0Kva39jGVVmioYHtAeE5rD5w+v5kseJR4jlQ8aKB5yOjYUQOIz2AHQyoidgaeR2jPWqZUeRQbACI+/p3CHO45r3hrjATtGloBg0xF95Qws7Be3mjHVhbBLOoob8MdZ8nYAGnhlWrZphlkvXsHC6OUkuDJW00tmMjWXRlFwhFJ+nqUQCgLVjxVHQJ5rq9GeXBUuNXAeCm5BKBsdq+9qqVlt7D9iGyfr0lcZ7peKz/96KwPCWpG2En1Ur0/cVcbWnXEfG/xWO10tQ== cardno:24_758_470"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,7 +66,7 @@
|
|||||||
homey.storage = {
|
homey.storage = {
|
||||||
# Replace with the actual by-id path of your USB drive.
|
# Replace with the actual by-id path of your USB drive.
|
||||||
# Find it: ls -la /dev/disk/by-id/ | grep -v part
|
# Find it: ls -la /dev/disk/by-id/ | grep -v part
|
||||||
device = "/dev/disk/by-id/usb-WD_Ext_HDD_1021_5743415A4146313531393031-0:0-part1";
|
device = "/dev/disk/by-label/homey-data";
|
||||||
mountPoint = "/mnt/data";
|
mountPoint = "/mnt/data";
|
||||||
fsType = "ext4";
|
fsType = "ext4";
|
||||||
};
|
};
|
||||||
@@ -98,14 +102,14 @@
|
|||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Local DNS overrides (optional — makes LAN clients hit the Pi directly
|
# Local DNS overrides (optional — makes LAN clients hit the Pi directly
|
||||||
# instead of going through Cloudflare for *.home.zakobar.com)
|
# instead of going through Cloudflare for *.zakobar.com)
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# If you run Pi-hole or Adguard, add these records there instead.
|
# If you run Pi-hole or Adguard, add these records there instead.
|
||||||
# networking.extraHosts = ''
|
# networking.extraHosts = ''
|
||||||
# 192.168.1.100 home.zakobar.com
|
# 192.168.1.100 zakobar.com
|
||||||
# 192.168.1.100 auth.home.zakobar.com
|
# 192.168.1.100 auth.zakobar.com
|
||||||
# 192.168.1.100 git.home.zakobar.com
|
# 192.168.1.100 git.zakobar.com
|
||||||
# 192.168.1.100 nextcloud.home.zakobar.com
|
# 192.168.1.100 nextcloud.zakobar.com
|
||||||
# 192.168.1.100 ldapadmin.home.zakobar.com
|
# 192.168.1.100 ldapadmin.zakobar.com
|
||||||
# '';
|
# '';
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-22
@@ -3,7 +3,7 @@
|
|||||||
# Caddy reverse proxy.
|
# Caddy reverse proxy.
|
||||||
#
|
#
|
||||||
# Features:
|
# Features:
|
||||||
# - DNS-01 ACME via Cloudflare API → real wildcard cert for *.home.zakobar.com
|
# - DNS-01 ACME via Cloudflare API → real wildcard cert for *.zakobar.com
|
||||||
# - forward_auth to Authelia for protected vhosts
|
# - forward_auth to Authelia for protected vhosts
|
||||||
# - Plain reverse_proxy for public vhosts (authelia itself, nextcloud)
|
# - Plain reverse_proxy for public vhosts (authelia itself, nextcloud)
|
||||||
# - Listens on :80 (redirect) and :443 (TLS)
|
# - Listens on :80 (redirect) and :443 (TLS)
|
||||||
@@ -23,11 +23,23 @@ let
|
|||||||
# under the hood to produce a fixed-output derivation.
|
# under the hood to produce a fixed-output derivation.
|
||||||
caddyWithCloudflare = pkgs.caddy.withPlugins {
|
caddyWithCloudflare = pkgs.caddy.withPlugins {
|
||||||
plugins = [
|
plugins = [
|
||||||
"github.com/caddy-dns/cloudflare@v0.2.2-0.20250724223520-f589a18c0f5d"
|
# v0.2.4 tag points to commit a8737d0 which includes the fix for
|
||||||
|
# cfut_/cfat_ token format validation (PR #123).
|
||||||
|
"github.com/caddy-dns/cloudflare@v0.2.4"
|
||||||
];
|
];
|
||||||
hash = "sha256-2Fb2fgM7YhWk9kBnnNGb85MJkAkgzXiI1fb6eK3ykIE=";
|
hash = "sha256-pRrLBlYRaAyMYwPXeTy4WqWNRu/L9K6Mn2src11dGh8=";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Reverse-proxy snippet for cloudflared http:// vhosts.
|
||||||
|
# Cloudflare terminates TLS; cloudflared connects to Caddy over plain HTTP.
|
||||||
|
# We must override X-Forwarded-Proto so upstream services (especially
|
||||||
|
# Authelia) know the client is actually on HTTPS.
|
||||||
|
cfProxy = port: ''
|
||||||
|
reverse_proxy localhost:${toString port} {
|
||||||
|
header_up X-Forwarded-Proto https
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
# Reusable Authelia forward_auth snippet
|
# Reusable Authelia forward_auth snippet
|
||||||
# Returns a Caddyfile snippet block that applies forward_auth.
|
# Returns a Caddyfile snippet block that applies forward_auth.
|
||||||
# copy_headers makes Authelia's Remote-* headers available downstream.
|
# copy_headers makes Authelia's Remote-* headers available downstream.
|
||||||
@@ -35,6 +47,9 @@ let
|
|||||||
forward_auth localhost:9091 {
|
forward_auth localhost:9091 {
|
||||||
uri /api/verify?rd=https://auth.${domain}
|
uri /api/verify?rd=https://auth.${domain}
|
||||||
copy_headers Remote-User Remote-Name Remote-Groups Remote-Email
|
copy_headers Remote-User Remote-Name Remote-Groups Remote-Email
|
||||||
|
# Always tell Authelia the scheme is https (cloudflared terminates TLS
|
||||||
|
# externally; Caddy's http:// vhosts are only for the tunnel loopback).
|
||||||
|
header_up X-Forwarded-Proto https
|
||||||
# On auth failure, redirect to the authelia login page
|
# On auth failure, redirect to the authelia login page
|
||||||
@goauth status 401
|
@goauth status 401
|
||||||
handle_response @goauth {
|
handle_response @goauth {
|
||||||
@@ -77,7 +92,15 @@ in
|
|||||||
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# Each virtual host
|
# Each virtual host.
|
||||||
|
#
|
||||||
|
# Each service gets two vhost entries:
|
||||||
|
# - "host" (no scheme) → Caddy handles HTTPS + auto cert (for LAN access)
|
||||||
|
# - "http://host" → plain HTTP for cloudflared on loopback (no redirect)
|
||||||
|
#
|
||||||
|
# Caddy auto-redirects HTTP→HTTPS only when no explicit http:// vhost exists.
|
||||||
|
# By defining http:// explicitly we suppress that redirect so cloudflared
|
||||||
|
# (which talks plain HTTP on port 80) gets a direct response.
|
||||||
virtualHosts = {
|
virtualHosts = {
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -88,21 +111,25 @@ in
|
|||||||
reverse_proxy localhost:9091
|
reverse_proxy localhost:9091
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
"http://auth.${domain}" = {
|
||||||
|
extraConfig = cfProxy 9091;
|
||||||
|
};
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Gitea — protected behind one_factor Authelia
|
# Gitea — no forward_auth; git HTTP clients can't handle SSO redirects.
|
||||||
|
# Access control is handled by Gitea itself (LDAP auth + private repos).
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
"git.${domain}" = {
|
"git.${domain}" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
${autheliaForwardAuth}
|
|
||||||
reverse_proxy localhost:3000
|
reverse_proxy localhost:3000
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
"http://git.${domain}" = {
|
||||||
|
extraConfig = cfProxy 3000;
|
||||||
|
};
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Nextcloud — public auth (Nextcloud manages its own users + LDAP)
|
# Nextcloud — public auth (Nextcloud manages its own users + LDAP)
|
||||||
# Authelia is not gating nextcloud directly because NC has its own
|
|
||||||
# login flow. We still want HTTPS.
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
"nextcloud.${domain}" = {
|
"nextcloud.${domain}" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
@@ -118,6 +145,18 @@ in
|
|||||||
reverse_proxy localhost:8080
|
reverse_proxy localhost:8080
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
"http://nextcloud.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
redir /.well-known/carddav /remote.php/dav/ 301
|
||||||
|
redir /.well-known/caldav /remote.php/dav/ 301
|
||||||
|
request_body {
|
||||||
|
max_size 5GB
|
||||||
|
}
|
||||||
|
reverse_proxy localhost:8080 {
|
||||||
|
header_up X-Forwarded-Proto https
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# phpLDAPadmin — two_factor, admins only (enforced by authelia policy)
|
# phpLDAPadmin — two_factor, admins only (enforced by authelia policy)
|
||||||
@@ -128,16 +167,25 @@ in
|
|||||||
reverse_proxy localhost:8081
|
reverse_proxy localhost:8081
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
"http://ldapadmin.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
${autheliaForwardAuth}
|
||||||
|
${cfProxy 8081}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Jellyfin — one_factor (added when enabled)
|
# Jellyfin — no forward_auth; Jellyfin has its own login UI and
|
||||||
|
# native app clients can't handle SSO redirects.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
"jellyfin.${domain}" = {
|
"jellyfin.${domain}" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
${autheliaForwardAuth}
|
|
||||||
reverse_proxy localhost:8096
|
reverse_proxy localhost:8096
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
"http://jellyfin.${domain}" = {
|
||||||
|
extraConfig = cfProxy 8096;
|
||||||
|
};
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Transmission — two_factor, admins only (enforced by authelia policy)
|
# Transmission — two_factor, admins only (enforced by authelia policy)
|
||||||
@@ -149,6 +197,12 @@ in
|
|||||||
'';
|
'';
|
||||||
# NOTE: transmission is bound to 9092 to avoid clash with authelia on 9091.
|
# NOTE: transmission is bound to 9092 to avoid clash with authelia on 9091.
|
||||||
};
|
};
|
||||||
|
"http://torrent.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
${autheliaForwardAuth}
|
||||||
|
${cfProxy 9092}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -163,18 +217,20 @@ in
|
|||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
systemd.services.caddy = {
|
systemd.services.caddy = {
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
EnvironmentFile = "/run/caddy-secrets.env";
|
# LoadCredential stages the sops-decrypted secret into a
|
||||||
ExecStartPre = [
|
# per-invocation directory ($CREDENTIALS_DIRECTORY) before any
|
||||||
(pkgs.writeShellScript "caddy-inject-cf-token" ''
|
# Exec* step. ExecStart then reads the file contents and exports
|
||||||
install -m 0600 /dev/null /run/caddy-secrets.env
|
# CLOUDFLARE_API_TOKEN before exec-ing caddy, so there is no
|
||||||
printf 'CLOUDFLARE_API_TOKEN=%s\n' \
|
# intermediate env file and no ordering race with EnvironmentFile.
|
||||||
"$(cat ${config.sops.secrets."cloudflare/api_token".path})" \
|
LoadCredential = "cloudflare_api_token:${config.sops.secrets."cloudflare/api_token".path}";
|
||||||
> /run/caddy-secrets.env
|
# Systemd requires clearing ExecStart= before setting a new value for
|
||||||
'')
|
# non-oneshot services. The empty string resets the list; the second
|
||||||
];
|
# entry is the actual start command.
|
||||||
ExecStopPost = [
|
ExecStart = lib.mkForce [
|
||||||
(pkgs.writeShellScript "caddy-cleanup-env" ''
|
""
|
||||||
rm -f /run/caddy-secrets.env
|
(pkgs.writeShellScript "caddy-start" ''
|
||||||
|
export CLOUDFLARE_API_TOKEN=$(cat "$CREDENTIALS_DIRECTORY/cloudflare_api_token")
|
||||||
|
exec ${caddyWithCloudflare}/bin/caddy run --environ --config /etc/caddy/caddy_config --adapter caddyfile
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
# 2. Name it (e.g. "pi-main")
|
# 2. Name it (e.g. "pi-main")
|
||||||
# 3. Copy the tunnel token — add it to secrets.yaml as cloudflare/tunnel_token
|
# 3. Copy the tunnel token — add it to secrets.yaml as cloudflare/tunnel_token
|
||||||
# 4. In the tunnel's "Public Hostnames" config, add routes:
|
# 4. In the tunnel's "Public Hostnames" config, add routes:
|
||||||
# auth.home.zakobar.com → http://localhost:80 (or https://localhost:443)
|
# auth.zakobar.com → http://localhost:80 (or https://localhost:443)
|
||||||
# git.home.zakobar.com → https://localhost:443
|
# git.zakobar.com → https://localhost:443
|
||||||
# nextcloud.home.zakobar.com → https://localhost:443
|
# nextcloud.zakobar.com → https://localhost:443
|
||||||
# ldapadmin.home.zakobar.com → https://localhost:443
|
# ldapadmin.zakobar.com → https://localhost:443
|
||||||
# jellyfin.home.zakobar.com → https://localhost:443
|
# jellyfin.zakobar.com → https://localhost:443
|
||||||
# torrent.home.zakobar.com → https://localhost:443
|
# torrent.zakobar.com → https://localhost:443
|
||||||
# Set "No TLS Verify" = true (Caddy's cert is from Let's Encrypt but
|
# Set "No TLS Verify" = true (Caddy's cert is from Let's Encrypt but
|
||||||
# the hostname seen by cloudflared is localhost, so hostname verification
|
# the hostname seen by cloudflared is localhost, so hostname verification
|
||||||
# would fail without this flag).
|
# would fail without this flag).
|
||||||
|
|||||||
@@ -16,13 +16,10 @@
|
|||||||
substituters = [
|
substituters = [
|
||||||
"https://cache.nixos.org"
|
"https://cache.nixos.org"
|
||||||
"https://nix-community.cachix.org"
|
"https://nix-community.cachix.org"
|
||||||
# Pre-built RPi vendor kernel + firmware (linuxPackages_rpi4, etc.)
|
|
||||||
"https://nixos-raspberrypi.cachix.org"
|
|
||||||
];
|
];
|
||||||
trusted-public-keys = [
|
trusted-public-keys = [
|
||||||
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||||
"nix-community.cachix.org-1:mB9FkXj6Q3Q4ohOcbM4FJ9Z1X2kCrVK4vZOqsDqqNqk="
|
"nix-community.cachix.org-1:mB9FkXj6Q3Q4ohOcbM4FJ9Z1X2kCrVK4vZOqsDqqNqk="
|
||||||
"nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI="
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
gc = {
|
gc = {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ let
|
|||||||
dataDir = config.homey.storage.mountPoint;
|
dataDir = config.homey.storage.mountPoint;
|
||||||
domain = homeyConfig.domain;
|
domain = homeyConfig.domain;
|
||||||
|
|
||||||
# LDAP base DN derived from domain: home.zakobar.com → dc=home,dc=zakobar,dc=com
|
# LDAP base DN derived from domain: zakobar.com → dc=zakobar,dc=com
|
||||||
ldapBaseDN = lib.concatStringsSep ","
|
ldapBaseDN = lib.concatStringsSep ","
|
||||||
(map (p: "dc=${p}") (lib.splitString "." domain));
|
(map (p: "dc=${p}") (lib.splitString "." domain));
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ in
|
|||||||
virtualisation.oci-containers.containers.authelia = {
|
virtualisation.oci-containers.containers.authelia = {
|
||||||
image = cfg.image;
|
image = cfg.image;
|
||||||
|
|
||||||
ports = [ "127.0.0.1:${toString cfg.port}:9091" ];
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
TZ = homeyConfig.timezone;
|
TZ = homeyConfig.timezone;
|
||||||
|
|||||||
+149
-118
@@ -9,7 +9,15 @@
|
|||||||
# Volume layout:
|
# Volume layout:
|
||||||
# <dataDir>/gitea/data/ → /data (repos, sqlite db, avatars, lfs, etc.)
|
# <dataDir>/gitea/data/ → /data (repos, sqlite db, avatars, lfs, etc.)
|
||||||
#
|
#
|
||||||
# The app.ini is rendered by Nix and bind-mounted read-only.
|
# Configuration strategy: all settings are passed as GITEA__<SECTION>__<KEY>
|
||||||
|
# environment variables. Gitea writes its own app.ini into /data/gitea/conf/
|
||||||
|
# on first start; the env vars override every key at runtime without touching
|
||||||
|
# that file. This avoids the bind-mount / read-only-fs problem where Gitea
|
||||||
|
# needs to rewrite its own config file on startup.
|
||||||
|
#
|
||||||
|
# Non-secret settings go in the `environment` block (they are fine in the
|
||||||
|
# Nix store). Secret settings go into /run/gitea-secrets.env via ExecStartPre
|
||||||
|
# (never in the store).
|
||||||
#
|
#
|
||||||
# Secrets consumed from sops:
|
# Secrets consumed from sops:
|
||||||
# gitea/admin_password
|
# gitea/admin_password
|
||||||
@@ -21,98 +29,6 @@ let
|
|||||||
cfg = config.homey.gitea;
|
cfg = config.homey.gitea;
|
||||||
dataDir = config.homey.storage.mountPoint;
|
dataDir = config.homey.storage.mountPoint;
|
||||||
domain = homeyConfig.domain;
|
domain = homeyConfig.domain;
|
||||||
|
|
||||||
# Gitea app.ini — generated at build time.
|
|
||||||
# Secrets that Gitea reads from env vars are referenced as env var names here.
|
|
||||||
# The actual values are injected by the ExecStartPre wrapper below.
|
|
||||||
giteaAppIni = ''
|
|
||||||
APP_NAME = ${homeyConfig.organization}
|
|
||||||
RUN_MODE = prod
|
|
||||||
RUN_USER = git
|
|
||||||
WORK_PATH = /data/gitea
|
|
||||||
|
|
||||||
[repository]
|
|
||||||
ROOT = /data/git/repositories
|
|
||||||
|
|
||||||
[repository.local]
|
|
||||||
LOCAL_COPY_PATH = /data/gitea/tmp/local-repo
|
|
||||||
|
|
||||||
[repository.upload]
|
|
||||||
TEMP_PATH = /data/gitea/uploads
|
|
||||||
|
|
||||||
[server]
|
|
||||||
APP_DATA_PATH = /data/gitea
|
|
||||||
DOMAIN = git.${domain}
|
|
||||||
HTTP_PORT = 3000
|
|
||||||
ROOT_URL = https://git.${domain}/
|
|
||||||
DISABLE_SSH = true
|
|
||||||
LFS_START_SERVER = true
|
|
||||||
; LFS_JWT_SECRET injected at container start via env var / startup script
|
|
||||||
LFS_JWT_SECRET = __GITEA_LFS_JWT_SECRET__
|
|
||||||
OFFLINE_MODE = false
|
|
||||||
|
|
||||||
[lfs]
|
|
||||||
PATH = /data/git/lfs
|
|
||||||
|
|
||||||
[database]
|
|
||||||
DB_TYPE = sqlite3
|
|
||||||
PATH = /data/gitea/gitea.db
|
|
||||||
LOG_SQL = false
|
|
||||||
|
|
||||||
[indexer]
|
|
||||||
ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve
|
|
||||||
|
|
||||||
[session]
|
|
||||||
PROVIDER_CONFIG = /data/gitea/sessions
|
|
||||||
PROVIDER = file
|
|
||||||
|
|
||||||
[picture]
|
|
||||||
AVATAR_UPLOAD_PATH = /data/gitea/avatars
|
|
||||||
REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars
|
|
||||||
DISABLE_GRAVATAR = false
|
|
||||||
|
|
||||||
[attachment]
|
|
||||||
PATH = /data/gitea/attachments
|
|
||||||
|
|
||||||
[log]
|
|
||||||
MODE = console
|
|
||||||
LEVEL = info
|
|
||||||
ROUTER = console
|
|
||||||
ROOT_PATH = /data/gitea/log
|
|
||||||
|
|
||||||
[security]
|
|
||||||
INSTALL_LOCK = true
|
|
||||||
REVERSE_PROXY_LIMIT = 1
|
|
||||||
REVERSE_PROXY_TRUSTED_PROXIES = *
|
|
||||||
; INTERNAL_TOKEN injected at container start
|
|
||||||
INTERNAL_TOKEN = __GITEA_INTERNAL_TOKEN__
|
|
||||||
|
|
||||||
[service]
|
|
||||||
DISABLE_REGISTRATION = true
|
|
||||||
REQUIRE_SIGNIN_VIEW = false
|
|
||||||
REGISTER_EMAIL_CONFIRM = false
|
|
||||||
ENABLE_NOTIFY_MAIL = false
|
|
||||||
ALLOW_ONLY_EXTERNAL_REGISTRATION = true
|
|
||||||
ENABLE_CAPTCHA = false
|
|
||||||
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
|
|
||||||
DEFAULT_ENABLE_TIMETRACKING = true
|
|
||||||
NO_REPLY_ADDRESS = noreply.localhost
|
|
||||||
ENABLE_REVERSE_PROXY_AUTHENTICATION = true
|
|
||||||
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = true
|
|
||||||
|
|
||||||
[mailer]
|
|
||||||
ENABLED = false
|
|
||||||
|
|
||||||
[openid]
|
|
||||||
ENABLE_OPENID_SIGNIN = false
|
|
||||||
ENABLE_OPENID_SIGNUP = false
|
|
||||||
|
|
||||||
[oauth2]
|
|
||||||
ENABLE = false
|
|
||||||
; JWT_SECRET injected at container start
|
|
||||||
JWT_SECRET = __GITEA_OAUTH2_JWT_SECRET__
|
|
||||||
'';
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.homey.gitea = {
|
options.homey.gitea = {
|
||||||
@@ -139,60 +55,175 @@ in
|
|||||||
sops.secrets."gitea/oauth2_jwt_secret" = { owner = "root"; };
|
sops.secrets."gitea/oauth2_jwt_secret" = { owner = "root"; };
|
||||||
sops.secrets."gitea/internal_token" = { owner = "root"; };
|
sops.secrets."gitea/internal_token" = { owner = "root"; };
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
|
||||||
# Write the app.ini template to /etc (will be processed by ExecStartPre)
|
|
||||||
# -----------------------------------------------------------------------
|
|
||||||
environment.etc."gitea/app.ini.tpl" = {
|
|
||||||
text = giteaAppIni;
|
|
||||||
mode = "0444";
|
|
||||||
};
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Container
|
# Container
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
virtualisation.oci-containers.containers.gitea = {
|
virtualisation.oci-containers.containers.gitea = {
|
||||||
image = cfg.image;
|
image = cfg.image;
|
||||||
ports = [ "127.0.0.1:${toString cfg.port}:3000" ];
|
# No ports mapping — --network=host means the container shares the host
|
||||||
|
# network stack directly. Gitea binds to 0.0.0.0:3000 on the host.
|
||||||
|
|
||||||
|
# All non-secret settings via GITEA__<SECTION>__<KEY> env vars.
|
||||||
|
# These are safe to store in the Nix store.
|
||||||
environment = {
|
environment = {
|
||||||
USER_UID = "1000";
|
USER_UID = "1000";
|
||||||
USER_GID = "1000";
|
USER_GID = "1000";
|
||||||
# Tell gitea where to look for the config
|
|
||||||
GITEA_CUSTOM = "/data/gitea";
|
GITEA_CUSTOM = "/data/gitea";
|
||||||
|
|
||||||
|
# [DEFAULT]
|
||||||
|
GITEA____APP_NAME = homeyConfig.organization;
|
||||||
|
GITEA____RUN_MODE = "prod";
|
||||||
|
|
||||||
|
# [repository]
|
||||||
|
GITEA__repository__ROOT = "/data/git/repositories";
|
||||||
|
|
||||||
|
# [server]
|
||||||
|
GITEA__server__APP_DATA_PATH = "/data/gitea";
|
||||||
|
GITEA__server__DOMAIN = "git.${domain}";
|
||||||
|
GITEA__server__HTTP_PORT = toString cfg.port;
|
||||||
|
GITEA__server__ROOT_URL = "https://git.${domain}/";
|
||||||
|
GITEA__server__DISABLE_SSH = "true";
|
||||||
|
GITEA__server__START_SSH_SERVER = "false";
|
||||||
|
GITEA__server__SSH_PORT = "2222";
|
||||||
|
GITEA__server__SSH_LISTEN_PORT = "2222";
|
||||||
|
GITEA__server__LFS_START_SERVER = "true";
|
||||||
|
GITEA__server__OFFLINE_MODE = "false";
|
||||||
|
|
||||||
|
# [lfs]
|
||||||
|
GITEA__lfs__PATH = "/data/git/lfs";
|
||||||
|
|
||||||
|
# [database]
|
||||||
|
GITEA__database__DB_TYPE = "sqlite3";
|
||||||
|
GITEA__database__PATH = "/data/gitea/gitea.db";
|
||||||
|
GITEA__database__LOG_SQL = "false";
|
||||||
|
|
||||||
|
# [indexer]
|
||||||
|
GITEA__indexer__ISSUE_INDEXER_PATH = "/data/gitea/indexers/issues.bleve";
|
||||||
|
|
||||||
|
# [session]
|
||||||
|
GITEA__session__PROVIDER = "file";
|
||||||
|
GITEA__session__PROVIDER_CONFIG = "/data/gitea/sessions";
|
||||||
|
|
||||||
|
# [picture]
|
||||||
|
GITEA__picture__AVATAR_UPLOAD_PATH = "/data/gitea/avatars";
|
||||||
|
GITEA__picture__REPOSITORY_AVATAR_UPLOAD_PATH = "/data/gitea/repo-avatars";
|
||||||
|
GITEA__picture__DISABLE_GRAVATAR = "false";
|
||||||
|
|
||||||
|
# [attachment]
|
||||||
|
GITEA__attachment__PATH = "/data/gitea/attachments";
|
||||||
|
|
||||||
|
# [log]
|
||||||
|
GITEA__log__MODE = "console";
|
||||||
|
GITEA__log__LEVEL = "info";
|
||||||
|
GITEA__log__ROOT_PATH = "/data/gitea/log";
|
||||||
|
|
||||||
|
# [security]
|
||||||
|
GITEA__security__INSTALL_LOCK = "true";
|
||||||
|
GITEA__security__REVERSE_PROXY_LIMIT = "1";
|
||||||
|
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES = "*";
|
||||||
|
|
||||||
|
# [service]
|
||||||
|
GITEA__service__DISABLE_REGISTRATION = "true";
|
||||||
|
GITEA__service__REQUIRE_SIGNIN_VIEW = "false";
|
||||||
|
GITEA__service__REGISTER_EMAIL_CONFIRM = "false";
|
||||||
|
GITEA__service__ENABLE_NOTIFY_MAIL = "false";
|
||||||
|
GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION = "true";
|
||||||
|
GITEA__service__ENABLE_CAPTCHA = "false";
|
||||||
|
GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION = "true";
|
||||||
|
GITEA__service__DEFAULT_ENABLE_TIMETRACKING = "true";
|
||||||
|
GITEA__service__NO_REPLY_ADDRESS = "noreply.localhost";
|
||||||
|
GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION = "true";
|
||||||
|
GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = "true";
|
||||||
|
|
||||||
|
# [mailer]
|
||||||
|
GITEA__mailer__ENABLED = "false";
|
||||||
|
|
||||||
|
# [openid]
|
||||||
|
GITEA__openid__ENABLE_OPENID_SIGNIN = "false";
|
||||||
|
GITEA__openid__ENABLE_OPENID_SIGNUP = "false";
|
||||||
|
|
||||||
|
# [oauth2]
|
||||||
|
GITEA__oauth2__ENABLED = "false";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Secret env vars written at runtime by ExecStartPre — never in store.
|
||||||
|
environmentFiles = [ "/run/gitea-secrets.env" ];
|
||||||
|
|
||||||
volumes = [
|
volumes = [
|
||||||
"${dataDir}/gitea/data:/data"
|
"${dataDir}/gitea/data:/data"
|
||||||
# The processed app.ini is written by ExecStartPre into /run/gitea-conf/
|
|
||||||
"/run/gitea-conf/app.ini:/data/gitea/conf/app.ini:ro"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
extraOptions = [ "--network=host" ];
|
extraOptions = [ "--network=host" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# ExecStartPre: substitute secret placeholders into the ini template
|
# ExecStartPre: write ephemeral secrets env file
|
||||||
|
# ExecStopPost: clean it up
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
systemd.services."podman-gitea" = {
|
systemd.services."podman-gitea" = {
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStartPre = [
|
ExecStartPre = [
|
||||||
(pkgs.writeShellScript "gitea-build-config" ''
|
(pkgs.writeShellScript "gitea-write-secrets" ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
install -d -m 700 /run/gitea-conf
|
|
||||||
LFS=$(cat ${config.sops.secrets."gitea/lfs_jwt_secret".path})
|
LFS=$(cat ${config.sops.secrets."gitea/lfs_jwt_secret".path})
|
||||||
OAUTH=$(cat ${config.sops.secrets."gitea/oauth2_jwt_secret".path})
|
OAUTH=$(cat ${config.sops.secrets."gitea/oauth2_jwt_secret".path})
|
||||||
TOKEN=$(cat ${config.sops.secrets."gitea/internal_token".path})
|
TOKEN=$(cat ${config.sops.secrets."gitea/internal_token".path})
|
||||||
sed \
|
printf '%s\n' \
|
||||||
-e "s|__GITEA_LFS_JWT_SECRET__|$LFS|g" \
|
"GITEA__server__LFS_JWT_SECRET=$LFS" \
|
||||||
-e "s|__GITEA_OAUTH2_JWT_SECRET__|$OAUTH|g" \
|
"GITEA__security__INTERNAL_TOKEN=$TOKEN" \
|
||||||
-e "s|__GITEA_INTERNAL_TOKEN__|$TOKEN|g" \
|
"GITEA__oauth2__JWT_SECRET=$OAUTH" \
|
||||||
/etc/gitea/app.ini.tpl > /run/gitea-conf/app.ini
|
> /run/gitea-secrets.env
|
||||||
chmod 444 /run/gitea-conf/app.ini
|
chmod 600 /run/gitea-secrets.env
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
ExecStopPost = [
|
||||||
|
(pkgs.writeShellScript "gitea-cleanup-secrets" ''
|
||||||
|
rm -f /run/gitea-secrets.env
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
|
after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
|
||||||
requires = lib.mkAfter [ "mnt-data.mount" ];
|
requires = lib.mkAfter [ "mnt-data.mount" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Ensure the Gitea admin user exists with the correct password after start.
|
||||||
|
# Runs as a oneshot after podman-gitea; idempotent (create or update).
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
systemd.services."gitea-admin-setup" = {
|
||||||
|
description = "Ensure Gitea admin user exists with correct password";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "podman-gitea.service" ];
|
||||||
|
requires = [ "podman-gitea.service" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
|
PASS=$(cat ${config.sops.secrets."gitea/admin_password".path})
|
||||||
|
|
||||||
|
# Wait until Gitea's HTTP endpoint is up (max 60 s)
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if ${pkgs.curl}/bin/curl -sf http://127.0.0.1:${toString cfg.port}/ -o /dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Sync password if admin exists; create if not.
|
||||||
|
if ! ${pkgs.podman}/bin/podman exec -u 1000 gitea \
|
||||||
|
gitea admin user change-password --username admin --password "$PASS" 2>/dev/null; then
|
||||||
|
${pkgs.podman}/bin/podman exec -u 1000 gitea \
|
||||||
|
gitea admin user create \
|
||||||
|
--username admin \
|
||||||
|
--password "$PASS" \
|
||||||
|
--email "admin@${domain}" \
|
||||||
|
--admin
|
||||||
|
fi
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ in
|
|||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
virtualisation.oci-containers.containers.jellyfin = {
|
virtualisation.oci-containers.containers.jellyfin = {
|
||||||
image = cfg.image;
|
image = cfg.image;
|
||||||
ports = [ "127.0.0.1:${toString cfg.port}:8096" ];
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
JELLYFIN_PublishedServerUrl = "https://jellyfin.${domain}";
|
JELLYFIN_PublishedServerUrl = "https://jellyfin.${domain}";
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ in
|
|||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
virtualisation.oci-containers.containers.nextcloud-postgres = {
|
virtualisation.oci-containers.containers.nextcloud-postgres = {
|
||||||
image = cfg.postgresImage;
|
image = cfg.postgresImage;
|
||||||
ports = [ "127.0.0.1:${toString cfg.postgresPort}:5432" ];
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
POSTGRES_DB = "nextcloud_db";
|
POSTGRES_DB = "nextcloud_db";
|
||||||
@@ -70,20 +70,25 @@ in
|
|||||||
"${dataDir}/nextcloud/db:/var/lib/postgresql/data"
|
"${dataDir}/nextcloud/db:/var/lib/postgresql/data"
|
||||||
];
|
];
|
||||||
|
|
||||||
extraOptions = [ "--network=host" ];
|
extraOptions = [
|
||||||
|
"--network=host"
|
||||||
|
"--env-file=/run/nc-postgres-secrets.env"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services."podman-nextcloud-postgres" = {
|
systemd.services."podman-nextcloud-postgres" = {
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
LoadCredential = [
|
||||||
|
"nextcloud_postgres_password:${config.sops.secrets."nextcloud/postgres_password".path}"
|
||||||
|
];
|
||||||
ExecStartPre = [
|
ExecStartPre = [
|
||||||
(pkgs.writeShellScript "nc-postgres-secrets-env" ''
|
(pkgs.writeShellScript "nc-postgres-secrets-env" ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
install -m 600 /dev/null /run/nc-postgres-secrets.env
|
install -m 600 /dev/null /run/nc-postgres-secrets.env
|
||||||
echo "POSTGRES_PASSWORD=$(cat ${config.sops.secrets."nextcloud/postgres_password".path})" \
|
echo "POSTGRES_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_postgres_password")" \
|
||||||
>> /run/nc-postgres-secrets.env
|
>> /run/nc-postgres-secrets.env
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
EnvironmentFile = "/run/nc-postgres-secrets.env";
|
|
||||||
};
|
};
|
||||||
postStop = "rm -f /run/nc-postgres-secrets.env";
|
postStop = "rm -f /run/nc-postgres-secrets.env";
|
||||||
after = lib.mkAfter [ "mnt-data.mount" ];
|
after = lib.mkAfter [ "mnt-data.mount" ];
|
||||||
@@ -95,7 +100,7 @@ in
|
|||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
virtualisation.oci-containers.containers.nextcloud = {
|
virtualisation.oci-containers.containers.nextcloud = {
|
||||||
image = cfg.image;
|
image = cfg.image;
|
||||||
ports = [ "127.0.0.1:${toString cfg.port}:80" ];
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
POSTGRES_HOST = "127.0.0.1";
|
POSTGRES_HOST = "127.0.0.1";
|
||||||
@@ -105,6 +110,10 @@ in
|
|||||||
NEXTCLOUD_TRUSTED_DOMAINS = "nextcloud.${domain}";
|
NEXTCLOUD_TRUSTED_DOMAINS = "nextcloud.${domain}";
|
||||||
OVERWRITEPROTOCOL = "https";
|
OVERWRITEPROTOCOL = "https";
|
||||||
OVERWRITECLIURL = "https://nextcloud.${domain}";
|
OVERWRITECLIURL = "https://nextcloud.${domain}";
|
||||||
|
# With --network=host, port mappings are ignored and the container's
|
||||||
|
# Apache binds directly on the host. Force it onto port 8080 so Caddy
|
||||||
|
# can own 80/443.
|
||||||
|
APACHE_HTTP_PORT_NUMBER = toString cfg.port;
|
||||||
# Passwords injected via env file
|
# Passwords injected via env file
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,20 +121,26 @@ in
|
|||||||
"${dataDir}/nextcloud/html:/var/www/html"
|
"${dataDir}/nextcloud/html:/var/www/html"
|
||||||
];
|
];
|
||||||
|
|
||||||
extraOptions = [ "--network=host" ];
|
extraOptions = [
|
||||||
|
"--network=host"
|
||||||
|
"--env-file=/run/nc-secrets.env"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services."podman-nextcloud" = {
|
systemd.services."podman-nextcloud" = {
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
LoadCredential = [
|
||||||
|
"nextcloud_postgres_password:${config.sops.secrets."nextcloud/postgres_password".path}"
|
||||||
|
"nextcloud_admin_password:${config.sops.secrets."nextcloud/admin_password".path}"
|
||||||
|
];
|
||||||
ExecStartPre = [
|
ExecStartPre = [
|
||||||
(pkgs.writeShellScript "nc-secrets-env" ''
|
(pkgs.writeShellScript "nc-secrets-env" ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
install -m 600 /dev/null /run/nc-secrets.env
|
install -m 600 /dev/null /run/nc-secrets.env
|
||||||
echo "POSTGRES_PASSWORD=$(cat ${config.sops.secrets."nextcloud/postgres_password".path})" >> /run/nc-secrets.env
|
echo "POSTGRES_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_postgres_password")" >> /run/nc-secrets.env
|
||||||
echo "NEXTCLOUD_ADMIN_PASSWORD=$(cat ${config.sops.secrets."nextcloud/admin_password".path})" >> /run/nc-secrets.env
|
echo "NEXTCLOUD_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_admin_password")" >> /run/nc-secrets.env
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
EnvironmentFile = "/run/nc-secrets.env";
|
|
||||||
};
|
};
|
||||||
postStop = "rm -f /run/nc-secrets.env";
|
postStop = "rm -f /run/nc-secrets.env";
|
||||||
after = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ];
|
after = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ];
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ in
|
|||||||
virtualisation.oci-containers.containers.openldap = {
|
virtualisation.oci-containers.containers.openldap = {
|
||||||
image = cfg.image;
|
image = cfg.image;
|
||||||
|
|
||||||
# Bind only to localhost — no external exposure
|
# No ports mapping — --network=host means the container shares the host
|
||||||
ports = [ "127.0.0.1:${toString cfg.port}:389" ];
|
# network stack. OpenLDAP binds to 0.0.0.0:389, but the firewall
|
||||||
|
# (common.nix) only opens 22/80/443, so port 389 is unreachable from
|
||||||
|
# the LAN or internet.
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
LDAP_ORGANISATION = homeyConfig.organization;
|
LDAP_ORGANISATION = homeyConfig.organization;
|
||||||
@@ -76,8 +78,8 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
extraOptions = [
|
extraOptions = [
|
||||||
"--network=host" # simplest for single-host: services talk on 127.0.0.1
|
"--network=host"
|
||||||
"--hostname=openldap"
|
"--env-file=/run/openldap-secrets.env"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,18 +90,25 @@ in
|
|||||||
# podman-<container-name>.service
|
# podman-<container-name>.service
|
||||||
systemd.services."podman-openldap" = {
|
systemd.services."podman-openldap" = {
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
# Write an env file with secret values before the container starts,
|
# LoadCredential stages the sops secrets into a per-invocation
|
||||||
# then pass it to podman run via EnvironmentFile.
|
# credential directory before any Exec* step, so they are available
|
||||||
|
# when ExecStartPre runs. ExecStartPre writes the env file that
|
||||||
|
# podman --env-file reads; this avoids the EnvironmentFile ordering
|
||||||
|
# race (EnvironmentFile is evaluated before ExecStartPre).
|
||||||
|
LoadCredential = [
|
||||||
|
"openldap_admin_password:${config.sops.secrets."openldap/admin_password".path}"
|
||||||
|
"openldap_config_password:${config.sops.secrets."openldap/config_password".path}"
|
||||||
|
"openldap_ro_password:${config.sops.secrets."openldap/ro_password".path}"
|
||||||
|
];
|
||||||
ExecStartPre = [
|
ExecStartPre = [
|
||||||
(pkgs.writeShellScript "openldap-secrets-env" ''
|
(pkgs.writeShellScript "openldap-secrets-env" ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
install -m 600 /dev/null /run/openldap-secrets.env
|
install -m 600 /dev/null /run/openldap-secrets.env
|
||||||
echo "LDAP_ADMIN_PASSWORD=$(cat ${config.sops.secrets."openldap/admin_password".path})" >> /run/openldap-secrets.env
|
echo "LDAP_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/openldap_admin_password")" >> /run/openldap-secrets.env
|
||||||
echo "LDAP_CONFIG_PASSWORD=$(cat ${config.sops.secrets."openldap/config_password".path})" >> /run/openldap-secrets.env
|
echo "LDAP_CONFIG_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/openldap_config_password")" >> /run/openldap-secrets.env
|
||||||
echo "LDAP_READONLY_USER_PASSWORD=$(cat ${config.sops.secrets."openldap/ro_password".path})" >> /run/openldap-secrets.env
|
echo "LDAP_READONLY_USER_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/openldap_ro_password")" >> /run/openldap-secrets.env
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
EnvironmentFile = "/run/openldap-secrets.env";
|
|
||||||
};
|
};
|
||||||
# Clean up the env file on stop
|
# Clean up the env file on stop
|
||||||
postStop = "rm -f /run/openldap-secrets.env";
|
postStop = "rm -f /run/openldap-secrets.env";
|
||||||
@@ -109,8 +118,8 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Firewall — openldap port is NOT opened externally (localhost only)
|
# Firewall — openldap port is NOT opened externally
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# No firewall rule needed; bound to 127.0.0.1.
|
# No firewall rule needed; common.nix only opens 22/80/443.
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
# Stateless container (no persistent volumes needed).
|
# Stateless container (no persistent volumes needed).
|
||||||
# Protected by Authelia two_factor, admins-only policy (defined in authelia.nix).
|
# Protected by Authelia two_factor, admins-only policy (defined in authelia.nix).
|
||||||
# Bound to localhost:8081; Caddy reverse-proxies it.
|
# Bound to localhost:8081; Caddy reverse-proxies it.
|
||||||
|
#
|
||||||
|
# Networking: uses default bridge (podman) network with a port mapping
|
||||||
|
# 127.0.0.1:8081->80 so Caddy can reach it. OpenLDAP runs on the host
|
||||||
|
# network at 127.0.0.1:389; the container reaches it via the special
|
||||||
|
# host.containers.internal DNS name that podman injects automatically.
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.homey.phpldapadmin;
|
cfg = config.homey.phpldapadmin;
|
||||||
@@ -28,14 +33,17 @@ in
|
|||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
virtualisation.oci-containers.containers.phpldapadmin = {
|
virtualisation.oci-containers.containers.phpldapadmin = {
|
||||||
image = cfg.image;
|
image = cfg.image;
|
||||||
ports = [ "127.0.0.1:${toString cfg.port}:80" ];
|
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
PHPLDAPADMIN_HTTPS = "false";
|
PHPLDAPADMIN_HTTPS = "false";
|
||||||
PHPLDAPADMIN_LDAP_HOSTS = "127.0.0.1"; # openldap on host network
|
# host.containers.internal resolves to the host from inside a podman
|
||||||
|
# bridge container — reaches openldap which is on --network=host at :389
|
||||||
|
PHPLDAPADMIN_LDAP_HOSTS = "host.containers.internal";
|
||||||
};
|
};
|
||||||
|
|
||||||
extraOptions = [ "--network=host" ];
|
# Bridge network (default) + port mapping: Apache binds inside the
|
||||||
|
# container on :80, podman maps it to 127.0.0.1:8081 on the host.
|
||||||
|
ports = [ "127.0.0.1:${toString cfg.port}:80" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services."podman-phpldapadmin" = {
|
systemd.services."podman-phpldapadmin" = {
|
||||||
|
|||||||
@@ -35,11 +35,16 @@ in
|
|||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
virtualisation.oci-containers.containers.transmission = {
|
virtualisation.oci-containers.containers.transmission = {
|
||||||
image = cfg.image;
|
image = cfg.image;
|
||||||
ports = [ "127.0.0.1:${toString cfg.port}:9091" ];
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
PUID = "1000";
|
PUID = "1000";
|
||||||
PGID = "1000";
|
PGID = "1000";
|
||||||
|
# With --network=host, port mappings are ignored; transmission binds
|
||||||
|
# directly on the host. Force it to cfg.port (9092) to avoid
|
||||||
|
# conflicting with Authelia on 9091.
|
||||||
|
TRANSMISSION_WEB_HOME = "/usr/share/transmission/web";
|
||||||
|
WEBUI_PORT = toString cfg.port;
|
||||||
};
|
};
|
||||||
|
|
||||||
volumes = [
|
volumes = [
|
||||||
|
|||||||
+2
-2
@@ -85,8 +85,8 @@ in
|
|||||||
"d ${cfg.mountPoint}/openldap/var-lib-ldap 0750 root root -"
|
"d ${cfg.mountPoint}/openldap/var-lib-ldap 0750 root root -"
|
||||||
"d ${cfg.mountPoint}/authelia 0750 root root -"
|
"d ${cfg.mountPoint}/authelia 0750 root root -"
|
||||||
"d ${cfg.mountPoint}/authelia/config 0750 root root -"
|
"d ${cfg.mountPoint}/authelia/config 0750 root root -"
|
||||||
"d ${cfg.mountPoint}/gitea 0750 root root -"
|
"d ${cfg.mountPoint}/gitea 0750 1000 1000 -"
|
||||||
"d ${cfg.mountPoint}/gitea/data 0750 root root -"
|
"d ${cfg.mountPoint}/gitea/data 0750 1000 1000 -"
|
||||||
"d ${cfg.mountPoint}/nextcloud 0750 root root -"
|
"d ${cfg.mountPoint}/nextcloud 0750 root root -"
|
||||||
"d ${cfg.mountPoint}/nextcloud/html 0750 root root -"
|
"d ${cfg.mountPoint}/nextcloud/html 0750 root root -"
|
||||||
"d ${cfg.mountPoint}/nextcloud/db 0750 root root -"
|
"d ${cfg.mountPoint}/nextcloud/db 0750 root root -"
|
||||||
|
|||||||
+52
-49
@@ -1,57 +1,60 @@
|
|||||||
#ENC[AES256_GCM,data:+YPw0Wd8uB5cwY/cszPqLohVhIEbju4QwYVS9vT/ci2MxO7Glw2uJbtS6dOLHqpzfU0b0mJ4IqUIaM08yMEM/TeB7hG1ASHWe3+qxyw8,iv:V49PRq4bEcYvcPMUIvBQkhuq28pVxrlRfVXUSzPxnb4=,tag:OSNXutH5EUwA3BujNZ3FoA==,type:comment]
|
openldap:
|
||||||
#ENC[AES256_GCM,data:QVC3QP3em1O3SYTAuK4kBchpTiwXH10f2R4YgK+t9QaiqZ1PWvo=,iv:R0lFtvg2T/Rllt1uiriTQvNbSw54jr0otU3E6XsIs00=,tag:9fAQCmuZZPUPLuDY8LZEUA==,type:comment]
|
admin_password: ENC[AES256_GCM,data:hg+Ly1bX4ao1AT4SDvQWXiT/KMzsz0wdnRauiB+FetE=,iv:TAX+NZCVUNiwMeBrW58IeI1OJX6rzzGAhWiQ+cZXreo=,tag:MrwYKKBb1Cg2JvADtQqYrQ==,type:str]
|
||||||
#
|
config_password: ENC[AES256_GCM,data:qKEurb0slGnr6nES7w7fTPDCy/DARns0BorDZMwpI/w=,iv:+p6Fh9a2g0eBueOxDk1J+hnM9fMgE6/NYwz+sAovGjE=,tag:kKZVsxdxdDACD9J0NAf4gQ==,type:str]
|
||||||
#ENC[AES256_GCM,data:IT6BEo5CjYm+15aeWl+S8M3B+SSmjPnhBRvYToWzezIweTl3MGBXtalvkV3NWkxH0EaHpueOMe6r,iv:7BDTiljEa59F13Pephw6MM+sZgL4jbfQafJyt0UU3hY=,tag:ia+7WUAl/45jrYrv3Pylxg==,type:comment]
|
ro_password: ENC[AES256_GCM,data:82htWXdJ07tdZ81o7o9a4hizxcn39yQidD5e9PijVpo=,iv:fDT1chX4ZPIS02IMEW02haPa2IIlLFhgFOpUwD7KL50=,tag:9pSo/3vFikKQAe8jS+3Q6A==,type:str]
|
||||||
#
|
authelia:
|
||||||
#ENC[AES256_GCM,data:zqAQYQCg/TRNtjDIdWTsgtRnQbijjYyLdQIAe9GkTubG9PSj7E8m7HFXmfG4eFNZR4S/Ql0dsM5gvLCu,iv:xSH8LMS7vqe2N9L/TOepKWhuIhVxmKN6kuB1iqUEOUw=,tag:rFYurrqfp1Zxggr5tiPKkQ==,type:comment]
|
jwt_secret: ENC[AES256_GCM,data:SwXd/mMsrgXItP8QZr4z9YaN1lgSSO4Cpdwl+XxFj6I=,iv:gAkyHKP5D5RGJ3X3hoh8oEJfYaFnvYxGAoKxe+G1N0I=,tag:NlfWC2pmjCYiiRn9prgl2w==,type:str]
|
||||||
#ENC[AES256_GCM,data:+YPw0Wd8uB5cwY/cszPqLohVhIEbju4QwYVS9vT/ci2MxO7Glw2uJbtS6dOLHqpzfU0b0mJ4IqUIaM08yMEM/TeB7hG1ASHWe3+qxyw8,iv:V49PRq4bEcYvcPMUIvBQkhuq28pVxrlRfVXUSzPxnb4=,tag:OSNXutH5EUwA3BujNZ3FoA==,type:comment]
|
session_secret: ENC[AES256_GCM,data:bbbgmxYLbtteuT628O+uSVeo7Gx3hI6uWVIV5l8AhtNSvXBHKb9i2NvQqJfVG8D2YR00+OW9XtreocpBuS7YJKp58cP/EgrF9x1im/8UaaifAYqD8YXZReUsBw+/PzIvqyA1K9tFd0coc8tSHJmwTwea4sf6Tc10N6j/nhNQfpQ=,iv:wrGE8XVsTINmf5505XVI7HHqA33w+SBh9lsJXD+YnwU=,tag:lvSoU+IRy8f+6VpYqzvacg==,type:str]
|
||||||
#ENC[AES256_GCM,data:yj4R8Yetc6EHWvQDu2/eaoY=,iv:Zbqfg9NRHy6ab10kxzq6qsLb7VHfLxhcpP3vUt2i4ns=,tag:udBGjJUupeADD78JQ8BwuQ==,type:comment]
|
storage_encryption_key: ENC[AES256_GCM,data:8KnaWBTlSStdC/uI4GUOYP9DJygjfCTu62zWTU9eeVE=,iv:DNl0L2QgT1lNUDdPNm9bXcGvrLXLDtWdJ9pPgRH20C8=,tag:hVhcCl0Vrt0ZnaVGaUokSQ==,type:str]
|
||||||
openldap/admin_password: ENC[AES256_GCM,data:DtVthpJqLdkI+5wxOMnCfBdqWkg0GSwUtsUeop24kd8=,iv:4e2Xn7B0M8yYEbs0V9ozn8WHJJMCBv6G46bdThufSXc=,tag:BsjKzh8teul6yLEKbvr93g==,type:str]
|
gitea:
|
||||||
openldap/config_password: ENC[AES256_GCM,data:6b9TIgOcmZfMDAVbJuqOoNS9kyrss/LMvySLyNonlRk=,iv:Jf9/triFouIDv7MY2J9W8ji7E5lUHqzwgBMqrcPuK1g=,tag:zQYZSesPiPVeNVBN1oEiHA==,type:str]
|
admin_password: ENC[AES256_GCM,data:WncwJlqb/3X5WZYgIXAu0niI0ISP0eHfmhsKDLeAvE4=,iv:JBZNZJRSHKG+cCoFNJBI+jS+/WcLueqJ/UN/7wXfK/0=,tag:f6+Lhfcxstq6pwS9xkVwqQ==,type:str]
|
||||||
openldap/ro_password: ENC[AES256_GCM,data:EHYUlIY24kY9K8opMi9MxSSosReZm5mEmbPFz+NdaXE=,iv:3pfVn4QDvJAVmWYWyX/Kko+K7nsE1yunLXN5uao+ea0=,tag:J954cH7a7Ey6Xq24ut5Jxw==,type:str]
|
lfs_jwt_secret: ENC[AES256_GCM,data:i05gr2ou03w0yu6/bhlJOW1huysAAPTidFEusWkhQfpDj4Pyh8LEKb09Og==,iv:aqkblyz0oIFHwzVCzlGDdQuCbsDPrfBaJMzgRTw+pYU=,tag:6gBSerOUK8Y3la/2Bg2AZQ==,type:str]
|
||||||
#ENC[AES256_GCM,data:upG3X+Z7di17BaWBQ/P0ohY=,iv:k3Kin642n4cJYwfPsQYE/4FokELFNDmMzxJ2D8S28HI=,tag:uYRnpeoCrwGQOEYWo2cBiw==,type:comment]
|
oauth2_jwt_secret: ENC[AES256_GCM,data:BVvQJCEfHPbemd1jz7MWpIRia1wfvPMGuLqoi/xUMSoQoN5RPefQnPR4Cg==,iv:JAZQUTxHZSnMEnl+BIZ1PXlznMwKuPtiPP/17rc6lSs=,tag:mUw5RuthZmZegXCtfsFNmQ==,type:str]
|
||||||
authelia/jwt_secret: ENC[AES256_GCM,data:pXTQ06OGEP1oYFM0mkyL+c/zNRUMgL9x1fCQsMo2bak=,iv:mnOBWBrSn4gTfMXR5PCThs0v9QRDR5pfOQA8u0cuGnI=,tag:YXGq6Hmv/chw8fcEQoNlGA==,type:str]
|
internal_token: ENC[AES256_GCM,data:gnOebJbRsh2Cues9WjGQp4rWa6OuE3xSnby9jc3Hk8ywvpL7CNWmlGW7zmmOyDAfIKfm8kf1FxotWLXtGZDretzdbMRM9c6gkwSJf5MCsdm27Er+IRKS/QFBuvLSTEH0,iv:aVgRvs3T3zCg+AV/BKUXQyZDKvunHvXsdfr9sqo2cI0=,tag:U9aP+N0CWyRQ/xJ27Vo2mw==,type:str]
|
||||||
authelia/session_secret: ENC[AES256_GCM,data:EgIyGv/K6xDCxOZWA9tzGoNS4m+p/EOPHL64/eN1oqwar2iJFSanbUfq8doHmN8n9sADmPIKUKaL8+WJWfyjtBBcCn74q5FL+kDu6ZYo4V5cjkj8jUhRC97TIJ+e0lVKFJ4s+i+/OcOsv2TPS/haylGHVn1fnlwvEd3kn/mO73w=,iv:6VPxOkriecJdtm2EBCiKkZBTzmas3DkQuYhivfygCT8=,tag:uXW1tcyAFSkiwMGNiZ663w==,type:str]
|
nextcloud:
|
||||||
authelia/storage_encryption_key: ENC[AES256_GCM,data:pM8oQ4t0HQLdUvuRayLOpEwdxzRQlvCOrMtSPIU8Ryo=,iv:AK2jR3Ij/dBplDc1PYXXLK8P327CYRx3kVZUCcIkO5k=,tag:kJSuyOIzT4/RNQXEal1ODA==,type:str]
|
admin_password: ENC[AES256_GCM,data:iK6VoE94vFQmn3i4XQc5r/c03u3b0knDgBNK8d1qyns=,iv:P1wax2vAjn9iwBe9T7SN+pKrtrWcOYb5OWUyHF4hlVg=,tag:ET8KU4IKzhWqIDeRihwcag==,type:str]
|
||||||
#ENC[AES256_GCM,data:teUPyCgpHCpIb0hXRUg=,iv:lTdYkYxQKHcJGE7lkkcsa8u9ZsZAVqpfauf5SzTv6G0=,tag:uKydCL14BvAaOpUHAMBirg==,type:comment]
|
postgres_password: ENC[AES256_GCM,data:ga4cwhYsAgEBvr+aDVwiRZXeT+TjXzeef1r3ud6uYHs=,iv:PMHCjO4wLW6PER4oGODEG9CHqrvVpAbgTGF7p49MCL0=,tag:mTNzsDhufqLlf1LFu7Rl1A==,type:str]
|
||||||
gitea/admin_password: ENC[AES256_GCM,data:/39FQYn5GQoq/a5chLd4JUvSXTU8tOdzc9uXxNqViiw=,iv:Ysq2QUgkmONGsfj6xHKN3G/eitBX1rm9LLH9REF2h8g=,tag:eiVtlaB/6VdNMEBy4mSrTg==,type:str]
|
cloudflare:
|
||||||
gitea/lfs_jwt_secret: ENC[AES256_GCM,data:gyd2OV0qcaaD6FTT9UwLV5vGJ4b/SNtG86oCQqUqB+DlZFLYe91YFNG/wA==,iv:fxD2NFbEYAsmrXaZT030f0MiAol2cwln0mIzLPCE+Lg=,tag:xQtehnHuj18WYeR2UyYeXw==,type:str]
|
api_token: ENC[AES256_GCM,data:Erzom4DKiam9SHGLdT3CQRkuT5kkhcuUaLwTbt2P5pPjr1V56p733KB1kHheO/PZ+TRsZg0=,iv:eO+ryffyoSkzAgUXe0MH+FKitgHCQ3ychLWEAShAd9o=,tag:2Z8Io0ylpAI9rws5NXCvIw==,type:str]
|
||||||
gitea/oauth2_jwt_secret: ENC[AES256_GCM,data:M5CzWG1FbjheX4QwDajVsAMl2nyfe4Z1u30D5hjCQbScDBtuw123ZMZjGQ==,iv:vOnMShn9nmLPzxXJqTNnCIf6GT6CrV3lAKrepmI7btc=,tag:pTdrbmZ+hntuwaLiLyUNHQ==,type:str]
|
tunnel_token: ENC[AES256_GCM,data:AFlD990L8l1Rh9i8wdyXwwyolrlw2ln1uuyCTiT7k1FVc2JjTOrgc8HiBIpxM40eqFGEnzDMs87tgzh1Pl8UThwV7WcLFWHMvtYHNez/F2+THknnW+ZinJbZnNSngicrhRIoNFhjQgjaR1LaS4kcNbkGBi655bl2uqNXoUpNThUKGAlZ4KwByEzK4B7QtCgAkqxQEFehtjdj41p8r58ViJTogXQKXmDYLjbqo8nPmUGSaiR/VCY7Gw==,iv:q755yc6wTMCuqHLvfHOZzBf3KoG4vcw431stQA78Bjs=,tag:Zt/LPG41HxSg2gSQOTC/6A==,type:str]
|
||||||
gitea/internal_token: ENC[AES256_GCM,data:ZbwvPcOseUHAGDr4dwNu9u+qcr0yYYGdH2OjcuXPtgUt7HFq1a9f0Faxiphsh+3OXb1KqLj8USB/1AxSvt5kSYM/vqzSLZ+e1OKy0oO3o8YouCJLhPNkNO6q0eguQF6+,iv:E3APR8h+iNECoThrvy6v4SEdAsfnPITXvhIFT1Ug5qA=,tag:lCxReGAxJyVhwMjxNenvxg==,type:str]
|
restic:
|
||||||
#ENC[AES256_GCM,data:r/uPlqg+7UGrM0G2xhmD6Bm1,iv:m/Ineh/mNfo1yUS+B8qtbMr1zRwiE6vw3EZIepB4QUA=,tag:/tB1W2JgyUQNvVWFM9478w==,type:comment]
|
password: ENC[AES256_GCM,data:X/pWmwakzQzRpSaY+T/kOqdrtXyvGPa3UQc/iIQFFAzUS1jHR9IvzW7KYdm/3IKXPBlZzPkWDsoRvVAChPwfQA==,iv:4+RD9UD5daMP04ixeagxbCNkTdOPx+BqfSOheh88OUU=,tag:SmrIz7jvbM74y5RBX0nCbA==,type:str]
|
||||||
nextcloud/admin_password: ENC[AES256_GCM,data:KwS0kEjTKn+IAtYTD17X4Y/3hT9bUgqKBQ0vfhDK99A=,iv:AbJfw6NWRnnB8zXIO6l3sIWiXXWfM1ePJ5bodNlgjgI=,tag:XSQM8SSnuh3wjyN3IQdArA==,type:str]
|
s3_access_key_id: ENC[AES256_GCM,data:XxElPQF28ThfYuiF4jQu7BiS8sh+c4V4ng==,iv:aLUIYnRGqFLYwlP3nFwDY5uvy8pXtX5QMKLMfRTxdNk=,tag:CscNNWnpvLuxw1DPK92GyA==,type:str]
|
||||||
nextcloud/postgres_password: ENC[AES256_GCM,data:dsdqeQhWFvidqOXopetb3G54Ft56ZhPheTB7uG2JuVc=,iv:ubKH3ihlPXZjPSkvgEYn/teG5SNSh04nb4Lh1e2cX8o=,tag:DWNXJXWjpCU8QEcnt0+phA==,type:str]
|
s3_secret_access_key: ENC[AES256_GCM,data:9ZWyhGJm4t2benDrLmnyQ9ZA5Jjl6l+pza1VmymTlw==,iv:xYsG6QlxXhQNO9szmsycxP6lT0cFF7lq3iNg6j+ED0E=,tag:wOJT4Vg3DuNFWTtx3QS9IQ==,type:str]
|
||||||
#ENC[AES256_GCM,data:riBX18BPE4XMBBv20JIEJbM6JS80e1jwiDq44KXMB6T/4Eehf2bgcFUm,iv:lDYdL1IvaBuixcw1BzPQxnM4HYZGA3YSDrJTxvz0QWs=,tag:tux8Mt56yw+7hE7BfgOXVw==,type:comment]
|
wifi:
|
||||||
cloudflare/api_token: ENC[AES256_GCM,data:te8SJz3sjnWX0MsacbEwYb0IC+SAlUBcSthLmHxpURTdpE3GfeNvjj5Z+il43cpFA33PaUY=,iv:XG2dt0Wc5jDcfGvKtRB1f6CAWXBmgnw+qqzMxDtmOok=,tag:PmEqZoKvqZm2vBxYSNH3Qg==,type:str]
|
psk: ENC[AES256_GCM,data:znk9Wr+vsntzbJ3H0TORUrAiDw==,iv:wbl8fUuKlgTqhajwjlTgFS7ijaTwXBFPRW2AmtiTklg=,tag:IK4oe8cJcccPaQ0V0NlncQ==,type:str]
|
||||||
cloudflare/tunnel_token: ENC[AES256_GCM,data:HupdN2MFeQ+NPwynI1SM07E7yA5b66lbudKt/pNOemf9Q3l4zrYidLFpiQk6L6ajQpM0WQbEDYG2I1sxybu4fUah79MSZO7BoolYy6l/NDE5G35e3Kw9Yu1cFAyNZJ9s/RU8nG24OAMX+pMOkjk4bX4tzrWUkHmebRJf7iBZxsSys6o83arpyKcucLOfTyyLSRemXF8IXr2MGMypHkPrx+4w5MnY9tyY8JcclaiLDkpbVVDUTarbkg==,iv:sVAnAqAMdTn8HpEwcIz2B57SrPlYqV2/Oi3sYHanYzo=,tag:BmhemprKvn33Wt595MjKcQ==,type:str]
|
|
||||||
#ENC[AES256_GCM,data:GpnZDeOAyr2pZxWHVd++1TMm230hvQ==,iv:jo8kWdd0Pm3d3xewCcyhauiBhI+SYIlWvczKn0PPZTg=,tag:INK0gZhKynkiOgi2ayrSMA==,type:comment]
|
|
||||||
restic/password: ENC[AES256_GCM,data:iZNRA8qNspy7WnK+Dg1OOZj9Gt2Y/AXUG1gKTBGUt+6q7T6Lv5AqbVkN8khwlKyQWK6FNLh3/9ejsM7mybiyog==,iv:XMxMAgVMdCWnDCkdTxL72pbrg8Dy0xz2EYou7AaNgS0=,tag:KW9Tjhql0yF6h81Il1htbw==,type:str]
|
|
||||||
restic/s3_access_key_id: ENC[AES256_GCM,data:XK8GqLHSC76K6z86RbqI4uNwZgcfl5R0Bg==,iv:t9+fGwwGX8PLwr30MJMYdOm02f/+XTcnMhSY1DP+nU0=,tag:fauNjH4lVtHa+L8Bfj8TOg==,type:str]
|
|
||||||
restic/s3_secret_access_key: ENC[AES256_GCM,data:GUx4FPaHWuzNwOju7CQoZc5U2SLG+3GOn0zJvvRXzQ==,iv:Oq0q9a+esPkLygMkGaFFNZOOfMGMFVPeb+yHUcLcNZE=,tag:Rwd0NNyXt+L8IJCCiDJh8w==,type:str]
|
|
||||||
#ENC[AES256_GCM,data:H+rGxOM6euNaSOval0ZXgKlRKQ==,iv:o0kU37iQzWAvTl5T9MK5RpHJ1eqhFftfVMEGMR40Hw8=,tag:rFcrmYZXpOpVdvW/zTul1A==,type:comment]
|
|
||||||
wifi/psk: ENC[AES256_GCM,data:bkZnP8S7yQlaEfH+kN1FfjQqJw==,iv:n1wOv6rXDbGucKryV9qV0fgqXNC/GwDeDlY2k9/hSOI=,tag:LdC2ahrXVBcqLWU5nFHMlQ==,type:str]
|
|
||||||
sops:
|
sops:
|
||||||
lastmodified: "2026-04-18T20:53:59Z"
|
age:
|
||||||
mac: ENC[AES256_GCM,data:nEP5XRzdYdFBWp9tqIgxcjjR7+X9ScpUew6SGfE6bKSQjvbwKTCGW6dSOTe7FmpUKrOS+dJnwpPsWKu0jbX/Qm5EtfXaB0GWiiMjfejwshmyULuJKipuq1rC+YX+DmOXoWIiNwKIwd4tBEOfYFBJVLFcoP8DSFjettymT0idvAQ=,iv:RnWzW+2hUScofJVom+csqEhYME8/roIzdRC/YC8opyk=,tag:22rjZO28mjPsp9p3iuoHSQ==,type:str]
|
- recipient: age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZSGpPdTBIaTZ0TER2NkNO
|
||||||
|
U2ZPKzNwelJHUEpyU2VBSmd5Yjd5bEtibFZzCjlZZTRFa2FHN1JtK2JUSm51a3By
|
||||||
|
QmFyV1ZZNWI0OGJVM1NNZERjd2hWcDAKLS0tIG9VSVFTSTJBMjk5ZzBSL0ZQV2Ev
|
||||||
|
QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE
|
||||||
|
wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
lastmodified: "2026-04-21T12:42:15Z"
|
||||||
|
mac: ENC[AES256_GCM,data:fNip/7A7iKCVZqP0EziyBG7K8SVfRJTBpn4RcDLOaciJHx5DkLLszE8we9MmzpKXQIiMcJl2BTj/uqJrgc5EHTSOHwRzNJ4s2NJfvQW+8QUDfTGzKOkP3L837RkEPzH4HZLqGlfYK7cNJU5qXRPbusKjAft7Fz3+ONXmodb/ONY=,iv:CdSs1a+74+MfzWyML2JQ/b2IKbktVdefFFYP5LOtUos=,tag:ikr9LwPnmdiPucOoBt3/Bw==,type:str]
|
||||||
pgp:
|
pgp:
|
||||||
- created_at: "2026-04-18T20:12:39Z"
|
- created_at: "2026-04-21T06:39:49Z"
|
||||||
enc: |-
|
enc: |-
|
||||||
-----BEGIN PGP MESSAGE-----
|
-----BEGIN PGP MESSAGE-----
|
||||||
|
|
||||||
hQIMAwdqopdXmgBkAQ/+OOgkrBhQBXcbxH2Rj3yQ5cDTkH3LZdbBH+vLvEFfoXLk
|
hQIMAwdqopdXmgBkAQ//TzlOz/QYwiYAc6NGo2O8YJi5ERkS1+0qNpptD51g2dLF
|
||||||
RI12n3y+gQo5Gbs1eD9tJOuBIqYZwG9JTHiv43d6DXRFdY9PlMWaL6HeG6le/dj7
|
V4iUx7400tc6IEEhZ0N54R7AO5mSX55XCWJxVQDTRJXmLDHcOR+9vThb4H571XBa
|
||||||
/JpirCofXhbL+GzLxQXnEOeMYm0Rhh5a9FbvqOwVkx2cCYlaWDYrZRPXFkjTw0et
|
3mcmE8Dj3sN3a1K2RwajZJXl1o5d1oNvWJ83pVsCnrJegi92+GmvmOt4QZ1l5aCf
|
||||||
DYv9a/ZUMAEKwSEJO7kRMpWYiPGI6KkArJrPBm7C6M4j5+KBv29FRSpw/IJiOMtT
|
TGYgUXAz1RreqsGKjJsSXscZOvRnp+cslJ9xY8OXeKLbQvLg0Z3pSQG2QGgDmHPD
|
||||||
CFWepDk+RJq+pMRNB91p/OO6YdrwMQJdCRcqC94I3TdxhVKoCCagULoE3vwHzxGQ
|
fRxYnlc2lKe32uoBlD2LXK+NoBnrRYEVrrwGf6P5GpTDpJbc0bR5BiRIYDhPxtqK
|
||||||
O5kDDc1GuQbIcNg2bfyWyKv6L9A30JaQT+8t3UMSHxAoWlvZes1y3tvquQeI8m+N
|
SiXWHaebg73+kbWdcm+2kiac6hW6xW/iJL4eFBT1v/NgZmNoQCnJOIA7v2vjv9vl
|
||||||
JILTmMWHjAplals4u+8BX7MCVolh4zJRNr1xiFy/UamYB70UORf2rjjGvMqOHsM+
|
81Y1FM5MpIfwNiTwkJjVsgM2tHkANlbixBHJdbjlnKpo9pTS7RuttWtdCmFdmXr0
|
||||||
IPJ2pIqbXDYs3syjKvWQFpxZczGgSPxHPlF9Tm+hu972ub9Ex2uVWntvjnt26H6+
|
oiuKDDRPVGvykPqvHzvCLf/k5j1nYvqvb7Wn2Bycc5kIOjFYEDEeM0r37vOX9nDM
|
||||||
/JbdV/7gW95AEkJ+HPjynDvYZ1tRBFGmwBOCsOkOfKmmopKcAooT6qDzC5hZBhBE
|
SW1HtaWoZuVceTJEit0WR63kmXYLZ/AHvXcmq6ucUw8Fmw79n+7brQiX2RMtCK1E
|
||||||
Yvl9TlC5GEBPnV4dtIxTZrqRqvbt5CvikmCI2h3/pcMWGM8a0iN2K0iNvlKGnKey
|
pfrNey3EEqvPs2RDd6XdF4/73CdMDN5s3xiFAIfLGeZ6h0Eq27fazSZNmdh4MGYb
|
||||||
jlGC+0nQzwLllFtGBgOGKeqG1HQ5yPf2W4Ic7uSVGI3xPHkd5gG1MAHORw/3cP3S
|
Wzj81ur8dimoSP+W9eW1TjIfY4deH5FRnN19ldKPuHdazvikWWsdN05evNlSZsDS
|
||||||
XgHadJRTvnNnDsZjT7P8rIYTBnpe2zx+I8N21r+Jh5/hCv8wSl819QaBA4IMC5kt
|
XgHafkhKiNSNZLw/VVzf+1SDLhN1H5QoxZ2YsxCc+psd5CFxU1x3llIDg4hXScAR
|
||||||
Os9nSYc1KzodkJR35O8Bdy/7H8SF34tXjpyhWvE4OEqEwN7AdI0L0PfOiGMBjms=
|
OQvRR1VjQOLFCwdFErW7sd6nQlkS7LnAskgT/0ZJGsxfkh1gJO3YqDnEKF7+P9w=
|
||||||
=7asV
|
=zKa+
|
||||||
-----END PGP MESSAGE-----
|
-----END PGP MESSAGE-----
|
||||||
fp: 076AA297579A0064
|
fp: 076AA297579A0064
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
|
|||||||
Reference in New Issue
Block a user