Everything changed - major rewrite
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
# AGENTS.md
|
||||
|
||||
Self-hosted home server configuration for a Raspberry Pi 4 (8 GB), managed
|
||||
entirely through NixOS. Services run as podman containers under systemd.
|
||||
Remote access is via Cloudflare Tunnel; local access goes through Caddy
|
||||
with Let's Encrypt TLS (DNS-01, Cloudflare API).
|
||||
entirely through NixOS. Services run as podman containers or native NixOS
|
||||
services under systemd. Remote access is via Cloudflare Tunnel; local access
|
||||
goes through Caddy with Let's Encrypt TLS (DNS-01, Cloudflare API).
|
||||
|
||||
The original Kubernetes/Helm setup is preserved on the `main` branch.
|
||||
This branch (`nixos-port`) is the active NixOS port.
|
||||
@@ -20,14 +20,21 @@ modules/
|
||||
caddy.nix # Caddy reverse proxy (DNS-01 ACME, forward_auth)
|
||||
cloudflared.nix # Cloudflare Tunnel for remote access
|
||||
backup.nix # Restic daily backups (S3 primary + manual offload)
|
||||
monitoring.nix # Prometheus + Grafana (native NixOS services)
|
||||
services/
|
||||
openldap.nix # OpenLDAP — central identity provider
|
||||
authelia.nix # Authelia — SSO gateway
|
||||
authelia.nix # Authelia — SSO gateway + accessControlRules option
|
||||
gitea.nix # Gitea — Git server
|
||||
gitea-runner.nix # Gitea Actions runner
|
||||
nextcloud.nix # Nextcloud + PostgreSQL
|
||||
phpldapadmin.nix # phpLDAPadmin — LDAP web UI
|
||||
jellyfin.nix # Jellyfin — media server (disabled by default)
|
||||
transmission.nix # Transmission — torrent client (disabled by default)
|
||||
jellyfin.nix # Jellyfin — media server (disabled)
|
||||
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/
|
||||
pi-main/
|
||||
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`.
|
||||
|
||||
| Service | URL | Auth |
|
||||
|---------|-----|------|
|
||||
| Authelia | `auth.zakobar.com` | Public (it is the auth portal) |
|
||||
| Gitea | `git.zakobar.com` | Gitea-native (LDAP) |
|
||||
| Nextcloud | `nextcloud.zakobar.com` | Nextcloud-native |
|
||||
| phpLDAPadmin | `ldapadmin.zakobar.com` | Authelia two_factor, admins only |
|
||||
| Jellyfin | `jellyfin.zakobar.com` | Jellyfin-native |
|
||||
| Transmission | `torrent.zakobar.com` | Authelia two_factor, admins only |
|
||||
| Service | URL | Auth | Runtime |
|
||||
|---------|-----|------|---------|
|
||||
| Authelia | `auth.zakobar.com` | Public (it is the auth portal) | container |
|
||||
| Gitea | `git.zakobar.com` | Gitea-native (LDAP) | container |
|
||||
| Nextcloud | `nextcloud.zakobar.com` | Nextcloud-native | container |
|
||||
| Mealie | `mealie.zakobar.com` | Mealie-native (LDAP) | container |
|
||||
| Paperless | `paperless.zakobar.com` | Authelia one_factor (SSO) | container |
|
||||
| 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
|
||||
|
||||
@@ -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
|
||||
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 |
|
||||
|-----------|-----------|----------------|
|
||||
@@ -73,6 +95,10 @@ Internal ports (all mapped to `127.0.0.1` on the host):
|
||||
| nextcloud | 8080 | 80 |
|
||||
| nextcloud-postgres | 5432 | 5432 |
|
||||
| phpldapadmin | 8081 | 80 |
|
||||
| uptime-kuma | 3001 | 3001 |
|
||||
| mealie | 9093 | 9000 |
|
||||
| paperless | 8083 | 8000 |
|
||||
| paperless-redis | (internal only) | 6379 |
|
||||
| jellyfin | 8096 | 8096 |
|
||||
| transmission | 9092 | 9091 |
|
||||
|
||||
@@ -98,11 +124,27 @@ All persistent data lives on the external HD at `/mnt/data/`:
|
||||
jellyfin/config/ → /config
|
||||
media/movies|tvshows|... → shared media (read-only to jellyfin)
|
||||
transmission/config/ → /config
|
||||
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
|
||||
`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
|
||||
|
||||
@@ -160,7 +202,8 @@ restic password, Cloudflare tokens) can be generated fresh.
|
||||
|
||||
### 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
|
||||
options.homey.myservice.enable = lib.mkEnableOption "My service";
|
||||
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,
|
||||
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
|
||||
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`
|
||||
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`.
|
||||
|
||||
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
|
||||
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)
|
||||
- `10–19` — blanket bypasses (e.g. ntfy)
|
||||
- `20–49` — admin-only two_factor + deny pairs
|
||||
- `50–64` — open one_factor services
|
||||
- `65–79` — 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
|
||||
|
||||
1. Create `modules/services/<name>.nix` following the existing module pattern.
|
||||
2. Add `homey.<name>.enable = false` as the default option.
|
||||
3. Import the new module in `flake.nix` (in the `modules` list inside `mkHost`).
|
||||
4. Enable it in `hosts/pi-main/default.nix`.
|
||||
5. Add a Caddy virtual host block in `modules/caddy.nix`.
|
||||
6. Add the service data directory to `modules/storage.nix` `tmpfiles.rules`.
|
||||
7. Add the data path to the `paths` list in `modules/backup.nix`.
|
||||
8. Add any new secrets to `secrets/secrets.yaml` (plaintext) and document them.
|
||||
2. Import it in `flake.nix` (in the `modules` list inside `mkHost`).
|
||||
3. Enable it in `hosts/pi-main/default.nix`.
|
||||
4. Inside the module's `config = lib.mkIf cfg.enable { ... }` block:
|
||||
- **Caddy**: add `homey.caddy.virtualHosts = [{ subdomain = "…"; port = …; auth = true/false; }]`
|
||||
- **Storage**: add `homey.storage.extraDirs = [{ path = "…"; }]` for each HD directory
|
||||
- **Backup**: add `homey.backup.extraPaths = [ "${dataDir}/…" ]`
|
||||
- **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
|
||||
|
||||
@@ -240,45 +314,48 @@ production-ready:
|
||||
DNS plugin uses `lib.fakeHash` as a placeholder. After the first `nix build`,
|
||||
replace it with the hash Nix reports in the error message.
|
||||
|
||||
- [ ] **`hosts/pi-main/default.nix` — fill in real values**:
|
||||
- SSH public key in `users.users.admin.openssh.authorizedKeys.keys`
|
||||
- External HD device path in `homey.storage.device`
|
||||
- Backup repository URL in `homey.backup.repository` — must be an S3-compatible
|
||||
URL, e.g. `"s3:https://s3.us-west-002.backblazeb2.com/your-bucket-name"`
|
||||
- [ ] **`monitoring.nix` — Grafana dashboard hash**: The Node Exporter Full
|
||||
dashboard `fetchurl` hash is a placeholder. Run:
|
||||
```bash
|
||||
nix store prefetch-file --hash-type sha256 \
|
||||
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
|
||||
values (old passwords from k8s + freshly generated ones, including
|
||||
`restic/s3_access_key_id` and `restic/s3_secret_access_key`), then run
|
||||
`sops --encrypt --in-place secrets/secrets.yaml` before committing.
|
||||
|
||||
- [x] **`secrets/.sops.yaml` — PGP key**: The encryption subkey
|
||||
`076AA297579A0064` is already in `.sops.yaml`.
|
||||
values, then run `sops --encrypt --in-place secrets/secrets.yaml` before
|
||||
committing. Secrets needed:
|
||||
- From old k8s deployment: openldap passwords, gitea/nextcloud passwords
|
||||
- Fresh: authelia JWT/session/encryption keys, gitea JWT tokens
|
||||
- New services: `uptime-kuma/admin_password`, `ntfy/admin_password`,
|
||||
`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,
|
||||
copy the tunnel token into secrets, and configure public hostnames. See
|
||||
`modules/cloudflared.nix` and Phase 3 of `PORTING.md` for details.
|
||||
copy the tunnel token into secrets, and configure public hostnames for all
|
||||
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
|
||||
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`.
|
||||
|
||||
- [ ] **Jellyfin and Transmission**: Both modules are written and importable
|
||||
but disabled. Enable in `hosts/pi-main/default.nix` when ready:
|
||||
- [ ] **Jellyfin and Transmission**: Both modules exist but are disabled.
|
||||
Enable in `hosts/pi-main/default.nix` when ready:
|
||||
```nix
|
||||
homey.jellyfin.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
|
||||
manually copying snapshots to a local disk (USB attached to Pi, or a disk
|
||||
on your workstation). Uses `restic copy` to clone from the S3 repo into a
|
||||
local restic repo on the target path. See `TODO.org` for design notes.
|
||||
manually copying snapshots to a local disk. Uses `restic copy` to clone from
|
||||
the S3 repo into a local restic repo. 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.
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
- Host: `127.0.0.1`, Port: `389`, Security: Unencrypted
|
||||
- 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
|
||||
the LDAP Users and Contacts app is still configured correctly
|
||||
(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.
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
./modules/services/transmission.nix
|
||||
./modules/services/gitea-runner.nix
|
||||
./modules/services/paperless.nix
|
||||
./modules/services/attic.nix
|
||||
./modules/services/mealie.nix
|
||||
./modules/services/uptime-kuma.nix
|
||||
./modules/services/ntfy.nix
|
||||
|
||||
@@ -96,6 +96,13 @@
|
||||
homey.caddy.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
|
||||
homey.giteaRunner.enable = true;
|
||||
|
||||
|
||||
@@ -205,6 +205,14 @@ in
|
||||
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
|
||||
# so Grafana's proxy auth auto-signs the user in
|
||||
|
||||
@@ -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`
|
||||
@@ -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;
|
||||
}];
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,10 @@
|
||||
# authelia/session_secret
|
||||
# authelia/storage_encryption_key
|
||||
# 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
|
||||
cfg = config.homey.authelia;
|
||||
@@ -27,9 +31,29 @@ let
|
||||
ldapBaseDN = lib.concatStringsSep ","
|
||||
(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
|
||||
# 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 = ''
|
||||
###############################################################
|
||||
# Authelia configuration #
|
||||
@@ -79,75 +103,7 @@ let
|
||||
access_control:
|
||||
default_policy: "deny"
|
||||
rules:
|
||||
- domain:
|
||||
- "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"
|
||||
|
||||
${rulesYaml}
|
||||
notifier:
|
||||
filesystem:
|
||||
filename: "/config/emails.txt"
|
||||
@@ -163,6 +119,40 @@ let
|
||||
in
|
||||
{
|
||||
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; };
|
||||
|
||||
image = lib.mkOption {
|
||||
@@ -178,6 +168,15 @@ in
|
||||
};
|
||||
|
||||
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
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# Authentication: Caddy forward_auth → Authelia; the app reads the
|
||||
# X-Remote-User header set by Caddy (from Authelia's Remote-User).
|
||||
# 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:
|
||||
# eurovote/secret_key
|
||||
@@ -48,6 +48,16 @@ in
|
||||
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
|
||||
# RemoteUserMiddleware for automatic SSO login
|
||||
|
||||
@@ -188,6 +188,17 @@ in
|
||||
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
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -52,6 +52,15 @@ in
|
||||
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
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#
|
||||
# Secrets consumed from sops:
|
||||
# mealie/secret_key
|
||||
# openldap/ro_password (shared with openldap module — used as LDAP_QUERY_PASSWORD)
|
||||
|
||||
let
|
||||
cfg = config.homey.mealie;
|
||||
@@ -42,6 +43,7 @@ in
|
||||
# Secrets
|
||||
# -----------------------------------------------------------------------
|
||||
sops.secrets."mealie/secret_key" = { owner = "root"; };
|
||||
sops.secrets."openldap/ro_password" = { owner = "root"; };
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Container
|
||||
@@ -55,12 +57,14 @@ in
|
||||
ALLOW_SIGNUP = "false";
|
||||
TZ = homeyConfig.timezone;
|
||||
|
||||
# LDAP auth — users log in with their LDAP uid and password.
|
||||
# Mealie binds directly as the user (no service account needed).
|
||||
# LDAP auth — Mealie binds as the readonly service account to search,
|
||||
# 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_SERVER_URL = "ldap://openldap:389";
|
||||
LDAP_ENABLE_STARTTLS = "false";
|
||||
LDAP_BASE_DN = "ou=users,${ldapBaseDn}";
|
||||
LDAP_QUERY_BIND = "cn=readonly,${ldapBaseDn}";
|
||||
LDAP_BIND_TEMPLATE = "uid={username},ou=users,${ldapBaseDn}";
|
||||
LDAP_ID_ATTRIBUTE = "uid";
|
||||
LDAP_NAME_ATTRIBUTE = "cn";
|
||||
@@ -87,6 +91,7 @@ in
|
||||
install -m 600 /dev/null /run/mealie-secrets.env
|
||||
printf '%s\n' \
|
||||
"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
|
||||
'')
|
||||
];
|
||||
|
||||
@@ -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
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
#
|
||||
# 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:
|
||||
# <dataDir>/paperless/data/ → /usr/src/paperless/data (DB, index)
|
||||
# <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" ];
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 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
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# phpLDAPadmin — web UI for OpenLDAP management.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Networking: uses default bridge (podman) network with a port mapping
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
let
|
||||
cfg = config.homey.phpldapadmin;
|
||||
domain = homeyConfig.domain;
|
||||
in
|
||||
{
|
||||
options.homey.phpldapadmin = {
|
||||
@@ -50,6 +51,14 @@ in
|
||||
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
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
let
|
||||
cfg = config.homey.transmission;
|
||||
dataDir = config.homey.storage.mountPoint;
|
||||
domain = homeyConfig.domain;
|
||||
in
|
||||
{
|
||||
options.homey.transmission = {
|
||||
@@ -61,6 +62,14 @@ in
|
||||
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
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -37,6 +37,9 @@ paperless:
|
||||
secret_key: ENC[AES256_GCM,data:jHbyLh4Yn0v7huw9oJiytMJ5KjifmEFsWh3u+YyOTlnm/M313dAigZItcX860oFVtZ8zZcuelUVAjcmIcl1LYw==,iv:PJhyXWa4r99dIXuKrEF+2wF9O8GEHIK8ereNQiXzO3Q=,tag:qDcPs3ulzjdQ2EUibo1Nlw==,type:str]
|
||||
mealie:
|
||||
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:
|
||||
age:
|
||||
- recipient: age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p
|
||||
@@ -48,8 +51,8 @@ sops:
|
||||
QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE
|
||||
wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-05-20T19:56:00Z"
|
||||
mac: ENC[AES256_GCM,data:i/uXzipvkadRGHxj7sk593SyALHVdv8wjH74xBduCI3y1cgsMYhAzH2+zY2N6BZ2ymrrcEI1+bVr2JAsCRASZ62dKEc3+m9H0+6ydpb5hl9kK7fLpuxy2nntMhTPnHznquysF4cRZoZeUJ0bDudo8mnog7GYoDI8LUvqEXZR/M8=,iv:X5WugE4STQEZHhaq6OzJLpXUgk+imbZNLZnXl5J1jUw=,tag:IDmsFOJEikpQKqi97ZKngg==,type:str]
|
||||
lastmodified: "2026-05-30T09:31:03Z"
|
||||
mac: ENC[AES256_GCM,data:Mnu3wtu6gfGWtU+03KyTKa9n0uWsRCISRZJcZaF2n9wCD/GDikqUX6QFFZcHHoablXEqN6yu5u0wc7efX80PCnDlkr8C0gQF3i9+p9Kj+i+pfguG47sfqP3ITXIjJpwwZwiFlbCJ/Hj3bpIpUCwr3gb6KQjQZ2bm7SGDlNeV9Ys=,iv:SbFCyuMKaYA3yKvh/DcslA98/cBXTBI7sn3TJ3RZ+y4=,tag:Eh9kMKe4pT8H9O1UZWaTRA==,type:str]
|
||||
pgp:
|
||||
- created_at: "2026-04-21T06:39:49Z"
|
||||
enc: |-
|
||||
|
||||
@@ -3,6 +3,7 @@ pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
alejandra
|
||||
sops
|
||||
openssl
|
||||
(pkgs.writeShellScriptBin "homey-deploy-rpi-main" ''
|
||||
nixos-rebuild switch \
|
||||
--flake .#pi-main \
|
||||
|
||||
Reference in New Issue
Block a user