Everything changed - major rewrite
This commit is contained in:
@@ -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 |
|
||||||
|
|
||||||
@@ -98,11 +124,27 @@ All persistent data lives on the external HD at `/mnt/data/`:
|
|||||||
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
|
||||||
|
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
|
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)
|
||||||
|
- `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
|
### 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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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/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
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -42,6 +43,7 @@ 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
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
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
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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: |-
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
Reference in New Issue
Block a user