Everything changed - major rewrite

This commit is contained in:
Aner Zakobar
2026-06-07 00:59:22 +03:00
parent 08e8b5edbe
commit 261cf892dd
20 changed files with 673 additions and 139 deletions
+153 -61
View File
@@ -1,9 +1,9 @@
# AGENTS.md # AGENTS.md
Self-hosted home server configuration for a Raspberry Pi 4 (8 GB), managed Self-hosted home server configuration for a Raspberry Pi 4 (8 GB), managed
entirely through NixOS. Services run as podman containers under systemd. entirely through NixOS. Services run as podman containers or native NixOS
Remote access is via Cloudflare Tunnel; local access goes through Caddy services under systemd. Remote access is via Cloudflare Tunnel; local access
with Let's Encrypt TLS (DNS-01, Cloudflare API). goes through Caddy with Let's Encrypt TLS (DNS-01, Cloudflare API).
The original Kubernetes/Helm setup is preserved on the `main` branch. The original Kubernetes/Helm setup is preserved on the `main` branch.
This branch (`nixos-port`) is the active NixOS port. This branch (`nixos-port`) is the active NixOS port.
@@ -20,14 +20,21 @@ modules/
caddy.nix # Caddy reverse proxy (DNS-01 ACME, forward_auth) caddy.nix # Caddy reverse proxy (DNS-01 ACME, forward_auth)
cloudflared.nix # Cloudflare Tunnel for remote access cloudflared.nix # Cloudflare Tunnel for remote access
backup.nix # Restic daily backups (S3 primary + manual offload) backup.nix # Restic daily backups (S3 primary + manual offload)
monitoring.nix # Prometheus + Grafana (native NixOS services)
services/ services/
openldap.nix # OpenLDAP — central identity provider openldap.nix # OpenLDAP — central identity provider
authelia.nix # Authelia — SSO gateway authelia.nix # Authelia — SSO gateway + accessControlRules option
gitea.nix # Gitea — Git server gitea.nix # Gitea — Git server
gitea-runner.nix # Gitea Actions runner
nextcloud.nix # Nextcloud + PostgreSQL nextcloud.nix # Nextcloud + PostgreSQL
phpldapadmin.nix # phpLDAPadmin — LDAP web UI phpldapadmin.nix # phpLDAPadmin — LDAP web UI
jellyfin.nix # Jellyfin — media server (disabled by default) jellyfin.nix # Jellyfin — media server (disabled)
transmission.nix # Transmission — torrent client (disabled by default) transmission.nix # Transmission — torrent client (disabled)
uptime-kuma.nix # Uptime Kuma + homey.monitoring.monitors option
ntfy.nix # Ntfy — push notification server (native NixOS)
mealie.nix # Mealie — recipe manager
paperless.nix # Paperless-ngx — document management
eurovote.nix # Eurovision Vote — Django voting app
hosts/ hosts/
pi-main/ pi-main/
default.nix # Service selection + host-specific overrides default.nix # Service selection + host-specific overrides
@@ -42,14 +49,20 @@ PORTING.md # Step-by-step migration guide from the old Helm s
All services live under `zakobar.com`. All services live under `zakobar.com`.
| Service | URL | Auth | | Service | URL | Auth | Runtime |
|---------|-----|------| |---------|-----|------|---------|
| Authelia | `auth.zakobar.com` | Public (it is the auth portal) | | Authelia | `auth.zakobar.com` | Public (it is the auth portal) | container |
| Gitea | `git.zakobar.com` | Gitea-native (LDAP) | | Gitea | `git.zakobar.com` | Gitea-native (LDAP) | container |
| Nextcloud | `nextcloud.zakobar.com` | Nextcloud-native | | Nextcloud | `nextcloud.zakobar.com` | Nextcloud-native | container |
| phpLDAPadmin | `ldapadmin.zakobar.com` | Authelia two_factor, admins only | | Mealie | `mealie.zakobar.com` | Mealie-native (LDAP) | container |
| Jellyfin | `jellyfin.zakobar.com` | Jellyfin-native | | Paperless | `paperless.zakobar.com` | Authelia one_factor (SSO) | container |
| Transmission | `torrent.zakobar.com` | Authelia two_factor, admins only | | phpLDAPadmin | `ldapadmin.zakobar.com` | Authelia two_factor, admins only | container |
| Uptime Kuma | `uptime.zakobar.com` | Authelia two_factor, admins only | container |
| Grafana | `grafana.zakobar.com` | Authelia two_factor, admins only | NixOS |
| Ntfy | `ntfy.zakobar.com` | Bypass (ntfy token/password auth) | NixOS |
| Eurovision Vote | `eurovision-vote.zakobar.com` | Authelia one_factor (`/admin` two_factor) | NixOS |
| Jellyfin | `jellyfin.zakobar.com` | Jellyfin-native | container (disabled) |
| Transmission | `torrent.zakobar.com` | Authelia two_factor, admins only | container (disabled) |
## Networking ## Networking
@@ -63,7 +76,16 @@ All containers join a private podman network named **`homey`**, created by the
- **Defence in depth** — even if the firewall were misconfigured, services are - **Defence in depth** — even if the firewall were misconfigured, services are
not bound to `0.0.0.0`. not bound to `0.0.0.0`.
Internal ports (all mapped to `127.0.0.1` on the host): Native NixOS services (not containers) listen on `127.0.0.1` directly:
| Service | Host port |
|---------|-----------|
| ntfy | 2586 |
| Eurovision Vote | 8007 |
| Prometheus | 9090 |
| Grafana | 3002 |
Container host-port mappings (all bound to `127.0.0.1`):
| Container | Host port | Container port | | Container | Host port | Container port |
|-----------|-----------|----------------| |-----------|-----------|----------------|
@@ -73,6 +95,10 @@ Internal ports (all mapped to `127.0.0.1` on the host):
| nextcloud | 8080 | 80 | | nextcloud | 8080 | 80 |
| nextcloud-postgres | 5432 | 5432 | | nextcloud-postgres | 5432 | 5432 |
| phpldapadmin | 8081 | 80 | | phpldapadmin | 8081 | 80 |
| uptime-kuma | 3001 | 3001 |
| mealie | 9093 | 9000 |
| paperless | 8083 | 8000 |
| paperless-redis | (internal only) | 6379 |
| jellyfin | 8096 | 8096 | | jellyfin | 8096 | 8096 |
| transmission | 9092 | 9091 | | transmission | 9092 | 9091 |
@@ -87,22 +113,38 @@ All persistent data lives on the external HD at `/mnt/data/`:
``` ```
/mnt/data/ /mnt/data/
openldap/ openldap/
etc-ldap-slapd.d/ → /etc/ldap/slapd.d in container etc-ldap-slapd.d/ → /etc/ldap/slapd.d in container
var-lib-ldap/ → /var/lib/ldap in container var-lib-ldap/ → /var/lib/ldap in container
authelia/config/ → /config authelia/config/ → /config
gitea/data/ → /data gitea/data/ → /data
nextcloud/ nextcloud/
html/ → /var/www/html html/ → /var/www/html
db/ → /var/lib/postgresql/data db/ → /var/lib/postgresql/data
db-dump/ → pg_dump output (pre-backup) db-dump/ → pg_dump output (pre-backup)
jellyfin/config/ → /config jellyfin/config/ → /config
media/movies|tvshows|... → shared media (read-only to jellyfin) media/movies|tvshows|... → shared media (read-only to jellyfin)
transmission/config/ → /config transmission/config/ → /config
restic-cache/ → restic local cache uptime-kuma/ → /app/data
mealie/data/ → /app/data
paperless/
data/ → /usr/src/paperless/data (DB, index)
media/ → /usr/src/paperless/media (document files)
consume/ → /usr/src/paperless/consume (drop folder)
export/ → /usr/src/paperless/export
ntfy/
auth.db → ntfy user/token database (host path)
cache.db → ntfy message cache (host path)
attachments/ → file attachments (host path)
restic-cache/ → restic local cache
``` ```
Grafana and Prometheus use system state dirs (`/var/lib/grafana`,
`/var/lib/prometheus2`) and are not backed up — dashboards are provisioned by
Nix and metrics are ephemeral.
The drive device path is set per-host in `hosts/<name>/default.nix` via The drive device path is set per-host in `hosts/<name>/default.nix` via
`homey.storage.device`. Use a `/dev/disk/by-id/` path for stability. `homey.storage.device`. Use a `/dev/disk/by-label/` or `/dev/disk/by-id/`
path for stability.
## Build / Validate Commands ## Build / Validate Commands
@@ -160,7 +202,8 @@ restic password, Cloudflare tokens) can be generated fresh.
### Nix ### Nix
1. **Module pattern** — every service is an opt-in module with an `enable` option: 1. **Module pattern** — every service is an opt-in module with an `enable` option
(defaulting to `false` for optional services):
```nix ```nix
options.homey.myservice.enable = lib.mkEnableOption "My service"; options.homey.myservice.enable = lib.mkEnableOption "My service";
config = lib.mkIf config.homey.myservice.enable { ... }; config = lib.mkIf config.homey.myservice.enable { ... };
@@ -168,7 +211,7 @@ restic password, Cloudflare tokens) can be generated fresh.
2. **`homeyConfig` specialArgs** — top-level site config (domain, org name, 2. **`homeyConfig` specialArgs** — top-level site config (domain, org name,
timezone) is passed via `specialArgs` in `flake.nix` and accessed as timezone) is passed via `specialArgs` in `flake.nix` and accessed as
`homeyConfig` in every module. Do not read domain/org from hardcoded strings. `homeyConfig` in every module. Do not hardcode domain/org strings.
3. **No secrets in the Nix store** — secrets are always read from sops-managed 3. **No secrets in the Nix store** — secrets are always read from sops-managed
files at runtime, never embedded in the built config. Use files at runtime, never embedded in the built config. Use
@@ -176,7 +219,7 @@ restic password, Cloudflare tokens) can be generated fresh.
4. **Secret injection pattern** — because `oci-containers` `environmentFiles` 4. **Secret injection pattern** — because `oci-containers` `environmentFiles`
is limited, use a `systemd ExecStartPre` script to write an ephemeral env is limited, use a `systemd ExecStartPre` script to write an ephemeral env
file at `/run/<service>-secrets.env` and reference it via `EnvironmentFile`. file at `/run/<service>-secrets.env` and reference it via `environmentFiles`.
Clean it up in `postStop`. Clean it up in `postStop`.
5. **`--network=homey`** — all containers join the private `homey` podman 5. **`--network=homey`** — all containers join the private `homey` podman
@@ -187,16 +230,47 @@ restic password, Cloudflare tokens) can be generated fresh.
explicitly. The external HD mount unit is `mnt-data.mount`; containers that explicitly. The external HD mount unit is `mnt-data.mount`; containers that
need storage must depend on it. need storage must depend on it.
### Module Contribution Options
Several cross-cutting concerns are wired up via list options that any service
module can append to, rather than editing central files:
| Option | Declared in | Purpose |
|--------|-------------|---------|
| `homey.caddy.virtualHosts` | `caddy.nix` | Add a reverse-proxy vhost |
| `homey.storage.extraDirs` | `storage.nix` | Create tmpfiles dirs on the HD |
| `homey.backup.extraPaths` | `backup.nix` | Include a path in restic backups |
| `homey.monitoring.monitors` | `uptime-kuma.nix` | Add an Uptime Kuma HTTP monitor |
| `homey.authelia.accessControlRules` | `authelia.nix` | Add Authelia access-control rules |
Each service module declares its own entries. No central file edits needed.
**`homey.authelia.accessControlRules`** — each rule has:
- `priority` (int) — lower = earlier in the list. Authelia stops at the first
match, so more-specific rules (e.g. `subject: group:admins`) must precede
their catch-all counterparts. Assigned priority ranges by category:
- `0` — auth bypass (Authelia itself)
- `1019` — blanket bypasses (e.g. ntfy)
- `2049` — admin-only two_factor + deny pairs
- `5064` — open one_factor services
- `6579` — per-path rules (resources + subject combinations)
- `domain` (list of strings)
- `policy` — `bypass` | `one_factor` | `two_factor` | `deny`
- `subject` (optional list) — e.g. `[ "group:admins" ]`
- `resources` (optional list) — URL path regexes
### Adding a New Service ### Adding a New Service
1. Create `modules/services/<name>.nix` following the existing module pattern. 1. Create `modules/services/<name>.nix` following the existing module pattern.
2. Add `homey.<name>.enable = false` as the default option. 2. Import it in `flake.nix` (in the `modules` list inside `mkHost`).
3. Import the new module in `flake.nix` (in the `modules` list inside `mkHost`). 3. Enable it in `hosts/pi-main/default.nix`.
4. Enable it in `hosts/pi-main/default.nix`. 4. Inside the module's `config = lib.mkIf cfg.enable { ... }` block:
5. Add a Caddy virtual host block in `modules/caddy.nix`. - **Caddy**: add `homey.caddy.virtualHosts = [{ subdomain = "…"; port = …; auth = true/false; }]`
6. Add the service data directory to `modules/storage.nix` `tmpfiles.rules`. - **Storage**: add `homey.storage.extraDirs = [{ path = "…"; }]` for each HD directory
7. Add the data path to the `paths` list in `modules/backup.nix`. - **Backup**: add `homey.backup.extraPaths = [ "${dataDir}/…" ]`
8. Add any new secrets to `secrets/secrets.yaml` (plaintext) and document them. - **Authelia**: add `homey.authelia.accessControlRules = [{ priority = …; domain = […]; policy = "…"; }]`
- **Monitoring**: add `homey.monitoring.monitors = [{ name = "…"; url = "…"; interval = 60; }]`
5. Add any new secrets to `secrets/secrets.yaml` and document them.
### Updating or Regenerating Secrets ### Updating or Regenerating Secrets
@@ -240,45 +314,48 @@ production-ready:
DNS plugin uses `lib.fakeHash` as a placeholder. After the first `nix build`, DNS plugin uses `lib.fakeHash` as a placeholder. After the first `nix build`,
replace it with the hash Nix reports in the error message. replace it with the hash Nix reports in the error message.
- [ ] **`hosts/pi-main/default.nix` — fill in real values**: - [ ] **`monitoring.nix` — Grafana dashboard hash**: The Node Exporter Full
- SSH public key in `users.users.admin.openssh.authorizedKeys.keys` dashboard `fetchurl` hash is a placeholder. Run:
- External HD device path in `homey.storage.device` ```bash
- Backup repository URL in `homey.backup.repository` — must be an S3-compatible nix store prefetch-file --hash-type sha256 \
URL, e.g. `"s3:https://s3.us-west-002.backblazeb2.com/your-bucket-name"` https://grafana.com/api/dashboards/1860/revisions/37/download
```
and replace the hash in `modules/monitoring.nix`.
- [ ] **`secrets/secrets.yaml` — populate and encrypt**: Fill in all secret - [ ] **`secrets/secrets.yaml` — populate and encrypt**: Fill in all secret
values (old passwords from k8s + freshly generated ones, including values, then run `sops --encrypt --in-place secrets/secrets.yaml` before
`restic/s3_access_key_id` and `restic/s3_secret_access_key`), then run committing. Secrets needed:
`sops --encrypt --in-place secrets/secrets.yaml` before committing. - From old k8s deployment: openldap passwords, gitea/nextcloud passwords
- Fresh: authelia JWT/session/encryption keys, gitea JWT tokens
- [x] **`secrets/.sops.yaml` — PGP key**: The encryption subkey - New services: `uptime-kuma/admin_password`, `ntfy/admin_password`,
`076AA297579A0064` is already in `.sops.yaml`. `grafana/secret_key`, `ntfy/web_push_private_key`
- Backup: `restic/s3_access_key_id`, `restic/s3_secret_access_key`
- WiFi: `wifi/psk`
- [ ] **Cloudflare Tunnel**: Create the tunnel in the Zero Trust dashboard, - [ ] **Cloudflare Tunnel**: Create the tunnel in the Zero Trust dashboard,
copy the tunnel token into secrets, and configure public hostnames. See copy the tunnel token into secrets, and configure public hostnames for all
`modules/cloudflared.nix` and Phase 3 of `PORTING.md` for details. enabled services. See `modules/cloudflared.nix` for details.
- [ ] **Cloudflare Tunnel — add new services**: After the initial tunnel is set
up, add public hostnames for: `uptime`, `ntfy`, `grafana`, `mealie`,
`paperless`, `eurovision-vote`.
- [ ] **Second machine**: When ready, add `hosts/pi-secondary/` and uncomment - [ ] **Second machine**: When ready, add `hosts/pi-secondary/` and uncomment
the `pi-secondary` entry in `flake.nix`. Services communicating cross-machine the `pi-secondary` entry in `flake.nix`. Services communicating cross-machine
should reference the primary Pi's LAN IP instead of `127.0.0.1`. should reference the primary Pi's LAN IP instead of `127.0.0.1`.
- [ ] **Jellyfin and Transmission**: Both modules are written and importable - [ ] **Jellyfin and Transmission**: Both modules exist but are disabled.
but disabled. Enable in `hosts/pi-main/default.nix` when ready: Enable in `hosts/pi-main/default.nix` when ready:
```nix ```nix
homey.jellyfin.enable = true; homey.jellyfin.enable = true;
homey.transmission.enable = true; homey.transmission.enable = true;
``` ```
- [ ] **Backup — S3 credentials**: Add `restic/s3_access_key_id` and
`restic/s3_secret_access_key` to secrets, and set `homey.backup.repository`
to your S3-compatible bucket URL in `hosts/pi-main/default.nix`.
- [ ] **Backup — offload script**: Write `scripts/offload-backup.sh` for - [ ] **Backup — offload script**: Write `scripts/offload-backup.sh` for
manually copying snapshots to a local disk (USB attached to Pi, or a disk manually copying snapshots to a local disk. Uses `restic copy` to clone from
on your workstation). Uses `restic copy` to clone from the S3 repo into a the S3 repo into a local restic repo. See `TODO.org` for design notes.
local restic repo on the target path. See `TODO.org` for design notes.
### Post- Pi first boot ### Post-Pi first boot
These items require the Pi to be built, flashed, and booted at least once. These items require the Pi to be built, flashed, and booted at least once.
@@ -293,7 +370,6 @@ These items require the Pi to be built, flashed, and booted at least once.
- [ ] **Gitea LDAP auth**: After first start, configure LDAP authentication - [ ] **Gitea LDAP auth**: After first start, configure LDAP authentication
in Gitea's admin panel (Admin → Authentication Sources → Add LDAP source). in Gitea's admin panel (Admin → Authentication Sources → Add LDAP source).
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=zakobar,dc=com` - Bind DN: `cn=readonly,dc=zakobar,dc=com`
@@ -302,3 +378,19 @@ These items require the Pi to be built, flashed, and booted at least once.
- [ ] **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
(Admin → LDAP/AD Integration). (Admin → LDAP/AD Integration).
- [ ] **Ntfy VAPID keys**: Generate Web Push keys on the Pi:
```bash
sudo ntfy webpush keys
```
Set `homey.ntfy.webPushPublicKey` in `default.nix` and add the private key
to sops as `ntfy/web_push_private_key`.
- [ ] **Uptime Kuma monitors**: On first boot, `uptime-kuma-sync` will
automatically create all monitors declared via `homey.monitoring.monitors`.
Verify they appear correctly in the UI at `https://uptime.zakobar.com`.
- [ ] **Paperless admin token (iOS Shortcut)**: After first start, generate a
dedicated API token in the Paperless web UI (Profile → API Auth Token) for
the iOS Shortcut upload flow. The `/api/documents/post_document/` path
bypasses Authelia — the token is the only auth.
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+1
View File
@@ -78,6 +78,7 @@
./modules/services/transmission.nix ./modules/services/transmission.nix
./modules/services/gitea-runner.nix ./modules/services/gitea-runner.nix
./modules/services/paperless.nix ./modules/services/paperless.nix
./modules/services/attic.nix
./modules/services/mealie.nix ./modules/services/mealie.nix
./modules/services/uptime-kuma.nix ./modules/services/uptime-kuma.nix
./modules/services/ntfy.nix ./modules/services/ntfy.nix
+7
View File
@@ -96,6 +96,13 @@
homey.caddy.enable = true; homey.caddy.enable = true;
homey.cloudflared.enable = true; homey.cloudflared.enable = true;
# Nix binary cache
homey.attic.enable = true;
nix.settings = {
substituters = lib.mkAfter [ "https://attic.zakobar.com/main" ];
trusted-public-keys = lib.mkAfter [ "main:9SZt/6plBU7jjQzz90J7O011I13hmJvOMYouxNqExNQ=" ];
};
# CI/CD # CI/CD
homey.giteaRunner.enable = true; homey.giteaRunner.enable = true;
+8
View File
@@ -205,6 +205,14 @@ in
mode = "0444"; mode = "0444";
}; };
# -----------------------------------------------------------------------
# Authelia access control — admins only, two_factor; all others denied.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [
{ priority = 35; domain = [ "grafana.${domain}" ]; subject = [ "group:admins" ]; policy = "two_factor"; }
{ priority = 36; domain = [ "grafana.${domain}" ]; policy = "deny"; }
];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — forward_auth; Caddy maps Remote-User → X-WEBAUTH-USER # Caddy virtual host — forward_auth; Caddy maps Remote-User → X-WEBAUTH-USER
# so Grafana's proxy auth auto-signs the user in # so Grafana's proxy auth auto-signs the user in
+160
View File
@@ -0,0 +1,160 @@
# Attic — Post-Deployment Setup
Steps to run once after the first `nixos-rebuild switch` with `homey.attic.enable = true`.
**Status as of 2026-05-30:** all steps complete. Cache `main` is live at
`https://attic.zakobar.com/main`. Lauretta is logged in and can push/pull.
---
## Known values
| Item | Value |
|------|-------|
| Server URL | `https://attic.zakobar.com` |
| Cache name | `main` |
| Binary cache endpoint | `https://attic.zakobar.com/main` |
| Public signing key | `main:9SZt/6plBU7jjQzz90J7O011I13hmJvOMYouxNqExNQ=` |
| Cache visibility | Private (token required to pull) |
| GC retention | 90 days |
| Attic login (lauretta) | `~/.config/attic/config.toml` → server `homey` |
---
## Token reference
Tokens are stateless signed JWTs — the server does not store them. If you lose
one, regenerate it with the same command; it will work identically to the original.
### Admin token (full access)
```bash
ssh admin@192.168.1.100 \
"sudo podman exec attic atticadm -f /etc/attic/server.toml make-token \
--sub admin \
--validity '10y' \
--pull '*' \
--push '*' \
--delete '*' \
--create-cache '*' \
--configure-cache '*' \
--configure-cache-retention '*' \
--destroy-cache '*'"
```
### Pull-only token (for non-admin clients)
```bash
ssh admin@192.168.1.100 \
"sudo podman exec attic atticadm -f /etc/attic/server.toml make-token \
--sub nixos-client \
--validity '10y' \
--pull '*'"
```
### Push-only token (e.g. for CI)
```bash
ssh admin@192.168.1.100 \
"sudo podman exec attic atticadm -f /etc/attic/server.toml make-token \
--sub ci \
--validity '10y' \
--push 'main'"
```
---
## Configuring a new client machine
### 1. Add to `~/.config/nix/nix.conf`
```
extra-substituters = https://attic.zakobar.com/main
extra-trusted-public-keys = main:9SZt/6plBU7jjQzz90J7O011I13hmJvOMYouxNqExNQ=
```
### 2. Add pull token to `~/.netrc`
Generate a pull-only token (see above), then append to `~/.netrc`:
```
machine attic.zakobar.com
login token
password <pull-token>
```
### 3. Log in for pushing (optional)
```bash
nix run github:zhaofengli/attic -- login homey https://attic.zakobar.com <admin-or-push-token>
```
### 4. Verify
```bash
nix store ping --store https://attic.zakobar.com/main
```
---
## Pushing builds
```bash
# Push a specific path and its closure
nix run github:zhaofengli/attic -- push homey:main <path>
# Push the current system closure
nix run github:zhaofengli/attic -- push homey:main /run/current-system
# Push after a nix build
nix build .#nixosConfigurations.pi-main.config.system.build.toplevel
nix run github:zhaofengli/attic -- push homey:main ./result
# Watch the store and push all new paths as they are built
nix run github:zhaofengli/attic -- watch-store homey:main
```
Paths already signed by `cache.nixos.org` are skipped automatically.
---
## Monitoring
- **Uptime Kuma**: monitor configured automatically via the NixOS module (5 min interval)
- **Disk usage**: `ssh admin@192.168.1.100 "du -sh /mnt/data/attic/"`
- **Grafana**: node exporter tracks `/mnt/data` filesystem usage
- **Logs**: `ssh admin@192.168.1.100 "journalctl -u podman-attic -n 50"`
### Manual GC
```bash
ssh admin@192.168.1.100 \
"sudo podman exec attic atticadm -f /etc/attic/server.toml run-gc"
```
---
## Signing key rotation
If the signing key is ever compromised or needs rotating:
```bash
nix run github:zhaofengli/attic -- cache configure homey:main --regenerate-keypair
nix run github:zhaofengli/attic -- cache info homey:main # get new public key
```
Then update `trusted-public-keys` in `hosts/pi-main/default.nix` and on all client machines.
---
## Initial setup steps (completed 2026-05-30)
For reference — these were run once during first deployment.
1. Deployed NixOS config with `homey.attic.enable = true`
2. Added `attic.zakobar.com` to Cloudflare Tunnel dashboard
3. Generated admin token via `atticadm` inside container
4. Logged in: `attic login homey https://attic.zakobar.com <token>`
5. Created cache: `attic cache create homey:main` (Attic generates signing key server-side)
6. Added public key and substituter to `hosts/pi-main/default.nix`
7. Configured lauretta: `~/.config/nix/nix.conf` + `~/.netrc`
+166
View File
@@ -0,0 +1,166 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Attic — self-hosted Nix binary cache (cachix alternative).
#
# Auth model: JWT token-based. No Authelia forward_auth — Attic manages its
# own token issuance and verification. Use `attic make-token` to create tokens.
# Push requires a write-scoped token; pull visibility is per-cache (public or
# token-gated, configurable via `attic cache configure` after first deploy).
#
# Volume layout:
# <dataDir>/attic/ → /data (SQLite DB)
# <dataDir>/attic/cache/ → /data/cache (content-addressed NAR store)
#
# NOT backed up: NAR content is fully reproducible from source.
#
# Secrets consumed from sops:
# attic/jwt_secret (base64-encoded HS256 secret for JWT token signing)
# attic/pull_token (JWT with pull:* scope — used by the local Nix daemon)
#
# See attic-setup.md for post-deploy steps and token generation commands.
let
cfg = config.homey.attic;
dataDir = config.homey.storage.mountPoint;
domain = homeyConfig.domain;
in
{
options.homey.attic = {
enable = lib.mkEnableOption "Attic Nix binary cache";
image = lib.mkOption {
type = lib.types.str;
default = "ghcr.io/zhaofengli/attic:latest";
};
port = lib.mkOption {
type = lib.types.port;
default = 8200;
description = "Host port Attic listens on (bound to 127.0.0.1).";
};
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."attic/jwt_secret" = { owner = "root"; };
sops.secrets."attic/pull_token" = { owner = "root"; };
# -----------------------------------------------------------------------
# Container
# If the container fails to start, check the expected config path with:
# podman inspect ghcr.io/zhaofengli/attic:latest | jq '.[].Config.Cmd'
# and adjust `cmd` below accordingly.
# -----------------------------------------------------------------------
virtualisation.oci-containers.containers.attic = {
image = cfg.image;
ports = [ "127.0.0.1:${toString cfg.port}:8080" ];
cmd = [ "--config" "/etc/attic/server.toml" ];
volumes = [
"${dataDir}/attic:/data"
"/run/attic-config.toml:/etc/attic/server.toml:ro"
];
extraOptions = [ "--network=homey" ];
};
# -----------------------------------------------------------------------
# ExecStartPre: write ephemeral TOML config with JWT secret interpolated
# -----------------------------------------------------------------------
systemd.services."podman-attic" = {
serviceConfig = {
ExecStartPre = [
(pkgs.writeShellScript "attic-write-config" ''
set -euo pipefail
JWT=$(cat ${config.sops.secrets."attic/jwt_secret".path})
install -m 600 /dev/null /run/attic-config.toml
printf '%s\n' \
'listen = "0.0.0.0:8080"' \
"" \
'[jwt.signing]' \
"token-hs256-secret-base64 = \"$JWT\"" \
"" \
'[database]' \
'url = "sqlite:///data/server.db?mode=rwc"' \
"" \
'[storage]' \
'type = "local"' \
'path = "/data/cache"' \
"" \
'[chunking]' \
'nar-size-threshold = 65536' \
'min-size = 16384' \
'avg-size = 65536' \
'max-size = 262144' \
"" \
'[garbage-collection]' \
'default-retention-period = "90 days"' \
"" \
'[compression]' \
'type = "zstd"' \
'level = 8' \
>> /run/attic-config.toml
'')
];
};
postStop = "rm -f /run/attic-config.toml";
after = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ];
requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ];
};
# -----------------------------------------------------------------------
# Caddy virtual host — no forward_auth; Attic handles its own auth
# -----------------------------------------------------------------------
homey.caddy.virtualHosts = [{
subdomain = "attic";
port = cfg.port;
auth = false;
}];
# -----------------------------------------------------------------------
# Storage directories (not backed up — no backup.extraPaths entry)
# -----------------------------------------------------------------------
homey.storage.extraDirs = [
{ path = "attic"; }
{ path = "attic/cache"; mode = "0755"; }
];
# -----------------------------------------------------------------------
# Nix daemon pull auth
# Writes a netrc file from the pull token so the system Nix daemon (and
# anything using it, e.g. the Gitea runner) can fetch from the private cache.
# -----------------------------------------------------------------------
systemd.services.attic-nix-netrc = {
description = "Write Attic pull token to netrc for Nix daemon";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "attic-write-netrc" ''
set -euo pipefail
TOKEN=$(cat ${config.sops.secrets."attic/pull_token".path})
install -m 600 /dev/null /run/attic-netrc
printf 'machine attic.${domain}\n login token\n password %s\n' "$TOKEN" \
> /run/attic-netrc
'';
};
postStop = "rm -f /run/attic-netrc";
};
nix.extraOptions = ''
netrc-file = /run/attic-netrc
'';
# -----------------------------------------------------------------------
# Uptime Kuma monitor
# -----------------------------------------------------------------------
homey.monitoring.monitors = [{
name = "Attic";
url = "https://attic.${domain}";
interval = 300;
}];
};
}
+69 -70
View File
@@ -17,6 +17,10 @@
# authelia/session_secret # authelia/session_secret
# authelia/storage_encryption_key # authelia/storage_encryption_key
# openldap/ro_password (shared with openldap module) # openldap/ro_password (shared with openldap module)
#
# Access control rules are NOT declared here. Each service module contributes
# its own rules via homey.authelia.accessControlRules, which are sorted by
# priority and merged into the final config at build time.
let let
cfg = config.homey.authelia; cfg = config.homey.authelia;
@@ -27,9 +31,29 @@ let
ldapBaseDN = lib.concatStringsSep "," ldapBaseDN = lib.concatStringsSep ","
(map (p: "dc=${p}") (lib.splitString "." domain)); (map (p: "dc=${p}") (lib.splitString "." domain));
# Render a single access_control rule attrset to a YAML list item.
# Indented for insertion into the access_control.rules block (4 spaces
# before "- domain:", matching the 2-space indent of "rules:").
renderRule = rule:
let
domainLines = lib.concatMapStringsSep "\n" (d: " - \"${d}\"") rule.domain;
subjectBlock = lib.optionalString (rule.subject != []) (
"\n subject:\n" +
lib.concatMapStringsSep "\n" (s: " - \"${s}\"") rule.subject
);
resourcesBlock = lib.optionalString (rule.resources != []) (
"\n resources:\n" +
lib.concatMapStringsSep "\n" (r: " - \"${r}\"") rule.resources
);
in
" - domain:\n${domainLines}${subjectBlock}${resourcesBlock}\n policy: \"${rule.policy}\"\n";
sortedRules = lib.sort (a: b: a.priority < b.priority) cfg.accessControlRules;
rulesYaml = lib.concatStrings (map renderRule sortedRules);
# The authelia config is written as a Nix string so all values are # The authelia config is written as a Nix string so all values are
# resolved at build time except for secrets, which are injected at # resolved at build time except for secrets, which are injected at
# runtime via a wrapper script (same pattern as openldap). # runtime via environment variables.
autheliaConfig = '' autheliaConfig = ''
############################################################### ###############################################################
# Authelia configuration # # Authelia configuration #
@@ -79,75 +103,7 @@ let
access_control: access_control:
default_policy: "deny" default_policy: "deny"
rules: rules:
- domain: ${rulesYaml}
- "auth.${domain}"
policy: "bypass"
- domain:
- "ldapadmin.${domain}"
subject:
- "group:admins"
policy: "two_factor"
- domain:
- "ldapadmin.${domain}"
policy: "deny"
- domain:
- "torrent.${domain}"
subject:
- "group:admins"
policy: "two_factor"
- domain:
- "torrent.${domain}"
policy: "deny"
- domain:
- "git.${domain}"
policy: "one_factor"
- domain:
- "nextcloud.${domain}"
policy: "one_factor"
- domain:
- "jellyfin.${domain}"
policy: "one_factor"
- domain:
- "uptime.${domain}"
subject:
- "group:admins"
policy: "two_factor"
- domain:
- "uptime.${domain}"
policy: "deny"
- domain:
- "grafana.${domain}"
subject:
- "group:admins"
policy: "two_factor"
- domain:
- "grafana.${domain}"
policy: "deny"
# ntfy: bypass ntfy enforces its own token/password auth;
# the mobile app must be able to connect without Authelia SSO.
- domain:
- "ntfy.${domain}"
policy: "bypass"
# Eurovision Vote: /admin/* for admins only; all others one_factor
- domain:
- "eurovision-vote.${domain}"
resources:
- "^/admin.*$"
subject:
- "group:admins"
policy: "two_factor"
- domain:
- "eurovision-vote.${domain}"
resources:
- "^/admin.*$"
policy: "deny"
- domain:
- "eurovision-vote.${domain}"
policy: "one_factor"
- domain:
- "paperless.${domain}"
policy: "one_factor"
notifier: notifier:
filesystem: filesystem:
filename: "/config/emails.txt" filename: "/config/emails.txt"
@@ -163,6 +119,40 @@ let
in in
{ {
options.homey.authelia = { options.homey.authelia = {
# Declared unconditionally so any service module can contribute rules
# even when Authelia itself is disabled.
accessControlRules = lib.mkOption {
type = lib.types.listOf (lib.types.submodule {
options = {
priority = lib.mkOption {
type = lib.types.int;
default = 100;
description = "Order within access_control.rules lower values appear first. Authelia evaluates rules top-to-bottom and stops at the first match.";
};
domain = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Domain glob(s) this rule matches.";
};
policy = lib.mkOption {
type = lib.types.enum [ "bypass" "one_factor" "two_factor" "deny" ];
description = "Authelia policy applied when the rule matches.";
};
subject = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Optional subject constraints (e.g. \"group:admins\").";
};
resources = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Optional URL path regex constraints.";
};
};
});
default = [];
description = "Access control rules contributed by service modules. Merged and sorted by priority at build time.";
};
enable = lib.mkEnableOption "Authelia SSO gateway" // { default = true; }; enable = lib.mkEnableOption "Authelia SSO gateway" // { default = true; };
image = lib.mkOption { image = lib.mkOption {
@@ -178,6 +168,15 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Authelia's own bypass rule — must be first so the login UI is reachable.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [{
priority = 0;
domain = [ "auth.${domain}" ];
policy = "bypass";
}];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Secrets # Secrets
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+11 -1
View File
@@ -12,7 +12,7 @@
# Authentication: Caddy forward_auth → Authelia; the app reads the # Authentication: Caddy forward_auth → Authelia; the app reads the
# X-Remote-User header set by Caddy (from Authelia's Remote-User). # X-Remote-User header set by Caddy (from Authelia's Remote-User).
# All authenticated users get app access; /admin/* is restricted to # All authenticated users get app access; /admin/* is restricted to
# group:admins by Authelia's access_control rules (see authelia.nix). # group:admins by Authelia's access_control rules (defined in this file).
# #
# Secrets consumed from sops: # Secrets consumed from sops:
# eurovote/secret_key # eurovote/secret_key
@@ -48,6 +48,16 @@ in
logoutRedirectUrl = "https://auth.${domain}/logout"; logoutRedirectUrl = "https://auth.${domain}/logout";
}; };
# -----------------------------------------------------------------------
# Authelia access control — /admin/* requires two_factor + admins group;
# all other paths require one_factor.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [
{ priority = 65; domain = [ "eurovision-vote.${domain}" ]; resources = [ "^/admin.*$" ]; subject = [ "group:admins" ]; policy = "two_factor"; }
{ priority = 66; domain = [ "eurovision-vote.${domain}" ]; resources = [ "^/admin.*$" ]; policy = "deny"; }
{ priority = 67; domain = [ "eurovision-vote.${domain}" ]; policy = "one_factor"; }
];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — forward_auth; X-Remote-User passed to Django's # Caddy virtual host — forward_auth; X-Remote-User passed to Django's
# RemoteUserMiddleware for automatic SSO login # RemoteUserMiddleware for automatic SSO login
+11
View File
@@ -188,6 +188,17 @@ in
requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ];
}; };
# -----------------------------------------------------------------------
# Authelia access control — one_factor for all authenticated users.
# Caddy does not apply forward_auth (git clients can't handle SSO redirects)
# but the rule is here for completeness/Cloudflare Tunnel path.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [{
priority = 50;
domain = [ "git.${domain}" ];
policy = "one_factor";
}];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — no forward_auth; git clients can't handle SSO redirects # Caddy virtual host — no forward_auth; git clients can't handle SSO redirects
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+9
View File
@@ -52,6 +52,15 @@ in
requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ];
}; };
# -----------------------------------------------------------------------
# Authelia access control — one_factor; Jellyfin has its own login UI.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [{
priority = 60;
domain = [ "jellyfin.${domain}" ];
policy = "one_factor";
}];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — no forward_auth; Jellyfin has its own login UI # Caddy virtual host — no forward_auth; Jellyfin has its own login UI
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+8 -3
View File
@@ -11,6 +11,7 @@
# #
# Secrets consumed from sops: # Secrets consumed from sops:
# mealie/secret_key # mealie/secret_key
# openldap/ro_password (shared with openldap module — used as LDAP_QUERY_PASSWORD)
let let
cfg = config.homey.mealie; cfg = config.homey.mealie;
@@ -41,7 +42,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Secrets # Secrets
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
sops.secrets."mealie/secret_key" = { owner = "root"; }; sops.secrets."mealie/secret_key" = { owner = "root"; };
sops.secrets."openldap/ro_password" = { owner = "root"; };
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Container # Container
@@ -55,12 +57,14 @@ in
ALLOW_SIGNUP = "false"; ALLOW_SIGNUP = "false";
TZ = homeyConfig.timezone; TZ = homeyConfig.timezone;
# LDAP auth — users log in with their LDAP uid and password. # LDAP auth — Mealie binds as the readonly service account to search,
# Mealie binds directly as the user (no service account needed). # then re-binds as the user to verify the password.
# LDAP_QUERY_PASSWORD is injected via the secrets env file.
LDAP_AUTH_ENABLED = "true"; LDAP_AUTH_ENABLED = "true";
LDAP_SERVER_URL = "ldap://openldap:389"; LDAP_SERVER_URL = "ldap://openldap:389";
LDAP_ENABLE_STARTTLS = "false"; LDAP_ENABLE_STARTTLS = "false";
LDAP_BASE_DN = "ou=users,${ldapBaseDn}"; LDAP_BASE_DN = "ou=users,${ldapBaseDn}";
LDAP_QUERY_BIND = "cn=readonly,${ldapBaseDn}";
LDAP_BIND_TEMPLATE = "uid={username},ou=users,${ldapBaseDn}"; LDAP_BIND_TEMPLATE = "uid={username},ou=users,${ldapBaseDn}";
LDAP_ID_ATTRIBUTE = "uid"; LDAP_ID_ATTRIBUTE = "uid";
LDAP_NAME_ATTRIBUTE = "cn"; LDAP_NAME_ATTRIBUTE = "cn";
@@ -87,6 +91,7 @@ in
install -m 600 /dev/null /run/mealie-secrets.env install -m 600 /dev/null /run/mealie-secrets.env
printf '%s\n' \ printf '%s\n' \
"SECRET_KEY=$(cat ${config.sops.secrets."mealie/secret_key".path})" \ "SECRET_KEY=$(cat ${config.sops.secrets."mealie/secret_key".path})" \
"LDAP_QUERY_PASSWORD=$(cat ${config.sops.secrets."openldap/ro_password".path})" \
>> /run/mealie-secrets.env >> /run/mealie-secrets.env
'') '')
]; ];
+9
View File
@@ -166,6 +166,15 @@ in
]; ];
}; };
# -----------------------------------------------------------------------
# Authelia access control — one_factor; Nextcloud manages its own login UI.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [{
priority = 55;
domain = [ "nextcloud.${domain}" ];
policy = "one_factor";
}];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — no forward_auth; Nextcloud manages its own auth # Caddy virtual host — no forward_auth; Nextcloud manages its own auth
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+10
View File
@@ -176,6 +176,16 @@ in
}; };
}; };
# -----------------------------------------------------------------------
# Authelia access control — bypass so the mobile app can connect without
# an Authelia session; ntfy enforces its own token/password auth.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [{
priority = 10;
domain = [ "ntfy.${domain}" ];
policy = "bypass";
}];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — no forward_auth; ntfy uses its own token auth # Caddy virtual host — no forward_auth; ntfy uses its own token auth
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+16
View File
@@ -12,6 +12,12 @@
# #
# Requires a Redis sidecar for Celery task workers. # Requires a Redis sidecar for Celery task workers.
# #
# iOS Shortcut upload: POST /api/documents/post_document/ with
# Authorization: Token <token>. Generate a dedicated token in the Paperless
# web UI (Profile → API Auth Token) and use it only for the Shortcut so it
# can be revoked independently. The /api/documents/post_document/ path bypasses
# Authelia (see accessControlRules below) — all other paths remain behind one_factor.
#
# Volume layout: # Volume layout:
# <dataDir>/paperless/data/ → /usr/src/paperless/data (DB, index) # <dataDir>/paperless/data/ → /usr/src/paperless/data (DB, index)
# <dataDir>/paperless/media/ → /usr/src/paperless/media (document files) # <dataDir>/paperless/media/ → /usr/src/paperless/media (document files)
@@ -124,6 +130,16 @@ in
requires = lib.mkAfter [ "mnt-data.mount" "podman-paperless-redis.service" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-paperless-redis.service" "podman-homey-network.service" ];
}; };
# -----------------------------------------------------------------------
# Authelia access control — bypass the upload API so token-authenticated
# clients (e.g. iOS Shortcut) can POST without an Authelia session;
# all other paths require one_factor.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [
{ priority = 70; domain = [ "paperless.${domain}" ]; resources = [ "^/api/documents/post_document/$" ]; policy = "bypass"; }
{ priority = 71; domain = [ "paperless.${domain}" ]; policy = "one_factor"; }
];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — forward_auth; Remote-User passed to Paperless for SSO # Caddy virtual host — forward_auth; Remote-User passed to Paperless for SSO
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+11 -2
View File
@@ -3,7 +3,7 @@
# phpLDAPadmin — web UI for OpenLDAP management. # phpLDAPadmin — web UI for OpenLDAP management.
# #
# 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.
# 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 # Networking: uses default bridge (podman) network with a port mapping
@@ -12,7 +12,8 @@
# host.containers.internal DNS name that podman injects automatically. # host.containers.internal DNS name that podman injects automatically.
let let
cfg = config.homey.phpldapadmin; cfg = config.homey.phpldapadmin;
domain = homeyConfig.domain;
in in
{ {
options.homey.phpldapadmin = { options.homey.phpldapadmin = {
@@ -50,6 +51,14 @@ in
wants = lib.mkAfter [ "podman-openldap.service" "podman-homey-network.service" ]; wants = lib.mkAfter [ "podman-openldap.service" "podman-homey-network.service" ];
}; };
# -----------------------------------------------------------------------
# Authelia access control — admins only, two_factor; all others denied.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [
{ priority = 20; domain = [ "ldapadmin.${domain}" ]; subject = [ "group:admins" ]; policy = "two_factor"; }
{ priority = 21; domain = [ "ldapadmin.${domain}" ]; policy = "deny"; }
];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — forward_auth + reverse_proxy # Caddy virtual host — forward_auth + reverse_proxy
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+9
View File
@@ -15,6 +15,7 @@
let let
cfg = config.homey.transmission; cfg = config.homey.transmission;
dataDir = config.homey.storage.mountPoint; dataDir = config.homey.storage.mountPoint;
domain = homeyConfig.domain;
in in
{ {
options.homey.transmission = { options.homey.transmission = {
@@ -61,6 +62,14 @@ in
requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ];
}; };
# -----------------------------------------------------------------------
# Authelia access control — admins only, two_factor; all others denied.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [
{ priority = 30; domain = [ "torrent.${domain}" ]; subject = [ "group:admins" ]; policy = "two_factor"; }
{ priority = 31; domain = [ "torrent.${domain}" ]; policy = "deny"; }
];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — forward_auth, admins only # Caddy virtual host — forward_auth, admins only
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+8
View File
@@ -285,6 +285,14 @@ in
}; };
}; };
# -----------------------------------------------------------------------
# Authelia access control — admins only, two_factor; all others denied.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [
{ priority = 25; domain = [ "uptime.${domain}" ]; subject = [ "group:admins" ]; policy = "two_factor"; }
{ priority = 26; domain = [ "uptime.${domain}" ]; policy = "deny"; }
];
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Caddy virtual host — forward_auth, admins only # Caddy virtual host — forward_auth, admins only
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+5 -2
View File
@@ -37,6 +37,9 @@ paperless:
secret_key: ENC[AES256_GCM,data:jHbyLh4Yn0v7huw9oJiytMJ5KjifmEFsWh3u+YyOTlnm/M313dAigZItcX860oFVtZ8zZcuelUVAjcmIcl1LYw==,iv:PJhyXWa4r99dIXuKrEF+2wF9O8GEHIK8ereNQiXzO3Q=,tag:qDcPs3ulzjdQ2EUibo1Nlw==,type:str] secret_key: ENC[AES256_GCM,data:jHbyLh4Yn0v7huw9oJiytMJ5KjifmEFsWh3u+YyOTlnm/M313dAigZItcX860oFVtZ8zZcuelUVAjcmIcl1LYw==,iv:PJhyXWa4r99dIXuKrEF+2wF9O8GEHIK8ereNQiXzO3Q=,tag:qDcPs3ulzjdQ2EUibo1Nlw==,type:str]
mealie: mealie:
secret_key: ENC[AES256_GCM,data:AmtyMMK2RMOy//o9G974wn5IcgZaqAn97OyNaY1AlMc5cCoydZhdAXymQ4RR8opWd+Oelx7vRcSscGJ0hTGakg==,iv:QH+iIbMoD33MAUraMTyuGghaWdjRBhypP9UEcEr9bL4=,tag:uHGW9OLqrDhRy+mnlfRmQA==,type:str] secret_key: ENC[AES256_GCM,data:AmtyMMK2RMOy//o9G974wn5IcgZaqAn97OyNaY1AlMc5cCoydZhdAXymQ4RR8opWd+Oelx7vRcSscGJ0hTGakg==,iv:QH+iIbMoD33MAUraMTyuGghaWdjRBhypP9UEcEr9bL4=,tag:uHGW9OLqrDhRy+mnlfRmQA==,type:str]
attic:
jwt_secret: ENC[AES256_GCM,data:6g1wDau2rEqrmirzamrE6q0Sf38tosCp7EM0EtMLHXANoEfUdK8aL2Jo6z+tWL5bhNTkHwOl55j2mbyUWDlFN3I9vtI9uPKjlP+SgGbSJoKv++UYIhBmcg==,iv:DBgrMPQG/V9g0vG6Ax/fb1xCpvTYSfvAhqojH84wgn8=,tag:9WJjMFuo9kSfxRI9DVpdlg==,type:str]
pull_token: ENC[AES256_GCM,data:FDMRf8El1APXdE1+CraGDKBk9PvAnLFNL9YqvDA++5keV/M7ynAdvAhzJV1dkQ2PcRJKalAkWY0zkoQsXzmWRdY/30WzhHa60GPRRfdX4Bc1N2DqK9mFfO4eWFSBRF5EgZkqWJ+XcijiKHTr3W6MNt8oD+YQ6XkKLvRs6tOep085g2ZdK9jmaQnWTsFMhYmUt//THscDPBq8Jh81Uh2WcLJYB4hEGxxIZZtsbdK6AsRjlMsxkzr+W4kwVKs8aGjqJ5LvUOCHPGY9DvdGtWMMvMs9aw20b05ViuKzemMfDd0=,iv:CzzhhbYtJhtrAIMkERGim+j0pvC5anHVwguV//VrJRQ=,tag:6uGz1f1w76Bk8bbZItYzDg==,type:str]
sops: sops:
age: age:
- recipient: age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p - recipient: age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p
@@ -48,8 +51,8 @@ sops:
QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE
wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g== wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-20T19:56:00Z" lastmodified: "2026-05-30T09:31:03Z"
mac: ENC[AES256_GCM,data:i/uXzipvkadRGHxj7sk593SyALHVdv8wjH74xBduCI3y1cgsMYhAzH2+zY2N6BZ2ymrrcEI1+bVr2JAsCRASZ62dKEc3+m9H0+6ydpb5hl9kK7fLpuxy2nntMhTPnHznquysF4cRZoZeUJ0bDudo8mnog7GYoDI8LUvqEXZR/M8=,iv:X5WugE4STQEZHhaq6OzJLpXUgk+imbZNLZnXl5J1jUw=,tag:IDmsFOJEikpQKqi97ZKngg==,type:str] mac: ENC[AES256_GCM,data:Mnu3wtu6gfGWtU+03KyTKa9n0uWsRCISRZJcZaF2n9wCD/GDikqUX6QFFZcHHoablXEqN6yu5u0wc7efX80PCnDlkr8C0gQF3i9+p9Kj+i+pfguG47sfqP3ITXIjJpwwZwiFlbCJ/Hj3bpIpUCwr3gb6KQjQZ2bm7SGDlNeV9Ys=,iv:SbFCyuMKaYA3yKvh/DcslA98/cBXTBI7sn3TJ3RZ+y4=,tag:Eh9kMKe4pT8H9O1UZWaTRA==,type:str]
pgp: pgp:
- created_at: "2026-04-21T06:39:49Z" - created_at: "2026-04-21T06:39:49Z"
enc: |- enc: |-
+1
View File
@@ -3,6 +3,7 @@ pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
alejandra alejandra
sops sops
openssl
(pkgs.writeShellScriptBin "homey-deploy-rpi-main" '' (pkgs.writeShellScriptBin "homey-deploy-rpi-main" ''
nixos-rebuild switch \ nixos-rebuild switch \
--flake .#pi-main \ --flake .#pi-main \