Compare commits
188 Commits
9b628caaef
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 10fb8e88ce | |||
| 5f300d9e7b | |||
| 99ac2ea3b0 | |||
| 2caa36c0ab | |||
| 55c15c790d | |||
| 450d9d6457 | |||
| e08bf42eaa | |||
| 8e0e5c0d16 | |||
| ada9db7942 | |||
| 5995612407 | |||
| 5762916970 | |||
| dd456eab69 | |||
| 18a8fde66e | |||
| f97c9185c1 | |||
| 8bf4b185a1 | |||
| 8424d771f6 | |||
| 840f99a7e9 | |||
| 1b27bafd41 | |||
| 4770d671c0 | |||
| 28a7bed3b9 | |||
| 170becceb0 | |||
| 6e8f530537 | |||
| 209bafd70f | |||
| 1d182437db | |||
| 6c046a549e | |||
| 0a30a2ac23 | |||
| 82c15e8d26 | |||
| f277d089bd | |||
| 7ed345b8e8 | |||
| bd6b15b617 | |||
| 3282b7d634 | |||
| 21ed381d18 | |||
| 4500f41983 | |||
| 1d30eeb939 | |||
| 537f144885 | |||
| dbada3c509 | |||
| c8be707420 | |||
| fdba2c75c7 | |||
| 55d600c0c0 | |||
| 71c5bd5e6c | |||
| 998f04713f | |||
| 6935fbea8b | |||
| 301e090251 | |||
| 58d8ef050c | |||
| 41cb2ec791 | |||
| 1faec5b2d1 | |||
| 111b8cec97 | |||
| 3aaebdb1c4 | |||
| 3e7b8c93e3 | |||
| 3e2f46377e | |||
| 38bead3dc8 | |||
| 351d36b217 | |||
| 59a37c9b46 | |||
| d7d3722ce7 | |||
| 6475524d23 | |||
| 1a70ca9564 | |||
| d6f206f0bb | |||
| b3c5366f31 | |||
| fab06ca4d5 | |||
| bd1d04943d | |||
| 8305d1b0c5 | |||
| 2d812c03eb | |||
| 156e63fd6c | |||
| a912c4dc55 | |||
| 8a2a68a91c | |||
| 01d3ab1357 | |||
| 20c5af7a69 | |||
| 865311bf49 | |||
| 9fab06795a | |||
| 038fb7ae76 | |||
| 3775e0dd7b | |||
| 66a5d69846 | |||
| 8747f887f8 | |||
| 39f4460e0a | |||
| 6f8626ca8a | |||
| 04c08bf419 | |||
| 709a24366a | |||
| 63dad8c626 | |||
| b57342f53e | |||
| 7cefa3a650 | |||
| 5d54ae898e | |||
| 794d5c2dad | |||
| 04cdf1bd2f | |||
| b4c4e31437 | |||
| 56bb321e4a | |||
| c0d868088e | |||
| df5c89f071 | |||
| b73bc3e80a | |||
| db25b2bfbb | |||
| 819bfc1531 | |||
| cfdb8d8474 | |||
| d50ed9858c | |||
| 7af4b6a5d1 | |||
| ca04f5d8c3 | |||
| a02cefc62a | |||
| 28974e9688 | |||
| aaf5f79895 | |||
| eccac4d4a2 | |||
| 399f67ba25 | |||
| 439a580dfe | |||
| bfae290927 | |||
| 1eeb0b7102 | |||
| f49ac19af1 | |||
| 27c85ff9d0 | |||
| b6d44b5a20 | |||
| b8b7574536 | |||
| 5ee2cb2b56 | |||
| 6c88bc1790 | |||
| 6be832b012 | |||
| fc9ef6b9ff | |||
| ec19103a81 | |||
| 7499a21cbd | |||
| 5758b3a320 | |||
| 7d2f818fca | |||
| 19d0946e06 | |||
| 7d5294e7b9 | |||
| 28ed3fcf74 | |||
| 5a35cd04a6 | |||
| 5648224062 | |||
| bbb9cacd71 | |||
| 40743442e9 | |||
| 7564c5d740 | |||
| 3a6d5bb8c4 | |||
| f256ca7fad | |||
| cb18e436ca | |||
| 019b1166ec | |||
| cc15f27205 | |||
| 356c049aaf | |||
| df6465fa8a | |||
| a05c33ad87 | |||
| 67906cbf16 | |||
| 09e381ecc4 | |||
| 7fd35b79c4 | |||
| c9900e4314 | |||
| 5ea3bac570 | |||
| eae7bb0e09 | |||
| 465daec0ab | |||
| f516f46b06 | |||
| 742d0172cf | |||
| e0568ddfdc | |||
| 9941dfa61f | |||
| aac9e9f38f | |||
| bdda87778c | |||
| fccec6d87c | |||
| 5e259e0b42 | |||
| 496c483050 | |||
| 1433f88d53 | |||
| 506c4f9357 | |||
| a4ed475237 | |||
| de43e917c5 | |||
| be515979cf | |||
| cc03069d57 | |||
| fe7aaadf64 | |||
| 0b6549a359 | |||
| af60555eea | |||
| 64334192de | |||
| 4751fb5582 | |||
| 34e56a13ea | |||
| c3f2603702 | |||
| 15f6b2edd0 | |||
| 6339b733c4 | |||
| 305ce21e41 | |||
| 8ab1c91b38 | |||
| bf5c7a74cb | |||
| b48ec98cb3 | |||
| 58089e558e | |||
| 97b6874258 | |||
| 8ad0c4d336 | |||
| 536fc2b463 | |||
| b7287b0d51 | |||
| a0ffb52f98 | |||
| eb40b7ff06 | |||
| b3a71cb9bc | |||
| 16594b3e7d | |||
| 7937e00018 | |||
| 0e91e1e7f5 | |||
| 99b387fe8b | |||
| fe53ea7551 | |||
| 541f9b3776 | |||
| 1c9302c773 | |||
| ba9ef3913d | |||
| 79b4a615f0 | |||
| 7225a5e787 | |||
| 467ade9340 | |||
| 619136674e | |||
| 3990566fe5 | |||
| 7f01dc4cac | |||
| da95b2fa71 |
8
.mcp.json
Normal file
8
.mcp.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"nixos": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["mcp-nixos"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
.sops.yaml
46
.sops.yaml
@@ -4,7 +4,7 @@
|
|||||||
# for a more complex example.
|
# for a more complex example.
|
||||||
keys:
|
keys:
|
||||||
- &bitwarden age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 # nixos age key
|
- &bitwarden age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 # nixos age key
|
||||||
- &dominik age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
|
- &dominik age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
|
||||||
- &dominik2 age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
- &dominik2 age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
||||||
- &git-server age106n5n3rrrss45eqqzz8pq90la3kqdtnw63uw0sfa2mahk5xpe30sxs5x58
|
- &git-server age106n5n3rrrss45eqqzz8pq90la3kqdtnw63uw0sfa2mahk5xpe30sxs5x58
|
||||||
- &web-02 age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw
|
- &web-02 age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw
|
||||||
@@ -14,6 +14,9 @@ keys:
|
|||||||
- &fw-new age12msc2c6drsaw0yk2hjlaw0q0lyq0emjx5e8rq7qc7ql689k593kqfmhss2
|
- &fw-new age12msc2c6drsaw0yk2hjlaw0q0lyq0emjx5e8rq7qc7ql689k593kqfmhss2
|
||||||
- &netboot age14uarclad0ty5supc8ep09793xrnwkv8a4h9j0fq8d8lc92n2dadqkf64vw
|
- &netboot age14uarclad0ty5supc8ep09793xrnwkv8a4h9j0fq8d8lc92n2dadqkf64vw
|
||||||
- &gpd-win4 age1ceg548u5ma6rgu3xgvd254y5xefqrdqfqhcjsjp3255q976fgd2qaua53d
|
- &gpd-win4 age1ceg548u5ma6rgu3xgvd254y5xefqrdqfqhcjsjp3255q976fgd2qaua53d
|
||||||
|
- &nb age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
|
||||||
|
- &amzebs-01 age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
|
||||||
|
- &nas age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek
|
||||||
|
|
||||||
creation_rules:
|
creation_rules:
|
||||||
- path_regex: ^[^/]+\.yaml$
|
- path_regex: ^[^/]+\.yaml$
|
||||||
@@ -22,12 +25,14 @@ creation_rules:
|
|||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- path_regex: hosts/nb/[^/]+\.yaml$
|
- path_regex: hosts/nb/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- path_regex: hosts/gpd-win4/[^/]+\.yaml$
|
- path_regex: hosts/gpd-win4/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
@@ -35,12 +40,14 @@ creation_rules:
|
|||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
- *gpd-win4
|
- *gpd-win4
|
||||||
|
- *nb
|
||||||
- path_regex: hosts/fw/[^/]+\.yaml$
|
- path_regex: hosts/fw/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- *fw
|
- *fw
|
||||||
- path_regex: hosts/fw-new/[^/]+\.yaml$
|
- path_regex: hosts/fw-new/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
@@ -48,21 +55,47 @@ creation_rules:
|
|||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- *fw
|
- *fw
|
||||||
- *fw-new
|
- *fw-new
|
||||||
|
- path_regex: hosts/fw-new/modules/web/[^/]+\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *bitwarden
|
||||||
|
- *dominik
|
||||||
|
- *dominik2
|
||||||
|
- *web-02
|
||||||
- path_regex: hosts/web-arm/[^/]+\.yaml$
|
- path_regex: hosts/web-arm/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- *web-arm
|
- *web-arm
|
||||||
|
- path_regex: hosts/amzebs-01/[^/]+\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *bitwarden
|
||||||
|
- *dominik
|
||||||
|
- *dominik2
|
||||||
|
- *nb
|
||||||
|
- *amzebs-01
|
||||||
|
- path_regex: hosts/nas/[^/]+\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *bitwarden
|
||||||
|
- *dominik
|
||||||
|
- *dominik2
|
||||||
|
- *nb
|
||||||
|
- *nas
|
||||||
- path_regex: hosts/mail/[^/]+\.yaml$
|
- path_regex: hosts/mail/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- *ldap-server-arm
|
- *ldap-server-arm
|
||||||
- path_regex: hosts/fw/modules/web/[^/]+\.yaml$
|
- path_regex: hosts/fw/modules/web/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
@@ -70,6 +103,7 @@ creation_rules:
|
|||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- *web-02
|
- *web-02
|
||||||
- path_regex: utils/modules/lego/[^/]+\.yaml$
|
- path_regex: utils/modules/lego/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
@@ -77,6 +111,7 @@ creation_rules:
|
|||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- *git-server
|
- *git-server
|
||||||
- *web-02
|
- *web-02
|
||||||
- *web-arm
|
- *web-arm
|
||||||
@@ -84,31 +119,38 @@ creation_rules:
|
|||||||
- *netboot
|
- *netboot
|
||||||
- *fw
|
- *fw
|
||||||
- *fw-new
|
- *fw-new
|
||||||
- path_regex: utils/modules/plausible/[^/]+\.yaml$
|
- path_regex: utils/modules/attic-cache/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- path_regex: utils/modules/promtail/[^/]+\.yaml$
|
- path_regex: utils/modules/promtail/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- *web-arm
|
- *web-arm
|
||||||
- *ldap-server-arm
|
- *ldap-server-arm
|
||||||
- *netboot
|
- *netboot
|
||||||
- *fw
|
- *fw
|
||||||
- *fw-new
|
- *fw-new
|
||||||
|
- *nas
|
||||||
|
- *amzebs-01
|
||||||
- path_regex: utils/modules/victoriametrics/[^/]+\.yaml$
|
- path_regex: utils/modules/victoriametrics/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *bitwarden
|
- *bitwarden
|
||||||
- *dominik
|
- *dominik
|
||||||
- *dominik2
|
- *dominik2
|
||||||
|
- *nb
|
||||||
- *web-arm
|
- *web-arm
|
||||||
- *ldap-server-arm
|
- *ldap-server-arm
|
||||||
- *netboot
|
- *netboot
|
||||||
- *fw
|
- *fw
|
||||||
- *fw-new
|
- *fw-new
|
||||||
|
- *nas
|
||||||
|
- *amzebs-01
|
||||||
|
|||||||
31
AGENTS.md
Normal file
31
AGENTS.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
- `hosts/<host>/configuration.nix` defines each machine; host modules, packages, and site configs live alongside for composability.
|
||||||
|
- Shared building blocks sit in `utils/` (`modules/`, `overlays/`, `pkgs/`, `bento.nix`), while `fleet.nix` centralizes cross-host user provisioning.
|
||||||
|
- Provisioning assets (ISO profiles, Raspberry Pi imaging, helper scripts) live under `iso/`, `raspberry*/`, and `scripts/`—refer to them before reinventing steps.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
- Enter the dev shell via `nix-shell` (uses `shell.nix`) to populate MCP helper configs and standard tooling.
|
||||||
|
- Dry-run any change with `./scripts/test-configuration <host>`; append `-v` to mirror `nixos-rebuild --show-trace` for deeper diagnostics.
|
||||||
|
- Deployment relies on the Git runner—once reviewed changes merge to main, the runner rebuilds and switches the relevant host automatically; treat a clean dry-run as the gate before pushing.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
- Format Nix files with two-space indentation; run `nixpkgs-fmt` (via `nix run nixpkgs#nixpkgs-fmt .`) before committing complex edits.
|
||||||
|
- Keep module and derivation names in lower kebab-case (`web-arm`, `home-assistant.nix`) and align attribute names with actual host or service identifiers.
|
||||||
|
- Use comments sparingly to justify non-obvious decisions (open ports, unusual service options) and prefer explicit imports over wildcard includes.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
- Always run `./scripts/test-configuration <host>` before raising a PR; it ensures evaluation succeeds and secrets are present.
|
||||||
|
- For service changes, confirm activation with `nixos-rebuild test` (or `switch`) on a staging machine and capture any notable logs.
|
||||||
|
- Document manual smoke checks (e.g., URLs defined in `hosts/web-arm/sites/`) in the PR so reviewers can repeat them quickly.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
- Follow the Conventional Commits pattern used in `git log` (`fix:`, `chore:`, `update:`) and scope by host when helpful (`fix(mail):`).
|
||||||
|
- Split refactors, secrets rotations, and package bumps into distinct commits to simplify review and rollback.
|
||||||
|
- PRs should call out affected hosts, link dry-build output (and confirm the runner result after merge), and tag the owners noted in `hosts/<host>/users/*.nix`; attach screenshots for UI-facing updates.
|
||||||
|
|
||||||
|
## Security & Configuration Tips
|
||||||
|
- Configure `config.sh` before provisioning SFTP users so the values consumed by `fleet.nix` stay in sync with the chroot layout.
|
||||||
|
- Store API keys referenced in `shell.nix` (such as the Brave Search token) under `~/.config/mcp-servers/` and keep real secrets out of version control.
|
||||||
|
- Rotate and edit encrypted `hosts/<host>/secrets.yaml` via `nix-shell -p sops --run 'sops hosts/<host>/secrets.yaml'`; commit only the encrypted output.
|
||||||
100
CLAUDE.md
Normal file
100
CLAUDE.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Repository Overview
|
||||||
|
|
||||||
|
This is a NixOS infrastructure repository managing multiple hosts (servers and personal machines) using a modular Nix configuration approach with SOPS for secrets management and Bento for deployment.
|
||||||
|
|
||||||
|
## Build and Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enter development shell (sets up MCP configs)
|
||||||
|
nix-shell
|
||||||
|
|
||||||
|
# Test configuration before deployment (required before PRs)
|
||||||
|
./scripts/test-configuration <hostname>
|
||||||
|
./scripts/test-configuration -v <hostname> # with --show-trace
|
||||||
|
|
||||||
|
# Edit encrypted secrets
|
||||||
|
nix-shell -p sops --run 'sops hosts/<hostname>/secrets.yaml'
|
||||||
|
|
||||||
|
# Update secrets keys after adding new age keys
|
||||||
|
./scripts/update-secrets-keys
|
||||||
|
|
||||||
|
# Format Nix files
|
||||||
|
nix run nixpkgs#nixpkgs-fmt .
|
||||||
|
|
||||||
|
# Compute hash for new packages
|
||||||
|
nix hash to-sri --type sha256 $(nix-prefetch-url https://example.com/file.tar.gz)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Host Structure
|
||||||
|
Each host in `hosts/<hostname>/` contains:
|
||||||
|
- `configuration.nix` - Main entry point importing modules
|
||||||
|
- `hardware-configuration.nix` - Machine-specific hardware config
|
||||||
|
- `secrets.yaml` - SOPS-encrypted secrets
|
||||||
|
- `modules/` - Host-specific service configurations
|
||||||
|
- `fleet.nix` → symlink to root `fleet.nix` (SFTP user provisioning)
|
||||||
|
- `utils/` → symlink to root `utils/` (shared modules)
|
||||||
|
|
||||||
|
Current hosts: `fw` (firewall/router), `nb` (notebook), `web-arm`, `mail`, `amzebs-01`, `nas`
|
||||||
|
|
||||||
|
### Shared Components (`utils/`)
|
||||||
|
- `modules/` - Reusable NixOS modules (nginx, sops, borgbackup, lego, promtail, etc.)
|
||||||
|
- `overlays/` - Nixpkgs overlays
|
||||||
|
- `pkgs/` - Custom package derivations
|
||||||
|
- `bento.nix` - Deployment helper module
|
||||||
|
|
||||||
|
### Secrets Management
|
||||||
|
- SOPS with age encryption; keys defined in `.sops.yaml`
|
||||||
|
- Each host has its own age key derived from SSH host key
|
||||||
|
- Host secrets in `hosts/<hostname>/secrets.yaml`
|
||||||
|
- Shared module secrets in `utils/modules/<module>/secrets.yaml`
|
||||||
|
|
||||||
|
**IMPORTANT: Never modify secrets files directly.** Instead, tell the user which secrets need to be added and where, so they can edit the encrypted files themselves using:
|
||||||
|
```bash
|
||||||
|
nix-shell -p sops --run 'sops hosts/<hostname>/secrets.yaml'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
The Git runner handles deployment automatically when changes merge to main. A successful `./scripts/test-configuration <host>` dry-build is the gate before pushing.
|
||||||
|
|
||||||
|
## Custom Packages
|
||||||
|
|
||||||
|
When creating a new package in `utils/pkgs/`, always include an `update.sh` script to automate version updates. See `utils/pkgs/claude-code/update.sh` for the pattern:
|
||||||
|
|
||||||
|
1. Fetch latest version from upstream (npm, GitHub, etc.)
|
||||||
|
2. Update version string in `default.nix`
|
||||||
|
3. Update source hash using `nix-prefetch-url`
|
||||||
|
4. Update dependency hashes (e.g., `npmDepsHash`) by triggering a build with a fake hash
|
||||||
|
5. Verify the final build succeeds
|
||||||
|
|
||||||
|
Example structure:
|
||||||
|
```
|
||||||
|
utils/pkgs/<package-name>/
|
||||||
|
├── default.nix
|
||||||
|
├── update.sh # Always include this
|
||||||
|
└── (other files like patches, lock files)
|
||||||
|
```
|
||||||
|
|
||||||
|
**IMPORTANT: When modifying a custom package** (patches, version updates, etc.), always test by building the package directly, not just running `test-configuration`. The configuration test only checks that the Nix expression evaluates, but doesn't verify the package actually builds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build a custom package directly to verify it works
|
||||||
|
nix-build -E 'with import <nixpkgs> { overlays = [ (import ./utils/overlays/packages.nix) ]; config.allowUnfree = true; }; <package-name>'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
**IMPORTANT: Always run `./scripts/test-configuration <hostname>` after making any changes** to verify the NixOS configuration builds successfully. This is required before committing.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Nix files: two-space indentation, lower kebab-case naming
|
||||||
|
- Commits: Conventional Commits format (`fix:`, `feat:`, `chore:`), scope by host when relevant (`fix(mail):`). Do not add "Generated with Claude Code" or "Co-Authored-By: Claude" footers.
|
||||||
|
- Modules import via explicit paths, not wildcards
|
||||||
|
- Comments explain non-obvious decisions (open ports, unusual service options)
|
||||||
|
- **Never update `system.stateVersion`** - it should remain at the original installation version. To upgrade NixOS, update the `channel` file instead.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
- install ubuntu 20.04
|
- install ubuntu 20.04
|
||||||
- get age key from SSH
|
- get age key from SSH
|
||||||
```console
|
```console
|
||||||
curl https://raw.githubusercontent.com/elitak/nixos-infect/master/nixos-infect | PROVIDER=hetznercloud NIX_CHANNEL=nixos-24.05 bash 2>&1 | tee /tmp/infect.log
|
curl https://raw.githubusercontent.com/elitak/nixos-infect/master/nixos-infect | PROVIDER=hetznercloud NIX_CHANNEL=nixos-25.05 bash 2>&1 | tee /tmp/infect.log
|
||||||
nix-shell -p ssh-to-age --run 'ssh-keyscan install.cloonar.com | ssh-to-age'
|
nix-shell -p ssh-to-age --run 'ssh-keyscan install.cloonar.com | ssh-to-age'
|
||||||
```
|
```
|
||||||
- fix secrets files
|
- fix secrets files
|
||||||
@@ -39,7 +39,7 @@ cat ~/.ssh/id_rsa.pub | ssh -p23 u149513-subx@u149513-subx.your-backup.de instal
|
|||||||
|
|
||||||
# 4. Add new Host
|
# 4. Add new Host
|
||||||
```console
|
```console
|
||||||
sftp host.cloonar.com@git.cloonar.com:/config/bootstrap.sh ./
|
sftp host@git.cloonar.com:/config/bootstrap.sh ./
|
||||||
```
|
```
|
||||||
|
|
||||||
# 5. Yubikey
|
# 5. Yubikey
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ https://github.com/tasmota/mgos32-to-tasmota32/releases
|
|||||||
In Tasmota make OTA Update to minimal:
|
In Tasmota make OTA Update to minimal:
|
||||||
http://ota.tasmota.com/tasmota/release/tasmota-minimal.bin.gz
|
http://ota.tasmota.com/tasmota/release/tasmota-minimal.bin.gz
|
||||||
Make ESPHome Configuration in Dashboard:
|
Make ESPHome Configuration in Dashboard:
|
||||||
docker run --rm -p 6052:6052 -e ESPHOME_DASHBOARD_USE_PING=true -v "${PWD}":/config -it ghcr.io/esphome/esphome
|
docker run --rm --network host -e ESPHOME_DASHBOARD_USE_PING=true -v "${PWD}":/config -it ghcr.io/esphome/esphome:latest
|
||||||
|
|||||||
19
esphome/archive/install.yaml
Normal file
19
esphome/archive/install.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
substitutions:
|
||||||
|
device_name: "install"
|
||||||
|
friendly_name: "Esphome Install"
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${device_name}
|
||||||
|
comment: ${friendly_name}
|
||||||
|
platform: ESP8266
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
|
web_server:
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: Cloonar-Smart
|
||||||
|
password: 0m6sY7Ue3G31
|
||||||
@@ -5,8 +5,6 @@ substitutions:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${device_name}
|
||||||
comment: ${friendly_name}
|
comment: ${friendly_name}
|
||||||
platform: ESP8266
|
|
||||||
board: esp01_1m
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 300
|
priority: 300
|
||||||
then:
|
then:
|
||||||
@@ -24,6 +22,9 @@ esphome:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
@@ -37,6 +38,7 @@ interval:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
platform: esphome
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ substitutions:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${device_name}
|
||||||
comment: ${friendly_name}
|
comment: ${friendly_name}
|
||||||
platform: ESP8266
|
|
||||||
board: esp01_1m
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 300
|
priority: 300
|
||||||
then:
|
then:
|
||||||
@@ -24,6 +22,9 @@ esphome:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
@@ -37,6 +38,7 @@ interval:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
platform: esphome
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ substitutions:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${device_name}
|
||||||
comment: ${friendly_name}
|
comment: ${friendly_name}
|
||||||
platform: ESP8266
|
|
||||||
board: esp01_1m
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 300
|
priority: 300
|
||||||
then:
|
then:
|
||||||
@@ -24,6 +22,9 @@ esphome:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
@@ -37,6 +38,7 @@ interval:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
platform: esphome
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ substitutions:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${device_name}
|
||||||
comment: ${friendly_name}
|
comment: ${friendly_name}
|
||||||
platform: ESP8266
|
|
||||||
board: esp01_1m
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 300
|
priority: 300
|
||||||
then:
|
then:
|
||||||
@@ -24,6 +22,9 @@ esphome:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
@@ -37,6 +38,7 @@ interval:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
platform: esphome
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ substitutions:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${device_name}
|
||||||
comment: ${friendly_name}
|
comment: ${friendly_name}
|
||||||
platform: ESP8266
|
|
||||||
board: esp01_1m
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 300
|
priority: 300
|
||||||
then:
|
then:
|
||||||
@@ -24,6 +22,9 @@ esphome:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
@@ -37,6 +38,7 @@ interval:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
platform: esphome
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ substitutions:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${device_name}
|
||||||
comment: ${friendly_name}
|
comment: ${friendly_name}
|
||||||
platform: ESP8266
|
|
||||||
board: esp01_1m
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 300
|
priority: 300
|
||||||
then:
|
then:
|
||||||
@@ -24,6 +22,9 @@ esphome:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
@@ -37,6 +38,7 @@ interval:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
platform: esphome
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ substitutions:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${device_name}
|
||||||
comment: ${friendly_name}
|
comment: ${friendly_name}
|
||||||
platform: ESP8266
|
|
||||||
board: esp01_1m
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 300
|
priority: 300
|
||||||
then:
|
then:
|
||||||
@@ -24,6 +22,9 @@ esphome:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
@@ -37,6 +38,7 @@ interval:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
platform: esphome
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ substitutions:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${device_name}
|
||||||
comment: ${friendly_name}
|
comment: ${friendly_name}
|
||||||
platform: ESP8266
|
|
||||||
board: esp01_1m
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 300
|
priority: 300
|
||||||
then:
|
then:
|
||||||
@@ -24,6 +22,9 @@ esphome:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
@@ -37,6 +38,7 @@ interval:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
platform: esphome
|
||||||
|
|||||||
@@ -56,13 +56,14 @@ preferences:
|
|||||||
flash_write_interval: 1min
|
flash_write_interval: 1min
|
||||||
|
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
|
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
|
||||||
fast_connect: false
|
fast_connect: True
|
||||||
domain: "${dns_domain}"
|
domain: "${dns_domain}"
|
||||||
|
|
||||||
# Your hidden network
|
# Your hidden network
|
||||||
@@ -84,7 +85,6 @@ wifi:
|
|||||||
ssid: "${name}_AP"
|
ssid: "${name}_AP"
|
||||||
password: "bulb_fallback_pw"
|
password: "bulb_fallback_pw"
|
||||||
ap_timeout: 2min # after 2 min of failed join, enable AP
|
ap_timeout: 2min # after 2 min of failed join, enable AP
|
||||||
reboot_timeout: 5min # if still not joined after 5 min, reboot and retry
|
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: status
|
- platform: status
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ substitutions:
|
|||||||
sntp_server_1: "0.pool.ntp.org"
|
sntp_server_1: "0.pool.ntp.org"
|
||||||
sntp_server_2: "1.pool.ntp.org"
|
sntp_server_2: "1.pool.ntp.org"
|
||||||
sntp_server_3: "2.pool.ntp.org"
|
sntp_server_3: "2.pool.ntp.org"
|
||||||
log_level: "WARN"
|
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: "${name}"
|
name: "${name}"
|
||||||
@@ -23,27 +22,27 @@ esphome:
|
|||||||
name: "${project_name}"
|
name: "${project_name}"
|
||||||
version: "${project_version}"
|
version: "${project_version}"
|
||||||
on_boot:
|
on_boot:
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
brightness: 20%
|
brightness: 20%
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
red: 100%
|
red: 100%
|
||||||
green: 50%
|
green: 50%
|
||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
api.connected: # check if api connected
|
api.connected:
|
||||||
else:
|
else:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
@@ -57,22 +56,35 @@ preferences:
|
|||||||
flash_write_interval: 1min
|
flash_write_interval: 1min
|
||||||
|
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
domain: .cloonar.smart
|
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
|
||||||
fast_connect: False
|
fast_connect: True
|
||||||
|
domain: "${dns_domain}"
|
||||||
|
|
||||||
|
# Your hidden network
|
||||||
networks:
|
networks:
|
||||||
- ssid: !secret wifi_ssid
|
- ssid: !secret wifi_ssid
|
||||||
password: !secret wifi_password
|
password: !secret wifi_password
|
||||||
channel: 1
|
channel: 1
|
||||||
hidden: True
|
hidden: true
|
||||||
|
|
||||||
manual_ip:
|
manual_ip:
|
||||||
static_ip: 10.42.100.12
|
static_ip: 10.42.100.12
|
||||||
gateway: 10.42.100.1
|
gateway: 10.42.100.1
|
||||||
subnet: 255.255.255.0
|
subnet: 255.255.255.0
|
||||||
|
dns1: 8.8.8.8
|
||||||
|
dns2: 1.1.1.1
|
||||||
|
|
||||||
|
# Fallback access point if Wi-Fi fails
|
||||||
|
ap:
|
||||||
|
ssid: "${name}_AP"
|
||||||
|
password: "bulb_fallback_pw"
|
||||||
|
ap_timeout: 2min # after 2 min of failed join, enable AP
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: status
|
- platform: status
|
||||||
@@ -90,7 +102,7 @@ sensor:
|
|||||||
name: "WiFi Signal dB"
|
name: "WiFi Signal dB"
|
||||||
id: wifi_signal_db
|
id: wifi_signal_db
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
|
|
||||||
- platform: copy
|
- platform: copy
|
||||||
source_id: wifi_signal_db
|
source_id: wifi_signal_db
|
||||||
@@ -98,8 +110,7 @@ sensor:
|
|||||||
filters:
|
filters:
|
||||||
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
||||||
unit_of_measurement: "Signal %"
|
unit_of_measurement: "Signal %"
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
device_class: ""
|
|
||||||
|
|
||||||
output:
|
output:
|
||||||
- platform: esp8266_pwm
|
- platform: esp8266_pwm
|
||||||
@@ -153,55 +164,47 @@ text_sensor:
|
|||||||
name: "Mac Address"
|
name: "Mac Address"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
|
|
||||||
# Creates a sensor showing when the device was last restarted
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: 'Last Restart'
|
name: 'Last Restart'
|
||||||
id: device_last_restart
|
id: device_last_restart
|
||||||
icon: mdi:clock
|
icon: mdi:clock
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
# device_class: timestamp
|
|
||||||
|
|
||||||
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: "Uptime"
|
name: "Uptime"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
lambda: |-
|
lambda: |-
|
||||||
int seconds = (id(uptime_sensor).state);
|
int seconds = (id(uptime_sensor).state);
|
||||||
int days = seconds / (24 * 3600);
|
int days = seconds / (24 * 3600);
|
||||||
seconds = seconds % (24 * 3600);
|
seconds %= (24 * 3600);
|
||||||
int hours = seconds / 3600;
|
int hours = seconds / 3600;
|
||||||
seconds = seconds % 3600;
|
seconds %= 3600;
|
||||||
int minutes = seconds / 60;
|
int minutes = seconds / 60;
|
||||||
seconds = seconds % 60;
|
seconds %= 60;
|
||||||
if ( days > 3650 ) {
|
if (days > 3650) {
|
||||||
return { "Starting up" };
|
return { "Starting up" };
|
||||||
} else if ( days ) {
|
} else if (days) {
|
||||||
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( hours ) {
|
} else if (hours) {
|
||||||
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( minutes ) {
|
} else if (minutes) {
|
||||||
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else {
|
} else {
|
||||||
return { (String(seconds) +"s").c_str() };
|
return { (String(seconds) + "s").c_str() };
|
||||||
}
|
}
|
||||||
icon: mdi:clock-start
|
icon: mdi:clock-start
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: sntp
|
- platform: sntp
|
||||||
id: sntp_time
|
id: sntp_time
|
||||||
# Define the timezone of the device
|
|
||||||
timezone: "${timezone}"
|
timezone: "${timezone}"
|
||||||
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
|
|
||||||
update_interval: ${sntp_update_interval}
|
update_interval: ${sntp_update_interval}
|
||||||
# Set specific sntp servers to use
|
|
||||||
servers:
|
servers:
|
||||||
- "${sntp_server_1}"
|
- "${sntp_server_1}"
|
||||||
- "${sntp_server_2}"
|
- "${sntp_server_2}"
|
||||||
- "${sntp_server_3}"
|
- "${sntp_server_3}"
|
||||||
# Publish the time the device was last restarted
|
|
||||||
on_time_sync:
|
on_time_sync:
|
||||||
then:
|
then:
|
||||||
# Update last restart time, but only once.
|
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
lambda: 'return id(device_last_restart).state == "";'
|
lambda: 'return id(device_last_restart).state == "";'
|
||||||
|
|||||||
@@ -10,18 +10,6 @@ substitutions:
|
|||||||
sntp_server_1: "0.pool.ntp.org"
|
sntp_server_1: "0.pool.ntp.org"
|
||||||
sntp_server_2: "1.pool.ntp.org"
|
sntp_server_2: "1.pool.ntp.org"
|
||||||
sntp_server_3: "2.pool.ntp.org"
|
sntp_server_3: "2.pool.ntp.org"
|
||||||
log_level: "WARN"
|
|
||||||
|
|
||||||
globals:
|
|
||||||
- id: fast_boot
|
|
||||||
type: int
|
|
||||||
restore_value: yes
|
|
||||||
initial_value: '0'
|
|
||||||
|
|
||||||
- id: restore_mode
|
|
||||||
type: int
|
|
||||||
restore_value: yes
|
|
||||||
initial_value: "1"
|
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: "${name}"
|
name: "${name}"
|
||||||
@@ -34,27 +22,27 @@ esphome:
|
|||||||
name: "${project_name}"
|
name: "${project_name}"
|
||||||
version: "${project_version}"
|
version: "${project_version}"
|
||||||
on_boot:
|
on_boot:
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
brightness: 20%
|
brightness: 20%
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
red: 100%
|
red: 100%
|
||||||
green: 50%
|
green: 50%
|
||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
api.connected: # check if api connected
|
api.connected:
|
||||||
else:
|
else:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
@@ -68,22 +56,35 @@ preferences:
|
|||||||
flash_write_interval: 1min
|
flash_write_interval: 1min
|
||||||
|
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
domain: .cloonar.smart
|
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
|
||||||
fast_connect: False
|
fast_connect: True
|
||||||
|
domain: "${dns_domain}"
|
||||||
|
|
||||||
|
# Your hidden network
|
||||||
networks:
|
networks:
|
||||||
- ssid: !secret wifi_ssid
|
- ssid: !secret wifi_ssid
|
||||||
password: !secret wifi_password
|
password: !secret wifi_password
|
||||||
hidden: True
|
|
||||||
channel: 1
|
channel: 1
|
||||||
|
hidden: true
|
||||||
|
|
||||||
manual_ip:
|
manual_ip:
|
||||||
static_ip: 10.42.100.13
|
static_ip: 10.42.100.13
|
||||||
gateway: 10.42.100.1
|
gateway: 10.42.100.1
|
||||||
subnet: 255.255.255.0
|
subnet: 255.255.255.0
|
||||||
|
dns1: 8.8.8.8
|
||||||
|
dns2: 1.1.1.1
|
||||||
|
|
||||||
|
# Fallback access point if Wi-Fi fails
|
||||||
|
ap:
|
||||||
|
ssid: "${name}_AP"
|
||||||
|
password: "bulb_fallback_pw"
|
||||||
|
ap_timeout: 2min # after 2 min of failed join, enable AP
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: status
|
- platform: status
|
||||||
@@ -101,7 +102,7 @@ sensor:
|
|||||||
name: "WiFi Signal dB"
|
name: "WiFi Signal dB"
|
||||||
id: wifi_signal_db
|
id: wifi_signal_db
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
|
|
||||||
- platform: copy
|
- platform: copy
|
||||||
source_id: wifi_signal_db
|
source_id: wifi_signal_db
|
||||||
@@ -109,8 +110,7 @@ sensor:
|
|||||||
filters:
|
filters:
|
||||||
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
||||||
unit_of_measurement: "Signal %"
|
unit_of_measurement: "Signal %"
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
device_class: ""
|
|
||||||
|
|
||||||
output:
|
output:
|
||||||
- platform: esp8266_pwm
|
- platform: esp8266_pwm
|
||||||
@@ -164,55 +164,47 @@ text_sensor:
|
|||||||
name: "Mac Address"
|
name: "Mac Address"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
|
|
||||||
# Creates a sensor showing when the device was last restarted
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: 'Last Restart'
|
name: 'Last Restart'
|
||||||
id: device_last_restart
|
id: device_last_restart
|
||||||
icon: mdi:clock
|
icon: mdi:clock
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
# device_class: timestamp
|
|
||||||
|
|
||||||
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: "Uptime"
|
name: "Uptime"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
lambda: |-
|
lambda: |-
|
||||||
int seconds = (id(uptime_sensor).state);
|
int seconds = (id(uptime_sensor).state);
|
||||||
int days = seconds / (24 * 3600);
|
int days = seconds / (24 * 3600);
|
||||||
seconds = seconds % (24 * 3600);
|
seconds %= (24 * 3600);
|
||||||
int hours = seconds / 3600;
|
int hours = seconds / 3600;
|
||||||
seconds = seconds % 3600;
|
seconds %= 3600;
|
||||||
int minutes = seconds / 60;
|
int minutes = seconds / 60;
|
||||||
seconds = seconds % 60;
|
seconds %= 60;
|
||||||
if ( days > 3650 ) {
|
if (days > 3650) {
|
||||||
return { "Starting up" };
|
return { "Starting up" };
|
||||||
} else if ( days ) {
|
} else if (days) {
|
||||||
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( hours ) {
|
} else if (hours) {
|
||||||
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( minutes ) {
|
} else if (minutes) {
|
||||||
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else {
|
} else {
|
||||||
return { (String(seconds) +"s").c_str() };
|
return { (String(seconds) + "s").c_str() };
|
||||||
}
|
}
|
||||||
icon: mdi:clock-start
|
icon: mdi:clock-start
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: sntp
|
- platform: sntp
|
||||||
id: sntp_time
|
id: sntp_time
|
||||||
# Define the timezone of the device
|
|
||||||
timezone: "${timezone}"
|
timezone: "${timezone}"
|
||||||
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
|
|
||||||
update_interval: ${sntp_update_interval}
|
update_interval: ${sntp_update_interval}
|
||||||
# Set specific sntp servers to use
|
|
||||||
servers:
|
servers:
|
||||||
- "${sntp_server_1}"
|
- "${sntp_server_1}"
|
||||||
- "${sntp_server_2}"
|
- "${sntp_server_2}"
|
||||||
- "${sntp_server_3}"
|
- "${sntp_server_3}"
|
||||||
# Publish the time the device was last restarted
|
|
||||||
on_time_sync:
|
on_time_sync:
|
||||||
then:
|
then:
|
||||||
# Update last restart time, but only once.
|
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
lambda: 'return id(device_last_restart).state == "";'
|
lambda: 'return id(device_last_restart).state == "";'
|
||||||
|
|||||||
@@ -10,18 +10,6 @@ substitutions:
|
|||||||
sntp_server_1: "0.pool.ntp.org"
|
sntp_server_1: "0.pool.ntp.org"
|
||||||
sntp_server_2: "1.pool.ntp.org"
|
sntp_server_2: "1.pool.ntp.org"
|
||||||
sntp_server_3: "2.pool.ntp.org"
|
sntp_server_3: "2.pool.ntp.org"
|
||||||
log_level: "WARN"
|
|
||||||
|
|
||||||
globals:
|
|
||||||
- id: fast_boot
|
|
||||||
type: int
|
|
||||||
restore_value: yes
|
|
||||||
initial_value: '0'
|
|
||||||
|
|
||||||
- id: restore_mode
|
|
||||||
type: int
|
|
||||||
restore_value: yes
|
|
||||||
initial_value: "1"
|
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: "${name}"
|
name: "${name}"
|
||||||
@@ -34,27 +22,27 @@ esphome:
|
|||||||
name: "${project_name}"
|
name: "${project_name}"
|
||||||
version: "${project_version}"
|
version: "${project_version}"
|
||||||
on_boot:
|
on_boot:
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
brightness: 20%
|
brightness: 20%
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
red: 100%
|
red: 100%
|
||||||
green: 50%
|
green: 50%
|
||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
api.connected: # check if api connected
|
api.connected:
|
||||||
else:
|
else:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
@@ -68,21 +56,35 @@ preferences:
|
|||||||
flash_write_interval: 1min
|
flash_write_interval: 1min
|
||||||
|
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
domain: .cloonar.smart
|
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
|
||||||
fast_connect: False
|
fast_connect: True
|
||||||
|
domain: "${dns_domain}"
|
||||||
|
|
||||||
|
# Your hidden network
|
||||||
networks:
|
networks:
|
||||||
- ssid: !secret wifi_ssid
|
- ssid: !secret wifi_ssid
|
||||||
password: !secret wifi_password
|
password: !secret wifi_password
|
||||||
hidden: True
|
channel: 1
|
||||||
|
hidden: true
|
||||||
|
|
||||||
manual_ip:
|
manual_ip:
|
||||||
static_ip: 10.42.100.14
|
static_ip: 10.42.100.14
|
||||||
gateway: 10.42.100.1
|
gateway: 10.42.100.1
|
||||||
subnet: 255.255.255.0
|
subnet: 255.255.255.0
|
||||||
|
dns1: 8.8.8.8
|
||||||
|
dns2: 1.1.1.1
|
||||||
|
|
||||||
|
# Fallback access point if Wi-Fi fails
|
||||||
|
ap:
|
||||||
|
ssid: "${name}_AP"
|
||||||
|
password: "bulb_fallback_pw"
|
||||||
|
ap_timeout: 2min # after 2 min of failed join, enable AP
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: status
|
- platform: status
|
||||||
@@ -100,7 +102,7 @@ sensor:
|
|||||||
name: "WiFi Signal dB"
|
name: "WiFi Signal dB"
|
||||||
id: wifi_signal_db
|
id: wifi_signal_db
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
|
|
||||||
- platform: copy
|
- platform: copy
|
||||||
source_id: wifi_signal_db
|
source_id: wifi_signal_db
|
||||||
@@ -108,8 +110,7 @@ sensor:
|
|||||||
filters:
|
filters:
|
||||||
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
||||||
unit_of_measurement: "Signal %"
|
unit_of_measurement: "Signal %"
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
device_class: ""
|
|
||||||
|
|
||||||
output:
|
output:
|
||||||
- platform: esp8266_pwm
|
- platform: esp8266_pwm
|
||||||
@@ -163,55 +164,47 @@ text_sensor:
|
|||||||
name: "Mac Address"
|
name: "Mac Address"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
|
|
||||||
# Creates a sensor showing when the device was last restarted
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: 'Last Restart'
|
name: 'Last Restart'
|
||||||
id: device_last_restart
|
id: device_last_restart
|
||||||
icon: mdi:clock
|
icon: mdi:clock
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
# device_class: timestamp
|
|
||||||
|
|
||||||
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: "Uptime"
|
name: "Uptime"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
lambda: |-
|
lambda: |-
|
||||||
int seconds = (id(uptime_sensor).state);
|
int seconds = (id(uptime_sensor).state);
|
||||||
int days = seconds / (24 * 3600);
|
int days = seconds / (24 * 3600);
|
||||||
seconds = seconds % (24 * 3600);
|
seconds %= (24 * 3600);
|
||||||
int hours = seconds / 3600;
|
int hours = seconds / 3600;
|
||||||
seconds = seconds % 3600;
|
seconds %= 3600;
|
||||||
int minutes = seconds / 60;
|
int minutes = seconds / 60;
|
||||||
seconds = seconds % 60;
|
seconds %= 60;
|
||||||
if ( days > 3650 ) {
|
if (days > 3650) {
|
||||||
return { "Starting up" };
|
return { "Starting up" };
|
||||||
} else if ( days ) {
|
} else if (days) {
|
||||||
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( hours ) {
|
} else if (hours) {
|
||||||
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( minutes ) {
|
} else if (minutes) {
|
||||||
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else {
|
} else {
|
||||||
return { (String(seconds) +"s").c_str() };
|
return { (String(seconds) + "s").c_str() };
|
||||||
}
|
}
|
||||||
icon: mdi:clock-start
|
icon: mdi:clock-start
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: sntp
|
- platform: sntp
|
||||||
id: sntp_time
|
id: sntp_time
|
||||||
# Define the timezone of the device
|
|
||||||
timezone: "${timezone}"
|
timezone: "${timezone}"
|
||||||
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
|
|
||||||
update_interval: ${sntp_update_interval}
|
update_interval: ${sntp_update_interval}
|
||||||
# Set specific sntp servers to use
|
|
||||||
servers:
|
servers:
|
||||||
- "${sntp_server_1}"
|
- "${sntp_server_1}"
|
||||||
- "${sntp_server_2}"
|
- "${sntp_server_2}"
|
||||||
- "${sntp_server_3}"
|
- "${sntp_server_3}"
|
||||||
# Publish the time the device was last restarted
|
|
||||||
on_time_sync:
|
on_time_sync:
|
||||||
then:
|
then:
|
||||||
# Update last restart time, but only once.
|
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
lambda: 'return id(device_last_restart).state == "";'
|
lambda: 'return id(device_last_restart).state == "";'
|
||||||
|
|||||||
@@ -10,18 +10,6 @@ substitutions:
|
|||||||
sntp_server_1: "0.pool.ntp.org"
|
sntp_server_1: "0.pool.ntp.org"
|
||||||
sntp_server_2: "1.pool.ntp.org"
|
sntp_server_2: "1.pool.ntp.org"
|
||||||
sntp_server_3: "2.pool.ntp.org"
|
sntp_server_3: "2.pool.ntp.org"
|
||||||
log_level: "WARN"
|
|
||||||
|
|
||||||
globals:
|
|
||||||
- id: fast_boot
|
|
||||||
type: int
|
|
||||||
restore_value: yes
|
|
||||||
initial_value: '0'
|
|
||||||
|
|
||||||
- id: restore_mode
|
|
||||||
type: int
|
|
||||||
restore_value: yes
|
|
||||||
initial_value: "1"
|
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: "${name}"
|
name: "${name}"
|
||||||
@@ -34,27 +22,27 @@ esphome:
|
|||||||
name: "${project_name}"
|
name: "${project_name}"
|
||||||
version: "${project_version}"
|
version: "${project_version}"
|
||||||
on_boot:
|
on_boot:
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
brightness: 20%
|
brightness: 20%
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
red: 100%
|
red: 100%
|
||||||
green: 50%
|
green: 50%
|
||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
api.connected: # check if api connected
|
api.connected:
|
||||||
else:
|
else:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
@@ -68,21 +56,35 @@ preferences:
|
|||||||
flash_write_interval: 1min
|
flash_write_interval: 1min
|
||||||
|
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
domain: .cloonar.smart
|
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
|
||||||
fast_connect: False
|
fast_connect: True
|
||||||
|
domain: "${dns_domain}"
|
||||||
|
|
||||||
|
# Your hidden network
|
||||||
networks:
|
networks:
|
||||||
- ssid: !secret wifi_ssid
|
- ssid: !secret wifi_ssid
|
||||||
password: !secret wifi_password
|
password: !secret wifi_password
|
||||||
hidden: True
|
channel: 1
|
||||||
|
hidden: true
|
||||||
|
|
||||||
manual_ip:
|
manual_ip:
|
||||||
static_ip: 10.42.100.15
|
static_ip: 10.42.100.15
|
||||||
gateway: 10.42.100.1
|
gateway: 10.42.100.1
|
||||||
subnet: 255.255.255.0
|
subnet: 255.255.255.0
|
||||||
|
dns1: 8.8.8.8
|
||||||
|
dns2: 1.1.1.1
|
||||||
|
|
||||||
|
# Fallback access point if Wi-Fi fails
|
||||||
|
ap:
|
||||||
|
ssid: "${name}_AP"
|
||||||
|
password: "bulb_fallback_pw"
|
||||||
|
ap_timeout: 2min # after 2 min of failed join, enable AP
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: status
|
- platform: status
|
||||||
@@ -100,7 +102,7 @@ sensor:
|
|||||||
name: "WiFi Signal dB"
|
name: "WiFi Signal dB"
|
||||||
id: wifi_signal_db
|
id: wifi_signal_db
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
|
|
||||||
- platform: copy
|
- platform: copy
|
||||||
source_id: wifi_signal_db
|
source_id: wifi_signal_db
|
||||||
@@ -108,8 +110,7 @@ sensor:
|
|||||||
filters:
|
filters:
|
||||||
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
||||||
unit_of_measurement: "Signal %"
|
unit_of_measurement: "Signal %"
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
device_class: ""
|
|
||||||
|
|
||||||
output:
|
output:
|
||||||
- platform: esp8266_pwm
|
- platform: esp8266_pwm
|
||||||
@@ -163,55 +164,47 @@ text_sensor:
|
|||||||
name: "Mac Address"
|
name: "Mac Address"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
|
|
||||||
# Creates a sensor showing when the device was last restarted
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: 'Last Restart'
|
name: 'Last Restart'
|
||||||
id: device_last_restart
|
id: device_last_restart
|
||||||
icon: mdi:clock
|
icon: mdi:clock
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
# device_class: timestamp
|
|
||||||
|
|
||||||
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: "Uptime"
|
name: "Uptime"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
lambda: |-
|
lambda: |-
|
||||||
int seconds = (id(uptime_sensor).state);
|
int seconds = (id(uptime_sensor).state);
|
||||||
int days = seconds / (24 * 3600);
|
int days = seconds / (24 * 3600);
|
||||||
seconds = seconds % (24 * 3600);
|
seconds %= (24 * 3600);
|
||||||
int hours = seconds / 3600;
|
int hours = seconds / 3600;
|
||||||
seconds = seconds % 3600;
|
seconds %= 3600;
|
||||||
int minutes = seconds / 60;
|
int minutes = seconds / 60;
|
||||||
seconds = seconds % 60;
|
seconds %= 60;
|
||||||
if ( days > 3650 ) {
|
if (days > 3650) {
|
||||||
return { "Starting up" };
|
return { "Starting up" };
|
||||||
} else if ( days ) {
|
} else if (days) {
|
||||||
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( hours ) {
|
} else if (hours) {
|
||||||
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( minutes ) {
|
} else if (minutes) {
|
||||||
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else {
|
} else {
|
||||||
return { (String(seconds) +"s").c_str() };
|
return { (String(seconds) + "s").c_str() };
|
||||||
}
|
}
|
||||||
icon: mdi:clock-start
|
icon: mdi:clock-start
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: sntp
|
- platform: sntp
|
||||||
id: sntp_time
|
id: sntp_time
|
||||||
# Define the timezone of the device
|
|
||||||
timezone: "${timezone}"
|
timezone: "${timezone}"
|
||||||
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
|
|
||||||
update_interval: ${sntp_update_interval}
|
update_interval: ${sntp_update_interval}
|
||||||
# Set specific sntp servers to use
|
|
||||||
servers:
|
servers:
|
||||||
- "${sntp_server_1}"
|
- "${sntp_server_1}"
|
||||||
- "${sntp_server_2}"
|
- "${sntp_server_2}"
|
||||||
- "${sntp_server_3}"
|
- "${sntp_server_3}"
|
||||||
# Publish the time the device was last restarted
|
|
||||||
on_time_sync:
|
on_time_sync:
|
||||||
then:
|
then:
|
||||||
# Update last restart time, but only once.
|
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
lambda: 'return id(device_last_restart).state == "";'
|
lambda: 'return id(device_last_restart).state == "";'
|
||||||
|
|||||||
@@ -10,18 +10,6 @@ substitutions:
|
|||||||
sntp_server_1: "0.pool.ntp.org"
|
sntp_server_1: "0.pool.ntp.org"
|
||||||
sntp_server_2: "1.pool.ntp.org"
|
sntp_server_2: "1.pool.ntp.org"
|
||||||
sntp_server_3: "2.pool.ntp.org"
|
sntp_server_3: "2.pool.ntp.org"
|
||||||
log_level: "WARN"
|
|
||||||
|
|
||||||
globals:
|
|
||||||
- id: fast_boot
|
|
||||||
type: int
|
|
||||||
restore_value: yes
|
|
||||||
initial_value: '0'
|
|
||||||
|
|
||||||
- id: restore_mode
|
|
||||||
type: int
|
|
||||||
restore_value: yes
|
|
||||||
initial_value: "1"
|
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: "${name}"
|
name: "${name}"
|
||||||
@@ -34,27 +22,27 @@ esphome:
|
|||||||
name: "${project_name}"
|
name: "${project_name}"
|
||||||
version: "${project_version}"
|
version: "${project_version}"
|
||||||
on_boot:
|
on_boot:
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
brightness: 20%
|
brightness: 20%
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
red: 100%
|
red: 100%
|
||||||
green: 50%
|
green: 50%
|
||||||
blue: 0%
|
blue: 0%
|
||||||
white: 100%
|
white: 100%
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
then:
|
then:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
api.connected: # check if api connected
|
api.connected:
|
||||||
else:
|
else:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: rgbww_light
|
id: rgbww_light
|
||||||
@@ -68,22 +56,35 @@ preferences:
|
|||||||
flash_write_interval: 1min
|
flash_write_interval: 1min
|
||||||
|
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
domain: .cloonar.smart
|
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
|
||||||
fast_connect: False
|
fast_connect: True
|
||||||
|
domain: "${dns_domain}"
|
||||||
|
|
||||||
|
# Your hidden network
|
||||||
networks:
|
networks:
|
||||||
- ssid: !secret wifi_ssid
|
- ssid: !secret wifi_ssid
|
||||||
password: !secret wifi_password
|
password: !secret wifi_password
|
||||||
hidden: True
|
|
||||||
channel: 1
|
channel: 1
|
||||||
|
hidden: true
|
||||||
|
|
||||||
manual_ip:
|
manual_ip:
|
||||||
static_ip: 10.42.100.16
|
static_ip: 10.42.100.16
|
||||||
gateway: 10.42.100.1
|
gateway: 10.42.100.1
|
||||||
subnet: 255.255.255.0
|
subnet: 255.255.255.0
|
||||||
|
dns1: 8.8.8.8
|
||||||
|
dns2: 1.1.1.1
|
||||||
|
|
||||||
|
# Fallback access point if Wi-Fi fails
|
||||||
|
ap:
|
||||||
|
ssid: "${name}_AP"
|
||||||
|
password: "bulb_fallback_pw"
|
||||||
|
ap_timeout: 2min # after 2 min of failed join, enable AP
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: status
|
- platform: status
|
||||||
@@ -101,7 +102,7 @@ sensor:
|
|||||||
name: "WiFi Signal dB"
|
name: "WiFi Signal dB"
|
||||||
id: wifi_signal_db
|
id: wifi_signal_db
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
|
|
||||||
- platform: copy
|
- platform: copy
|
||||||
source_id: wifi_signal_db
|
source_id: wifi_signal_db
|
||||||
@@ -109,8 +110,7 @@ sensor:
|
|||||||
filters:
|
filters:
|
||||||
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
|
||||||
unit_of_measurement: "Signal %"
|
unit_of_measurement: "Signal %"
|
||||||
entity_category: "diagnostic"
|
entity_category: diagnostic
|
||||||
device_class: ""
|
|
||||||
|
|
||||||
output:
|
output:
|
||||||
- platform: esp8266_pwm
|
- platform: esp8266_pwm
|
||||||
@@ -164,55 +164,47 @@ text_sensor:
|
|||||||
name: "Mac Address"
|
name: "Mac Address"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
|
|
||||||
# Creates a sensor showing when the device was last restarted
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: 'Last Restart'
|
name: 'Last Restart'
|
||||||
id: device_last_restart
|
id: device_last_restart
|
||||||
icon: mdi:clock
|
icon: mdi:clock
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
# device_class: timestamp
|
|
||||||
|
|
||||||
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
|
|
||||||
- platform: template
|
- platform: template
|
||||||
name: "Uptime"
|
name: "Uptime"
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
lambda: |-
|
lambda: |-
|
||||||
int seconds = (id(uptime_sensor).state);
|
int seconds = (id(uptime_sensor).state);
|
||||||
int days = seconds / (24 * 3600);
|
int days = seconds / (24 * 3600);
|
||||||
seconds = seconds % (24 * 3600);
|
seconds %= (24 * 3600);
|
||||||
int hours = seconds / 3600;
|
int hours = seconds / 3600;
|
||||||
seconds = seconds % 3600;
|
seconds %= 3600;
|
||||||
int minutes = seconds / 60;
|
int minutes = seconds / 60;
|
||||||
seconds = seconds % 60;
|
seconds %= 60;
|
||||||
if ( days > 3650 ) {
|
if (days > 3650) {
|
||||||
return { "Starting up" };
|
return { "Starting up" };
|
||||||
} else if ( days ) {
|
} else if (days) {
|
||||||
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( hours ) {
|
} else if (hours) {
|
||||||
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else if ( minutes ) {
|
} else if (minutes) {
|
||||||
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
|
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
|
||||||
} else {
|
} else {
|
||||||
return { (String(seconds) +"s").c_str() };
|
return { (String(seconds) + "s").c_str() };
|
||||||
}
|
}
|
||||||
icon: mdi:clock-start
|
icon: mdi:clock-start
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: sntp
|
- platform: sntp
|
||||||
id: sntp_time
|
id: sntp_time
|
||||||
# Define the timezone of the device
|
|
||||||
timezone: "${timezone}"
|
timezone: "${timezone}"
|
||||||
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
|
|
||||||
update_interval: ${sntp_update_interval}
|
update_interval: ${sntp_update_interval}
|
||||||
# Set specific sntp servers to use
|
|
||||||
servers:
|
servers:
|
||||||
- "${sntp_server_1}"
|
- "${sntp_server_1}"
|
||||||
- "${sntp_server_2}"
|
- "${sntp_server_2}"
|
||||||
- "${sntp_server_3}"
|
- "${sntp_server_3}"
|
||||||
# Publish the time the device was last restarted
|
|
||||||
on_time_sync:
|
on_time_sync:
|
||||||
then:
|
then:
|
||||||
# Update last restart time, but only once.
|
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
lambda: 'return id(device_last_restart).state == "";'
|
lambda: 'return id(device_last_restart).state == "";'
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ substitutions:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${device_name}
|
||||||
comment: ${friendly_name}
|
comment: ${friendly_name}
|
||||||
platform: ESP8266
|
|
||||||
board: esp01_1m
|
|
||||||
on_boot:
|
on_boot:
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
@@ -20,7 +18,8 @@ esphome:
|
|||||||
id: my_light
|
id: my_light
|
||||||
color_temperature: 2700 K
|
color_temperature: 2700 K
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 15s
|
- interval: 15s
|
||||||
@@ -40,6 +39,7 @@ interval:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
|
batch_delay: 0ms
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
platform: esphome
|
platform: esphome
|
||||||
|
|||||||
13
fleet.nix
13
fleet.nix
@@ -29,6 +29,10 @@
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
username = "nb";
|
username = "nb";
|
||||||
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6g/lXONzSW1JbyXnj+/0QPWtaiNxu9A0GOCbi96603";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
username = "nb-new";
|
||||||
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC1dDoAJUY58I+4SSfDAkO5kInsMcJT/r/mW+MYXLQVR";
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC1dDoAJUY58I+4SSfDAkO5kInsMcJT/r/mW+MYXLQVR";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -43,6 +47,15 @@
|
|||||||
username = "gpd-win4";
|
username = "gpd-win4";
|
||||||
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjfS2DtS8PQgkf86dU+EVu5t+r/QlCWmY7+RPYprQrO";
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjfS2DtS8PQgkf86dU+EVu5t+r/QlCWmY7+RPYprQrO";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
username = "nas";
|
||||||
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICS6b97LPUpr7/kWvOcI40s5e+gfbfz0I2/hAPL6zTmU";
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
username = "amzebs-01";
|
||||||
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMkFZ60SPl8pzEtGrFq1+n6ZkDuNe3xJaccJMjr3y/q";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
in {
|
in {
|
||||||
imports = builtins.map create_users users;
|
imports = builtins.map create_users users;
|
||||||
|
|||||||
471
hosts/amzebs-01/EMAIL_SETUP.md
Normal file
471
hosts/amzebs-01/EMAIL_SETUP.md
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
# Email Setup for amzebs-01 (amz.at)
|
||||||
|
|
||||||
|
This host is configured to send emails via Laravel with DKIM signing.
|
||||||
|
|
||||||
|
## Configuration Overview
|
||||||
|
|
||||||
|
- **Postfix**: Localhost-only SMTP server (no external access)
|
||||||
|
- **Rspamd**: DKIM signing with host-specific key
|
||||||
|
- **Domain**: amz.at
|
||||||
|
- **DKIM Selector**: amzebs-01
|
||||||
|
- **Secret Management**: DKIM private key stored in sops
|
||||||
|
|
||||||
|
## Initial Setup (Before First Deployment)
|
||||||
|
|
||||||
|
### 1. Generate DKIM Key Pair
|
||||||
|
|
||||||
|
You need to generate a DKIM key pair locally first. You'll need `rspamd` package installed.
|
||||||
|
|
||||||
|
#### Option A: Using rspamd (if installed locally)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a temporary directory
|
||||||
|
mkdir -p /tmp/dkim-gen
|
||||||
|
|
||||||
|
# Generate the key pair
|
||||||
|
rspamadm dkim_keygen -s amzebs-01 -d amz.at -k /tmp/dkim-gen/amz.at.amzebs-01.key
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output:
|
||||||
|
- **Private key** saved to `/tmp/dkim-gen/amz.at.amzebs-01.key`
|
||||||
|
- **Public key** printed to stdout (starts with `v=DKIM1; k=rsa; p=...`)
|
||||||
|
|
||||||
|
#### Option B: Using OpenSSL (alternative)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create temporary directory
|
||||||
|
mkdir -p /tmp/dkim-gen
|
||||||
|
|
||||||
|
# Generate private key (2048-bit RSA)
|
||||||
|
openssl genrsa -out /tmp/dkim-gen/amz.at.amzebs-01.key 2048
|
||||||
|
|
||||||
|
# Extract public key in the correct format for DNS
|
||||||
|
openssl rsa -in /tmp/dkim-gen/amz.at.amzebs-01.key -pubout -outform PEM | \
|
||||||
|
grep -v '^-----' | tr -d '\n' > /tmp/dkim-gen/public.txt
|
||||||
|
|
||||||
|
# Display the DNS record value
|
||||||
|
echo "v=DKIM1; k=rsa; p=$(cat /tmp/dkim-gen/public.txt)"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Save the public key output!** You'll need it for DNS configuration later.
|
||||||
|
|
||||||
|
### 2. Add DKIM Private Key to Sops Secrets
|
||||||
|
|
||||||
|
Now you need to encrypt and add the private key to your secrets file.
|
||||||
|
|
||||||
|
#### Step 1: View the private key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat /tmp/dkim-gen/amz.at.amzebs-01.key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Edit the secrets file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/dominik/projects/cloonar/cloonar-nixos/hosts/amzebs-01
|
||||||
|
sops secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Add the key to secrets.yaml
|
||||||
|
|
||||||
|
In the sops editor, add a new key called `rspamd-dkim-key` with the **entire private key content** including the BEGIN/END markers:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rspamd-dkim-key: |
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
|
||||||
|
(paste the entire key content here)
|
||||||
|
...
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
- Make sure to use the pipe `|` character for multiline content
|
||||||
|
- Keep the proper indentation (2 spaces before each line of the key)
|
||||||
|
- Include the full BEGIN/END markers
|
||||||
|
|
||||||
|
#### Step 4: Save and exit
|
||||||
|
|
||||||
|
Save the file in sops (it will be encrypted automatically).
|
||||||
|
|
||||||
|
#### Step 5: Clean up temporary files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf /tmp/dkim-gen
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verify Secret is Encrypted
|
||||||
|
|
||||||
|
Check that the secret is properly encrypted:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat hosts/amzebs-01/secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see encrypted content, not the plain private key.
|
||||||
|
|
||||||
|
### 4. Extract Public Key for DNS (if needed later)
|
||||||
|
|
||||||
|
If you didn't save the public key earlier, you can extract it after deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the server after deployment
|
||||||
|
sudo cat /var/lib/rspamd/dkim/amz.at.amzebs-01.key | \
|
||||||
|
openssl rsa -pubout -outform PEM 2>/dev/null | \
|
||||||
|
grep -v '^-----' | tr -d '\n'
|
||||||
|
```
|
||||||
|
|
||||||
|
Then format it as:
|
||||||
|
```
|
||||||
|
v=DKIM1; k=rsa; p=<output_from_above>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### 1. Deploy Configuration
|
||||||
|
|
||||||
|
After adding the DKIM private key to sops, deploy the configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and switch on the remote host
|
||||||
|
nixos-rebuild switch --flake .#amzebs-01 --target-host amzebs-01 --use-remote-sudo
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if deploying locally on the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nixos-rebuild switch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Verify Deployment
|
||||||
|
|
||||||
|
Check that the services are running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check rspamd-dkim-setup service
|
||||||
|
systemctl status rspamd-dkim-setup
|
||||||
|
|
||||||
|
# Check that rspamd is running
|
||||||
|
systemctl status rspamd
|
||||||
|
|
||||||
|
# Check that postfix is running
|
||||||
|
systemctl status postfix
|
||||||
|
|
||||||
|
# Verify DKIM key was deployed
|
||||||
|
ls -la /var/lib/rspamd/dkim/amz.at.amzebs-01.key
|
||||||
|
```
|
||||||
|
|
||||||
|
## DNS Configuration
|
||||||
|
|
||||||
|
Add the following DNS records to ensure proper email delivery and avoid spam classification.
|
||||||
|
|
||||||
|
### Critical: PTR Record (Reverse DNS)
|
||||||
|
|
||||||
|
**This is CRITICAL for email deliverability!** Without a proper PTR record, most mail servers will reject or spam your emails.
|
||||||
|
|
||||||
|
#### What is a PTR Record?
|
||||||
|
A PTR (pointer) record is a reverse DNS entry that maps your IP address back to your hostname. Mail servers use this to verify you're a legitimate mail server.
|
||||||
|
|
||||||
|
#### Required PTR Record
|
||||||
|
```
|
||||||
|
IP Address: 23.88.38.1
|
||||||
|
Points to: amzebs-01.amz.at
|
||||||
|
```
|
||||||
|
|
||||||
|
#### How to Configure PTR Record
|
||||||
|
|
||||||
|
**Step 1: Contact Your Hosting Provider**
|
||||||
|
|
||||||
|
PTR records MUST be configured through your hosting provider (e.g., Hetzner, OVH, AWS, etc.). You cannot set PTR records through your domain registrar.
|
||||||
|
|
||||||
|
1. Log into your hosting provider's control panel
|
||||||
|
2. Find the "Reverse DNS" or "PTR Record" section
|
||||||
|
3. Set the PTR record for IP `23.88.38.1` to point to `amzebs-01.amz.at`
|
||||||
|
|
||||||
|
**Common Provider Links:**
|
||||||
|
- **Hetzner**: Robot panel → IPs → Edit reverse DNS
|
||||||
|
- **OVH**: Network → IP → ... → Modify reverse
|
||||||
|
- **AWS EC2**: Select instance → Networking → Request reverse DNS
|
||||||
|
|
||||||
|
**Step 2: Verify Forward DNS First**
|
||||||
|
|
||||||
|
Before setting the PTR record, ensure your forward DNS is correct:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# This should return 23.88.38.1
|
||||||
|
dig +short amzebs-01.amz.at A
|
||||||
|
host amzebs-01.amz.at
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify PTR Record**
|
||||||
|
|
||||||
|
After configuring, verify the PTR record is working:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Method 1: Using dig
|
||||||
|
dig +short -x 23.88.38.1
|
||||||
|
|
||||||
|
# Method 2: Using host
|
||||||
|
host 23.88.38.1
|
||||||
|
|
||||||
|
# Method 3: Using nslookup
|
||||||
|
nslookup 23.88.38.1
|
||||||
|
```
|
||||||
|
|
||||||
|
All commands should return: `amzebs-01.amz.at`
|
||||||
|
|
||||||
|
**Step 4: Verify FCrDNS (Forward-Confirmed Reverse DNS)**
|
||||||
|
|
||||||
|
This ensures forward and reverse DNS match properly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Forward lookup
|
||||||
|
dig +short amzebs-01.amz.at
|
||||||
|
# Should output: 23.88.38.1
|
||||||
|
|
||||||
|
# Reverse lookup
|
||||||
|
dig +short -x 23.88.38.1
|
||||||
|
# Should output: amzebs-01.amz.at.
|
||||||
|
```
|
||||||
|
|
||||||
|
If both work correctly, FCrDNS passes! ✓
|
||||||
|
|
||||||
|
**Why PTR Records Matter:**
|
||||||
|
- Gmail, Microsoft, Yahoo require valid PTR records
|
||||||
|
- Missing PTR = automatic spam classification or rejection
|
||||||
|
- Can add 5-10 points to spam score alone
|
||||||
|
- Required for professional email delivery
|
||||||
|
|
||||||
|
### Domain DNS Records (amz.at)
|
||||||
|
|
||||||
|
Add these records through your domain registrar's DNS management:
|
||||||
|
|
||||||
|
#### SPF Record
|
||||||
|
```
|
||||||
|
Type: TXT
|
||||||
|
Name: @
|
||||||
|
Value: v=spf1 mx a:amzebs-01.amz.at ~all
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DKIM Record
|
||||||
|
```
|
||||||
|
Type: TXT
|
||||||
|
Name: amzebs-01._domainkey
|
||||||
|
Value: [Your public key from step 1 above]
|
||||||
|
```
|
||||||
|
|
||||||
|
The DKIM record will look something like:
|
||||||
|
```
|
||||||
|
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DMARC Record
|
||||||
|
```
|
||||||
|
Type: TXT
|
||||||
|
Name: _dmarc
|
||||||
|
Value: v=DMARC1; p=quarantine; rua=mailto:postmaster@amz.at; ruf=mailto:postmaster@amz.at; fo=1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Explanation:**
|
||||||
|
- `p=quarantine`: Failed messages should be quarantined (you can change to `p=reject` after testing)
|
||||||
|
- `rua=mailto:...`: Aggregate reports sent to this address
|
||||||
|
- `ruf=mailto:...`: Forensic reports sent to this address
|
||||||
|
- `fo=1`: Generate forensic reports for any failure
|
||||||
|
|
||||||
|
## Laravel Configuration
|
||||||
|
|
||||||
|
Update your Laravel application's `.env` file:
|
||||||
|
|
||||||
|
#### Option A: Using sendmail (Recommended)
|
||||||
|
```env
|
||||||
|
MAIL_MAILER=sendmail
|
||||||
|
MAIL_FROM_ADDRESS=noreply@amz.at
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B: Using SMTP
|
||||||
|
```env
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=25
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_ENCRYPTION=null
|
||||||
|
MAIL_FROM_ADDRESS=noreply@amz.at
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Laravel can use ANY email address with @amz.at domain. All will be DKIM signed automatically.
|
||||||
|
|
||||||
|
## Testing Email
|
||||||
|
|
||||||
|
### Test from Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send a test email
|
||||||
|
echo "Test email body" | mail -s "Test Subject" test@example.com -aFrom:test@amz.at
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Postfix Queue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View mail queue
|
||||||
|
mailq
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
journalctl -u postfix -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Rspamd Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View rspamd logs
|
||||||
|
journalctl -u rspamd -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test DKIM Signature and Deliverability
|
||||||
|
|
||||||
|
Send an email to test your complete email configuration:
|
||||||
|
|
||||||
|
#### Email Testing Services
|
||||||
|
1. **Mail Tester** (https://www.mail-tester.com/)
|
||||||
|
- Provides a temporary email address
|
||||||
|
- Shows comprehensive spam score (0-10, higher is better)
|
||||||
|
- Checks DKIM, SPF, DMARC, PTR, blacklists, content
|
||||||
|
- **Target: 9/10 or higher**
|
||||||
|
|
||||||
|
2. **MXToolbox Email Health** (https://mxtoolbox.com/emailhealth/)
|
||||||
|
- Comprehensive deliverability check
|
||||||
|
- Checks DNS records, blacklists, configuration
|
||||||
|
|
||||||
|
3. **Google Admin Toolbox** (https://toolbox.googleapps.com/apps/messageheader/)
|
||||||
|
- Paste email headers to see how Gmail scored your email
|
||||||
|
- Shows SPF, DKIM, DMARC results
|
||||||
|
|
||||||
|
#### What to Check
|
||||||
|
- ✓ DKIM signature is valid
|
||||||
|
- ✓ SPF passes
|
||||||
|
- ✓ DMARC passes
|
||||||
|
- ✓ PTR record (reverse DNS) matches
|
||||||
|
- ✓ Not on any blacklists
|
||||||
|
- ✓ Spam score < 2.0 (lower is better)
|
||||||
|
|
||||||
|
#### Common Issues & Fixes
|
||||||
|
|
||||||
|
**High Spam Score (> 5.0)**
|
||||||
|
- Check: PTR record configured correctly? (Critical!)
|
||||||
|
- Check: HELO name matches hostname?
|
||||||
|
- Check: All headers present (To:, From:, Subject:)?
|
||||||
|
- Check: IP not blacklisted?
|
||||||
|
|
||||||
|
**Missing "To:" Header**
|
||||||
|
Your Laravel app must set a recipient. In your code:
|
||||||
|
```php
|
||||||
|
Mail::to('recipient@example.com')
|
||||||
|
->send(new YourMailable());
|
||||||
|
```
|
||||||
|
|
||||||
|
**HELO/EHLO Mismatch**
|
||||||
|
After applying this configuration, HELO should be `amzebs-01.amz.at`, not `localhost`
|
||||||
|
|
||||||
|
**Check Current HELO Name**
|
||||||
|
```bash
|
||||||
|
# On the server
|
||||||
|
echo "HELO test" | nc localhost 25
|
||||||
|
# Should see: 250 amzebs-01.amz.at
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if Postfix is running
|
||||||
|
systemctl status postfix
|
||||||
|
|
||||||
|
# Check if Rspamd is running
|
||||||
|
systemctl status rspamd
|
||||||
|
|
||||||
|
# Check if Postfix is listening on localhost only
|
||||||
|
ss -tlnp | grep master
|
||||||
|
|
||||||
|
# View DKIM public key again
|
||||||
|
systemctl start rspamd-show-dkim
|
||||||
|
journalctl -u rspamd-show-dkim
|
||||||
|
|
||||||
|
# Check if DKIM key exists
|
||||||
|
ls -la /var/lib/rspamd/dkim/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
1. **Localhost-only**: Postfix is configured to listen ONLY on 127.0.0.1
|
||||||
|
2. **No authentication**: Not needed since only local processes can connect
|
||||||
|
3. **No firewall changes**: No external ports opened for email
|
||||||
|
4. **DKIM signing**: All outgoing emails are automatically signed with DKIM
|
||||||
|
5. **Host-specific key**: Using selector "amzebs-01" allows multiple hosts to send for amz.at
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Email not being sent
|
||||||
|
|
||||||
|
1. Check Postfix status: `systemctl status postfix`
|
||||||
|
2. Check queue: `mailq`
|
||||||
|
3. Check logs: `journalctl -u postfix -n 100`
|
||||||
|
|
||||||
|
### DKIM not signing
|
||||||
|
|
||||||
|
1. Check Rspamd status: `systemctl status rspamd`
|
||||||
|
2. Check if key exists: `ls -la /var/lib/rspamd/dkim/amz.at.amzebs-01.key`
|
||||||
|
3. Check Rspamd logs: `journalctl -u rspamd -n 100`
|
||||||
|
|
||||||
|
### Permission errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure proper ownership
|
||||||
|
chown -R rspamd:rspamd /var/lib/rspamd/dkim/
|
||||||
|
chmod 600 /var/lib/rspamd/dkim/*.key
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rotate DKIM key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Generate new key pair locally (follow "Initial Setup" steps)
|
||||||
|
# 2. Update the rspamd-dkim-key in secrets.yaml with new key
|
||||||
|
# 3. Deploy the configuration
|
||||||
|
nixos-rebuild switch
|
||||||
|
|
||||||
|
# 4. Restart the setup service to copy new key
|
||||||
|
systemctl restart rspamd-dkim-setup
|
||||||
|
|
||||||
|
# 5. Restart rspamd to use new key
|
||||||
|
systemctl restart rspamd
|
||||||
|
|
||||||
|
# 6. Update DNS with new public key
|
||||||
|
# 7. Wait for DNS propagation before removing old DNS record
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- Postfix config: `hosts/amzebs-01/modules/postfix.nix`
|
||||||
|
- Rspamd config: `hosts/amzebs-01/modules/rspamd.nix`
|
||||||
|
- Main config: `hosts/amzebs-01/configuration.nix`
|
||||||
|
- Secrets file: `hosts/amzebs-01/secrets.yaml` (encrypted)
|
||||||
|
|
||||||
|
## Sops Secret Configuration
|
||||||
|
|
||||||
|
The DKIM private key is stored as a sops secret with the following configuration:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
sops.secrets.rspamd-dkim-key = {
|
||||||
|
owner = "rspamd";
|
||||||
|
group = "rspamd";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures:
|
||||||
|
- Only the rspamd user can read the key
|
||||||
|
- The key is decrypted at boot time by sops-nix
|
||||||
|
- The key is encrypted in version control
|
||||||
|
- The key persists across rebuilds
|
||||||
|
|
||||||
|
The key is automatically copied from the sops secret path to `/var/lib/rspamd/dkim/amz.at.amzebs-01.key` by the `rspamd-dkim-setup.service` on every boot.
|
||||||
1
hosts/amzebs-01/channel
Normal file
1
hosts/amzebs-01/channel
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://channels.nixos.org/nixos-25.11
|
||||||
81
hosts/amzebs-01/configuration.nix
Normal file
81
hosts/amzebs-01/configuration.nix
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{ config, lib, pkgs, ... }: {
|
||||||
|
imports = [
|
||||||
|
./utils/bento.nix
|
||||||
|
./utils/modules/sops.nix
|
||||||
|
./utils/modules/nginx.nix
|
||||||
|
./utils/modules/set-nix-channel.nix
|
||||||
|
|
||||||
|
./modules/mysql.nix
|
||||||
|
./modules/web/stack.nix
|
||||||
|
./modules/laravel-storage.nix
|
||||||
|
./modules/laravel-scheduler.nix
|
||||||
|
./modules/blackbox-exporter.nix
|
||||||
|
./modules/postfix.nix
|
||||||
|
./modules/rspamd.nix
|
||||||
|
|
||||||
|
./utils/modules/autoupgrade.nix
|
||||||
|
./utils/modules/promtail
|
||||||
|
./utils/modules/victoriametrics
|
||||||
|
./utils/modules/borgbackup.nix
|
||||||
|
|
||||||
|
./hardware-configuration.nix
|
||||||
|
|
||||||
|
./sites
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
vim
|
||||||
|
screen
|
||||||
|
php82
|
||||||
|
];
|
||||||
|
|
||||||
|
time.timeZone = "Europe/Vienna";
|
||||||
|
|
||||||
|
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||||
|
sops.defaultSopsFile = ./secrets.yaml;
|
||||||
|
|
||||||
|
nix.gc = {
|
||||||
|
automatic = true;
|
||||||
|
options = "--delete-older-than 60d";
|
||||||
|
};
|
||||||
|
|
||||||
|
boot.tmp.cleanOnBoot = true;
|
||||||
|
zramSwap.enable = true;
|
||||||
|
|
||||||
|
networking.hostName = "amzebs-01";
|
||||||
|
networking.domain = "cloonar.com";
|
||||||
|
|
||||||
|
services.openssh.enable = true;
|
||||||
|
users.users.root.openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7"
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFshMhXwS0FQFPlITipshvNKrV8sA52ZFlnaoHd1thKg"
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRQuPqH5fdX3KEw7DXzWEdO3AlUn1oSmtJtHB71ICoH Generated By Termius"
|
||||||
|
];
|
||||||
|
|
||||||
|
programs.ssh = {
|
||||||
|
knownHosts = {
|
||||||
|
"git.cloonar.com" = {
|
||||||
|
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDlUj7eEfS/4+z/3IhFhOTXAfpGEpNv6UWuYSL5OAhus";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# backups - adjust repo for this host
|
||||||
|
borgbackup.repo = "u149513-sub10@u149513-sub10.your-backup.de:borg";
|
||||||
|
|
||||||
|
# Use HTTP-01 challenge for Let's Encrypt (not DNS)
|
||||||
|
security.acme.acceptTerms = true;
|
||||||
|
security.acme.defaults.email = "admin+acme@cloonar.com";
|
||||||
|
|
||||||
|
networking.firewall = {
|
||||||
|
enable = true;
|
||||||
|
allowedTCPPorts = [ 22 80 443 3306 ];
|
||||||
|
|
||||||
|
# Allow MariaDB access only from specific IP
|
||||||
|
extraCommands = ''
|
||||||
|
iptables -A nixos-fw -p tcp --dport 3306 -s 77.119.230.30 -j nixos-fw-accept
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "25.11";
|
||||||
|
}
|
||||||
27
hosts/amzebs-01/hardware-configuration.nix
Normal file
27
hosts/amzebs-01/hardware-configuration.nix
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Hardware configuration for amzebs-01
|
||||||
|
# This is a template - update with actual hardware configuration after installation
|
||||||
|
{ modulesPath, ... }:
|
||||||
|
{
|
||||||
|
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
|
||||||
|
|
||||||
|
boot.loader.grub = {
|
||||||
|
efiSupport = true;
|
||||||
|
efiInstallAsRemovable = true;
|
||||||
|
device = "nodev";
|
||||||
|
configurationLimit = 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Update these with actual device UUIDs and paths after installation
|
||||||
|
fileSystems."/boot" = {
|
||||||
|
device = "/dev/sda15";
|
||||||
|
fsType = "vfat";
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/" = {
|
||||||
|
device = "/dev/sda1";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" ];
|
||||||
|
boot.initrd.kernelModules = [ "nvme" ];
|
||||||
|
}
|
||||||
83
hosts/amzebs-01/modules/blackbox-exporter.nix
Normal file
83
hosts/amzebs-01/modules/blackbox-exporter.nix
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
hostname = config.networking.hostName;
|
||||||
|
|
||||||
|
cfg = config.services.blackbox-exporter;
|
||||||
|
nginxVHosts = config.services.nginx.virtualHosts or {};
|
||||||
|
allDomains = lib.attrNames nginxVHosts;
|
||||||
|
filteredDomains = builtins.filter (d: !builtins.elem d cfg.blacklistDomains) allDomains;
|
||||||
|
httpsDomains = lib.map (d: "https://${d}") filteredDomains;
|
||||||
|
domainsString = builtins.concatStringsSep "\n "
|
||||||
|
(map (d: "\"${d}\",") httpsDomains);
|
||||||
|
in {
|
||||||
|
options.services.blackbox-exporter.blacklistDomains = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
description = "List of domains to exclude from Blackbox Exporter monitoring";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
services.blackbox-exporter = {
|
||||||
|
blacklistDomains = [
|
||||||
|
# Currently no domains blacklisted - monitoring all nginx virtualHosts
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Systemd service for Blackbox Exporter
|
||||||
|
systemd.services.blackbox-exporter = {
|
||||||
|
description = "Blackbox Exporter";
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig.ExecStart = ''
|
||||||
|
${pkgs.prometheus-blackbox-exporter}/bin/blackbox_exporter \
|
||||||
|
--config.file=/etc/blackbox_exporter/blackbox.yml
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Configuration file for Blackbox Exporter
|
||||||
|
environment.etc."blackbox_exporter/blackbox.yml".text = ''
|
||||||
|
modules:
|
||||||
|
http_200_final:
|
||||||
|
prober: http
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
follow_redirects: true
|
||||||
|
preferred_ip_protocol: "ip4" # avoid blanket IPv6 failures
|
||||||
|
valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
|
||||||
|
valid_status_codes: [200]
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Add scrape config for VictoriaMetrics agent
|
||||||
|
services.victoriametrics.extraScrapeConfigs = [
|
||||||
|
''
|
||||||
|
- job_name: "blackbox_http_all_domains"
|
||||||
|
metrics_path: "/probe"
|
||||||
|
params:
|
||||||
|
module: ["http_200_final"]
|
||||||
|
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
[
|
||||||
|
${domainsString}
|
||||||
|
]
|
||||||
|
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: ["__address__"]
|
||||||
|
target_label: "__param_target"
|
||||||
|
regex: '(.*)'
|
||||||
|
replacement: "$1"
|
||||||
|
- source_labels: ["__param_target"]
|
||||||
|
target_label: "instance"
|
||||||
|
- target_label: "__address__"
|
||||||
|
replacement: "127.0.0.1:9115"
|
||||||
|
- source_labels: ["__address__"]
|
||||||
|
regex: "127\\.0\\.0\\.1:9115"
|
||||||
|
target_label: "__scheme__"
|
||||||
|
replacement: "http"
|
||||||
|
''
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
51
hosts/amzebs-01/modules/laravel-scheduler.nix
Normal file
51
hosts/amzebs-01/modules/laravel-scheduler.nix
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
# Daily scheduled Laravel artisan jobs
|
||||||
|
# Runs artisan finish:reports at 01:00 for production and staging APIs
|
||||||
|
|
||||||
|
let
|
||||||
|
php = pkgs.php82;
|
||||||
|
|
||||||
|
sites = [
|
||||||
|
{
|
||||||
|
domain = "api.ebs.amz.at";
|
||||||
|
user = "api_ebs_amz_at";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
domain = "api.stage.ebs.amz.at";
|
||||||
|
user = "api_stage_ebs_amz_at";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
mkArtisanService = site: {
|
||||||
|
name = "artisan-finish-reports-${site.domain}";
|
||||||
|
value = {
|
||||||
|
description = "Laravel artisan finish:reports for ${site.domain}";
|
||||||
|
after = [ "network.target" "mysql.service" "phpfpm-${site.domain}.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
User = site.user;
|
||||||
|
Group = "nginx";
|
||||||
|
WorkingDirectory = "/var/www/${site.domain}";
|
||||||
|
ExecStart = "${php}/bin/php artisan finish:reports";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mkArtisanTimer = site: {
|
||||||
|
name = "artisan-finish-reports-${site.domain}";
|
||||||
|
value = {
|
||||||
|
description = "Daily timer for artisan finish:reports on ${site.domain}";
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "*-*-* 01:00:00";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
systemd.services = builtins.listToAttrs (map mkArtisanService sites);
|
||||||
|
systemd.timers = builtins.listToAttrs (map mkArtisanTimer sites);
|
||||||
|
}
|
||||||
30
hosts/amzebs-01/modules/laravel-storage.nix
Normal file
30
hosts/amzebs-01/modules/laravel-storage.nix
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
# Create Laravel storage directories for all API instances
|
||||||
|
# These directories are required for Laravel to function properly
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
# api.ebs.cloonar.dev
|
||||||
|
"d /var/www/api.ebs.cloonar.dev/storage/framework/cache 0775 api_ebs_cloonar_dev nginx -"
|
||||||
|
"d /var/www/api.ebs.cloonar.dev/storage/framework/testing 0775 api_ebs_cloonar_dev nginx -"
|
||||||
|
"d /var/www/api.ebs.cloonar.dev/storage/framework/sessions 0775 api_ebs_cloonar_dev nginx -"
|
||||||
|
"d /var/www/api.ebs.cloonar.dev/storage/framework/views 0775 api_ebs_cloonar_dev nginx -"
|
||||||
|
"d /var/www/api.ebs.cloonar.dev/storage/logs 0775 api_ebs_cloonar_dev nginx -"
|
||||||
|
"d /var/www/api.ebs.cloonar.dev/bootstrap/cache 0775 api_ebs_cloonar_dev nginx -"
|
||||||
|
|
||||||
|
# api.ebs.amz.at
|
||||||
|
"d /var/www/api.ebs.amz.at/storage/framework/cache 0775 api_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.ebs.amz.at/storage/framework/testing 0775 api_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.ebs.amz.at/storage/framework/sessions 0775 api_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.ebs.amz.at/storage/framework/views 0775 api_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.ebs.amz.at/storage/logs 0775 api_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.ebs.amz.at/bootstrap/cache 0775 api_ebs_amz_at nginx -"
|
||||||
|
|
||||||
|
# api.stage.ebs.amz.at
|
||||||
|
"d /var/www/api.stage.ebs.amz.at/storage/framework/cache 0775 api_stage_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.stage.ebs.amz.at/storage/framework/testing 0775 api_stage_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.stage.ebs.amz.at/storage/framework/sessions 0775 api_stage_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.stage.ebs.amz.at/storage/framework/views 0775 api_stage_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.stage.ebs.amz.at/storage/logs 0775 api_stage_ebs_amz_at nginx -"
|
||||||
|
"d /var/www/api.stage.ebs.amz.at/bootstrap/cache 0775 api_stage_ebs_amz_at nginx -"
|
||||||
|
];
|
||||||
|
}
|
||||||
43
hosts/amzebs-01/modules/mysql.nix
Normal file
43
hosts/amzebs-01/modules/mysql.nix
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{ pkgs, config, ... }:
|
||||||
|
{
|
||||||
|
|
||||||
|
services.mysql = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.mariadb;
|
||||||
|
settings = {
|
||||||
|
mysqld = {
|
||||||
|
max_allowed_packet = "64M";
|
||||||
|
transaction_isolation = "READ-COMMITTED";
|
||||||
|
binlog_format = "ROW";
|
||||||
|
# Allow remote connections
|
||||||
|
bind-address = "0.0.0.0";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create read-only user for remote access after MySQL starts
|
||||||
|
systemd.services.mysql-setup-readonly-user = {
|
||||||
|
description = "Setup MySQL read-only user";
|
||||||
|
after = [ "mysql.service" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
User = "root";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
PASSWORD=$(cat ${config.sops.secrets.mysql-readonly-password.path})
|
||||||
|
${pkgs.mariadb}/bin/mysql -u root <<EOF
|
||||||
|
CREATE USER IF NOT EXISTS 'api_ebs_amz_at_ro'@'%' IDENTIFIED BY '$PASSWORD';
|
||||||
|
GRANT SELECT ON api_ebs_amz_at.* TO 'api_ebs_amz_at_ro'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.mysqlBackup.enable = true;
|
||||||
|
|
||||||
|
sops.secrets.mysql-readonly-password = {
|
||||||
|
owner = "mysql";
|
||||||
|
};
|
||||||
|
}
|
||||||
56
hosts/amzebs-01/modules/postfix.nix
Normal file
56
hosts/amzebs-01/modules/postfix.nix
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{ pkgs
|
||||||
|
, lib
|
||||||
|
, config
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
headerChecksFile = pkgs.writeText "header_checks" ''
|
||||||
|
# Warn about missing critical headers (but don't reject from localhost)
|
||||||
|
# These help identify misconfigured applications
|
||||||
|
/^$/ WARN Missing headers detected
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
services.postfix = {
|
||||||
|
mapFiles."header_checks" = headerChecksFile;
|
||||||
|
enable = true;
|
||||||
|
hostname = "amzebs-01.amz.at";
|
||||||
|
domain = "amz.at";
|
||||||
|
|
||||||
|
config = {
|
||||||
|
# Explicitly set hostname to prevent "localhost" HELO issues
|
||||||
|
myhostname = "amzebs-01.amz.at";
|
||||||
|
|
||||||
|
# Set proper HELO name for outgoing SMTP connections
|
||||||
|
smtp_helo_name = "amzebs-01.amz.at";
|
||||||
|
|
||||||
|
# Professional SMTP banner (prevents appearing as default/misconfigured)
|
||||||
|
smtpd_banner = "$myhostname ESMTP";
|
||||||
|
|
||||||
|
# Listen only on localhost for security
|
||||||
|
# Laravel will send via localhost, no external access needed
|
||||||
|
inet_interfaces = "loopback-only";
|
||||||
|
|
||||||
|
# Compatibility
|
||||||
|
compatibility_level = "2";
|
||||||
|
|
||||||
|
# Only accept mail from localhost
|
||||||
|
mynetworks = [ "127.0.0.0/8" "[::1]/128" ];
|
||||||
|
|
||||||
|
# Larger message size limits for attachments
|
||||||
|
mailbox_size_limit = 202400000; # ~200MB
|
||||||
|
message_size_limit = 51200000; # ~50MB
|
||||||
|
|
||||||
|
# Ensure proper header handling
|
||||||
|
# Reject mail that's missing critical headers
|
||||||
|
header_checks = "regexp:/var/lib/postfix/conf/header_checks";
|
||||||
|
|
||||||
|
# Rate limiting to prevent spam-like behavior
|
||||||
|
# Allow reasonable sending rates for applications
|
||||||
|
smtpd_client_message_rate_limit = 100;
|
||||||
|
smtpd_client_recipient_rate_limit = 200;
|
||||||
|
|
||||||
|
# Milter configuration is handled automatically by rspamd.postfix.enable
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
84
hosts/amzebs-01/modules/rspamd.nix
Normal file
84
hosts/amzebs-01/modules/rspamd.nix
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{ pkgs
|
||||||
|
, config
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
domain = "amz.at";
|
||||||
|
selector = "amzebs-01";
|
||||||
|
|
||||||
|
localConfig = pkgs.writeText "local.conf" ''
|
||||||
|
logging {
|
||||||
|
level = "notice";
|
||||||
|
}
|
||||||
|
|
||||||
|
# DKIM signing configuration with host-specific selector
|
||||||
|
dkim_signing {
|
||||||
|
path = "/var/lib/rspamd/dkim/${domain}.${selector}.key";
|
||||||
|
selector = "${selector}";
|
||||||
|
allow_username_mismatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ARC signing (Authenticated Received Chain)
|
||||||
|
arc {
|
||||||
|
path = "/var/lib/rspamd/dkim/${domain}.${selector}.key";
|
||||||
|
selector = "${selector}";
|
||||||
|
allow_username_mismatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add authentication results to headers
|
||||||
|
milter_headers {
|
||||||
|
use = ["authentication-results"];
|
||||||
|
authenticated_headers = ["authentication-results"];
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
services.rspamd = {
|
||||||
|
enable = true;
|
||||||
|
extraConfig = ''
|
||||||
|
.include(priority=1,duplicate=merge) "${localConfig}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Enable Postfix milter integration
|
||||||
|
postfix.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Copy DKIM key from sops secret to rspamd directory
|
||||||
|
systemd.services.rspamd-dkim-setup = {
|
||||||
|
description = "Setup DKIM key from sops secret for ${domain}";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "rspamd.service" ];
|
||||||
|
after = [ "sops-nix.service" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
DKIM_DIR="/var/lib/rspamd/dkim"
|
||||||
|
DKIM_KEY="$DKIM_DIR/${domain}.${selector}.key"
|
||||||
|
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
mkdir -p "$DKIM_DIR"
|
||||||
|
|
||||||
|
# Copy key from sops secret
|
||||||
|
if [ -f "${config.sops.secrets.rspamd-dkim-key.path}" ]; then
|
||||||
|
cp "${config.sops.secrets.rspamd-dkim-key.path}" "$DKIM_KEY"
|
||||||
|
chown rspamd:rspamd "$DKIM_KEY"
|
||||||
|
chmod 600 "$DKIM_KEY"
|
||||||
|
echo "DKIM key deployed successfully from sops secret"
|
||||||
|
else
|
||||||
|
echo "ERROR: DKIM key not found in sops secrets!"
|
||||||
|
echo "Please ensure rspamd-dkim-key is defined in secrets.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
sops.secrets.rspamd-dkim-key = {
|
||||||
|
owner = "rspamd";
|
||||||
|
group = "rspamd";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
}
|
||||||
321
hosts/amzebs-01/modules/web/stack.nix
Normal file
321
hosts/amzebs-01/modules/web/stack.nix
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.webstack;
|
||||||
|
|
||||||
|
instanceOpts = { name, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
user = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
User of the typo3 instance. Defaults to attribute name in instances.
|
||||||
|
'';
|
||||||
|
example = "example.org";
|
||||||
|
};
|
||||||
|
|
||||||
|
domain = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Domain of the typo3 instance. Defaults to attribute name in instances.
|
||||||
|
'';
|
||||||
|
example = "example.org";
|
||||||
|
};
|
||||||
|
|
||||||
|
domainAliases = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
example = [ "www.example.org" "example.org" ];
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Additional domains served by this typo3 instance.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
phpPackage = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
example = literalExpression "pkgs.php";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Which PHP package to use in this typo3 instance.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
phpOptions = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
description = ''
|
||||||
|
"Options appended to the PHP configuration file {file}`php.ini` used for this PHP-FPM pool."
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
enableMysql = mkEnableOption (lib.mdDoc "MySQL Database");
|
||||||
|
enableDefaultLocations = mkEnableOption (lib.mdDoc "Create default nginx location directives") // { default = true; };
|
||||||
|
enablePhp = mkEnableOption (lib.mdDoc "PHP-FPM support") // { default = true; };
|
||||||
|
|
||||||
|
authorizedKeys = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = null;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Authorized keys for the typo3 instance ssh user.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraConfig = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = ''
|
||||||
|
if (!-e $request_filename) {
|
||||||
|
rewrite ^/(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gzip)$ /$1.$3 last;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
These lines go to the end of the vhost verbatim.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
locations = mkOption {
|
||||||
|
type = types.attrsOf (types.submodule (import <nixpkgs/nixos/modules/services/web-servers/nginx/location-options.nix> {
|
||||||
|
inherit lib config;
|
||||||
|
}));
|
||||||
|
default = {};
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
"/" = {
|
||||||
|
proxyPass = "http://localhost:3000";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
'';
|
||||||
|
description = lib.mdDoc "Declarative location config";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
options.services.webstack = {
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/www";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
The data directory for MySQL.
|
||||||
|
|
||||||
|
::: {.note}
|
||||||
|
If left as the default value of `/var/www` this directory will automatically be created before the web
|
||||||
|
server starts, otherwise you are responsible for ensuring the directory exists with appropriate ownership and permissions.
|
||||||
|
:::
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
instances = mkOption {
|
||||||
|
type = types.attrsOf (types.submodule instanceOpts);
|
||||||
|
default = {};
|
||||||
|
description = lib.mdDoc "Create vhosts for typo3";
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
"typo3.example.com" = {
|
||||||
|
domain = "example.com";
|
||||||
|
domainAliases = [ "www.example.com" ];
|
||||||
|
phpPackage = pkgs.php81;
|
||||||
|
authorizedKeys = [
|
||||||
|
"ssh-rsa AZA=="
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
systemd.services = mapAttrs' (instance: instanceOpts:
|
||||||
|
let
|
||||||
|
domain = if instanceOpts.domain != null then instanceOpts.domain else instance;
|
||||||
|
in
|
||||||
|
nameValuePair "phpfpm-${domain}" {
|
||||||
|
serviceConfig = {
|
||||||
|
ProtectHome = lib.mkForce "tmpfs";
|
||||||
|
BindPaths = "BindPaths=/var/www/${domain}:/var/www/${domain}";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) (lib.filterAttrs (name: opts: opts.enablePhp) cfg.instances);
|
||||||
|
|
||||||
|
services.phpfpm.pools = mapAttrs' (instance: instanceOpts:
|
||||||
|
let
|
||||||
|
domain = if instanceOpts.domain != null then instanceOpts.domain else instance;
|
||||||
|
user = if instanceOpts.user != null
|
||||||
|
then instanceOps.user
|
||||||
|
else builtins.replaceStrings ["." "-"] ["_" "_"] domain;
|
||||||
|
in
|
||||||
|
nameValuePair domain {
|
||||||
|
user = user;
|
||||||
|
settings = {
|
||||||
|
"listen.owner" = config.services.nginx.user;
|
||||||
|
"pm" = "dynamic";
|
||||||
|
"pm.max_children" = 32;
|
||||||
|
"pm.max_requests" = 500;
|
||||||
|
"pm.start_servers" = 2;
|
||||||
|
"pm.min_spare_servers" = 2;
|
||||||
|
"pm.max_spare_servers" = 5;
|
||||||
|
"php_admin_value[error_log]" = "syslog";
|
||||||
|
"php_admin_value[max_execution_time]" = 240;
|
||||||
|
"php_admin_value[max_input_vars]" = 1500;
|
||||||
|
"access.log" = "/var/log/$pool.access.log";
|
||||||
|
};
|
||||||
|
phpOptions = instanceOpts.phpOptions;
|
||||||
|
phpPackage = instanceOpts.phpPackage;
|
||||||
|
phpEnv."PATH" = pkgs.lib.makeBinPath [ instanceOpts.phpPackage ];
|
||||||
|
}
|
||||||
|
) (lib.filterAttrs (name: opts: opts.enablePhp) cfg.instances);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
config.services.nginx.virtualHosts = mapAttrs' (instance: instanceOpts:
|
||||||
|
let
|
||||||
|
domain = if instanceOpts.domain != null then instanceOpts.domain else instance;
|
||||||
|
user = if instanceOpts.user != null
|
||||||
|
then instanceOps.user
|
||||||
|
else builtins.replaceStrings ["." "-"] ["_" "_"] domain;
|
||||||
|
in
|
||||||
|
nameValuePair domain {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
acmeRoot = null;
|
||||||
|
root = cfg.dataDir + "/" + domain + "/public";
|
||||||
|
|
||||||
|
locations = lib.mkMerge [
|
||||||
|
instanceOpts.locations
|
||||||
|
(mkIf instanceOpts.enableDefaultLocations {
|
||||||
|
"/favicon.ico".extraConfig = ''
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Cache.appcache, your document html and data
|
||||||
|
"~* \\.(?:manifest|appcache|html?|xml|json)$".extraConfig = ''
|
||||||
|
expires -1;
|
||||||
|
# access_log logs/static.log; # I don't usually include a static log
|
||||||
|
'';
|
||||||
|
|
||||||
|
"~* \\.(jpe?g|png)$".extraConfig = ''
|
||||||
|
set $red Z;
|
||||||
|
|
||||||
|
if ($http_accept ~* "webp") {
|
||||||
|
set $red A;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-f $document_root/webp/$request_uri.webp) {
|
||||||
|
set $red "''${red}B";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($red = "AB") {
|
||||||
|
add_header Vary Accept;
|
||||||
|
rewrite ^ /webp/$request_uri.webp;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Cache Media: images, icons, video, audio, HTC
|
||||||
|
"~* \\.(?:css|js|jpg|jpeg|gif|png|webp|avif|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff2)$".extraConfig = ''
|
||||||
|
expires 1y;
|
||||||
|
access_log off;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Feed
|
||||||
|
"~* \\.(?:rss|atom)$".extraConfig = ''
|
||||||
|
expires 1h;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
'';
|
||||||
|
|
||||||
|
"/".extraConfig = ''
|
||||||
|
index index.php index.html;
|
||||||
|
try_files $uri $uri/ /index.php$is_args$args;
|
||||||
|
'';
|
||||||
|
})
|
||||||
|
(mkIf instanceOpts.enablePhp {
|
||||||
|
"~ [^/]\\.php(/|$)".extraConfig = ''
|
||||||
|
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
|
||||||
|
if (!-f $document_root$fastcgi_script_name) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
include ${pkgs.nginx}/conf/fastcgi_params;
|
||||||
|
include ${pkgs.nginx}/conf/fastcgi.conf;
|
||||||
|
fastcgi_buffer_size 32k;
|
||||||
|
fastcgi_buffers 8 16k;
|
||||||
|
fastcgi_connect_timeout 240s;
|
||||||
|
fastcgi_read_timeout 240s;
|
||||||
|
fastcgi_send_timeout 240s;
|
||||||
|
fastcgi_pass unix:${config.services.phpfpm.pools."${domain}".socket};
|
||||||
|
fastcgi_index index.php;
|
||||||
|
'';
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
extraConfig = instanceOpts.extraConfig;
|
||||||
|
|
||||||
|
|
||||||
|
# locations = mapAttrs' (location: locationOpts:
|
||||||
|
# nameValuePair location locationOpts) instanceOpts.locations;
|
||||||
|
|
||||||
|
}
|
||||||
|
) cfg.instances;
|
||||||
|
|
||||||
|
config.users.users = mapAttrs' (instance: instanceOpts:
|
||||||
|
let
|
||||||
|
domain = if instanceOpts.domain != null then instanceOpts.domain else instance;
|
||||||
|
user = if instanceOpts.user != null
|
||||||
|
then instanceOps.user
|
||||||
|
else builtins.replaceStrings ["." "-"] ["_" "_"] domain;
|
||||||
|
in
|
||||||
|
nameValuePair user {
|
||||||
|
isNormalUser = true;
|
||||||
|
createHome = true;
|
||||||
|
home = "/var/www/" + domain;
|
||||||
|
homeMode= "770";
|
||||||
|
group = config.services.nginx.group;
|
||||||
|
openssh.authorizedKeys.keys = instanceOpts.authorizedKeys;
|
||||||
|
}
|
||||||
|
) cfg.instances;
|
||||||
|
config.users.groups = mapAttrs' (instance: instanceOpts:
|
||||||
|
let
|
||||||
|
domain = if instanceOpts.domain != null then instanceOpts.domain else instance;
|
||||||
|
user = if instanceOpts.user != null
|
||||||
|
then instanceOps.user
|
||||||
|
else builtins.replaceStrings ["." "-"] ["_" "_"] domain;
|
||||||
|
in nameValuePair user {}) cfg.instances;
|
||||||
|
|
||||||
|
config.services.mysql.ensureUsers = mapAttrsToList (instance: instanceOpts:
|
||||||
|
let
|
||||||
|
domain = if instanceOpts.domain != null then instanceOpts.domain else instance;
|
||||||
|
user = if instanceOpts.user != null
|
||||||
|
then instanceOps.user
|
||||||
|
else builtins.replaceStrings ["." "-"] ["_" "_"] domain;
|
||||||
|
in
|
||||||
|
mkIf instanceOpts.enableMysql {
|
||||||
|
name = user;
|
||||||
|
ensurePermissions = {
|
||||||
|
"${user}.*" = "ALL PRIVILEGES";
|
||||||
|
};
|
||||||
|
}) cfg.instances;
|
||||||
|
|
||||||
|
config.services.mysql.ensureDatabases = mapAttrsToList (instance: instanceOpts:
|
||||||
|
let
|
||||||
|
domain = if instanceOpts.domain != null then instanceOpts.domain else instance;
|
||||||
|
user = if instanceOpts.user != null
|
||||||
|
then instanceOps.user
|
||||||
|
else builtins.replaceStrings ["." "-"] ["_" "_"] domain;
|
||||||
|
in
|
||||||
|
mkIf instanceOpts.enableMysql user
|
||||||
|
) cfg.instances;
|
||||||
|
config.services.mysqlBackup.databases = mapAttrsToList (instance: instanceOpts:
|
||||||
|
let
|
||||||
|
domain = if instanceOpts.domain != null then instanceOpts.domain else instance;
|
||||||
|
user = if instanceOpts.user != null
|
||||||
|
then instanceOps.user
|
||||||
|
else builtins.replaceStrings ["." "-"] ["_" "_"] domain;
|
||||||
|
in
|
||||||
|
mkIf instanceOpts.enableMysql user
|
||||||
|
) cfg.instances;
|
||||||
|
}
|
||||||
46
hosts/amzebs-01/secrets.yaml
Normal file
46
hosts/amzebs-01/secrets.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
borg-passphrase: ENC[AES256_GCM,data:Q2GvEat5EHmshFiya3yNqFTVS+oJv0al+bYMRwysb0yu7F2gCJd000Y3ibA+tUPSL9iSlMSy0cTkesGVEGBt9w==,iv:/kUJXgibF1cyaCPB55/0nKYq9sSva6psxu2P/l7iRN4=,tag:velr9LTfoj7gEWhUmvPtQg==,type:str]
|
||||||
|
borg-ssh-key: ENC[AES256_GCM,data:0YEvv7QDmGsur0PFMmz5HqDgDCEk0kRaOu1n7GGWAwmmr0K0bVbpKzHw5wMiMnXEBrvj0izo4P4LYGAlAAYn22Bhgi2eN/vdvSO5V5uDV1ep2dV/TN2m3oYgTIgot47YgwBhcNunIUtEsbZuhAsTGL6LFBPJ3OCLKvhXTNTDaajgH/e4CvyxHHl63MBzr0i1ajigl1IKCk2hhZF4Kd1YGBCVZRoNyyNXywihlcFeskNfldW/sd5Qn2nowVf1MEV9n6Il6Zc1FX69WUVy1k+kOT7HJZGq3uDmgwXQgQhqKm1wh5uOlLkGUX6fz/nz+YFzLFMuUVvs34CzbbEFuWmGU+aNQrfCfI1hqwB5s6wVNdpUmigX9AQMQklu85tHFJg1AaRvhA24Cp/GrptggrTThcjwVFoe9NSQouNYn+ImTvlsE4HuDRRFE6YUounGd2lpRd40LsEjwKiLtwBwqG94u4ZOI91+LG6ZqHftRehE9r/CtedLyqtluNyyQyUNKPraUOm9Rrapewsj0ZCZgGQU,iv:xdRUBQlZlwVIog5KgZRmGNxdmhFE9HgnK3Ahfo+zT9k=,tag:McsJKUEGnKXxiv8Tg5zA4A==,type:str]
|
||||||
|
mysql-readonly-password: ENC[AES256_GCM,data:k2RplkUZPGZlh29KXXdtwe+MCqKzTI/bLdyuEeicdkbGlBk1SGyLF8vW4t8=,iv:a14IrXYVCDqPKGfJSEPP8g19sPvRTx5NT8IVJJeL48s=,tag:GWgU3oa/+u21/L2y3+vOsw==,type:str]
|
||||||
|
rspamd-dkim-key: ENC[AES256_GCM,data:maOnsx8AQUIjXqHEzHLxtSvAkr9+YCZid9xWaflkffS0gHd/hoHrozHy+rHSjU7Mz7QHYhjUjFY7Hp7wdKQnHpQLJRV96iNPXTXXYtBr7oDL51cq8ozd094FuMeLNSPitV89OHDcM+9h1F4dsdDWPUiw7eoijQeZ8vx1/VCVAp4FVxTFX3qhoMhXlFabiyM85eKMwJG4BdSwqS624f2Z4tvECRp0pBGtd/3r4/EVRDV1qNsiFvH8mi8eyg9xiWDLDrePq4TuWSu1Xc7z0qpDy0o8iAwGhPu9egIyzHEPk07j9U7PpK56C2UCSY0JBm0hkBGbqLXyRklSMytxoKgw4GJykMwNPNXmA9yuLPanxagJB/z8b7X4HTuYhExzQcC6ke/y8xKcxU4qGt8Ayy5v+QoNpdqXIPsZkIuw9uWm6RIgDt2dCaOdI06lesZKjqU/T6EhDfGoGZX7DwQ7uV9xNDM4NW2jsKpUdFKnzPCCe7/jO/ck4P4i8V+6NWDjj4+/BXDNnKJbMcHIHoSvckCGZiginJsbGvSWd0HfbpR7GQAnL3uKB5/HuFAaUkx+dPHmmP2tOBv6vNt+tq+V9i4kQmwAdl8a9KI456tw9vLwXcBDZOO7n4X5H0jc4afoYCnvLxahvbIXm2QNcBYVKxkqBCvoYEMrBjrnujwbQdEfDKQf3g5p8LQwAfCQ3ng+XH/BDF3qMBdsN1u5Di0FQpCDaGKX8pJ1gg+il76fJgSU8ftoaT32hJnLAjal4cgNIxbta2UYQLixUqaWZ8xqvxrSopkWYrlBBUyQh9jMEoTzpxwCsEPQ72qgVcQfJYlMl4WUBwcasfJnySR+qZ22g3fhStpAQ2HuTGhLjTG1QOewYdwDXDhNmcbqZ478Sp1t7qbBx0R7vWFSyYCMlbmLmvzPm6Z3ET7lkfCrMjMNXaQ8cWSF19QaHAfqRwQooLL93yx7U0KHCilEg4bUjsw8MLQNa4A0ohpq6CG4s5O1+di7W4/h71/moggIebFb2eGLJ8BvbkwiVozXI9L77IGd9RswlEjZed18u7fqetS7dyDthhVG2pvya4zZI/cxIq6oJkNr2RIt3NgYChOh0I/17DuSJJ1jAmPB0Evj8QtCCo49ENnyO5cGWn12DZWybwYkg2jQC4aDFA/u5ajTo3wOKdwHj5hgMz/z05Bn2vAdhCGl6uWWNzcNnDiu3/rjqsjOkfkp0hCP7Q==,iv:FORxJ8htcoLIEJihUN7im3dN4jhnigB70InTohtpWwU=,tag:e2DHBd2dn3piCkEdkbHdoA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhQUpWNUgxVnhuTXd2TkF0
|
||||||
|
SVVHemFKRWlYczZ0TnBESVNRczhuRUNnUG1BCmJKQ2JZbHhFcXJidHJzci9OaFBm
|
||||||
|
ZTd0MGhsaVBic3dMb3psUHRCRnR3ODQKLS0tIERrSG1GVTRHdkJpVWpqdTZ4Yytq
|
||||||
|
OHhlZjV6MjRVbXFsWjlQSU03ZDNwYm8KAswHRSdV0BW/oJyZx63iZRHsF7SZ6PO+
|
||||||
|
hajQqmEyfcVfEu39zZzxQ2mtWlOr69I++irOhE3NeiFeJ1yIRQDJEQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUaUNnY0hpdDAzMTNIUS9D
|
||||||
|
RmdKbmplUk9DRXlLRXEvSnVjT05sQjcvTnpJCkd6bGRINm5yYUZOUTVzWEdjRmtG
|
||||||
|
Mmx0ci93N2wvTWV5MzlRVnlYdUxoUWsKLS0tIEVHUlNWYStWTG01RzRrVnNXc3BW
|
||||||
|
VkRkUXROU3plNmwvTUVhYmhCS2syQkEKKgC0EmUu1u2vZ/SZTnam+h846gZSyY4V
|
||||||
|
JyMzkws8O5TY9juWdDzXJIU67mIgc4qrWWN3uh8k28JBZGc078b5bg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoK2JUNVdYTzkvM1BBWXRm
|
||||||
|
citCNlE4Z1NLdEZ2R0tNZTVSMlFSeGxGOURnClJnYURYa0JZaVprQWdBcmVnOWVj
|
||||||
|
TGVCK1JWMVlueHJUaTZZYmROM0E5aDAKLS0tIEJxYkdadGtZM250d2d6Ujl2UU9C
|
||||||
|
YUpkVll2S2RpT0I1UVZiZFRKS1prMEEKp/bGImanJ/58vTQG/gUun/Y2QdmOEi3h
|
||||||
|
hVS0V2QcfuGgi0/YofLOM3+M6k6ViXw07XfXmR+puvLIHKr2y11x1Q==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
- recipient: age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDSGdEZnZEaDRpWUJVcnds
|
||||||
|
VGFSQklvczBZdEdEbXhodW8vME9wMUpVRENjClFZcnVqYkJxdlBiZFhma0tmZjgz
|
||||||
|
YXlIdlRDTDU4MHg1dzhGVDRJb2FGYVUKLS0tIDBXSWZ2NkxzdEk0ZlFRM00ybFNy
|
||||||
|
M0doaWl5R2cwU2RxQm5DbWxXeTZ5S2MKwrB3SysmgzCThQOhEVx18dxIfko0+oZY
|
||||||
|
9BSZOoFbfuwiLbtpL4J8bzxDvxn6sXxB8EBJH1hbpID53AquWDsxSw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
lastmodified: "2025-11-19T11:16:25Z"
|
||||||
|
mac: ENC[AES256_GCM,data:x4yor9G+QirceSYSX1K9GdfyGellT4JCkE09Tl9/mOX8HMOKFAQGknuwwU6SNGg+ciBFk4TdjQnDmVai4T8JQo9W/DLiZ+GKnWO3s+ZLDX30sEF0aMjKa43R5CCPO/Fl2XH96TaPC+8itTJQ6TpBSg51QLPcpqrMljiBNWvEoTU=,iv:Zi9rglAwgsejUmIpLN/1QlL80BSp3HP32k1xkWt2b+o=,tag:2ADk8d2G4OezkQjcV3CZuA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.11.0
|
||||||
37
hosts/amzebs-01/sites/api.ebs.amz.at.nix
Normal file
37
hosts/amzebs-01/sites/api.ebs.amz.at.nix
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{ pkgs, lib, config, ... }:
|
||||||
|
{
|
||||||
|
services.webstack.instances."api.ebs.amz.at" = {
|
||||||
|
enableDefaultLocations = false;
|
||||||
|
enableMysql = true;
|
||||||
|
authorizedKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTsA1z6/vOshSqmEUGO6vFbAYCrucgNORMKyoQ5/9/l"
|
||||||
|
];
|
||||||
|
extraConfig = ''
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN";
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
error_page 404 /index.php;
|
||||||
|
'';
|
||||||
|
locations."/favicon.ico".extraConfig = ''
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
'';
|
||||||
|
locations."/robots.txt".extraConfig = ''
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
'';
|
||||||
|
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
try_files $uri $uri/ /index.php$is_args$args;
|
||||||
|
'';
|
||||||
|
phpPackage = pkgs.php82.withExtensions ({ enabled, all }:
|
||||||
|
enabled ++ [ all.imagick ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
# Use HTTP-01 challenge for Let's Encrypt
|
||||||
|
services.nginx.virtualHosts."api.ebs.amz.at".acmeRoot = lib.mkForce "/var/lib/acme/acme-challenge";
|
||||||
|
}
|
||||||
37
hosts/amzebs-01/sites/api.ebs.cloonar.dev.nix
Normal file
37
hosts/amzebs-01/sites/api.ebs.cloonar.dev.nix
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{ pkgs, lib, config, ... }:
|
||||||
|
{
|
||||||
|
services.webstack.instances."api.ebs.cloonar.dev" = {
|
||||||
|
enableDefaultLocations = false;
|
||||||
|
enableMysql = true;
|
||||||
|
authorizedKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqpF703JmLTBpBjTSvC0bnYu+lSYdmaGPHxMnHEbMmp"
|
||||||
|
];
|
||||||
|
extraConfig = ''
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN";
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
error_page 404 /index.php;
|
||||||
|
'';
|
||||||
|
locations."/favicon.ico".extraConfig = ''
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
'';
|
||||||
|
locations."/robots.txt".extraConfig = ''
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
'';
|
||||||
|
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
try_files $uri $uri/ /index.php$is_args$args;
|
||||||
|
'';
|
||||||
|
phpPackage = pkgs.php82.withExtensions ({ enabled, all }:
|
||||||
|
enabled ++ [ all.imagick ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
# Use HTTP-01 challenge for Let's Encrypt
|
||||||
|
services.nginx.virtualHosts."api.ebs.cloonar.dev".acmeRoot = lib.mkForce "/var/lib/acme/acme-challenge";
|
||||||
|
}
|
||||||
37
hosts/amzebs-01/sites/api.stage.ebs.amz.at.nix
Normal file
37
hosts/amzebs-01/sites/api.stage.ebs.amz.at.nix
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{ pkgs, lib, config, ... }:
|
||||||
|
{
|
||||||
|
services.webstack.instances."api.stage.ebs.amz.at" = {
|
||||||
|
enableDefaultLocations = false;
|
||||||
|
enableMysql = true;
|
||||||
|
authorizedKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqpF703JmLTBpBjTSvC0bnYu+lSYdmaGPHxMnHEbMmp"
|
||||||
|
];
|
||||||
|
extraConfig = ''
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN";
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
error_page 404 /index.php;
|
||||||
|
'';
|
||||||
|
locations."/favicon.ico".extraConfig = ''
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
'';
|
||||||
|
locations."/robots.txt".extraConfig = ''
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
'';
|
||||||
|
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
try_files $uri $uri/ /index.php$is_args$args;
|
||||||
|
'';
|
||||||
|
phpPackage = pkgs.php82.withExtensions ({ enabled, all }:
|
||||||
|
enabled ++ [ all.imagick ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
# Use HTTP-01 challenge for Let's Encrypt
|
||||||
|
services.nginx.virtualHosts."api.stage.ebs.amz.at".acmeRoot = lib.mkForce "/var/lib/acme/acme-challenge";
|
||||||
|
}
|
||||||
14
hosts/amzebs-01/sites/default.nix
Normal file
14
hosts/amzebs-01/sites/default.nix
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{ ... }: {
|
||||||
|
imports = [
|
||||||
|
# Enabled vhosts (cloonar.dev)
|
||||||
|
./api.ebs.cloonar.dev.nix
|
||||||
|
./ebs.cloonar.dev.nix
|
||||||
|
./ebs-mobile.cloonar.dev.nix
|
||||||
|
|
||||||
|
# Disabled vhosts (amz.at) - uncomment to enable
|
||||||
|
./api.ebs.amz.at.nix
|
||||||
|
./api.stage.ebs.amz.at.nix
|
||||||
|
./ebs.amz.at.nix
|
||||||
|
./stage.ebs.amz.at.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
49
hosts/amzebs-01/sites/ebs-mobile.cloonar.dev.nix
Normal file
49
hosts/amzebs-01/sites/ebs-mobile.cloonar.dev.nix
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{ pkgs, lib, config, ... }:
|
||||||
|
let
|
||||||
|
domain = "ebs-mobile.cloonar.dev";
|
||||||
|
dataDir = "/var/www/${domain}";
|
||||||
|
in {
|
||||||
|
services.nginx.virtualHosts."${domain}" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
# Use HTTP-01 challenge for Let's Encrypt
|
||||||
|
acmeRoot = lib.mkForce "/var/lib/acme/acme-challenge";
|
||||||
|
root = "${dataDir}";
|
||||||
|
|
||||||
|
locations."/favicon.ico".extraConfig = ''
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# React client-side routing support
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html$is_args$args;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
locations."~* \\.(js|jpg|gif|png|webp|css|woff2|svg|ico)$".extraConfig = ''
|
||||||
|
expires 365d;
|
||||||
|
add_header Pragma "public";
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Deny PHP execution
|
||||||
|
locations."~ [^/]\\.php(/|$)".extraConfig = ''
|
||||||
|
deny all;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users."${domain}" = {
|
||||||
|
isNormalUser = true;
|
||||||
|
createHome = true;
|
||||||
|
home = dataDir;
|
||||||
|
homeMode = "770";
|
||||||
|
group = "nginx";
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIErjoADQK5SJ5si/iezzwQn5xH1RkgnTIlbeE4BRU1FN"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.${domain} = {};
|
||||||
|
}
|
||||||
49
hosts/amzebs-01/sites/ebs.amz.at.nix
Normal file
49
hosts/amzebs-01/sites/ebs.amz.at.nix
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{ pkgs, lib, config, ... }:
|
||||||
|
let
|
||||||
|
domain = "ebs.amz.at";
|
||||||
|
dataDir = "/var/www/${domain}";
|
||||||
|
in {
|
||||||
|
services.nginx.virtualHosts."${domain}" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
# Use HTTP-01 challenge for Let's Encrypt
|
||||||
|
acmeRoot = lib.mkForce "/var/lib/acme/acme-challenge";
|
||||||
|
root = "${dataDir}";
|
||||||
|
|
||||||
|
locations."/favicon.ico".extraConfig = ''
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# React client-side routing support
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html$is_args$args;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
locations."~* \\.(js|jpg|gif|png|webp|css|woff2|svg|ico)$".extraConfig = ''
|
||||||
|
expires 365d;
|
||||||
|
add_header Pragma "public";
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Deny PHP execution
|
||||||
|
locations."~ [^/]\\.php(/|$)".extraConfig = ''
|
||||||
|
deny all;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users."${domain}" = {
|
||||||
|
isNormalUser = true;
|
||||||
|
createHome = true;
|
||||||
|
home = dataDir;
|
||||||
|
homeMode = "770";
|
||||||
|
group = "nginx";
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIInwmhTIPw7NnR3LDn2T5N6by0ZPXdL3r2O/8oRUc/ki"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.${domain} = {};
|
||||||
|
}
|
||||||
49
hosts/amzebs-01/sites/ebs.cloonar.dev.nix
Normal file
49
hosts/amzebs-01/sites/ebs.cloonar.dev.nix
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{ pkgs, lib, config, ... }:
|
||||||
|
let
|
||||||
|
domain = "ebs.cloonar.dev";
|
||||||
|
dataDir = "/var/www/${domain}";
|
||||||
|
in {
|
||||||
|
services.nginx.virtualHosts."${domain}" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
# Use HTTP-01 challenge for Let's Encrypt
|
||||||
|
acmeRoot = lib.mkForce "/var/lib/acme/acme-challenge";
|
||||||
|
root = "${dataDir}";
|
||||||
|
|
||||||
|
locations."/favicon.ico".extraConfig = ''
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# React client-side routing support
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html$is_args$args;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
locations."~* \\.(js|jpg|gif|png|webp|css|woff2|svg|ico)$".extraConfig = ''
|
||||||
|
expires 365d;
|
||||||
|
add_header Pragma "public";
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Deny PHP execution
|
||||||
|
locations."~ [^/]\\.php(/|$)".extraConfig = ''
|
||||||
|
deny all;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users."${domain}" = {
|
||||||
|
isNormalUser = true;
|
||||||
|
createHome = true;
|
||||||
|
home = dataDir;
|
||||||
|
homeMode = "770";
|
||||||
|
group = "nginx";
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIErjoADQK5SJ5si/iezzwQn5xH1RkgnTIlbeE4BRU1FN"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.${domain} = {};
|
||||||
|
}
|
||||||
49
hosts/amzebs-01/sites/stage.ebs.amz.at.nix
Normal file
49
hosts/amzebs-01/sites/stage.ebs.amz.at.nix
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{ pkgs, lib, config, ... }:
|
||||||
|
let
|
||||||
|
domain = "stage.ebs.amz.at";
|
||||||
|
dataDir = "/var/www/${domain}";
|
||||||
|
in {
|
||||||
|
services.nginx.virtualHosts."${domain}" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
# Use HTTP-01 challenge for Let's Encrypt
|
||||||
|
acmeRoot = lib.mkForce "/var/lib/acme/acme-challenge";
|
||||||
|
root = "${dataDir}";
|
||||||
|
|
||||||
|
locations."/favicon.ico".extraConfig = ''
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# React client-side routing support
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html$is_args$args;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
locations."~* \\.(js|jpg|gif|png|webp|css|woff2|svg|ico)$".extraConfig = ''
|
||||||
|
expires 365d;
|
||||||
|
add_header Pragma "public";
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Deny PHP execution
|
||||||
|
locations."~ [^/]\\.php(/|$)".extraConfig = ''
|
||||||
|
deny all;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users."${domain}" = {
|
||||||
|
isNormalUser = true;
|
||||||
|
createHome = true;
|
||||||
|
home = dataDir;
|
||||||
|
homeMode = "770";
|
||||||
|
group = "nginx";
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIErjoADQK5SJ5si/iezzwQn5xH1RkgnTIlbeE4BRU1FN"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.${domain} = {};
|
||||||
|
}
|
||||||
1
hosts/amzebs-01/utils
Symbolic link
1
hosts/amzebs-01/utils
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../utils
|
||||||
@@ -1 +1 @@
|
|||||||
https://channels.nixos.org/nixos-25.05
|
https://channels.nixos.org/nixos-25.11
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
./utils/modules/nginx.nix
|
./utils/modules/nginx.nix
|
||||||
|
|
||||||
./utils/modules/autoupgrade.nix
|
./utils/modules/autoupgrade.nix
|
||||||
|
./utils/modules/victoriametrics
|
||||||
./utils/modules/promtail
|
./utils/modules/promtail
|
||||||
./utils/modules/borgbackup.nix
|
./utils/modules/borgbackup.nix
|
||||||
|
./utils/modules/set-nix-channel.nix
|
||||||
|
|
||||||
# fw
|
# fw
|
||||||
./modules/network-prefix.nix
|
./modules/network-prefix.nix
|
||||||
@@ -25,7 +27,6 @@
|
|||||||
./modules/podman.nix
|
./modules/podman.nix
|
||||||
./modules/omada.nix
|
./modules/omada.nix
|
||||||
./modules/ddclient.nix
|
./modules/ddclient.nix
|
||||||
./utils/modules/victoriametrics
|
|
||||||
# ./modules/wol.nix
|
# ./modules/wol.nix
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@
|
|||||||
./modules/ha-customers
|
./modules/ha-customers
|
||||||
|
|
||||||
./modules/firefox-sync.nix
|
./modules/firefox-sync.nix
|
||||||
|
./modules/fivefilters.nix
|
||||||
|
# ./modules/pyload
|
||||||
|
|
||||||
# home assistant
|
# home assistant
|
||||||
./modules/home-assistant
|
./modules/home-assistant
|
||||||
@@ -84,11 +87,24 @@
|
|||||||
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
|
||||||
"mongodb"
|
"mongodb"
|
||||||
"ai-mailer"
|
"ai-mailer"
|
||||||
|
"filebot"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Intel N100 Graphics Support for hardware transcoding
|
||||||
|
hardware.graphics = {
|
||||||
|
enable = true;
|
||||||
|
extraPackages = with pkgs; [
|
||||||
|
intel-media-driver # VAAPI driver (iHD) for modern Intel GPUs
|
||||||
|
vpl-gpu-rt # Intel VPL/QSV runtime for Gen 12+ (N100)
|
||||||
|
intel-compute-runtime # OpenCL support for tone-mapping
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
hardware.enableRedistributableFirmware = true;
|
||||||
|
|
||||||
time.timeZone = "Europe/Vienna";
|
time.timeZone = "Europe/Vienna";
|
||||||
|
|
||||||
services.logind.extraConfig = "RuntimeDirectorySize=2G";
|
services.logind.settings.Login.RuntimeDirectorySize = "2G";
|
||||||
|
|
||||||
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||||
sops.defaultSopsFile = ./secrets.yaml;
|
sops.defaultSopsFile = ./secrets.yaml;
|
||||||
@@ -106,7 +122,21 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
nix = {
|
nix = {
|
||||||
settings.auto-optimise-store = true;
|
settings = {
|
||||||
|
auto-optimise-store = true;
|
||||||
|
# Build performance optimizations
|
||||||
|
max-jobs = 4;
|
||||||
|
cores = 4;
|
||||||
|
# Enable eval caching for faster rebuilds
|
||||||
|
eval-cache = true;
|
||||||
|
# Use binary caches to avoid unnecessary rebuilds
|
||||||
|
substituters = [
|
||||||
|
"https://cache.nixos.org"
|
||||||
|
];
|
||||||
|
trusted-public-keys = [
|
||||||
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||||
|
];
|
||||||
|
};
|
||||||
gc = {
|
gc = {
|
||||||
automatic = true;
|
automatic = true;
|
||||||
dates = "weekly";
|
dates = "weekly";
|
||||||
@@ -122,8 +152,8 @@
|
|||||||
services.tlp = {
|
services.tlp = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings = {
|
settings = {
|
||||||
CPU_SCALING_GOVERNOR_ON_AC = "powersave"; # powersave or performance
|
CPU_SCALING_GOVERNOR_ON_AC = "performance"; # powersave or performance
|
||||||
CPU_ENERGY_PERF_POLICY_ON_AC = "power"; # power or performance
|
CPU_ENERGY_PERF_POLICY_ON_AC = "performance"; # power or performance
|
||||||
# CPU_MIN_PERF_ON_AC = 0;
|
# CPU_MIN_PERF_ON_AC = 0;
|
||||||
# CPU_MAX_PERF_ON_AC = 100; # max 100
|
# CPU_MAX_PERF_ON_AC = 100; # max 100
|
||||||
};
|
};
|
||||||
@@ -154,6 +184,9 @@
|
|||||||
|
|
||||||
# backups
|
# backups
|
||||||
borgbackup.repo = "u149513-sub2@u149513-sub2.your-backup.de:borg";
|
borgbackup.repo = "u149513-sub2@u149513-sub2.your-backup.de:borg";
|
||||||
|
services.borgbackup.jobs.default.paths = lib.mkAfter [
|
||||||
|
"/var/lib/microvms/persist/web-02/var/backup"
|
||||||
|
];
|
||||||
|
|
||||||
system.stateVersion = "22.05";
|
system.stateVersion = "22.05";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,13 @@
|
|||||||
|
|
||||||
ai:
|
ai:
|
||||||
openrouter_api_key: "file://${config.sops.secrets.ai-mailer-openrouter-key.path}"
|
openrouter_api_key: "file://${config.sops.secrets.ai-mailer-openrouter-key.path}"
|
||||||
model: "deepseek/deepseek-r1-distill-llama-70b"
|
model: "openai/gpt-5-mini"
|
||||||
temperature: 0.3
|
temperature: 0.3
|
||||||
max_tokens: 100000
|
max_tokens: 200000
|
||||||
|
|
||||||
context:
|
context:
|
||||||
urls:
|
urls:
|
||||||
|
- "https://paraclub.cloonar.dev/de/tandemfallschirmspringen/faq/"
|
||||||
- "https://paraclub.at/de/"
|
- "https://paraclub.at/de/"
|
||||||
- "https://paraclub.at/de/tandemfallschirmspringen/alle-infos/"
|
- "https://paraclub.at/de/tandemfallschirmspringen/alle-infos/"
|
||||||
- "https://paraclub.at/de/tandemfallschirmspringen/kosten-tandemsprung/"
|
- "https://paraclub.at/de/tandemfallschirmspringen/kosten-tandemsprung/"
|
||||||
@@ -47,6 +48,10 @@
|
|||||||
polling:
|
polling:
|
||||||
interval: "300s"
|
interval: "300s"
|
||||||
|
|
||||||
|
processing:
|
||||||
|
max_tokens: 30000
|
||||||
|
skip_junk_emails: false
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level: "info"
|
level: "info"
|
||||||
file_path: "/var/log/ai-mailer/ai-mailer.log"
|
file_path: "/var/log/ai-mailer/ai-mailer.log"
|
||||||
@@ -99,6 +104,8 @@
|
|||||||
|
|
||||||
restartTriggers = [
|
restartTriggers = [
|
||||||
"/etc/ai-mailer/config.yaml"
|
"/etc/ai-mailer/config.yaml"
|
||||||
|
config.sops.secrets.ai-mailer-imap-password.path
|
||||||
|
config.sops.secrets.ai-mailer-openrouter-key.path
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,33 @@
|
|||||||
virtualisation = {
|
virtualisation = {
|
||||||
oci-containers.containers = {
|
oci-containers.containers = {
|
||||||
deconz = {
|
deconz = {
|
||||||
autoStart = false;
|
autoStart = true;
|
||||||
image = "marthoc/deconz";
|
image = "marthoc/deconz";
|
||||||
volumes = [
|
volumes = [
|
||||||
"/etc/localtime:/etc/localtime:ro"
|
"/etc/localtime:/etc/localtime:ro"
|
||||||
"/var/lib/deconz:/root/.local/share/dresden-elektronik/deCONZ"
|
"/var/lib/deconz:/root/.local/share/dresden-elektronik/deCONZ"
|
||||||
|
"/dev/bus/usb:/dev/bus/usb:ro"
|
||||||
|
"/run/udev:/run/udev:ro"
|
||||||
];
|
];
|
||||||
environment = {
|
environment = {
|
||||||
DECONZ_DEVICE = "/dev/ttyACM0";
|
DECONZ_DEVICE = "/dev/ttyACM0";
|
||||||
TZ = "Europe/Vienna";
|
TZ = "Europe/Vienna";
|
||||||
|
DECONZ_UID = "0";
|
||||||
|
DECONZ_GID = "0";
|
||||||
|
DECONZ_START_VERBOSE = "1";
|
||||||
};
|
};
|
||||||
extraOptions = [
|
extraOptions = [
|
||||||
"--network=server"
|
"--network=server"
|
||||||
"--ip=${config.networkPrefix}.97.22"
|
"--ip=${config.networkPrefix}.97.22"
|
||||||
"--device=/dev/ttyACM0"
|
"--device=/dev/ttyACM0"
|
||||||
"--hostname=deconz"
|
"--hostname=deconz"
|
||||||
|
"--mac-address=1a:c4:04:6e:29:bd"
|
||||||
|
"--cap-add=CAP_MKNOD"
|
||||||
|
"--cap-add=CAP_NET_RAW"
|
||||||
|
"--cap-add=CAP_NET_ADMIN"
|
||||||
|
"--device-cgroup-rule=c 166:* rmw"
|
||||||
|
"--device-cgroup-rule=c 188:* rmw"
|
||||||
|
"--security-opt=label=disable"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,20 +66,20 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
dhcp-host = [
|
dhcp-host = [
|
||||||
"30:05:5c:56:62:37,${config.networkPrefix}.96.100,brn30055c566237"
|
|
||||||
"24:df:a7:b1:1b:74,${config.networkPrefix}.96.101,rmproplus-b1-1b-74"
|
"24:df:a7:b1:1b:74,${config.networkPrefix}.96.101,rmproplus-b1-1b-74"
|
||||||
|
|
||||||
|
"30:05:5c:56:62:37,${config.networkPrefix}.99.100,brn30055c566237"
|
||||||
"1a:c4:04:6e:29:bd,${config.networkPrefix}.97.2,omada"
|
"1a:c4:04:6e:29:bd,${config.networkPrefix}.97.2,omada"
|
||||||
"02:00:00:00:00:04,${config.networkPrefix}.97.6,matrix"
|
"02:00:00:00:00:04,${config.networkPrefix}.97.6,matrix"
|
||||||
"ea:db:d4:c1:18:ba,${config.networkPrefix}.97.50,git"
|
"ea:db:d4:c1:18:ba,${config.networkPrefix}.97.50,git"
|
||||||
"c2:4f:64:dd:13:0c,${config.networkPrefix}.97.20,home-assistant"
|
"c2:4f:64:dd:13:0c,${config.networkPrefix}.97.20,home-assistant"
|
||||||
|
"6c:1f:f7:8e:a9:86,${config.networkPrefix}.97.11,nas"
|
||||||
"1a:c4:04:6e:29:02,${config.networkPrefix}.101.25,deconz"
|
"1a:c4:04:6e:29:02,${config.networkPrefix}.101.25,deconz"
|
||||||
|
|
||||||
"c4:a7:2b:c7:ea:30,${config.networkPrefix}.99.10,metz"
|
"c4:a7:2b:c7:ea:30,${config.networkPrefix}.99.10,metz"
|
||||||
"f0:2f:9e:d4:3b:21,${config.networkPrefix}.99.11,firetv-living"
|
"f0:2f:9e:d4:3b:21,${config.networkPrefix}.99.11,firetv-living"
|
||||||
"e4:2a:ac:32:3f:79,${config.networkPrefix}.99.13,xbox"
|
"e4:2a:ac:32:3f:79,${config.networkPrefix}.99.13,xbox"
|
||||||
"f0:2f:9e:c1:74:72,${config.networkPrefix}.99.21,firetv-bedroom"
|
"f0:2f:9e:c1:74:72,${config.networkPrefix}.99.21,firetv-bedroom"
|
||||||
"30:05:5c:56:62:37,${config.networkPrefix}.99.100,brn30055c566237"
|
|
||||||
|
|
||||||
"fc:ee:28:03:63:e9,${config.networkPrefix}.100.148,k1c"
|
"fc:ee:28:03:63:e9,${config.networkPrefix}.100.148,k1c"
|
||||||
"cc:50:e3:bc:27:64,${config.networkPrefix}.100.112,Nuki_Bridge_1A753F72"
|
"cc:50:e3:bc:27:64,${config.networkPrefix}.100.112,Nuki_Bridge_1A753F72"
|
||||||
@@ -91,7 +91,12 @@
|
|||||||
"/fw.cloonar.com/${config.networkPrefix}.97.1"
|
"/fw.cloonar.com/${config.networkPrefix}.97.1"
|
||||||
"/omada.cloonar.com/${config.networkPrefix}.97.2"
|
"/omada.cloonar.com/${config.networkPrefix}.97.2"
|
||||||
"/web-02.cloonar.com/${config.networkPrefix}.97.5"
|
"/web-02.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
"/phpldapadmin.cloonar.com/${config.networkPrefix}.97.5"
|
"/pla.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
|
"/piped.cloonar.com/${config.networkPrefix}.97.5" # Replaced by Invidious
|
||||||
|
"/pipedapi.cloonar.com/${config.networkPrefix}.97.5" # Replaced by Invidious
|
||||||
|
"/invidious.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
|
"/fivefilters.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
|
"/n8n.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
"/home-assistant.cloonar.com/${config.networkPrefix}.97.20"
|
"/home-assistant.cloonar.com/${config.networkPrefix}.97.20"
|
||||||
"/mopidy.cloonar.com/${config.networkPrefix}.97.21"
|
"/mopidy.cloonar.com/${config.networkPrefix}.97.21"
|
||||||
"/snapcast.cloonar.com/${config.networkPrefix}.97.21"
|
"/snapcast.cloonar.com/${config.networkPrefix}.97.21"
|
||||||
@@ -100,6 +105,7 @@
|
|||||||
"/feeds.cloonar.com/188.34.191.144"
|
"/feeds.cloonar.com/188.34.191.144"
|
||||||
"/nukibridge1a753f72.cloonar.smart/${config.networkPrefix}.100.112"
|
"/nukibridge1a753f72.cloonar.smart/${config.networkPrefix}.100.112"
|
||||||
"/allywatch.cloonar.com/${config.networkPrefix}.97.5"
|
"/allywatch.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
|
"/brn30055c566237.cloonar.multimedia/${config.networkPrefix}.99.100"
|
||||||
|
|
||||||
"/stage.wsw.at/10.254.235.22"
|
"/stage.wsw.at/10.254.235.22"
|
||||||
"/prod.wsw.at/10.254.217.23"
|
"/prod.wsw.at/10.254.217.23"
|
||||||
@@ -128,6 +134,10 @@
|
|||||||
"/foundry-vtt.cloonar.com/${config.networkPrefix}.97.5"
|
"/foundry-vtt.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
"/sync.cloonar.com/${config.networkPrefix}.97.5"
|
"/sync.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
|
|
||||||
|
# multimedia
|
||||||
|
"/dl.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
|
"/jellyfin.cloonar.com/${config.networkPrefix}.97.5"
|
||||||
|
|
||||||
"/deconz.cloonar.multimedia/${config.networkPrefix}.97.22"
|
"/deconz.cloonar.multimedia/${config.networkPrefix}.97.22"
|
||||||
|
|
||||||
"/ddl-warez.to/172.67.184.30"
|
"/ddl-warez.to/172.67.184.30"
|
||||||
|
|||||||
32
hosts/fw/modules/fivefilters.nix
Normal file
32
hosts/fw/modules/fivefilters.nix
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{ config, pkgs, ... }: {
|
||||||
|
users.users.fivefilters = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "omada";
|
||||||
|
home = "/var/lib/fivefilters";
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
users.groups.fivefilters = { };
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
# parent is created by createHome already, but harmless to repeat
|
||||||
|
"d /var/lib/fivefilters 0755 fivefilters fivefilters - -"
|
||||||
|
"d /var/lib/fivefilters/cache 0755 fivefilters fivefilters - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
# TODO: check if we can run docker service as other user than root
|
||||||
|
virtualisation = {
|
||||||
|
oci-containers.containers = {
|
||||||
|
fivefilters = {
|
||||||
|
autoStart = true;
|
||||||
|
image = "heussd/fivefilters-full-text-rss:3.8.1";
|
||||||
|
volumes = [
|
||||||
|
"/var/lib/fivefilters/cache:/var/www/html/cache"
|
||||||
|
];
|
||||||
|
extraOptions = [
|
||||||
|
"--network=server"
|
||||||
|
"--ip=${config.networkPrefix}.97.10"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,54 @@ let
|
|||||||
foundry-vtt = pkgs.callPackage ../pkgs/foundry-vtt {};
|
foundry-vtt = pkgs.callPackage ../pkgs/foundry-vtt {};
|
||||||
cids = import ../modules/staticids.nix;
|
cids = import ../modules/staticids.nix;
|
||||||
hostConfig = config;
|
hostConfig = config;
|
||||||
|
url = "https://foundry-vtt.cloonar.com"; # URL to check
|
||||||
|
targetService = "container@foundry-vtt.service"; # systemd unit to restart (e.g. "docker-container@myapp.service")
|
||||||
|
threshold = 3; # consecutive failures before restart
|
||||||
|
interval = "1min"; # how often to run
|
||||||
|
timeoutSeconds = 10; # curl timeout
|
||||||
|
|
||||||
|
checkUrlScript = pkgs.writeShellScript "check-foundry-up" ''
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
URL="$1"
|
||||||
|
TARGET="$2"
|
||||||
|
THRESHOLD="$3"
|
||||||
|
TIMEOUT="$4"
|
||||||
|
|
||||||
|
STATE_DIR="/run/url-watchdog"
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
SAFE_TARGET="$(systemd-escape --path "$TARGET")"
|
||||||
|
STATE_FILE="$STATE_DIR/$SAFE_TARGET.count"
|
||||||
|
|
||||||
|
TMP="$(mktemp)"
|
||||||
|
# Get HTTP status; "000" if curl fails.
|
||||||
|
status="$(curl -sS -m "$TIMEOUT" -o "$TMP" -w "%{http_code}" "$URL" || echo "000")"
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
if [[ "$status" == "502" || "$status" == "504" || "$status" == "000" ]]; then
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
count=0
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
count="$(cat "$STATE_FILE" 2>/dev/null || echo 0)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$fail" -eq 1 ]]; then
|
||||||
|
count=$((count+1))
|
||||||
|
else
|
||||||
|
count=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$count" -ge "$THRESHOLD" ]]; then
|
||||||
|
printf '[%s] %s failing (%s) %sx -> restarting %s\n' "$(date -Is)" "$URL" "$status" "$count" "$TARGET"
|
||||||
|
systemctl restart "$TARGET"
|
||||||
|
count=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$count" > "$STATE_FILE"
|
||||||
|
rm -f "$TMP"
|
||||||
|
'';
|
||||||
in {
|
in {
|
||||||
users.users.foundry-vtt = {
|
users.users.foundry-vtt = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
@@ -35,9 +83,10 @@ in {
|
|||||||
hostName = "foundry-vtt";
|
hostName = "foundry-vtt";
|
||||||
useHostResolvConf = false;
|
useHostResolvConf = false;
|
||||||
defaultGateway = {
|
defaultGateway = {
|
||||||
address = "${hostConfig.networkPrefix}.97.1";
|
address = "${hostConfig.networkPrefix}.96.1";
|
||||||
interface = "eth0";
|
interface = "eth0";
|
||||||
};
|
};
|
||||||
|
firewall.enable = false;
|
||||||
nameservers = [ "${hostConfig.networkPrefix}.97.1" ];
|
nameservers = [ "${hostConfig.networkPrefix}.97.1" ];
|
||||||
};
|
};
|
||||||
systemd.services.foundry-vtt = {
|
systemd.services.foundry-vtt = {
|
||||||
@@ -48,7 +97,7 @@ in {
|
|||||||
NODE_ENV = "production";
|
NODE_ENV = "production";
|
||||||
};
|
};
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.nodejs}/bin/node ${foundry-vtt}/share/foundry-vtt/resources/app/main.js --dataPath=${config.users.users.foundry-vtt.home}";
|
ExecStart = "${pkgs.nodejs}/bin/node ${foundry-vtt}/share/foundry-vtt/main.js --dataPath=${config.users.users.foundry-vtt.home}";
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
User = "foundry-vtt";
|
User = "foundry-vtt";
|
||||||
WorkingDirectory = "${config.users.users.foundry-vtt.home}";
|
WorkingDirectory = "${config.users.users.foundry-vtt.home}";
|
||||||
@@ -66,13 +115,48 @@ in {
|
|||||||
gid = cids.gids.foundry-vtt;
|
gid = cids.gids.foundry-vtt;
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = {
|
|
||||||
enable = true;
|
|
||||||
allowedTCPPorts = [ 30000 ];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
system.stateVersion = "24.05";
|
system.stateVersion = "24.05";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.services."restart-foundry-vtt" = {
|
||||||
|
description = "Restart foundry-vtt container";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = "${pkgs.systemd}/bin/systemctl restart container@foundry-vtt.service";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers."restart-foundry-vtt" = {
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
# 03:00 local time (Europe/Vienna for you)
|
||||||
|
OnCalendar = "03:00";
|
||||||
|
# If the machine was off at 03:00, run once at next boot
|
||||||
|
Persistent = true;
|
||||||
|
Unit = "restart-foundry-vtt.service";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.foundry-vtt-watchdog = {
|
||||||
|
description = "Foundry VTT watchdog: restart ${targetService} on Nginx gateway errors";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = "${checkUrlScript} ${url} ${targetService} ${toString threshold} ${toString timeoutSeconds}";
|
||||||
|
};
|
||||||
|
# Ensure needed tools are on PATH inside the unit
|
||||||
|
path = [ pkgs.curl pkgs.coreutils pkgs.systemd ];
|
||||||
|
# Wait until networking is really up
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers.foundry-vtt-watchdog = {
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnBootSec = interval;
|
||||||
|
OnUnitActiveSec = interval;
|
||||||
|
AccuracySec = "10s";
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
44
hosts/fw/modules/gitea-runner-image-README.md
Normal file
44
hosts/fw/modules/gitea-runner-image-README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Gitea Runner Docker Image
|
||||||
|
|
||||||
|
This directory contains the Dockerfile for the custom Gitea Actions runner image that includes additional dependencies needed for CI workflows.
|
||||||
|
|
||||||
|
## Included Tools
|
||||||
|
|
||||||
|
- **Base**: `shivammathur/node:latest` (includes Node.js and common development tools)
|
||||||
|
- **Chrome dependencies**: Full Puppeteer/Chromium dependencies for headless browser testing
|
||||||
|
- **webp**: WebP image format tools (`cwebp`, `dwebp`)
|
||||||
|
- **libavif-bin**: AVIF image format tools (`avifenc`, `avifdec`)
|
||||||
|
|
||||||
|
## Building the Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd hosts/fw/modules
|
||||||
|
docker build -f gitea-runner.Dockerfile -t git.cloonar.com/infrastructure/gitea-runner:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pushing to Registry
|
||||||
|
|
||||||
|
First, authenticate with your Gitea container registry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login git.cloonar.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Then push the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker push git.cloonar.com/infrastructure/gitea-runner:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using the Image
|
||||||
|
|
||||||
|
The image is already configured in `gitea-vm.nix` and will be used automatically by the Gitea Actions runners for jobs labeled with `ubuntu-latest`.
|
||||||
|
|
||||||
|
## Updating the Image
|
||||||
|
|
||||||
|
When you need to add new dependencies:
|
||||||
|
|
||||||
|
1. Edit `gitea-runner.Dockerfile`
|
||||||
|
2. Rebuild the image with the commands above
|
||||||
|
3. Push to the registry
|
||||||
|
4. Restart the runner VMs: `systemctl restart microvm@git-runner-1.service microvm@git-runner-2.service`
|
||||||
54
hosts/fw/modules/gitea-runner.Dockerfile
Normal file
54
hosts/fw/modules/gitea-runner.Dockerfile
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
FROM shivammathur/node:latest
|
||||||
|
|
||||||
|
# Install Chrome dependencies for Puppeteer
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
fonts-liberation \
|
||||||
|
libappindicator3-1 \
|
||||||
|
libasound2t64 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libc6 \
|
||||||
|
libcairo2 \
|
||||||
|
libcups2 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
libexpat1 \
|
||||||
|
libfontconfig1 \
|
||||||
|
libgbm1 \
|
||||||
|
libgcc-s1 \
|
||||||
|
libglib2.0-0 \
|
||||||
|
libgtk-3-0 \
|
||||||
|
libnspr4 \
|
||||||
|
libnss3 \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libpangocairo-1.0-0 \
|
||||||
|
libstdc++6 \
|
||||||
|
libx11-6 \
|
||||||
|
libx11-xcb1 \
|
||||||
|
libxcb1 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxcursor1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libxext6 \
|
||||||
|
libxfixes3 \
|
||||||
|
libxi6 \
|
||||||
|
libxrandr2 \
|
||||||
|
libxrender1 \
|
||||||
|
libxss1 \
|
||||||
|
libxtst6 \
|
||||||
|
lsb-release \
|
||||||
|
wget \
|
||||||
|
xdg-utils \
|
||||||
|
webp \
|
||||||
|
libavif-bin \
|
||||||
|
chromium \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - && \
|
||||||
|
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y google-chrome-stable && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Verify installations
|
||||||
|
RUN cwebp -version && avifenc --version
|
||||||
@@ -55,12 +55,18 @@ in {
|
|||||||
name = runner;
|
name = runner;
|
||||||
tokenFile = "/run/secrets/gitea-runner-token";
|
tokenFile = "/run/secrets/gitea-runner-token";
|
||||||
labels = [
|
labels = [
|
||||||
"ubuntu-latest:docker://shivammathur/node:latest"
|
# "ubuntu-latest:docker://shivammathur/node:latest"
|
||||||
|
"ubuntu-latest:docker://git.cloonar.com/infrastructure/gitea-runner:1.0.0"
|
||||||
];
|
];
|
||||||
settings = {
|
settings = {
|
||||||
container = {
|
container = {
|
||||||
network = "podman";
|
network = "podman";
|
||||||
};
|
};
|
||||||
|
cache = {
|
||||||
|
enabled = true;
|
||||||
|
host = "${config.networkPrefix}.97.5${toString idx}"; # LAN IP of the machine running act_runner
|
||||||
|
port = 8088; # any free TCP port
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,6 +75,11 @@ in {
|
|||||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7"
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
networking.firewall = {
|
||||||
|
enable = true; # default, but being explicit is fine
|
||||||
|
allowedTCPPorts = [ 8088 ];
|
||||||
|
};
|
||||||
|
|
||||||
system.stateVersion = "22.05";
|
system.stateVersion = "22.05";
|
||||||
};
|
};
|
||||||
}) (lib.listToAttrs (lib.lists.imap1 (i: v: { name=v; value=i; }) runners));
|
}) (lib.listToAttrs (lib.lists.imap1 (i: v: { name=v; value=i; }) runners));
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ in
|
|||||||
sslCertificateKey = "/var/lib/acme/gitea/key.pem";
|
sslCertificateKey = "/var/lib/acme/gitea/key.pem";
|
||||||
sslTrustedCertificate = "/var/lib/acme/gitea/chain.pem";
|
sslTrustedCertificate = "/var/lib/acme/gitea/chain.pem";
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
|
extraConfig = ''
|
||||||
|
client_max_body_size 2048M;
|
||||||
|
'';
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://localhost:3001/";
|
proxyPass = "http://localhost:3001/";
|
||||||
};
|
};
|
||||||
@@ -109,6 +112,12 @@ in
|
|||||||
USER = "gitea@cloonar.com";
|
USER = "gitea@cloonar.com";
|
||||||
};
|
};
|
||||||
actions.ENABLED=true;
|
actions.ENABLED=true;
|
||||||
|
attachment = {
|
||||||
|
MAX_SIZE = 2048; # 2GB in MB for general attachments
|
||||||
|
};
|
||||||
|
packages = {
|
||||||
|
ENABLED = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
{ config, ... }:
|
{ config, pkgs, ... }:
|
||||||
let
|
let
|
||||||
unstable = import
|
|
||||||
(builtins.fetchTarball https://github.com/nixos/nixpkgs/tarball/nixpkgs-unstable)
|
|
||||||
# reuse the current configuration
|
|
||||||
{ config = config.nixpkgs.config; };
|
|
||||||
in {
|
in {
|
||||||
services.home-assistant.customComponents = with unstable.home-assistant-custom-components; [
|
services.home-assistant.customComponents = with pkgs.home-assistant-custom-components; [
|
||||||
epex_spot
|
epex_spot
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -274,46 +274,88 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"automation bed_button_1" = {
|
"automation bedroom light" = {
|
||||||
alias = "bed_button_1";
|
alias = "bedroom light";
|
||||||
trigger = {
|
trigger = [
|
||||||
platform = "event";
|
{
|
||||||
event_type = "shelly.click";
|
platform = "event";
|
||||||
event_data = {
|
event_type = "button_pressed";
|
||||||
device = "shellybutton1-E8DB84AA196D";
|
event_data = {
|
||||||
};
|
id = [ 254 207 162 105 ];
|
||||||
};
|
which = 1;
|
||||||
|
onoff = 1;
|
||||||
|
pushed = 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
platform = "event";
|
||||||
|
event_type = "shelly.click";
|
||||||
|
event_data = {
|
||||||
|
device = "shellybutton1-E8DB84AA136D";
|
||||||
|
click_type = "double";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
action = [
|
action = [
|
||||||
{
|
{
|
||||||
choose = [
|
service = "light.toggle";
|
||||||
{
|
target = {
|
||||||
conditions = [ "{{ trigger.event.data.click_type == \"single\" }}" ];
|
entity_id = "light.bedroom_lights";
|
||||||
sequence = [
|
};
|
||||||
{
|
}
|
||||||
service = "light.toggle";
|
];
|
||||||
entity_id = "light.bed_reading_1";
|
};
|
||||||
}
|
"automation bed light" = {
|
||||||
];
|
alias = "bed light";
|
||||||
}
|
trigger = [
|
||||||
{
|
{
|
||||||
conditions = [ "{{ trigger.event.data.click_type == \"double\" }}" ];
|
platform = "event";
|
||||||
sequence = [
|
event_type = "button_pressed";
|
||||||
{
|
event_data = {
|
||||||
service = "light.toggle";
|
id = [ 254 207 162 105 ];
|
||||||
entity_id = "light.bedroom_lights";
|
which = 0;
|
||||||
}
|
onoff = 1;
|
||||||
];
|
pushed = 1;
|
||||||
}
|
};
|
||||||
{
|
}
|
||||||
conditions = [ "{{ trigger.event.data.click_type == \"triple\" }}" ];
|
{
|
||||||
sequence = [
|
platform = "event";
|
||||||
{
|
event_type = "shelly.click";
|
||||||
service = "light.toggle";
|
event_data = {
|
||||||
entity_id = "light.bedroom_bed";
|
device = "shellybutton1-E8DB84AA136D";
|
||||||
}
|
click_type = "triple";
|
||||||
];
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
action = [
|
||||||
|
{
|
||||||
|
service = "light.toggle";
|
||||||
|
target = {
|
||||||
|
entity_id = "light.bedroom_bed";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"automation reading 1 light" = {
|
||||||
|
alias = "reading 1 light";
|
||||||
|
trigger = [
|
||||||
|
{
|
||||||
|
platform = "event";
|
||||||
|
event_type = "button_pressed";
|
||||||
|
event_data = {
|
||||||
|
id = [ 254 207 162 105 ];
|
||||||
|
which = 0;
|
||||||
|
onoff = 0;
|
||||||
|
pushed = 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
action = [
|
||||||
|
{
|
||||||
|
service = "light.toggle";
|
||||||
|
target = {
|
||||||
|
entity_id = "light.bed_reading_1";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -338,24 +380,6 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
{
|
|
||||||
conditions = [ "{{ trigger.event.data.click_type == \"double\" }}" ];
|
|
||||||
sequence = [
|
|
||||||
{
|
|
||||||
service = "light.toggle";
|
|
||||||
entity_id = "light.bedroom_lights";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
conditions = [ "{{ trigger.event.data.click_type == \"triple\" }}" ];
|
|
||||||
sequence = [
|
|
||||||
{
|
|
||||||
service = "light.toggle";
|
|
||||||
entity_id = "light.bedroom_bed";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -372,12 +396,13 @@
|
|||||||
all = true;
|
all = true;
|
||||||
entities = [
|
entities = [
|
||||||
"light.livingroom_switch"
|
"light.livingroom_switch"
|
||||||
"light.livingroom_bulb_1_rgbcw_bulb"
|
"light.living_bulb_1"
|
||||||
"light.livingroom_bulb_2_rgbcw_bulb"
|
"light.living_bulb_2"
|
||||||
"light.livingroom_bulb_3_rgbcw_bulb"
|
"light.living_bulb_3"
|
||||||
"light.livingroom_bulb_4_rgbcw_bulb"
|
"light.living_bulb_4"
|
||||||
"light.livingroom_bulb_5_rgbcw_bulb"
|
"light.living_bulb_5"
|
||||||
"light.livingroom_bulb_6_rgbcw_bulb"
|
"light.living_bulb_6"
|
||||||
|
# "light.living_room"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -391,6 +416,7 @@
|
|||||||
all = true;
|
all = true;
|
||||||
entities = [
|
entities = [
|
||||||
"light.kitchen_switch"
|
"light.kitchen_switch"
|
||||||
|
"light.kitchen_bulb_1"
|
||||||
"light.kitchen"
|
"light.kitchen"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,16 +290,6 @@
|
|||||||
command = "b64:JgDaAAABKZMUERMSExITEhMSExETEhMSExITEhMSExETNxQ2ExITEhMSEzcTNxM3ExITEhM3ExITNxMSEhITEhM3EzcTEhM3EwAFyAABKJQUERMSEhITEhMSExITEhMSEhITEhMSExITNxM3ExITEhMREzcTNxQ3EhITEhM3ExITNxMSExITEhM3EzcTEhM3EwAFyAABKJQUERMSExETEhMSExITEhMSExETEhMSExITNxM3ExITEhMREzcTOBI4ExETEhM3ExITNxMSExITEhM3EzcTEhM3E5IGAA0FAAAAAAAAAAAAAAAAAAA=";
|
command = "b64:JgDaAAABKZMUERMSExITEhMSExETEhMSExITEhMSExETNxQ2ExITEhMSEzcTNxM3ExITEhM3ExITNxMSEhITEhM3EzcTEhM3EwAFyAABKJQUERMSEhITEhMSExITEhMSEhITEhMSExITNxM3ExITEhMREzcTNxQ3EhITEhM3ExITNxMSExITEhM3EzcTEhM3EwAFyAABKJQUERMSExETEhMSExITEhMSExETEhMSExITNxM3ExITEhMREzcTOBI4ExETEhM3ExITNxMSExITEhM3EzcTEhM3E5IGAA0FAAAAAAAAAAAAAAAAAAA=";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
{
|
|
||||||
delay = 30;
|
|
||||||
}
|
|
||||||
# turn off tv switch
|
|
||||||
{
|
|
||||||
service = "switch.turn_off";
|
|
||||||
target = {
|
|
||||||
entity_id = "switch.tv_switch";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
"automation all_multimedia_on" = {
|
"automation all_multimedia_on" = {
|
||||||
|
|||||||
@@ -41,8 +41,6 @@
|
|||||||
service = "wake_on_lan.send_magic_packet";
|
service = "wake_on_lan.send_magic_packet";
|
||||||
data = {
|
data = {
|
||||||
mac = "04:7c:16:d5:63:5e";
|
mac = "04:7c:16:d5:63:5e";
|
||||||
broadcast_address = "${config.networkPrefix}.96.5";
|
|
||||||
broadcast_port = 9;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
scene = [
|
scene = [
|
||||||
{
|
{
|
||||||
name = "Date Night";
|
name = "Date Night";
|
||||||
|
icon = "mdi:heart";
|
||||||
entities = {
|
entities = {
|
||||||
"light.livingroom_showcase" = {
|
"light.livingroom_showcase" = {
|
||||||
state = "on";
|
state = "on";
|
||||||
|
|||||||
@@ -7,10 +7,6 @@
|
|||||||
at = "input_datetime.wakeup";
|
at = "input_datetime.wakeup";
|
||||||
};
|
};
|
||||||
action = [
|
action = [
|
||||||
{
|
|
||||||
service = "switch.turn_on";
|
|
||||||
entity_id = "switch.coffee";
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
delay = 1700;
|
delay = 1700;
|
||||||
}
|
}
|
||||||
@@ -27,34 +23,21 @@
|
|||||||
trigger = [
|
trigger = [
|
||||||
{
|
{
|
||||||
platform = "event";
|
platform = "event";
|
||||||
event_type = "shelly.click";
|
event_type = "button_pressed";
|
||||||
event_data = {
|
event_data = {
|
||||||
device = "shellybutton1-E8DB84AA196D";
|
id = [ 254 207 162 105 ];
|
||||||
};
|
which = 1;
|
||||||
}
|
onoff = 0;
|
||||||
{
|
pushed = 1;
|
||||||
platform = "event";
|
|
||||||
event_type = "shelly.click";
|
|
||||||
event_data = {
|
|
||||||
device = "shellybutton1-E8DB84AA136D";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
action = [
|
action = [
|
||||||
{
|
{
|
||||||
choose = [
|
service = "script.turn_on";
|
||||||
{
|
target = {
|
||||||
conditions = [ "{{ trigger.event.data.click_type == \"long\" }}" ];
|
entity_id = "script.turn_off_everything";
|
||||||
sequence = [
|
};
|
||||||
{
|
|
||||||
service = "script.turn_on";
|
|
||||||
target = {
|
|
||||||
entity_id = "script.turn_off_everything";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -65,22 +48,18 @@
|
|||||||
service = "light.turn_off";
|
service = "light.turn_off";
|
||||||
entity_id = "all";
|
entity_id = "all";
|
||||||
}
|
}
|
||||||
{
|
# {
|
||||||
service = "switch.turn_off";
|
# service = "switch.turn_off";
|
||||||
entity_id = "switch.coffee";
|
# entity_id = "switch.78_8c_b5_fe_41_62_port_2_poe";
|
||||||
}
|
# }
|
||||||
{
|
# {
|
||||||
service = "switch.turn_off";
|
# service = "switch.turn_off";
|
||||||
entity_id = "switch.78_8c_b5_fe_41_62_port_2_poe";
|
# entity_id = "switch.78_8c_b5_fe_41_62_port_3_poe";
|
||||||
}
|
# }
|
||||||
{
|
# {
|
||||||
service = "switch.turn_off";
|
# service = "switch.turn_off";
|
||||||
entity_id = "switch.78_8c_b5_fe_41_62_port_3_poe";
|
# entity_id = "switch.hallway_circuit";
|
||||||
}
|
# }
|
||||||
{
|
|
||||||
service = "switch.turn_off";
|
|
||||||
entity_id = "switch.hallway_circuit";
|
|
||||||
}
|
|
||||||
# TODO: needs to stay on because phone is not loading otherwise
|
# TODO: needs to stay on because phone is not loading otherwise
|
||||||
# {
|
# {
|
||||||
# service = "switch.turn_off";
|
# service = "switch.turn_off";
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
{
|
{
|
||||||
imports = [ (builtins.fetchGit {
|
imports = [ (builtins.fetchGit {
|
||||||
url = "https://github.com/astro/microvm.nix";
|
url = "https://github.com/astro/microvm.nix";
|
||||||
|
ref = "main";
|
||||||
|
rev = "42628f7c61b02d385ce2cb1f66f9be333ac20140";
|
||||||
} + "/nixos-modules/host") ];
|
} + "/nixos-modules/host") ];
|
||||||
|
|
||||||
systemd.network.networks."31-server".matchConfig.Name = [ "vm-*" ];
|
systemd.network.networks."31-server".matchConfig.Name = [ "vm-*" ];
|
||||||
|
|||||||
40
hosts/fw/modules/phpldapadmin.nix
Normal file
40
hosts/fw/modules/phpldapadmin.nix
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
virtualisation.oci-containers.backend = "podman";
|
||||||
|
virtualisation.oci-containers.containers = {
|
||||||
|
phpldapadmin = {
|
||||||
|
image = "phpldapadmin/phpldapadmin:2.2.2";
|
||||||
|
autoStart = true;
|
||||||
|
ports = [
|
||||||
|
"80:8087/tcp"
|
||||||
|
];
|
||||||
|
environmentFiles = [
|
||||||
|
config.sops.secrets.phpldapadmin.path
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers."restart-phpldapadmin" = {
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "*-*-* 3:00:00";
|
||||||
|
Unit = "restart-phpldapadmin.service";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services."restart-phpldapadmin" = {
|
||||||
|
script = ''
|
||||||
|
set -eu
|
||||||
|
if ${pkgs.systemd}/bin/systemctl is-active --quiet podman-phpldapadmin.service; then
|
||||||
|
${pkgs.systemd}/bin/systemctl restart podman-phpldapadmin.service
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
User = "root";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sops.secrets.phpldapadmin = {};
|
||||||
|
}
|
||||||
153
hosts/fw/modules/pyload/default.nix
Normal file
153
hosts/fw/modules/pyload/default.nix
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cids = import ../staticids.nix;
|
||||||
|
networkPrefix = config.networkPrefix;
|
||||||
|
filebotScript = pkgs.callPackage ./filebot-process.nix {};
|
||||||
|
|
||||||
|
pyloadUser = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = cids.uids.pyload;
|
||||||
|
group = "pyload";
|
||||||
|
home = "/var/lib/pyload";
|
||||||
|
createHome = true;
|
||||||
|
extraGroups = [ "jellyfin" ]; # Access to multimedia directories
|
||||||
|
};
|
||||||
|
pyloadGroup = {
|
||||||
|
gid = cids.gids.pyload;
|
||||||
|
};
|
||||||
|
|
||||||
|
jellyfinUser = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = cids.uids.jellyfin;
|
||||||
|
group = "jellyfin";
|
||||||
|
home = "/var/lib/jellyfin";
|
||||||
|
createHome = true;
|
||||||
|
extraGroups = [ "render" "video" ];
|
||||||
|
};
|
||||||
|
jellyfinGroup = {
|
||||||
|
gid = cids.gids.jellyfin;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
users.users.pyload = pyloadUser;
|
||||||
|
users.groups.pyload = pyloadGroup;
|
||||||
|
users.users.jellyfin = jellyfinUser;
|
||||||
|
users.groups.jellyfin = jellyfinGroup;
|
||||||
|
|
||||||
|
# Create the directory structure on the host
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/downloads 0755 pyload pyload - -"
|
||||||
|
"d /var/lib/multimedia 0775 root jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/movies 0775 jellyfin jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/tv-shows 0775 jellyfin jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/music 0755 jellyfin jellyfin - -"
|
||||||
|
"d /var/lib/jellyfin 0755 jellyfin jellyfin - -"
|
||||||
|
|
||||||
|
# PyLoad hook scripts directory
|
||||||
|
"d /var/lib/pyload/config 0755 pyload pyload - -"
|
||||||
|
"d /var/lib/pyload/config/scripts 0755 pyload pyload - -"
|
||||||
|
"d /var/lib/pyload/config/scripts/package_extracted 0755 pyload pyload - -"
|
||||||
|
"L+ /var/lib/pyload/config/scripts/package_extracted/filebot-process.sh - - - - ${filebotScript}/bin/filebot-process"
|
||||||
|
];
|
||||||
|
|
||||||
|
# FileBot license secret
|
||||||
|
sops.secrets.filebot-license = {
|
||||||
|
mode = "0440";
|
||||||
|
owner = "pyload";
|
||||||
|
group = "pyload";
|
||||||
|
};
|
||||||
|
|
||||||
|
containers.pyload = {
|
||||||
|
autoStart = true;
|
||||||
|
ephemeral = false;
|
||||||
|
privateNetwork = true;
|
||||||
|
hostBridge = "server";
|
||||||
|
hostAddress = "${networkPrefix}.97.1";
|
||||||
|
localAddress = "${networkPrefix}.97.11/24";
|
||||||
|
|
||||||
|
# GPU device passthrough for hardware transcoding
|
||||||
|
allowedDevices = [
|
||||||
|
{
|
||||||
|
modifier = "rwm";
|
||||||
|
node = "/dev/dri/card0";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
modifier = "rwm";
|
||||||
|
node = "/dev/dri/renderD128";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
bindMounts = {
|
||||||
|
"/dev/dri" = {
|
||||||
|
hostPath = "/dev/dri";
|
||||||
|
isReadOnly = false;
|
||||||
|
};
|
||||||
|
"/run/opengl-driver" = {
|
||||||
|
hostPath = "/run/opengl-driver";
|
||||||
|
isReadOnly = true;
|
||||||
|
};
|
||||||
|
"/nix/store" = {
|
||||||
|
hostPath = "/nix/store";
|
||||||
|
isReadOnly = true;
|
||||||
|
};
|
||||||
|
"/var/lib/pyload" = {
|
||||||
|
hostPath = "/var/lib/pyload";
|
||||||
|
isReadOnly = false;
|
||||||
|
};
|
||||||
|
"/var/lib/jellyfin" = {
|
||||||
|
hostPath = "/var/lib/jellyfin";
|
||||||
|
isReadOnly = false;
|
||||||
|
};
|
||||||
|
"/downloads" = {
|
||||||
|
hostPath = "/var/lib/downloads";
|
||||||
|
isReadOnly = false;
|
||||||
|
};
|
||||||
|
"/multimedia" = {
|
||||||
|
hostPath = "/var/lib/multimedia";
|
||||||
|
isReadOnly = false;
|
||||||
|
};
|
||||||
|
"/var/lib/pyload/filebot-license.psm" = {
|
||||||
|
hostPath = config.sops.secrets.filebot-license.path;
|
||||||
|
isReadOnly = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = { lib, config, pkgs, ... }: {
|
||||||
|
nixpkgs.overlays = [
|
||||||
|
(import ../../utils/overlays/packages.nix)
|
||||||
|
];
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
./pyload.nix
|
||||||
|
./jellyfin.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
|
||||||
|
"unrar"
|
||||||
|
"filebot"
|
||||||
|
];
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
hostName = "pyload";
|
||||||
|
useHostResolvConf = false;
|
||||||
|
defaultGateway = {
|
||||||
|
address = "${networkPrefix}.97.1";
|
||||||
|
interface = "eth0";
|
||||||
|
};
|
||||||
|
nameservers = [ "${networkPrefix}.97.1" ];
|
||||||
|
firewall.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Ensure render/video groups exist with consistent GIDs for GPU access
|
||||||
|
users.groups.render = { gid = 303; };
|
||||||
|
users.groups.video = { gid = 26; };
|
||||||
|
|
||||||
|
users.users.pyload = pyloadUser;
|
||||||
|
users.groups.pyload = pyloadGroup;
|
||||||
|
users.users.jellyfin = jellyfinUser;
|
||||||
|
users.groups.jellyfin = jellyfinGroup;
|
||||||
|
|
||||||
|
system.stateVersion = "24.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
89
hosts/fw/modules/pyload/filebot-process.nix
Normal file
89
hosts/fw/modules/pyload/filebot-process.nix
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{ pkgs }:
|
||||||
|
|
||||||
|
pkgs.writeShellScriptBin "filebot-process" ''
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# FileBot AMC script for automated media organization
|
||||||
|
# Called by PyLoad's package_extracted hook with parameters:
|
||||||
|
# $1 = package_id
|
||||||
|
# $2 = package_name
|
||||||
|
# $3 = download_folder (actual path to extracted files)
|
||||||
|
# $4 = password (optional)
|
||||||
|
|
||||||
|
PACKAGE_ID="''${1:-}"
|
||||||
|
PACKAGE_NAME="''${2:-unknown}"
|
||||||
|
DOWNLOAD_DIR="''${3:-/downloads}"
|
||||||
|
PASSWORD="''${4:-}"
|
||||||
|
|
||||||
|
OUTPUT_DIR="/multimedia"
|
||||||
|
LOG_FILE="/var/lib/pyload/filebot-amc.log"
|
||||||
|
EXCLUDE_LIST="/var/lib/pyload/filebot-exclude-list.txt"
|
||||||
|
|
||||||
|
# Ensure FileBot data directory exists
|
||||||
|
mkdir -p /var/lib/pyload/.local/share/filebot/data
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
touch "$EXCLUDE_LIST"
|
||||||
|
|
||||||
|
# Install FileBot license if not already installed
|
||||||
|
if [ ! -f /var/lib/pyload/.local/share/filebot/data/.license ]; then
|
||||||
|
echo "$(date): Installing FileBot license..." >> "$LOG_FILE"
|
||||||
|
${pkgs.filebot}/bin/filebot --license /var/lib/pyload/filebot-license.psm || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===========================================" >> "$LOG_FILE"
|
||||||
|
echo "$(date): PyLoad package extracted hook triggered" >> "$LOG_FILE"
|
||||||
|
echo "Package ID: $PACKAGE_ID" >> "$LOG_FILE"
|
||||||
|
echo "Package Name: $PACKAGE_NAME" >> "$LOG_FILE"
|
||||||
|
echo "Download Directory: $DOWNLOAD_DIR" >> "$LOG_FILE"
|
||||||
|
echo "===========================================" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Check if download directory exists and has media files
|
||||||
|
if [ ! -d "$DOWNLOAD_DIR" ]; then
|
||||||
|
echo "$(date): Download directory does not exist: $DOWNLOAD_DIR" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if there are any video/media files to process
|
||||||
|
if ! find "$DOWNLOAD_DIR" -type f \( -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.m4v" -o -iname "*.mov" \) -print -quit | grep -q .; then
|
||||||
|
echo "$(date): No media files found in: $DOWNLOAD_DIR" >> "$LOG_FILE"
|
||||||
|
echo "$(date): Skipping FileBot processing" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date): Starting FileBot processing" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Run FileBot AMC script
|
||||||
|
set +e # Temporarily disable exit on error to capture exit code
|
||||||
|
${pkgs.filebot}/bin/filebot \
|
||||||
|
-script fn:amc \
|
||||||
|
--output "$OUTPUT_DIR" \
|
||||||
|
--action move \
|
||||||
|
--conflict auto \
|
||||||
|
-non-strict \
|
||||||
|
--log-file "$LOG_FILE" \
|
||||||
|
--def \
|
||||||
|
excludeList="$EXCLUDE_LIST" \
|
||||||
|
movieFormat="$OUTPUT_DIR/movies/{n} ({y})/{n} ({y}) - {vf}" \
|
||||||
|
seriesFormat="$OUTPUT_DIR/tv-shows/{n}/Season {s.pad(2)}/{n} - {s00e00} - {t}" \
|
||||||
|
ut_dir="$DOWNLOAD_DIR" \
|
||||||
|
ut_kind=multi \
|
||||||
|
clean=y \
|
||||||
|
skipExtract=y
|
||||||
|
|
||||||
|
FILEBOT_EXIT_CODE=$?
|
||||||
|
set -e # Re-enable exit on error
|
||||||
|
|
||||||
|
if [ $FILEBOT_EXIT_CODE -ne 0 ]; then
|
||||||
|
echo "$(date): FileBot processing failed with exit code $FILEBOT_EXIT_CODE" >> "$LOG_FILE"
|
||||||
|
exit 0 # Don't fail the hook even if FileBot fails
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date): FileBot processing completed successfully" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Clean up any remaining empty directories
|
||||||
|
find "$DOWNLOAD_DIR" -type d -empty -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "$(date): All processing completed" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
''
|
||||||
36
hosts/fw/modules/pyload/jellyfin.nix
Normal file
36
hosts/fw/modules/pyload/jellyfin.nix
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{ lib, pkgs, ... }: {
|
||||||
|
# Intel graphics support for hardware transcoding
|
||||||
|
hardware.graphics = {
|
||||||
|
enable = true;
|
||||||
|
extraPackages = with pkgs; [
|
||||||
|
intel-media-driver
|
||||||
|
vpl-gpu-rt
|
||||||
|
intel-compute-runtime
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Set VA-API driver to iHD (modern Intel driver for N100)
|
||||||
|
environment.sessionVariables = {
|
||||||
|
LIBVA_DRIVER_NAME = "iHD";
|
||||||
|
};
|
||||||
|
|
||||||
|
services.jellyfin = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Override systemd hardening for GPU access
|
||||||
|
systemd.services.jellyfin = {
|
||||||
|
serviceConfig = {
|
||||||
|
PrivateUsers = lib.mkForce false; # Disable user namespacing - breaks GPU device access
|
||||||
|
DeviceAllow = [
|
||||||
|
"/dev/dri/card0 rw"
|
||||||
|
"/dev/dri/renderD128 rw"
|
||||||
|
];
|
||||||
|
SupplementaryGroups = [ "render" "video" ]; # Critical: Explicit group membership for GPU access
|
||||||
|
};
|
||||||
|
environment = {
|
||||||
|
LIBVA_DRIVER_NAME = "iHD"; # Ensure service sees this variable
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
69
hosts/fw/modules/pyload/pyload.nix
Normal file
69
hosts/fw/modules/pyload/pyload.nix
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{ pkgs, lib, ... }:
|
||||||
|
{
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
unrar # Required for RAR archive extraction
|
||||||
|
p7zip # Required for 7z and other archive formats
|
||||||
|
];
|
||||||
|
|
||||||
|
services.pyload = {
|
||||||
|
enable = true;
|
||||||
|
downloadDirectory = "/downloads";
|
||||||
|
listenAddress = "0.0.0.0";
|
||||||
|
port = 8000;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Configure pyload service
|
||||||
|
systemd.services.pyload = {
|
||||||
|
# Add extraction tools to service PATH
|
||||||
|
path = with pkgs; [
|
||||||
|
unrar # For RAR extraction
|
||||||
|
p7zip # For 7z extraction
|
||||||
|
];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
# Disable SSL certificate verification
|
||||||
|
PYLOAD__GENERAL__SSL_VERIFY = "0";
|
||||||
|
|
||||||
|
# Download speed limiting (150 Mbit/s = 19200 KiB/s)
|
||||||
|
PYLOAD__DOWNLOAD__LIMIT_SPEED = "1";
|
||||||
|
PYLOAD__DOWNLOAD__MAX_SPEED = "19200";
|
||||||
|
|
||||||
|
# Enable ExtractArchive plugin
|
||||||
|
PYLOAD__EXTRACTARCHIVE__ENABLED = "1";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__DELETE = "1";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__DELTOTRASH = "0";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__REPAIR = "1";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__RECURSIVE = "1";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__FULLPATH = "1";
|
||||||
|
|
||||||
|
# Enable ExternalScripts plugin for hooks
|
||||||
|
PYLOAD__EXTERNALSCRIPTS__ENABLED = "1";
|
||||||
|
PYLOAD__EXTERNALSCRIPTS__UNLOCK = "1"; # Run hooks asynchronously
|
||||||
|
};
|
||||||
|
|
||||||
|
# Bind-mount DNS configuration files into the chroot
|
||||||
|
serviceConfig = {
|
||||||
|
BindReadOnlyPaths = [
|
||||||
|
"/etc/resolv.conf"
|
||||||
|
"/etc/nsswitch.conf"
|
||||||
|
"/etc/hosts"
|
||||||
|
"/etc/ssl"
|
||||||
|
"/etc/static/ssl"
|
||||||
|
];
|
||||||
|
# Bind mount multimedia directory as writable for FileBot hook scripts
|
||||||
|
BindPaths = [ "/multimedia" ];
|
||||||
|
|
||||||
|
# Override SystemCallFilter to allow @resources syscalls
|
||||||
|
# FileBot (Java) needs resource management syscalls like setpriority
|
||||||
|
# during cleanup operations. Still block privileged syscalls for security.
|
||||||
|
# Use mkForce to completely replace the NixOS module's default filter.
|
||||||
|
SystemCallFilter = lib.mkForce [
|
||||||
|
"@system-service"
|
||||||
|
"@resources" # Explicitly allow resource management syscalls
|
||||||
|
"~@privileged" # Still block privileged operations
|
||||||
|
"fchown" # Re-allow fchown for FileBot file operations
|
||||||
|
"fchown32" # 32-bit compatibility
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
gitea-runner = 10003;
|
gitea-runner = 10003;
|
||||||
podman = 10004;
|
podman = 10004;
|
||||||
foundry-vtt = 10005;
|
foundry-vtt = 10005;
|
||||||
|
pyload = 10006;
|
||||||
|
jellyfin = 10007;
|
||||||
|
filebot = 10008;
|
||||||
};
|
};
|
||||||
gids = {
|
gids = {
|
||||||
unbound = 10001;
|
unbound = 10001;
|
||||||
@@ -12,5 +15,8 @@
|
|||||||
gitea-runner = 10003;
|
gitea-runner = 10003;
|
||||||
podman = 10004;
|
podman = 10004;
|
||||||
foundry-vtt = 10005;
|
foundry-vtt = 10005;
|
||||||
|
pyload = 10006;
|
||||||
|
jellyfin = 10007;
|
||||||
|
filebot = 10008;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
let
|
let
|
||||||
hostname = "vscode-server";
|
hostname = "vscode-server";
|
||||||
unstable = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz") {
|
|
||||||
config = config.nixpkgs.config;
|
|
||||||
system = pkgs.system;
|
|
||||||
};
|
|
||||||
in {
|
in {
|
||||||
microvm.vms.${hostname} = {
|
microvm.vms.${hostname} = {
|
||||||
autostart = true;
|
autostart = true;
|
||||||
@@ -24,7 +20,7 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
unstable.ddev
|
pkgs.ddev
|
||||||
];
|
];
|
||||||
|
|
||||||
# Docker is required for ddev
|
# Docker is required for ddev
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ in {
|
|||||||
# needed for matrix
|
# needed for matrix
|
||||||
"olm-3.2.16"
|
"olm-3.2.16"
|
||||||
];
|
];
|
||||||
|
allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
|
||||||
|
"n8n"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config = {
|
config = {
|
||||||
@@ -54,10 +57,13 @@ in {
|
|||||||
../../utils/modules/lego/lego.nix
|
../../utils/modules/lego/lego.nix
|
||||||
# ../../utils/modules/borgbackup.nix
|
# ../../utils/modules/borgbackup.nix
|
||||||
|
|
||||||
# ./phpldapadmin.nix
|
./phpldapadmin.nix
|
||||||
./zammad.nix
|
|
||||||
./proxies.nix
|
./proxies.nix
|
||||||
./matrix.nix
|
# ./matrix.nix
|
||||||
|
./n8n.nix
|
||||||
|
# ./piped.nix # Replaced by Invidious
|
||||||
|
./invidious.nix
|
||||||
|
./invidious-init-user.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
networkPrefix = config.networkPrefix;
|
networkPrefix = config.networkPrefix;
|
||||||
@@ -79,8 +85,10 @@ in {
|
|||||||
directories = [
|
directories = [
|
||||||
"/var/lib/zammad"
|
"/var/lib/zammad"
|
||||||
"/var/lib/postgresql"
|
"/var/lib/postgresql"
|
||||||
|
"/var/lib/n8n"
|
||||||
"/var/log"
|
"/var/log"
|
||||||
"/var/lib/systemd/coredump"
|
"/var/lib/systemd/coredump"
|
||||||
|
"/var/backup"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
64
hosts/fw/modules/web/invidious-init-user.nix
Normal file
64
hosts/fw/modules/web/invidious-init-user.nix
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
pythonWithBcrypt = pkgs.python3.withPackages (ps: [ ps.bcrypt ]);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# Invidious admin user initialization
|
||||||
|
# Creates the initial admin user directly in the PostgreSQL database
|
||||||
|
|
||||||
|
# Secret for admin user password
|
||||||
|
sops.secrets."invidious-admin-password" = {
|
||||||
|
sopsFile = ./secrets.yaml;
|
||||||
|
};
|
||||||
|
|
||||||
|
# One-time service to create admin user
|
||||||
|
systemd.services.invidious-init-admin-user = {
|
||||||
|
description = "Initialize Invidious admin user";
|
||||||
|
after = [ "invidious.service" "postgresql.service" ];
|
||||||
|
wants = [ "invidious.service" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
User = "postgres";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
LoadCredential = [ "admin_password:${config.sops.secrets."invidious-admin-password".path}" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
# Wait for Invidious to initialize the database schema
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Check if user already exists
|
||||||
|
USER_EXISTS=$(${pkgs.postgresql}/bin/psql -d invidious -tAc "SELECT COUNT(*) FROM users WHERE email = 'admin@cloonar.com';")
|
||||||
|
|
||||||
|
if [ "$USER_EXISTS" -eq "0" ]; then
|
||||||
|
echo "Creating admin user..."
|
||||||
|
|
||||||
|
# Read password from credential and trim whitespace
|
||||||
|
PASSWORD=$(cat $CREDENTIALS_DIRECTORY/admin_password | tr -d '\n\r')
|
||||||
|
|
||||||
|
# Truncate to 55 characters (Invidious password limit)
|
||||||
|
PASSWORD="''${PASSWORD:0:55}"
|
||||||
|
|
||||||
|
# Generate bcrypt hash
|
||||||
|
HASH=$(${pythonWithBcrypt}/bin/python3 -c "import bcrypt; import sys; print(bcrypt.hashpw('$PASSWORD'.encode(), bcrypt.gensalt(rounds=10)).decode())")
|
||||||
|
|
||||||
|
# Generate random token
|
||||||
|
TOKEN=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 32)
|
||||||
|
|
||||||
|
# Insert user into database
|
||||||
|
${pkgs.postgresql}/bin/psql -d invidious <<-SQL
|
||||||
|
INSERT INTO users (email, password, preferences, updated, notifications, subscriptions, watched, token)
|
||||||
|
VALUES ('admin@cloonar.com', '$HASH', '{}', NOW(), ARRAY[]::text[], ARRAY[]::text[], ARRAY[]::text[], '$TOKEN')
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
echo "Admin user created successfully"
|
||||||
|
else
|
||||||
|
echo "Admin user already exists, skipping..."
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
231
hosts/fw/modules/web/invidious.nix
Normal file
231
hosts/fw/modules/web/invidious.nix
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
{
|
||||||
|
# Invidious - Privacy-focused YouTube frontend
|
||||||
|
# Replaces Piped with native NixOS service
|
||||||
|
|
||||||
|
# Secret for Invidious companion authentication
|
||||||
|
sops.secrets.invidious-companion-key = {
|
||||||
|
key = "invidious-companion-key";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Main Invidious service
|
||||||
|
services.invidious = {
|
||||||
|
enable = true;
|
||||||
|
domain = "invidious.cloonar.com";
|
||||||
|
port = 3000;
|
||||||
|
|
||||||
|
# PostgreSQL database configuration
|
||||||
|
database = {
|
||||||
|
createLocally = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable nginx reverse proxy with automatic TLS
|
||||||
|
nginx.enable = true;
|
||||||
|
|
||||||
|
# Enable http3-ytproxy for video/image proxying
|
||||||
|
# Handles /videoplayback, /vi/, /ggpht/, /sb/ paths
|
||||||
|
http3-ytproxy.enable = true;
|
||||||
|
|
||||||
|
# Signature helper - crashes with current YouTube player format
|
||||||
|
# sig-helper = {
|
||||||
|
# enable = true;
|
||||||
|
# };
|
||||||
|
|
||||||
|
# Service settings
|
||||||
|
settings = {
|
||||||
|
# Disable registration - admin user created via init script
|
||||||
|
registration_enabled = false;
|
||||||
|
|
||||||
|
# Disable CAPTCHA (not needed for private instance)
|
||||||
|
captcha_enabled = false;
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
check_tables = true;
|
||||||
|
db = {
|
||||||
|
user = "invidious";
|
||||||
|
dbname = "invidious";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Optional: Instance customization
|
||||||
|
default_home = "Popular";
|
||||||
|
feed_menu = [ "Popular" "Trending" "Subscriptions" ];
|
||||||
|
|
||||||
|
# HTTPS configuration for proper URL generation
|
||||||
|
external_port = mkForce 443;
|
||||||
|
https_only = mkForce true;
|
||||||
|
|
||||||
|
# YouTube compatibility settings
|
||||||
|
use_quic = true;
|
||||||
|
force_resolve = "ipv4";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Use Podman for OCI containers
|
||||||
|
virtualisation.oci-containers.backend = "podman";
|
||||||
|
|
||||||
|
# Create Invidious network for container communication
|
||||||
|
systemd.services.init-invidious-network = {
|
||||||
|
description = "Create Podman network for Invidious companion";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "podman-invidious-companion.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
${pkgs.podman}/bin/podman network exists invidious-net || \
|
||||||
|
${pkgs.podman}/bin/podman network create --interface-name=podman2 --subnet=10.90.0.0/24 invidious-net
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create systemd tmpfiles directory for Invidious config
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/invidious 0755 root root - -"
|
||||||
|
"d /run/invidious-companion 0700 root root - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Generate companion environment file with secret key
|
||||||
|
systemd.services.invidious-companion-env-generate = {
|
||||||
|
description = "Generate Invidious companion environment file";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "podman-invidious-companion.service" ];
|
||||||
|
after = [ "init-invidious-network.service" ];
|
||||||
|
requires = [ "init-invidious-network.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
COMPANION_KEY=$(cat ${config.sops.secrets.invidious-companion-key.path})
|
||||||
|
cat > /run/invidious-companion/env <<EOF
|
||||||
|
PORT=8282
|
||||||
|
HOST=0.0.0.0
|
||||||
|
SERVER_SECRET_KEY=$COMPANION_KEY
|
||||||
|
EOF
|
||||||
|
chmod 600 /run/invidious-companion/env
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Invidious Companion container (handles PO token generation and video streams)
|
||||||
|
virtualisation.oci-containers.containers.invidious-companion = {
|
||||||
|
image = "quay.io/invidious/invidious-companion:latest";
|
||||||
|
ports = [ "127.0.0.1:8282:8282" ];
|
||||||
|
volumes = [
|
||||||
|
"invidious-companion-cache:/var/tmp:rw"
|
||||||
|
];
|
||||||
|
environmentFiles = [
|
||||||
|
"/run/invidious-companion/env"
|
||||||
|
];
|
||||||
|
extraOptions = [
|
||||||
|
"--pull=newer"
|
||||||
|
"--network=invidious-net"
|
||||||
|
"--cap-drop=ALL"
|
||||||
|
"--security-opt=no-new-privileges:true"
|
||||||
|
"--read-only"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Ensure companion container depends on env file generation
|
||||||
|
systemd.services."podman-invidious-companion" = {
|
||||||
|
after = mkAfter [ "invidious-companion-env-generate.service" ];
|
||||||
|
requires = mkAfter [ "invidious-companion-env-generate.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Generate Invidious companion config with actual secret key
|
||||||
|
systemd.services.invidious-companion-config-generate = {
|
||||||
|
description = "Generate Invidious companion configuration";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "invidious.service" ];
|
||||||
|
after = [ "init-invidious-network.service" ];
|
||||||
|
requires = [ "init-invidious-network.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
mkdir -p /var/lib/invidious
|
||||||
|
COMPANION_KEY=$(cat ${config.sops.secrets.invidious-companion-key.path})
|
||||||
|
cat > /var/lib/invidious/companion-config.json <<EOF
|
||||||
|
{
|
||||||
|
"invidious_companion": [
|
||||||
|
{
|
||||||
|
"private_url": "http://127.0.0.1:8282/companion"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"invidious_companion_key": "$COMPANION_KEY"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
chmod 644 /var/lib/invidious/companion-config.json
|
||||||
|
chown root:root /var/lib/invidious/companion-config.json
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Configure Invidious to use companion via extraSettingsFile
|
||||||
|
services.invidious.extraSettingsFile = "/var/lib/invidious/companion-config.json";
|
||||||
|
|
||||||
|
# Ensure Invidious service depends on companion config generation
|
||||||
|
systemd.services.invidious = {
|
||||||
|
after = mkAfter [ "invidious-companion-config-generate.service" ];
|
||||||
|
requires = mkAfter [ "invidious-companion-config-generate.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Override nginx vhost configuration
|
||||||
|
services.nginx.virtualHosts."invidious.cloonar.com" = {
|
||||||
|
acmeRoot = null;
|
||||||
|
|
||||||
|
# Complete http3-ytproxy configuration with proper headers and buffering
|
||||||
|
# This overrides the minimal config from the NixOS module
|
||||||
|
locations."~ (^/videoplayback|^/vi/|^/ggpht/|^/sb/)" = {
|
||||||
|
proxyPass = "http://unix:/run/http3-ytproxy/socket/http-proxy.sock";
|
||||||
|
extraConfig = ''
|
||||||
|
# Enable buffering for large video files
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffers 1024 16k;
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
|
||||||
|
# Use HTTP/1.1 with keepalive for better performance
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
|
||||||
|
# Hide headers that might cause issues
|
||||||
|
proxy_hide_header Cache-Control;
|
||||||
|
proxy_hide_header etag;
|
||||||
|
proxy_hide_header "alt-svc";
|
||||||
|
proxy_hide_header Access-Control-Allow-Origin;
|
||||||
|
proxy_hide_header Access-Control-Allow-Methods;
|
||||||
|
proxy_hide_header Access-Control-Allow-Headers;
|
||||||
|
proxy_hide_header Access-Control-Expose-Headers;
|
||||||
|
|
||||||
|
# CORS headers for iOS clients like Yattee
|
||||||
|
add_header Access-Control-Allow-Origin * always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "Range, Content-Type" always;
|
||||||
|
add_header Access-Control-Expose-Headers "Content-Length, Content-Range" always;
|
||||||
|
|
||||||
|
# Handle preflight requests
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optimize for large file transfers
|
||||||
|
sendfile on;
|
||||||
|
sendfile_max_chunk 512k;
|
||||||
|
tcp_nopush on;
|
||||||
|
|
||||||
|
# Disable access logging for video traffic
|
||||||
|
access_log off;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Firewall configuration for Invidious
|
||||||
|
# (nginx handles external access on ports 80/443)
|
||||||
|
|
||||||
|
# PostgreSQL backup for Invidious database
|
||||||
|
services.postgresqlBackup = {
|
||||||
|
databases = [ "invidious" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
94
hosts/fw/modules/web/n8n.nix
Normal file
94
hosts/fw/modules/web/n8n.nix
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Create static user instead of using DynamicUser
|
||||||
|
users.users.n8n = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "n8n";
|
||||||
|
home = "/var/lib/n8n";
|
||||||
|
};
|
||||||
|
users.groups.n8n = {};
|
||||||
|
|
||||||
|
# PostgreSQL database setup
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
ensureDatabases = [ "n8n" ];
|
||||||
|
ensureUsers = [{
|
||||||
|
name = "n8n";
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
# n8n service configuration
|
||||||
|
services.n8n.enable = true;
|
||||||
|
|
||||||
|
# Configure n8n via environment variables
|
||||||
|
systemd.services.n8n = {
|
||||||
|
environment = lib.mkForce {
|
||||||
|
# Database configuration (migrated from services.n8n.settings)
|
||||||
|
DB_TYPE = "postgresdb";
|
||||||
|
DB_POSTGRESDB_HOST = "/run/postgresql";
|
||||||
|
DB_POSTGRESDB_DATABASE = "n8n";
|
||||||
|
DB_POSTGRESDB_USER = "n8n";
|
||||||
|
EXECUTIONS_DATA_PRUNE = "true";
|
||||||
|
EXECUTIONS_DATA_MAX_AGE = "168"; # 7 days
|
||||||
|
# Other settings
|
||||||
|
N8N_ENCRYPTION_KEY = ""; # Will be set via environmentFile
|
||||||
|
N8N_VERSION_NOTIFICATIONS_ENABLED = "false";
|
||||||
|
N8N_DIAGNOSTICS_ENABLED = "false";
|
||||||
|
N8N_PERSONALIZATION_ENABLED = "false";
|
||||||
|
WEBHOOK_URL = "https://n8n.cloonar.com";
|
||||||
|
N8N_HOST = "n8n.cloonar.com";
|
||||||
|
N8N_PROTOCOL = "https";
|
||||||
|
N8N_PORT = "5678";
|
||||||
|
};
|
||||||
|
serviceConfig = {
|
||||||
|
DynamicUser = lib.mkForce false;
|
||||||
|
User = "n8n";
|
||||||
|
Group = "n8n";
|
||||||
|
EnvironmentFile = config.sops.secrets.n8n-env.path;
|
||||||
|
};
|
||||||
|
preStart = lib.mkAfter ''
|
||||||
|
# Setup git SSH key if provided
|
||||||
|
if [ -n "$N8N_GIT_SSH_KEY_PATH" ] && [ -f "$N8N_GIT_SSH_KEY_PATH" ]; then
|
||||||
|
mkdir -p /var/lib/n8n/.ssh
|
||||||
|
chmod 700 /var/lib/n8n/.ssh
|
||||||
|
cp "$N8N_GIT_SSH_KEY_PATH" /var/lib/n8n/.ssh/id_ed25519
|
||||||
|
chmod 600 /var/lib/n8n/.ssh/id_ed25519
|
||||||
|
chown -R n8n:n8n /var/lib/n8n/.ssh
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# SOPS secrets (managed within the web microvm)
|
||||||
|
sops.secrets.n8n-env = {
|
||||||
|
owner = "n8n";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
sops.secrets.n8n-git-key = {
|
||||||
|
owner = "n8n";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
|
||||||
|
# PostgreSQL backup
|
||||||
|
services.postgresqlBackup.enable = true;
|
||||||
|
services.postgresqlBackup.databases = [ "n8n" ];
|
||||||
|
|
||||||
|
# Nginx reverse proxy
|
||||||
|
services.nginx.virtualHosts."n8n.cloonar.com" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
acmeRoot = null;
|
||||||
|
|
||||||
|
# Restrict to internal LAN only
|
||||||
|
extraConfig = ''
|
||||||
|
allow ${config.networkPrefix}.96.0/24;
|
||||||
|
allow ${config.networkPrefix}.98.0/24;
|
||||||
|
deny all;
|
||||||
|
'';
|
||||||
|
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:5678";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,94 +2,51 @@
|
|||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
|
|
||||||
let
|
|
||||||
phpldapadmin = pkgs.callPackage ../../pkgs/phpldapadmin.nix {};
|
|
||||||
fpm = config.services.phpfpm.pools.phpldapadmin;
|
|
||||||
stateDir = "/var/lib/phpldapadmin";
|
|
||||||
domain = "phpldapadmin.cloonar.com";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
|
virtualisation.oci-containers.backend = "podman";
|
||||||
users.users.phpldapadmin = {
|
virtualisation.oci-containers.containers = {
|
||||||
description = "PHPLdapAdmin Service";
|
phpldapadmin = {
|
||||||
home = stateDir;
|
image = "phpldapadmin/phpldapadmin:2.2.2";
|
||||||
useDefaultShell = true;
|
autoStart = true;
|
||||||
group = "phpldapadmin";
|
ports = [
|
||||||
isSystemUser = true;
|
"8087:8080/tcp"
|
||||||
|
];
|
||||||
|
environmentFiles = [
|
||||||
|
config.sops.secrets.phpldapadmin.path
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
users.groups.phpldapadmin = { };
|
systemd.timers."restart-phpldapadmin" = {
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
sops.secrets.phpldapadmin.owner = "phpldapadmin";
|
timerConfig = {
|
||||||
|
OnCalendar = "*-*-* 3:00:00";
|
||||||
environment.etc."phpldapadmin/env".source = config.sops.secrets.phpldapadmin.path;
|
Unit = "restart-phpldapadmin.service";
|
||||||
|
|
||||||
services.nginx = {
|
|
||||||
enable = true;
|
|
||||||
virtualHosts = {
|
|
||||||
"${domain}" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
acmeRoot = null;
|
|
||||||
root = stateDir;
|
|
||||||
locations."/" = {
|
|
||||||
root = "${phpldapadmin}/public";
|
|
||||||
index = "index.php";
|
|
||||||
extraConfig = ''
|
|
||||||
location ~* \.php(/|$) {
|
|
||||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
|
||||||
fastcgi_pass unix:${fpm.socket};
|
|
||||||
|
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
||||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
|
||||||
|
|
||||||
include ${pkgs.nginx}/conf/fastcgi_params;
|
|
||||||
include ${pkgs.nginx}/conf/fastcgi.conf;
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."pla.cloonar.com" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
acmeRoot = null;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://localhost:8087";
|
||||||
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
environment.etc.nginx_allowed_groups = {
|
systemd.services."restart-phpldapadmin" = {
|
||||||
text = "employees";
|
script = ''
|
||||||
mode = "0444";
|
set -eu
|
||||||
};
|
if ${pkgs.systemd}/bin/systemctl is-active --quiet podman-phpldapadmin.service; then
|
||||||
|
${pkgs.systemd}/bin/systemctl restart podman-phpldapadmin.service
|
||||||
security.pam.services.nginx.text = ''
|
fi
|
||||||
# auth required pam_listfile.so \
|
|
||||||
# item=group sense=allow onerr=fail file=/etc/nginx_allowed_groups
|
|
||||||
auth required ${pkgs.nss_pam_ldapd}/lib/security/pam_ldap.so
|
|
||||||
account required ${pkgs.nss_pam_ldapd}/lib/security/pam_ldap.so
|
|
||||||
'';
|
|
||||||
|
|
||||||
services.phpfpm.pools.phpldapadmin = {
|
|
||||||
user = "phpldapadmin";
|
|
||||||
phpOptions = ''
|
|
||||||
error_log = 'stderr'
|
|
||||||
log_errors = on
|
|
||||||
'';
|
'';
|
||||||
settings = mapAttrs (name: mkDefault) {
|
serviceConfig = {
|
||||||
"listen.owner" = "nginx";
|
Type = "oneshot";
|
||||||
"listen.group" = "nginx";
|
User = "root";
|
||||||
"listen.mode" = "0660";
|
|
||||||
"pm" = "dynamic";
|
|
||||||
"pm.max_children" = 75;
|
|
||||||
"pm.start_servers" = 2;
|
|
||||||
"pm.min_spare_servers" = 1;
|
|
||||||
"pm.max_spare_servers" = 20;
|
|
||||||
"pm.max_requests" = 500;
|
|
||||||
"catch_workers_output" = true;
|
|
||||||
};
|
};
|
||||||
phpEnv."PATH" = pkgs.lib.makeBinPath [
|
|
||||||
pkgs.which
|
|
||||||
phpldapadmin
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
sops.secrets.phpldapadmin = {};
|
||||||
"d '${stateDir}' 0750 phpldapadmin phpldapadmin - -"
|
|
||||||
];
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
334
hosts/fw/modules/web/piped.nix
Normal file
334
hosts/fw/modules/web/piped.nix
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
# Piped domains
|
||||||
|
domain = "piped.cloonar.com";
|
||||||
|
apiDomain = "pipedapi.cloonar.com";
|
||||||
|
|
||||||
|
# Port configuration
|
||||||
|
backendPort = 8082;
|
||||||
|
proxyPort = 8081;
|
||||||
|
bgHelperPort = 3000;
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
dbName = "piped";
|
||||||
|
dbUser = "piped";
|
||||||
|
|
||||||
|
# Piped backend configuration file
|
||||||
|
backendConfig = pkgs.writeText "config.properties" ''
|
||||||
|
# Database configuration
|
||||||
|
# 10.88.0.1 is the default Podman bridge gateway IP
|
||||||
|
hibernate.connection.url=jdbc:postgresql://10.89.0.1:5432/${dbName}
|
||||||
|
hibernate.connection.driver_class=org.postgresql.Driver
|
||||||
|
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
hibernate.connection.username=${dbUser}
|
||||||
|
hibernate.connection.password=PLACEHOLDER_DB_PASSWORD
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
|
PORT=${toString backendPort}
|
||||||
|
HTTP_WORKERS=2
|
||||||
|
# Proxy configuration
|
||||||
|
PROXY_PART=https://${apiDomain}/proxy
|
||||||
|
|
||||||
|
# API URL
|
||||||
|
API_URL=https://${apiDomain}
|
||||||
|
|
||||||
|
# Frontend URL
|
||||||
|
FRONTEND_URL=https://${domain}
|
||||||
|
|
||||||
|
# Disable registration (private instance)
|
||||||
|
DISABLE_REGISTRATION=false
|
||||||
|
|
||||||
|
# ReCaptcha (disabled for private instance)
|
||||||
|
CAPTCHA_ENABLED=false
|
||||||
|
|
||||||
|
# Matrix support (optional)
|
||||||
|
MATRIX_SERVER=
|
||||||
|
|
||||||
|
# Sentry (disabled)
|
||||||
|
SENTRY_DSN=
|
||||||
|
|
||||||
|
# Compromised password check (optional)
|
||||||
|
COMPROMISED_PASSWORD_CHECK=true
|
||||||
|
|
||||||
|
# Feed retention (days)
|
||||||
|
FEED_RETENTION=30
|
||||||
|
|
||||||
|
# Background helper for iOS compatibility (generates PoTokens)
|
||||||
|
BG_HELPER_URL=http://piped-bg-helper:${toString bgHelperPort}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Piped frontend configuration
|
||||||
|
# Note: Piped requires the config in a specific format as a JSON file served at /config/config.json
|
||||||
|
frontendConfig = pkgs.writeText "config.json" (builtins.toJSON {
|
||||||
|
apiUrl = "https://${apiDomain}";
|
||||||
|
disableRegistration = false;
|
||||||
|
instanceName = "Piped (Private)";
|
||||||
|
});
|
||||||
|
in
|
||||||
|
{
|
||||||
|
sops.secrets = {
|
||||||
|
# Database password for postgres user (used by piped-db-init)
|
||||||
|
piped-db-password-postgres = {
|
||||||
|
key = "piped-db-password";
|
||||||
|
owner = "postgres";
|
||||||
|
};
|
||||||
|
# Database password for piped user (used by piped-config-generate)
|
||||||
|
piped-db-password-piped = {
|
||||||
|
key = "piped-db-password";
|
||||||
|
owner = "piped";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create system user for Piped
|
||||||
|
users.users.piped = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "piped";
|
||||||
|
home = "/var/lib/piped";
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
users.groups.piped = { };
|
||||||
|
|
||||||
|
# Note: piped user doesn't need special group membership for Podman
|
||||||
|
|
||||||
|
# Create piped config directory structure
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/piped 0700 piped piped - -"
|
||||||
|
"d /var/lib/piped/config 0700 piped piped - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
# PostgreSQL database setup
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
ensureDatabases = [ dbName ];
|
||||||
|
ensureUsers = [{
|
||||||
|
name = dbUser;
|
||||||
|
ensureDBOwnership = true;
|
||||||
|
}];
|
||||||
|
# Allow connections from Podman containers
|
||||||
|
settings = {
|
||||||
|
listen_addresses = mkForce "*";
|
||||||
|
};
|
||||||
|
authentication = pkgs.lib.mkOverride 10 ''
|
||||||
|
# Allow local connections
|
||||||
|
local all all trust
|
||||||
|
# Allow connections from localhost
|
||||||
|
host all all 127.0.0.1/32 trust
|
||||||
|
host all all ::1/128 trust
|
||||||
|
# Allow connections from Podman network (typically 10.88.0.0/16)
|
||||||
|
host ${dbName} ${dbUser} 10.88.0.0/16 scram-sha-256
|
||||||
|
host ${dbName} ${dbUser} 10.89.0.0/16 scram-sha-256
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# PostgreSQL backup
|
||||||
|
services.postgresqlBackup.databases = [ dbName ];
|
||||||
|
|
||||||
|
# Allow Podman containers to connect to PostgreSQL
|
||||||
|
networking.firewall.interfaces."podman1".allowedTCPPorts = [ 5432 ];
|
||||||
|
networking.firewall.interfaces."podman1".allowedUDPPorts = [ 53 5432 ];
|
||||||
|
|
||||||
|
# Setup database password (runs before containers start)
|
||||||
|
systemd.services.piped-db-init = {
|
||||||
|
description = "Initialize Piped database password";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "postgresql.service" ];
|
||||||
|
requires = [ "postgresql.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
User = "postgres";
|
||||||
|
Group = "postgres";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
password=$(cat ${config.sops.secrets.piped-db-password-postgres.path})
|
||||||
|
${config.services.postgresql.package}/bin/psql -c "ALTER USER ${dbUser} WITH PASSWORD '$password';"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create Piped backend config with actual password
|
||||||
|
systemd.services.piped-config-generate = {
|
||||||
|
description = "Generate Piped backend configuration";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "podman-piped-backend.service" ];
|
||||||
|
after = [ "piped-db-init.service" ];
|
||||||
|
requires = [ "piped-db-init.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
User = "piped";
|
||||||
|
Group = "piped";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
mkdir -p /var/lib/piped/config
|
||||||
|
password=$(cat ${config.sops.secrets.piped-db-password-piped.path})
|
||||||
|
sed "s|PLACEHOLDER_DB_PASSWORD|$password|" ${backendConfig} > /var/lib/piped/config/config.properties
|
||||||
|
chmod 600 /var/lib/piped/config/config.properties
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Use Podman for OCI containers
|
||||||
|
virtualisation.oci-containers.backend = "podman";
|
||||||
|
|
||||||
|
# Create Piped network for container-to-container communication
|
||||||
|
systemd.services.init-piped-network = {
|
||||||
|
description = "Create Podman network for Piped services";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [
|
||||||
|
"podman-piped-backend.service"
|
||||||
|
"podman-piped-bg-helper.service"
|
||||||
|
"podman-piped-proxy.service"
|
||||||
|
];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
${pkgs.podman}/bin/podman network exists piped-net || \
|
||||||
|
${pkgs.podman}/bin/podman network create --interface-name=podman1 --subnet=10.89.0.0/24 piped-net
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Piped Backend Podman container (using official upstream image)
|
||||||
|
virtualisation.oci-containers.containers.piped-backend = {
|
||||||
|
image = "1337kavin/piped:latest";
|
||||||
|
ports = [ "127.0.0.1:${toString backendPort}:${toString backendPort}" ];
|
||||||
|
volumes = [
|
||||||
|
"/var/lib/piped/config/config.properties:/app/config.properties:ro"
|
||||||
|
];
|
||||||
|
extraOptions = [
|
||||||
|
"--pull=newer"
|
||||||
|
"--network=podman" # Default bridge for PostgreSQL access at 10.88.0.1
|
||||||
|
"--network=piped-net" # Custom network for DNS resolution to bg-helper
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Ensure config is generated before backend container starts
|
||||||
|
systemd.services."podman-piped-backend" = {
|
||||||
|
after = mkAfter [ "piped-config-generate.service" ];
|
||||||
|
requires = mkAfter [ "piped-config-generate.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Piped Background Helper (generates PoTokens for iOS compatibility)
|
||||||
|
virtualisation.oci-containers.containers.piped-bg-helper = {
|
||||||
|
image = "1337kavin/bg-helper-server:latest";
|
||||||
|
ports = [ "127.0.0.1:${toString bgHelperPort}:3000" ];
|
||||||
|
extraOptions = [
|
||||||
|
"--pull=newer"
|
||||||
|
"--network=piped-net"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Piped Proxy Podman container
|
||||||
|
virtualisation.oci-containers.containers.piped-proxy = {
|
||||||
|
image = "1337kavin/piped-proxy:latest";
|
||||||
|
ports = [ "127.0.0.1:${toString proxyPort}:8080" ];
|
||||||
|
environment = {
|
||||||
|
UDS = "0"; # Disable Unix domain sockets
|
||||||
|
};
|
||||||
|
extraOptions = [
|
||||||
|
"--pull=newer"
|
||||||
|
"--network=piped-net"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Nginx configuration
|
||||||
|
services.nginx.virtualHosts.${domain} = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
acmeRoot = null;
|
||||||
|
|
||||||
|
# Serve Piped frontend static files
|
||||||
|
root = "${pkgs.piped}";
|
||||||
|
|
||||||
|
# Frontend (root)
|
||||||
|
locations."/" = {
|
||||||
|
extraConfig = ''
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Serve custom frontend config
|
||||||
|
locations."= /config/config.json" = {
|
||||||
|
alias = frontendConfig;
|
||||||
|
extraConfig = ''
|
||||||
|
add_header Content-Type application/json;
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# API and Proxy domain
|
||||||
|
services.nginx.virtualHosts.${apiDomain} = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
acmeRoot = null;
|
||||||
|
|
||||||
|
# Backend API (served at root)
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:${toString backendPort}/";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
# Hide CORS headers from backend to avoid duplicates
|
||||||
|
proxy_hide_header Access-Control-Allow-Origin;
|
||||||
|
proxy_hide_header Access-Control-Allow-Methods;
|
||||||
|
proxy_hide_header Access-Control-Allow-Headers;
|
||||||
|
proxy_hide_header Access-Control-Expose-Headers;
|
||||||
|
proxy_hide_header Access-Control-Allow-Credentials;
|
||||||
|
|
||||||
|
# CORS headers for iOS API requests
|
||||||
|
add_header Access-Control-Allow-Origin * always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, POST, HEAD, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "Range, Content-Type, Authorization" always;
|
||||||
|
add_header Access-Control-Expose-Headers "Content-Length, Content-Range" always;
|
||||||
|
|
||||||
|
# Handle preflight requests
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Increase timeouts for long-running requests
|
||||||
|
proxy_connect_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# YouTube Proxy
|
||||||
|
locations."/proxy/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:${toString proxyPort}/";
|
||||||
|
extraConfig = ''
|
||||||
|
# Hide CORS headers from proxy to avoid duplicates
|
||||||
|
proxy_hide_header Access-Control-Allow-Origin;
|
||||||
|
proxy_hide_header Access-Control-Allow-Methods;
|
||||||
|
proxy_hide_header Access-Control-Allow-Headers;
|
||||||
|
proxy_hide_header Access-Control-Expose-Headers;
|
||||||
|
proxy_hide_header Access-Control-Allow-Credentials;
|
||||||
|
|
||||||
|
# CORS headers for iOS HLS video streaming
|
||||||
|
add_header Access-Control-Allow-Origin * always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "Range, Content-Type" always;
|
||||||
|
add_header Access-Control-Expose-Headers "Content-Length, Content-Range" always;
|
||||||
|
|
||||||
|
# Handle preflight requests
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_buffering on;
|
||||||
|
|
||||||
|
# Increase buffer sizes for video streaming
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 256 16k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
|
||||||
|
# Increase timeouts for video streaming
|
||||||
|
proxy_connect_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -25,4 +25,61 @@
|
|||||||
recommendedProxySettings = true;
|
recommendedProxySettings = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
services.nginx.virtualHosts."fivefilters.cloonar.com" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
acmeRoot = null;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://${config.networkPrefix}.97.10";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services.nginx.virtualHosts."dl.cloonar.com" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
acmeRoot = null;
|
||||||
|
|
||||||
|
# Restrict to internal LAN only
|
||||||
|
extraConfig = ''
|
||||||
|
allow ${config.networkPrefix}.96.0/24;
|
||||||
|
allow ${config.networkPrefix}.97.0/24;
|
||||||
|
allow ${config.networkPrefix}.98.0/24;
|
||||||
|
deny all;
|
||||||
|
'';
|
||||||
|
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://${config.networkPrefix}.97.11:8000";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."jellyfin.cloonar.com" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
acmeRoot = null;
|
||||||
|
|
||||||
|
# Restrict to internal LAN only
|
||||||
|
extraConfig = ''
|
||||||
|
allow ${config.networkPrefix}.96.0/24;
|
||||||
|
allow ${config.networkPrefix}.97.0/24;
|
||||||
|
allow ${config.networkPrefix}.98.0/24;
|
||||||
|
allow ${config.networkPrefix}.99.0/24;
|
||||||
|
deny all;
|
||||||
|
'';
|
||||||
|
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://${config.networkPrefix}.97.11:8096";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
# Jellyfin-specific headers for proper streaming
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
|
||||||
|
# Disable buffering for better streaming performance
|
||||||
|
proxy_buffering off;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,54 @@
|
|||||||
borg-passphrase: ENC[AES256_GCM,data:2WjoqMRmXvW9EGMmpMYhrC0Qt0Dk7QWlbEncZPdK2SxVljEoFibjVEr6jeYdAx6UkaXdjk9pD3PBbls2tWt0TiNQdh8=,iv:bHzASNjqqfPsQ/1w/oM7x0FubAzzRkn+iWrZlenU9rs=,tag:ektqi0rqEywg9YGybPQesw==,type:str]
|
borg-passphrase: ENC[AES256_GCM,data:seAsFcQcBiIUnkoUYGoY6uEKbjf0TMJZklkE6TFwlHkdzwBqoKD0ASNzsIlrqEkQaLG7zlHpFci6SVnlMjSQsywZ2z8=,iv:E1Z/ttSVUvm8PTXq9lh12I0ogdQwORawm7DsUXh+04Q=,tag:pwZVzgO/MdIrKSNhutT+og==,type:str]
|
||||||
borg-ssh-key: ENC[AES256_GCM,data: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,iv:U3+fjacm8+gZAjPQNz2mjFYTUbLyltTaPiSKb3lvCmk=,tag:ZR6zI1UijDayIvH3v35Hqg==,type:str]
|
borg-ssh-key: ENC[AES256_GCM,data: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,iv:3O+SAjX/D4k9SUmGKAfriyOAKaH2Jm4tAbfKDOoZts0=,tag:3yeNyl9TjlENfV+IxZkj/w==,type:str]
|
||||||
zammad-key-base: ENC[AES256_GCM,data:HO9MuwcwjryuXr5No8sCPfso5bpLtQCoczrC/R214ecVIFwwH1uhMeNO8Tlh6EjRLPo7aVTSz87Vx5yaNVezvHCs55G6TT9mcNS/v/V7sbFz9dNIgbFblY3gFIAa4cViioYc71wdb7d4Tta7qhse5zQ41KhAqCWuGDgFErQA4Oc=,iv:b1wY8fW0psircSlNXwDjPzNWK8NyAMNqegitNcqV6U4=,tag:oQ7nyO9TKOOu6IF7ODzpPA==,type:str]
|
zammad-key-base: ENC[AES256_GCM,data:62Gj7zyDGGMTVOv2YvrNVDIX+fxt94KVQ/EJBIqXssM6nrKN7veh4sIoLy3+/KwEMpCL3cnb3x7BKXDndnjulfVuF6pTDUEaiH/8YC5YPp+N3imWRTFYDsCJEkB7AsXkuVCH7f0MoMO3v+56BBYFEAp9E7wVT4Jdid7h56zfzaY=,iv:MMvHSUhNaIZb6XBBXBJlqolmXzPKuiFH0jQxlKnK7GY=,tag:2PPZtcD9wWBI1mokXAfMxw==,type:str]
|
||||||
dendrite-private-key: ENC[AES256_GCM,data:ZHDIa/iYSZGofE67JU63fHRdKbs/ZyEJY45tV6H8WZAOcduGafPYBo2NCZ7nqLbc2Z9dUUgsrpzvkQ3+VaWqFUv7YsE+CbCx4CeiLGMkj8EAGzX4rkJGHMzkkc2UT7v9znCnKACS3fZtU69trqVMcf1PzgqepOHMBku37dzpwOQC/Tc3UTuO72M=,iv:Ljun1/ruY9cDBm9vu62riUrpGjrWtFFx90GeE7uc3Yo=,tag:FF4xPb1SDhK/4ITr/idvYg==,type:str]
|
invidious-hmac-key: ENC[AES256_GCM,data:UZM6COUUHgLu/OVjkkp7rvzhiXBo5O4V/X+8ig==,iv:VX46ainev8JfGNic9FpnYlP7ZQMpTrMwDH0kW/l776s=,tag:HkQwPt23lHw7TQj9HqFIhw==,type:str]
|
||||||
matrix-shared-secret: ENC[AES256_GCM,data:HeS4PT0R+TRU6Htwa5TChjK1VAjAdgSS8tSnva+ga3f+mEfJPTQ02pEvS2WFvcnchmEjNYy39zL/rbtX,iv:4yR+VgdJY3VcvLg18v+5jbJDSkFzaeyLNAZ0k8ivjdQ=,tag:RA96iSFDUdlXq30c/vkvpA==,type:str]
|
invidious-admin-password: ENC[AES256_GCM,data:YEdvUckgHhq23fa0ZDLvZM9/yRiClqT5LsoX6tPhNTi9rKTFUHwNrCKLZAr8dafbaw==,iv:romRuJxhQqSQMNepVS04Fu4e3SpA0yl7P8LUI1R76Iw=,tag:puncGz4EH5qkNfHRzjEdBg==,type:str]
|
||||||
phpldapadmin: ENC[AES256_GCM,data:CJBFQfi0qJmPQcxPcneHcXFsIku0a+xdv7rmrKzC0XsBcn3N/dP8cGBbkC/GcH2OWBhRWFNFm0GOEALbJa/1z/hFxbxn1QJlfglglaXHNjiwJqND51GmNzd+5GJ39RHR7w06fVABgCrDM60DChJLy0Iql/eCITYhZUGpoLd4I+fKXy9zggVIzAA3tTYziJNuaBQuMe/i8V8AIt0DBefrEBITyl3wi/+Y4utLXiEUPOWPGCYfS+Xp7LcHiTJ2rZzwKJjYPiPs+7UYx2IsT2+ksJtSHR0+ibUHXNzebBTmAZ3+YBoyeBvdw2VmsgJeCUTC2SLnBAsR4J3AoSDQcZ0XrHq2oIzZC/Mf5g==,iv:iHx495CM8LHqrsiNPwzFXZQxWJZ5kCgWYvgwirjy7Uw=,tag:c7FvYuYzYjqH/Bqs7FbMzA==,type:str]
|
invidious-companion-key: ENC[AES256_GCM,data:HERKJBEyZbdxLcButZZ+OA==,iv:RHXz3OdnR+6Y/GefC7NoSYAmJ9RrxkCO25jms0E0fRo=,tag:pDtK/LCVnXmCJBHfo9Yz1A==,type:str]
|
||||||
|
dendrite-private-key: ENC[AES256_GCM,data:ANQ9bFh4z03C755/Q1CKdPDMkBzqKXS64pJAvj975eMJfsfEXUfX72tRAJL/p3ok7iC4PZkM0Rp+ILjS24PyJHjAzIRLhP1P4NE0PFLQEuIBr7Z5D+s2E9rUDno7HtUSC5/Ht4qPTe7nhwwk+KM+OsuQJHfjm/gRxp3izcuha5qbErW/IWgiy6o=,iv:kwdbrb3UGx/3viNve6Zg1KrE4djt5pO02Nxdl6h7jhA=,tag:EXtYO6+TTnOJxYjelCRvKw==,type:str]
|
||||||
|
matrix-shared-secret: ENC[AES256_GCM,data:jDQXFuBtWrDRWG8y/4pT67oNyHmkTyzwvMb9daAmlNBwqNc79fKS28ODbbkcHUkDl2ueDdysLY3l4zmM,iv:tkHi0ufo2rLm88gEPn4I3knl61raFWbYbJvRCl5Vwr0=,tag:JBo1ud52EOWxowStCdU/ug==,type:str]
|
||||||
|
n8n-env: ENC[AES256_GCM,data:+pYI9J8wqY19IInhlomeGraw0zTFuHh3q6hPfGmcUrRzijc1xW1qI0sMADoakKrcN4mh/G+DzSu3D5fdWpEME5874xWn4iJvLuySVfjIyAzUWzadv3BDqMVEri6MdQUGuI71eBACb8iYLyHUUcy2Tso/KCPfT84oAJ5DXN3ccUBOKpAcjSbR3f1vuZfKBknAuomYmu2py+lOQnmXYvVbwkbstK77U38OOIJTktJE7BYqqt/m6NbGRZpm2Muu8l/NHHtiK1UeK9LtUlXm98iKC7ZOylQb+zAILhAihAohOaFIni1DRFKVv4FqaoQ2UsfQKyYoqKnbm/UraE27TQ+4Jpi28nbOR4GuZpkF29mZ7e/9OxOcJztgjUW+zKQKXsE=,iv:mmtFUEanzDIKuoOQvJ+Tm/Dn7DKXeXqO5geOFwxyVzo=,tag:8BSrrzu+rX6VpcEhUt32ag==,type:str]
|
||||||
|
n8n-git-key: ENC[AES256_GCM,data:owmAGcmEOw3HKVY7QMko7/5gO/6nWkWO5q+R1C/Q0KwPwVLKU/4GKS1TXcIPo+kB74nlgiKJ77hu7GczzJT8qIQp2kYi6OE+y/+sSBx90T5DBjS2CSJWZOuAgJOmaPE7zmEnEK+PQWMwfqgDOi2WnV9g/AH1ZtIVqbrarAX20Ma6Cqxcd8ZvGsA3tG79jhWt3VCXhM0Zy2sIbin7uafgKdBtswbdzTxS3UCVaFpJqFkeK7oKLATP2NC2MuB9V/DxbN3QK3TzRkjxmD+msgEhnVRyCbo6Y1SBbLfIv9QUcdwZ/gSurJGUh8JyfQykDsn2smLocTqtM2vau21LQBS2ODMhIs3+0DNzz2sPK+PNuR1qJVM9El0pe7mpgSP3EP2OQZIPwZgYq5HW825Qsnpslrcb6Rx2Csx01P4Xx4PATX5fGbnAlOTGwm34juOHYkWqPVwUqmx53evv4lNuj8xJi97s2zbIzFpDgOU/+fZ0NM0UYfwI8Vp4UAcKUiOSUzyzNODEfkpQtLAc8oqXq14C,iv:0GtMeydlw3hWzLSoLQfH88STq2lRC01xKRTMaWocsFc=,tag:eVbQnugJ8wMHQAAmRKtEkQ==,type:str]
|
||||||
|
phpldapadmin: ENC[AES256_GCM,data:0ce+P1JUQY6PrC/NX3gNIFPBA/1gAqzYTjR+yW7WqeOkmqKI+R/oSNIdnscrbHnz3rrGcCOV7xD0u18YBeiAbLVCxVfLePa04C5l3bGMWmhqGvcrNLoxk6dK/M0FXgf4qoVTBfwsUWDaTHUFTfLgffPFR/Qcpvc0Zl447CTQzGBPr7IfpiioR6Lt8LAyKLyc/C7NezEFZtDhNK9dsFGAAPKGykKGQUW6J3E+hs/iF38lTmUFM1Jo8z71N/2faBYwggoRLFcvZIZA2g1xKPHzUqbvU2lemE7bQiwi50bSGvAb39OszCZhAGo=,iv:zde4W0Jv8gtUuMsctrc7moOjF2ci+U9+7Mx3X0doMJg=,tag:6cyKEeVOUvqhFhznzWgVcg==,type:str]
|
||||||
|
piped-db-password: ENC[AES256_GCM,data:AWCGHzXnZ4KPgrzPyJVzyQKBwcAa2NDwfOTiitvoAJ6qG/7eeBieFD1L3MU=,iv:A6YYQBOGzkqPEGWdJmGmaxYlMsTUw6CiwriWWIo6T1M=,tag:ANWKHBKTgfq+sbif0yQ4XA==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
|
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiZmRBZm8wL3ZQdUZMSjRG
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYNVBOdGZHbmc2b2UyVGNj
|
||||||
cnFWTjNhc2gvd3pURkdjdEpZdUE2ZE9nVFdnCnEvRGlScFJVUGZRenV3VXI5cU85
|
aVJkV2RwYVlvTzBURndiVTVhOFFJRDVEckJ3CndyVk50UGUxNDE1dS9rblQ5VkZh
|
||||||
NkZ6clplbzZnR1ZWY0YvMy84WWRiMUEKLS0tIHliOE9KYTdlUlFEb2NuRE0yYWJm
|
aGtYZitMNDhDUi9xczQ4cGNXK1NvOGsKLS0tIFVHSWErVHhma09BcFlzd2x1ZTBj
|
||||||
OEhCZmphWVVjU3k1VHRDMnJWTUpQQVUK1M7fgK+d/KlbTzvt9CKj6cGgzZ+vwsfE
|
eXN6NlpjdDNVci9oQTZGRTJ3ZHNHencK53kJSr3udGgPUsaDxYny6gkWXRCldSfM
|
||||||
zqUbyJ/5UpmrU/3kQMxBMBmb8HsA8b/1itzOn4F54SF1Xm7CFDLTUQ==
|
kGYpeGMh2CGc9x5x2L3JlS3EbGPerblva/6wvmoszI2uL/hZzU/g8g==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
|
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwaEpvdkdvSHZQTXZXbXZa
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhZDJUVXo3bkY0Mzc1RDNG
|
||||||
ckZrTW9qNW9SMzN2TkVaZTRlT1NKSm56Q1JzCnIxY1k3Q2VjTy9OSlZPbEZkVDBi
|
RXZBRXVSVXpDKzJFdWNpS1RqcEdQUEF5d3hrCkNqcC9CY1I0RzRWdGcwREpzbEt5
|
||||||
UWVCRHE5bWlDaVEyWERXeUdsL1BFYkUKLS0tIEhoK05uMVpzYXJFZHBRcDlZb296
|
d1liOVZEWUEzTWxzZU8vaTZFQzNWQ2sKLS0tIDNYeEt1RlozaGdYNnhpWmZwMGVE
|
||||||
YWRTZmljUTJEQW5lUzdMa3N0Y045MlUK0lAs4L5D0DIKuxuHJmGbOu6SX1Y4KNJo
|
c256c284cmFhUWhQeE5rY3JDY3liekUKIPl8/qYgp2JlVjw5t5PnvS4II+YU1V+C
|
||||||
VsgVUd9wU9r/ApoiaicAPNn0jyH3B8sGk1JGtrisL5eldc6Z5phR4g==
|
K7WOVpqIGi5F4Taa3SOtNBzRs7jbdTEE231C4zwZnBucZoX9gGVC+w==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlVEdRWkR4YzJNU2Z3ZW9t
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTTERxOHJxTW5Gdk45Ukxx
|
||||||
VGNHM3gxZUM0SDlaMzBleHU3a3lsZ1M0dlNJCnF3R1JtUUZCZE9CV2NUVG9la2I5
|
MjkzWFpuSmdBT1psbWFsSENBVURnb2pObzBFCkZNY3dEeHVSbWpKUVlRSnVEWk9r
|
||||||
R0hadEw2RldTS3J3cDdDQkp0OG4vZmsKLS0tIHl4UVpBejlFbkRycEZjSTNyditY
|
cUs4NHNRSWd2OGZ3R2tqcDZqR3I1SmMKLS0tIC8wTkpBUW5PLzFidVJkcFVIL3pP
|
||||||
S3VRckhkNGRzR0VOOVBaRmZCT1lxM0kKThIJN/jw3tjaqaf1C5s6+K5BMBrMer2z
|
elhqbCtTN0FyYVdBNEhyTkVacHEyY0kKJam6XZgN7INkIThBPyZ+vi4xhknY7GVJ
|
||||||
YNhhar3iomZbWvwJ5OW4dneU9p0drrcl5LR9tSAoTiSxIbfBZf+d0A==
|
57aIYLI6xMvs5E+120qVjXxoo29kzs2uwnKzlbAqMJIY/eoDWW33XQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw
|
- recipient: age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzOEhSaklkdnJoY0dOU3dt
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlTUJHTnJOL2pJM0ViVEZh
|
||||||
T1lyVVdVZ1VoRmQ4RURPN1ZjYWhPeU01T2gwCjFmbHZ3SThub2psTjBHOWk3M0hP
|
SzdNdjlScG1OMDlTSTlLSXdOcTJwOUpWeFJBCjA4UEpFaEZNRGRMSENHWXdyd2RE
|
||||||
WFk2RXFnM3AzSHhraEJmRmxWZzRFVE0KLS0tIDdteWVZKzJVNXdyZDJTbE43Zldr
|
akE3T00wUC84Y0JSSldkaGh3TU5CL2cKLS0tIHpURHdSMnRBVHNrSk44ZzBUQTRC
|
||||||
WDdHb1I5dVFCcHJ0ejVhOXFIb1pKRlUKkCS05OVL7xvkZ1oh16GTCnateuXao9ZK
|
QlRRcFVzby9namhkOGJ6Zk9TVGxMbDQKEyd9Tf67JclHM+kWZxpXl+g3+cMimfHI
|
||||||
6sMZ7/c9tafLH52psnjeUEJK15Bw8DihFjFctyIh242j8TtXXqxBYg==
|
VfeF0z7zBcPuLb6xIzEHmXDn6Z3EeCYOq975nQde2JSpmKIZagerhw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2025-06-10T11:35:59Z"
|
lastmodified: "2025-11-03T12:45:54Z"
|
||||||
mac: ENC[AES256_GCM,data:1r8IFSyvVmwSR9j9DROAbN6GmnQo8cg+Z1wCvg2hv/lql5FbeLgFUvVHYQvPGJK6cRUTM+7T010AZOZSWKJM2K3KqiinWLdVVM1G1Bvhv8T4epL2RHq65OgMd5jJFrMLYoyJmHUp3AkzlPeYJDtrvxGCB5B88H1L+ifZtV0pKJQ=,iv:uOnWxuPiPJkmc+wBf4EYihTLeugcyM4MX4AkYncfAFg=,tag:HWHGROye6YMR/cLm/C2G1Q==,type:str]
|
mac: ENC[AES256_GCM,data:1+D1VGQ19gAfEL30hUN6BeBxVnYLvkQ1lV48WHeTcM0mlWl9z1KI6eencwNfHw04fnJzt9VNOClw+p8ekRTygUnUOMSEh9QQCGuCaFU7s+vRwtafO4t5Ip00b5P+TM3HEbSFNBRO19+Btqk1sfYZv1u3YemST/v57Y9tk/yx09c=,iv:Ea1KnGony2CvGHB5x0PBqLSb8faxTVbjDPo1F1Sf9bo=,tag:hduJprCw11FJB0bpF8dSHA==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.10.2
|
version: 3.11.0
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
allowedIPs = [ "${config.networkPrefix}.98.202/32" ];
|
allowedIPs = [ "${config.networkPrefix}.98.202/32" ];
|
||||||
}
|
}
|
||||||
{ # GPD Win 4
|
{ # GPD Win 4
|
||||||
publicKey = "HE4eX4IMKG8eRDzcriy6XdIPV71uBY5VTqjKzfHPsFI=";
|
publicKey = "p3wnxXK7hurOKxruFCRoefj6gCoQeD5XXxD/ogMpew8=";
|
||||||
allowedIPs = [ "${config.networkPrefix}.98.203/32" ];
|
allowedIPs = [ "${config.networkPrefix}.98.203/32" ];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|||||||
7
hosts/fw/pkgs/default.nix
Normal file
7
hosts/fw/pkgs/default.nix
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# default.nix
|
||||||
|
let
|
||||||
|
pkgs = import <nixpkgs> {};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
phpLDAPadmin = pkgs.callPackage ./phpldapadmin.nix { };
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
stdenv.mkDerivation rec {
|
stdenv.mkDerivation rec {
|
||||||
pname = "foundry-vtt";
|
pname = "foundry-vtt";
|
||||||
version = "12.331";
|
version = "13.350";
|
||||||
|
|
||||||
src = ./FoundryVTT-12.331.zip;
|
src = ./FoundryVTT-Node-13.350.zip;
|
||||||
|
|
||||||
nativeBuildInputs = [ unzip ];
|
nativeBuildInputs = [ unzip ];
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,16 @@
|
|||||||
{ fetchurl, lib, stdenv, nodejs_24, php, phpPackages }:
|
{ fetchurl, lib, stdenv }:
|
||||||
|
|
||||||
stdenv.mkDerivation rec {
|
stdenv.mkDerivation rec {
|
||||||
pname = "phpLDAPadmin";
|
pname = "phpLDAPadmin";
|
||||||
version = "2.1.4";
|
version = "2.1.4";
|
||||||
|
|
||||||
src = fetchurl {
|
src = fetchurl {
|
||||||
url = "https://github.com/leenooks/phpLDAPadmin/archive/${version}.tar.gz";
|
url = "https://github.com/leenooks/phpLDAPadmin/archive/${version}.tar.gz";
|
||||||
sha256 = "sha256-hkigC458YSgAZVCzVznix8ktDBuQm+UH3ujXn9Umylc=";
|
sha256 = "hkigC458YSgAZVCzVznix8ktDBuQm+UH3ujXn9Umylc=";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Pull in PHP itself and Composer
|
|
||||||
buildInputs = [ php nodejs_24 ];
|
|
||||||
nativeBuildInputs = [ phpPackages.composer ];
|
|
||||||
|
|
||||||
# Let composer do its work
|
|
||||||
buildPhase = ''
|
|
||||||
# install all PHP dependencies into vendor/
|
|
||||||
npm i
|
|
||||||
npm run prod
|
|
||||||
composer i --no-dev
|
|
||||||
'';
|
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
mkdir -p $out
|
mkdir -p $out
|
||||||
# copy everything—including the newly created vendor/ directory
|
|
||||||
cp -r . $out/
|
cp -r . $out/
|
||||||
ln -sf /etc/phpldapadmin/env $out/.env
|
ln -sf /etc/phpldapadmin/env $out/.env
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -1,66 +1,69 @@
|
|||||||
ai-mailer-imap-password: ENC[AES256_GCM,data:kMxDPUK9rk7mbel5JDT03m3Y2w==,iv:cbnkNIVRXd7OLqueSrfYRzfaW9TzI+FauuQD8lgYIy0=,tag:63W7seIgt5TPVFQc84semQ==,type:str]
|
ai-mailer-imap-password: ENC[AES256_GCM,data:q9eJ9Tom+X6KxQJhWQTUB61k5A==,iv:FH+IUWi2yZBBgMiL/kNW470GEVHEG3fImf0bel9og/c=,tag:RSlcpXwmNyLB8Oc/K2Epvw==,type:str]
|
||||||
ai-mailer-openrouter-key: ENC[AES256_GCM,data:PCe8kt/M+7g087AKzYMY2H5WO4L+NGkHLsh47fMK36kz+Ju5kd/kpmM4GQcDbI3LgWm/P+T0/mv7kGGOL6KLmBFaFmGV/88cGw==,iv:ruVftGvnv+PX1Zd92tfOezpyaMbYrqCrexelyPUYFMc=,tag:z4JVUCfz/frehar6y+fOlQ==,type:str]
|
ai-mailer-openrouter-key: ENC[AES256_GCM,data:EvI0BuCBA1uYOderjAVcB8RSk7un7tiKmgsSe70KQcmfu3CxmQerP/2kQsRTJ0/6pWf4QqNpaes691O3nf+UG1qgG2CUcIaYRQ==,iv:OYEy0xMs+vkGa0qMtY4UP/iol5JPQ0eFVyPpPXLAmUE=,tag:5PeXZcI8TRSUOyuKs0STWg==,type:str]
|
||||||
borg-passphrase: ENC[AES256_GCM,data:jHb+yXK0RqNdVYtWiueztZFlHC/xQ6ZiAOUcLt6BxmZQewuL3mh4AZ+lQdmA/4EaaTTIhVMR3xFx5fU6b2CtNLiGb/0=,iv:IW09B1EE1OupMCOvv13MXRYiMsD4VmIfyYONUyrPX1c=,tag:3ankeLOaDJkwRUGCd72DuA==,type:str]
|
borg-passphrase: ENC[AES256_GCM,data:GGmf09zX5wQ8Fih1EyP1p3up9ckFjVKsktU6ZFwvuZnG/O2OyOod66qXc/IXx8GQordubZ3TgisOeMLNnSowp2qylh8=,iv:fFgw/x8Ww9cInkNlPIoE3stUfISbfk46PBj7aimuXNA=,tag:hnNYrkLgt1qJc+gN5s9L2Q==,type:str]
|
||||||
borg-ssh-key: ENC[AES256_GCM,data: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,iv:9E1uiPqM3Hh4KWtL8haxm6PRm2VPc+DggrA135FvfB8=,tag:QSOgzVH9IBMgZxJvUhvY2w==,type:str]
|
borg-ssh-key: ENC[AES256_GCM,data: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,iv:f7rBK8aNqX8dGyzjoeRX6yl20XsnLU8b4gitaw9+O+0=,tag:WvfUw1JgFBAtS3vsVIvM6Q==,type:str]
|
||||||
ddclient: ENC[AES256_GCM,data:EaXjXS/bwL3S/Fr+rzQ7dXA1eIzeFpHH7H+SvoNhVSg=,iv:3BzjnJG5yT1W8ob2nm0oUlr+sSJ73W/ctl48xyxeeWM=,tag:TqKSwfxF0V1v5T8VT/qblw==,type:str]
|
ddclient: ENC[AES256_GCM,data:dS6TVVNb6R7EE1JVMDfSnRYCZyHHqEPvwaYpkTSj+VA=,iv:9uMo+9X7dFdVW4wuSgrqIAaQelXuA4cek2oif0GRHow=,tag:ncQq4UeUzWtjPNxEUOlqNA==,type:str]
|
||||||
gitea-mailer-password: ENC[AES256_GCM,data:M4qCWNt1oQVJzxThIjocm2frwuVMyx+69TBpke25RwxJxEQnvHL1CM579OVroTm7+gGE/oOJqAwDIepfiDtyM1xm,iv:jayFZMbu3uDimS/rIKZSeoU0MsYwWp880iEMs1oQE4k=,tag:qGDncRkyuCWaELhcxUrqtQ==,type:str]
|
filebot-license: ENC[AES256_GCM,data:jY7E29fFJ/h9NIgIjuX++WBhnLk6Mm4iRfMh4P0pUDdqH231gXDsTZ6pJ1rpFXdEHSuNN4LfznDTKgZ2azKid4WprDUzGkN0uJD6CfSR8gTIx5Rq0M8vkRah51LC36bop4hTMzECYQd1YA47hOBV/gfyg3RIw95coWamV9FebnQjIBgWYxE+wTvO5iRvWpiCHd6VZQfkiiR0KF1DrkYkuxlX0piGEKmIgyYCiKMFZ4nrrIe58x5lEQA9uPVjE7vmq3c3ge6tJzjVVaaNocbJhxhA18GLMqTSHfnBsOLRlA8qSQ3xX/VRzKQmaYQHIM77Ylb9ZQsvFt6EDlzQMl5NqT7OJZUW/0jwNaEXHURjeTOC3Hr1HugiDGm+uLXEraaJ6Na2AbFDn28o+3J22p9xNg6vWL0FElzKuaz5TFDzdZLZsD9HOPQm95/ZM8JymDjN4qxkkd2o9rEKY6to1MVDarj+lDxIHhf4pL23YhZsn3esNlEbFswzHQiH7nMsu9Jg6a0rPu7IYylDnH/soBjxSKmf2dhH1LLsDm8It9K/7NnXwmvncFXaqNBqm/e7JzCDBCCyVUf/BXbBc3xwLwZf5MiirZ/iYiYnRtUssveh7BV7ICigRj5Ewtr+n97+IGI+FyonkvOgM0bn8nHf79ZzJCKMuntcw3FlGd1nIkmcehkC79PlKIS95oV/wypl1OmU0CVel+D8hsMuONmF9NPHgFk/ztp4GF+XXRO4ExNotX1XrUlvLOccoHDsl1TedUOISzgAK71edxfI8y110shIe9OfsCEUAbmWMmjGVWH2fKu/IrYYQTry6pYFOjG2bIEUXMaiIP0lALbq/QNgleqMNPY8wGzFbP+/jaYzTbw9KXH4bwYQCl1hSI8THfV7lLE=,iv:4ik/aQqi/hIqH8ix3ejgUiXGY7ycw0ymdVrV+CEQe1o=,tag:7ymc4QZEezJVPlYTlU4H/g==,type:str]
|
||||||
gitea-runner: ENC[AES256_GCM,data:NYG3qRLiMjmfA+oHYBXBbxpuX2ZjB/VgvLaS7yr5kJeDN/NukB/B3OZcEfsUWgbBS5IsLENESngWTFmK4W3htN4lSqdg/g4UsUr20beNov+pbyPN05rkBYmSCZZFwZ1L9POEE4GF4LuuoNpDlWIw0mrA8oV8MoI4W5QS2IGranBTIQQaYXU5TEGYa4XMVo4oC75iuH6DIq1KD6OgFAfMhm/wlbP8CP/Iaw2K8CNPxktk93pm3OSmggf22Z4JPEnvV25sc9iBkxLkDk9FXYFys0g=,iv:UzL5ncVOC/loJwcFSG1QJHnzLp3il4Hf3qDwLWxrIlo=,tag:w0Zn/E+02KyAsPXZdOLrew==,type:str]
|
gitea-mailer-password: ENC[AES256_GCM,data:lEv5euTCHG6pyNqrVtKK7oE8wLvk+q8ABXOzFSizQ2TVFi35lyGPzOTel/dCCC0Je5GAHE1KQQ4Y4/iHghZgb5Ft,iv:gt/mCzLbDrHFNqW+Lkd2dy9nRIBKO+rqsVuXM45zJ8k=,tag:gCxTSzY7GZ+jQP9SCsdUtw==,type:str]
|
||||||
gitea-runner-token: ENC[AES256_GCM,data:HpBjLS10w78ihbnAUrlCRGvwrXLBYKH5v/P7XggoUSWLoAazSVQArABxaK7PJas=,iv:q3Y6jV0gmug06O0EYqGVyIJ4AvMGr2ydwY17YKxo0Qw=,tag:Ws5HLbdaeYGGXzDZW/FX4w==,type:str]
|
gitea-runner: ENC[AES256_GCM,data:HLjSETmu2C2ROf6kqUuIzQl/t4Fe5EOVkMqdTeLNnb6AJ95l6M/WUk//dnPMrWVvEq7rV07awUiyvyJcYQzMgPNddCrfcn2Xr0dYK4XFenz/sdhknVex9uS/RhK8fOqdYJ6djpynikMKddZMQr9AOVfpF5mea//87+Az9rOrlzLdgNtf5HyBEAFKaOFbkZboAsP+jlxyyYurGHPr8LxxikewDVxnpB+XzMc6RAnesrZPOTDQlkMiPZ2t2o0klhD/4VomgiHEklULxCCmIAHaqDo=,iv:1FwTespqVTnKFbyf9Unbbod08D36MKsVbDhIBNGBkHg=,tag:rgVvyxUCwzYB2CqWm2fwgg==,type:str]
|
||||||
home-assistant-ldap: ENC[AES256_GCM,data:uZEPbSnkgQYSd8ev6FD8TRHWWr+vusadtMcvP7KKL2AZAV0h1hga5fODN6I5u0DNL9hq2pNM+FwU0E/svWLRww==,iv:IhmUgSu34NaAY+kUZehx40uymydUYYAyte1aGqQ33/8=,tag:BKFCJPr7Vz4EG78ry/ZD7g==,type:str]
|
gitea-runner-token: ENC[AES256_GCM,data:pzJp7j1Ktz+27oU+qtESk7D32w7+BSEUkPSX4xuFml0i10z12Gzu0QHXL9s3734=,iv:U77b5515H1URfz5BCdzuY03zVkhSRsL9d+HdHUJFx9U=,tag:QvooaT4TS/X5R5KGdaVpVQ==,type:str]
|
||||||
home-assistant-secrets.yaml: ENC[AES256_GCM,data:m7uOVo7hPk/RmqqRS6y7NKoMKsR9Bdi1ntatsZdDOAbJMjZmZL2FgPEHi/zF73zCfRfTOca3dwpulR3WXZ9Ic1sbUIggmusJMg4Gellw1CUhx7SbQN5nieAbPbB9GVxMuV4OakD1u7Swz8JggDT6IwojSnuD5omCRCyUH1wvKB+Re59q6EStderlm5MJNVFlVrbKVbLKLcw4yRgTh34BGnTTjcJmgSlQjO1ciu2B7YQmdl0Fw6d8AdbEzgB5TFG5ONc85UhJDE8Wlw==,iv:GCtpcVChN2UMWtfnWURozCfVj2YbRPqp/bH4Jjntybs=,tag:pcxP7gTBtXMNT5iyW5YXTw==,type:str]
|
home-assistant-ldap: ENC[AES256_GCM,data:4kofJzPbiLXILxjuAZWiTb9hu2Gver/IHBCXDnrmrKuCSII6SJ9FrSi67nl7SHdoA6xe22GSMfmPrKzy5sGiow==,iv:F8mIHhWHpaI6kzRV9du6uW/Fj07PbEIU1goSDmeSD5E=,tag:6NIC6sN8OclinribZhrLLw==,type:str]
|
||||||
pushover-api-token: ENC[AES256_GCM,data:W2ILPksaNeDvbSlSJztu1vu23kQKLDRHYKoUIvyd,iv:RYFAN6AU+DALphpqpiifhOoEQ8++6DEgo2wETSwxBCg=,tag:pRfaNuz4564LvRuaLggatg==,type:str]
|
home-assistant-secrets.yaml: ENC[AES256_GCM,data:rns9heAmVMxB6WWlGMXvF/ianFUnja3FObiLTEKJmodePNsJ8ah3OhuCAX5jON+/7NZ+3JN/hIJjXsORC5WYhr01DvO9meykf0aMpbmAnYI+cmPEPvcunF4NNInl96rpcI519nMiHDSh5J7pD74CxHZcXSV4c9ZR5UBymchrwmHyZMF6dVrD9Jbr9yph1r7iq6S5wlI2ZImWRjaoGDZ1x+ZU8XnsUmYcP4pa1Yt8JBxSnyUw5gxgBkVCh4eSZBsUCt0cd9P0i7qWVg==,iv:YXQsawXZsQb9ZUt1/lkpfTa4tfKIQrLkkyShFtBRaIQ=,tag:/vSnipGiMntdMqHLePSEQw==,type:str]
|
||||||
pushover-user-key: ENC[AES256_GCM,data:mh3u3FAdFkGD1d4UKcTwLOsCB2vfhEADI5cd1aT4,iv:4bkR7ZNJwWAYBdu435SPZUovGsfb8qivuDOQdGkPd/U=,tag:5UO4vGt75CCFEM5jxTGkGg==,type:str]
|
piped-db-password: ENC[AES256_GCM,data:5atQccdHYDEf638bpiON9VO14jqNDtzZ8nnXVW0/cqtWkZJc8RYn9N7QhAw=,iv:Gwyf1R+mpmX+TFuoYLPHjXwSDwzJhSEpnj5ZsJgmrtk=,tag:zm4zNkzbqbCyTN6o3lQQfg==,type:str]
|
||||||
wrwks_vpn_key: ENC[AES256_GCM,data:gGipXC8JJO59b4KWMSo0+r761raQl7RzgBuUbXmPEKlZR21bs5XRAQalzDCFNtjcpNkXiGqAHCLkDTtjPagMsw==,iv:MH1EBJEOdQDEgm9E0F884fynhsH8KiS5QSc605XbASQ=,tag:FUM1eptHS0rpt6ILyQjGOg==,type:str]
|
pushover-api-token: ENC[AES256_GCM,data:cMBDdySEBQ7vS7FUC2DsCcSvEMpapWvMFmnuCsY6,iv:SVDrrDm2pcAfwUVAC5j47YwF4s/FWNARlZdIZ1Wgwgw=,tag:w7ZeNMPXWc9j+zVaSxq1cQ==,type:str]
|
||||||
wg_cloonar_key: ENC[AES256_GCM,data:Dtp6I5J0jU5LLVwEFU4DFCpUngPRmFMebGXnk2oSwsKtsir/DtRBFG7ictM=,iv:1Abx/EAZRJrRQURljofzUYDgJpuREriX0nSrFbH5Npw=,tag:l4uFl9Uc+W0XeLVfLGmgZA==,type:str]
|
pushover-user-key: ENC[AES256_GCM,data:fjoA2YQxmeWEbSKWWE5iyi+CUh1vtW9usVCm5EGk,iv:p4YwYIhpgn/bY9t61//CDrDmZrsj9B/naZit62lCpwo=,tag:pqEw3pDlX7i87tE0Nsy0/Q==,type:str]
|
||||||
wg_epicenter_works_key: ENC[AES256_GCM,data:LeLjfwfaz+loWyHYRgIMIPzHzlOnhl9tluKcQFgdes6r+deft1JfnUzDuF0=,iv:DKrc3I+U2hWDH8nnc8ZQeaVtA1eVXu7SXdTn1fxHoH4=,tag:V0PL0GrL2NEPVslAZa801A==,type:str]
|
wrwks_vpn_key: ENC[AES256_GCM,data:VEHqnr/bDtmyLzs0wnmZ0jCWS0BGJWu6Wjq0ZHJuEz8PH3j/E54S9NUe6WRIo+BJCsh1PlRqw/PD9xSqlW5uPg==,iv:OMP0s8Lc2CmFgwRuwB3UWJVuQFqvpy+BiyhnIKbVIb8=,tag:x1LvSf6i8khd8jKgv/284g==,type:str]
|
||||||
wg_epicenter_works_psk: ENC[AES256_GCM,data:Den3NDWdP013Or6/2Vll1igUahuRSNW4hu+nDa5vkr93bbveQTaWFT4TD4U=,iv:r3UsD3+3lUIP2X3Grti7wpXTQBXtu1/MdrycEmpZfsI=,tag:ghbAcxmjGVOe9jCZsmFzjA==,type:str]
|
wg_cloonar_key: ENC[AES256_GCM,data:1OfHD8yX+pgCXqqxn7cddnnCA9HBjGra4eht7uLxdcbdG9vDvxUoE1x6aWg=,iv:/NBEbmA3wP/zwrqCeBKDzaoSMqz3f4ZeMlWbu81R5Pg=,tag:Apt8x/j0qiJAKR4UEVSkrA==,type:str]
|
||||||
wg_ghetto_at_key: ENC[AES256_GCM,data:OIHmoy3SpIi9aefZnZ1PzpyHbEso18ceoTULf2eQkx1rJbaxC6PD1lma7eQ=,iv:u0eFjHHOBzPTmBvBEQsYY5flcBayiAQKd6e7RyiPwJI=,tag:731C9wvv8bA5fuuQq+weVQ==,type:str]
|
wg_epicenter_works_key: ENC[AES256_GCM,data:CTZkVGEVRlCdt6W0BGPmX0SZbuBBH5IIlUsi44SGXi7gdmrZNwv2zDv6zjA=,iv:4ZDDKqR6pBq8cjX763tBxOvWFaS2IiGaBxJu6L2JYig=,tag:H8p63BvXSx1SKPFw5gnptw==,type:str]
|
||||||
matrix-shared-secret: ENC[AES256_GCM,data:67imd3m6WBeGP/5Msmjy8B6sP983jMyWzRIzWgNVV5jZslX+GBJyEYzm3OTDs1iTZf4ScvuYheTH0QFPfw==,iv:7ElCpESWumbIHmmFaedcpkFm5M58ZT3vW9wb9e1Sbh4=,tag:wr4FIymtJBtCerVqae+Xlw==,type:str]
|
wg_epicenter_works_psk: ENC[AES256_GCM,data:K0SDlDWfUk9vIGP5U1j8p6TJ9GsydJTuKPb4kMgde1CILOia0S9/+4AkMWY=,iv:ITwLoWZXR6NxRFF3eBvOogiWHLmXnf7S1e2FW0ofr/M=,tag:2OVi3OBFYT0nlCx8gf2AdA==,type:str]
|
||||||
palworld: ENC[AES256_GCM,data:rdqChPt4gSJHS1D60+HJ+4m5mg35JbC+pOmevK21Y95QyAIeyBLVGhRYlOaUcqdZM2e4atyTTSf6z4nHsm539ddCbW7J2DCdF5PQkrAGDmmdTVq+jyJAT8gTrbXXCglT1wvFYY5dbf2NKA4ASJIA8bdVNuwRZU0CtFiishzLuc9m8ZcGCNwQ/+xkMZgkUAHYRlEJAZyMpXR6KkFftiR05JRAFczD4N7GXPPe+vyvgXg7QBGtf20Qd4SGBUw0zI/SNTRmifHUuc4Z6+Fe9JHgvTc3uFcTMVnty0fEuL+a29liaVdAFq8BnqJfc5CNV401ZSUeMbG41lCn1cegP/WChs9J6HXNrhWDgiXa6ln++NoKcfOHIfZVbYOCoOxFR6+YWeBU2+sHmdwI9j5XQf5Ly2hmg12j0Ds2Cn8k4PG5aQP+HT2bedqyxwSt6fi97A0Osnh4ig7+DzYAjSNLewbYLzVdK39VdvB9hqLto+yFS3gAaeYOHwPwtqa+COI85c55lHiyKHlSwPhBqYaaiDu00lQTUzq9R5vz6F/l+T3bUjuna5RryUu8yhnk5DyK834KycTOg4ETcZTqro6prfiEBxc+Utsc9JvEtZgwFv6fsVLOu7nHxuiYuvseZ4YA8LlYdwPJboMPO2XsuhwWtT1uz/rh2orH7/vsXvzA/kF8NFemWBEMVLYA8byC5ze8doiGDYp4T5AAf10nJB1ceQ==,iv:gs78fxhvo9KlTaR5nzs12/LdgPChSFPHD2k4VQp3ARo=,tag:lpWBOi9xh2cWkS+71KD/UQ==,type:str]
|
wg_ghetto_at_key: ENC[AES256_GCM,data:+bonpVjV1hxwaqtR7ywshmoDxCnFPD11q0OiNLzxUJIaYrDeS1srpyo6rlE=,iv:Djn16kuXTWqJZy/AT77GpH8RcNtUMZ6zcIdKIMHv+PM=,tag:LP2JCaPKpzeOKvBc2bMr4w==,type:str]
|
||||||
ark: ENC[AES256_GCM,data:YYGyzoVIKI9Ac1zGOr0BEpd3fgBsvp1hSwAvfO07/EQdg8ufMWUkNvqNHDKN62ZK5A1NnY3JTA1p4gyZ4ryQeAOsbwqU1GSk2YKHFyPeEnpLz/Ml82KMsv7XPGXuKRXZ4v3UcLu0R8k1Q0gQsMWo4FjCs3FF5mVtJG/YWxxbCYHoBLJ/di5p0DgjuFgJBQknYBpuLzr+yIoeqEyN7XcGYAJO53trEJuOOxLILULifkqISHjZ66i5F1fHW0iUdRbmeWV4aOAeOrsQqXYv,iv:gJwV5ip84zHqpU0l0uESfWWOtcgihMvEEdLaeI+twcU=,tag:sy8udVQsKxV/jOqwhJmWAg==,type:str]
|
matrix-shared-secret: ENC[AES256_GCM,data:nVSHwPa8xYUaDCxL+5neFtzc11DDNzJtoDCSHYXZ+bZXVAAbp6/Pjx6UkTdAA8B2GOM09nFAsBuLnQfJ3w==,iv:WU3hnRlWVwx7Qin3ejw7V4VhAmYLf6oXzVk6xQgZPgA=,tag:O2hJ2q8XDxYF+rHPNgATgA==,type:str]
|
||||||
firefox-sync: ENC[AES256_GCM,data:uAJAdyKAuXRuqCFl8742vIejU5RnAPpUxUFCC0s0QeXZR5oH2YOrDh+3vKUmckW4V1cIhSHoe+4+I4HuU5E73DDrJThfIzBEw+spo4HXwZf5KBtu3ujgX6/fSTlPWV7pEsDDsZ0y6ziKPADBDym8yEk0bU9nRedvTBUhVryo3aolzF/c+gJvdeDvKUYa8+8=,iv:yuvE4KG7z7Rp9ZNlLiJ2rh0keed3DuvrELzsfJu4+bs=,tag:HFo1A53Eva31NJ8fRE7TlA==,type:str]
|
phpldapadmin: ENC[AES256_GCM,data:94jCcgGJ89Er5ENLqhFZ1qY44Qp709SuUhBUuED6v/a7mPPjrJGDmi0Gm3r1Hb4CDPGkWf+x4NStY7LSQ2bHEzjyMPMS23wvSLTmC5b2TVca1UI8vZRTD1R7OvdWo8d1oNweSpYEnAXGv3USYF0NZo8DrPLM5G8lG5Tk/rKS/mxU5ZRhPyA60rbmIiy3Mk4yNcs1tvTEckxU/zMVl7zUPAsOOlmYGuwJrHmmh9p7YIWHGIgZNiLs3U0BvSKzN7WktmlwqjfWpeLn4dusqgov4SSQ2otAkxLHIH8mGhyotd1wgXJDZc6tilMe+WPHQDz9db7FT0VdeKggQ94FD+8rP0OsIjR4AdjZ,iv:C8X10wtA9jPgS41pxasaZJTO/XFcRymOyTDZCWJlhmg=,tag:xkMJsGubny+Di+GucAqypQ==,type:str]
|
||||||
knot-tsig-key: ENC[AES256_GCM,data:H2jEkRSVSIJl1dSolAXj9uUmzD6eEh9zPpoajZLxfuuFt7/LJF8aCEHyk+Q=,iv:9aqywuaILYtejuZGd+Cy8oErrHIoL2XhL1g9HtcUn/o=,tag:K3SnVEXGC/NhlchU7OyA6Q==,type:str]
|
palworld: ENC[AES256_GCM,data:iR9nceVotLKrFHnPIVskCYVLev9OzGLLlmfCGQq5hqB1HveXjhjkfm/NMmqnSi9o776+Ezy7l3kkS0R+0cFJ2B9kaWGsdJtdYDwQevmf6Nq5eaBYmvu8kTnaatqZ5e/1BQzcF3to6MA061XL54YGqsAV5FpnDVLhyyzIvaR3gMvMqJ748NL7K+hbBqMFuWcSH3hKXwxtDK7SLtcgx93W5ZgXkZMMumtlH9hSSlZL4yxuQDAQUwHrEBL+rdphA0m27dyS87DA3Av5ZL1MZ+Vlm4uAHM68T+rtVYXTakNImDTc0WrhIP8FZD/UKTAhVYAbA9oz6cbeC574vchuEY1z19SY9+2HshZZBOiPDMqdvrqyszMQCo5I9dUzAJCemQQTlYG8ekREQ0wxARnBYi3iy5PbmgDQWdM3+ff4yhMmGiHtiMQLHzrquKy8nvS9lDp9uT7njkaI0QAt3eNWa2DAQRqXQAtmuRVob5+GS2Nt6XMTWRkbeEb1phwbTqZD5mH4p2TiyMKCn6KOXsgQTxqGr35Izbe+bfptCmUeyscTKq01IZg77w/dvg3AX4iHAcMNgJ9LIHDLibGHQzu9fGN6alpeyy788GDwRY4glYyKxPhCasKkBSj/uhcDAtdg+c63vDTBhqRjNr6+v1NeRW6lVBzrgq+f9QvO5RVKrvdsVHnTA3CMGQzUPAaluNZMpzV5KqxqIrpAAPXnN0ktig==,iv:kkcm/alLHwC84IKK//OJpa36ec9ddOARTIM+KJlOHHs=,tag:jV1DjfNzRgNaCGgJTKIy5g==,type:str]
|
||||||
mopidy-spotify: ENC[AES256_GCM,data:O3s6UvTP8z5KZPCq10GaaEQntWAEoxGFMnTkeUz9AfobrpsGZJcQgyazFX2u4DgAaIjNb34032MISotmuVQDJ14mi8xI5vC9w/Vf16v3TFu/dSKGZNb5ZPQwTUQ+iMJf7chgwOV9guThhutVJokb6pLxzt7fSht7,iv:j8+X1AmuWzIJdafzgrE7WBIlZ7coNNi0/Zn6JObR6rw=,tag:fiw6M2/6nfEPqEgV2YOWLg==,type:str]
|
ark: ENC[AES256_GCM,data:TRTwxqkeUGbtgrWuj1YEFr73+nxCXmt/fR5vVnYR+k4FpNBB2FoY/gXl0kqeFKPDcajwn8nYBs8YE9vmYtAX/Qs4g5OyU9qC/pkmSV7/gbGfqLLqcbIlbWrZzeM8gRW0fp6h1TMPsGO8/iYdF4bmInfuZW+fKr0i7ZRgrtOpPiRCOI/ztPGkFaduuwGIy+yVoS64b9r7ZLRnOZT7ghVv80GKorJuuOQIipNAJMzEqtSA2IqaxWeb13v8wdQoKuMNcD6dCYVJnvgwf4R+,iv:+F9+yJUZBzPSSIt4uLHxjjXAjzRojLxKAyrd8grMXkk=,tag:VrIr4FFbIGTq9RBJMz8/Ig==,type:str]
|
||||||
lms-spotify: ENC[AES256_GCM,data:gh5kx/MDSefNLbZsnovRc3rNWxp/RTrJ4A2WIs1QMi4JVGFj9SppdsErMXW4y/IFj/YxH1X7JtwvhptO/p3P2CFK0XL2I1vFVqPuj7LavDHJK7GXPAV6+x17ldvPXgym5NqHjzHi4gtj7U/bMJlz0NxrFsrrjMcY9nmNX2vVwKlINUFqWb1JRvQsJ8ujSutjJbGtAY/bVQI8OFtU29QGKw1CU3RH/bgXIzxGiLQsUd68w7N17oKYj8MiTpGVcovMCRKwwUbd9w==,iv:4aVy+r//s1Cs9q4GasR3vSAb8b/VB/8Mx5E1jWAUA+E=,tag:TgTSLLH1OG9ySi2tZ+hK1Q==,type:str]
|
firefox-sync: ENC[AES256_GCM,data:guNgEVi9n8uJuLkkX2Z3tMY/NVqzQ2tdIutZAqleah9qBri0/3dzVHF2xvztLeAgm/59tN7TtAlAH2SMK6gcfAZDasAWOJ/rGEASxLi6VRjqCe25glDMp2YrA0/mcqZVYMCg+QZ5OPA56b55WDqPHPoBJkPDuTm9axwm6AOxdNi5BkDzMw12fVBxlJL/Rm8=,iv:yD+MkZK5vvZ85vYGd9X2Dv6KkSvMUsMGLrwlJ1pRqlk=,tag:YA379QupHh7aJZKcQxB7bA==,type:str]
|
||||||
|
knot-tsig-key: ENC[AES256_GCM,data:CBFaRKPr+HRVM01fA9/OLWeD1O33axQKEKJuqDRfcGmuDeP3oXf+ccEJhQE=,iv:2O5y24YenpiMc9txPx8kz8x0aO37LpLjIcwlNywPEak=,tag:J4bVZ7RNSR9fiOBQ2HKpnQ==,type:str]
|
||||||
|
mopidy-spotify: ENC[AES256_GCM,data:irBeIh2FieNkdf6Hls/Oj+qYxj1U7R7/Ffq6dx+JCS0PdOiFWIHXtccY+PXPKP7RhhaQOgZtIcgPyqTiML52P0c8AwN6UHMl7kgUcKnk60AI0IUZNWorCBZluHhEpf2e2OISlFzDGjSHk+zAzh2eDS1lJ9lCRYEC,iv:r6aZmlVHdRsA9DxkelcIVVpwwm32jaOgP429h61NL/U=,tag:FvPIr0HX/V7+G9kal4nO8w==,type:str]
|
||||||
|
lms-spotify: ENC[AES256_GCM,data:E53aUSNxE30SSrG6Y6SWKVzmsv0lu8aZvjk1RBgSj3q4m65dPLwGM9HcagN3BPoVTc0tKJaccrjoL2k5FOMnwcTXIz3qgiZGbnB6hVCoOhMrrkoFRN2JzSIA5WxKOT8VuMoC4/a6WaWbY8SWAdhgRQb9uq1hUxdkMCoNRLNJnPqR/0w07lCDVHvkj8XuBV4rGl93VVT3rCzjVTL+Vigv38WZ2il2aANkCz3joNeN8Uod3K/HA5uXLw3cLFmD7eI7LBDSTHpMEg==,iv:iRKrij3TRaufB5BXy7Xhiu3asClZ6hpkbMV14aod7jk=,tag:hpUwP/OHygqfgI6j6q2sKQ==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
|
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsNzNjZ1o1dXFxalFiRXUx
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrTDFvM2l3Tm5lU0paWXpF
|
||||||
U3NQK0gvQWVRbnAxam8yZmJTTmRTaVVZdkdrCnQ0R1ZBWEVmcE12NWNuaDFtRGlj
|
cjVBSFhENW5mNG9DSFM1NXh3UHdaKzlKMGlZCnRmNFBFVWY4N0FqLzF1bUMyUDdL
|
||||||
UFRManh2VFgwUFJaNFpVZFNqc01oSkEKLS0tIHA5UDlHY1lDWUtwTk10RHZoQWQ1
|
U091VENiVFhYeEJ5K0xodXlHVkhHKzgKLS0tIGxta3A2TjJiMUtiR2RzcU02Rys5
|
||||||
bzZ6MzhQQmYrZ3JKUDZoa1lDZXRHRDAKHtzHnt+zHgMsuyX0vP6xapvJ8796/vkn
|
U1c0SjRKK2UwbTVIQUMrT1pOOVFmOVkKY3UyGNIPZJLE8GG124y0pLgqGub9SMCq
|
||||||
u9U56OdFlqthTy870vMMoJWW3wAFfj/QV124bG63lJ02gAHEr/PGJw==
|
plK5H+kASOB1X6pK+3PBFuDYT1AbsRxXvWgAEMvVI7eBcxQlSrrB4Q==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
|
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLc0ZsVlNzQ0d1dGJlSzN6
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVaXBqMGl1UytNL3BkZEhQ
|
||||||
bzB0bnhHTzlodWJveFBmdVVCdjJ5c2V0dkM4Cmt1cHhJa2U4NmJZSUFGYzhCQmdH
|
S3RFL3lZRVZKTGVRTGFMNlFlWFRCNDNvRTM4CnpWZWovSDZaclQvN2Vwa0dWZGgz
|
||||||
eVJDUjc0LzdIOHo4TWlCeVEvQUg1b1EKLS0tIGRpTFA4TkgvU2ZLOXM3NktMbjRP
|
Q1ZLM0sveXBxOVpvNHkycWJWWXdmVE0KLS0tIHl2bFk3RE03N01IdDJPWk5HT1Np
|
||||||
aGM2aVdRSUpsRXRCZE02MXJ3MVpxK00KO2dZUNZ1KQFg4bnNp1PEntL2fY1h+JCK
|
Qm82Sit3Q0haaDdnbzFjendMUm04Wk0KYp09dxXjzvC4IlH6Ilip8YjTz0mFeu/0
|
||||||
l7CnGwotydc9NybwYtisv9XVrz3QoiD09OiLvg7VkmfzEaGmqmja/g==
|
5IDMYjT1BuW5YiKgIJVd+UgOd6ysZLFFwk+Us2AcV7z110xk/askqQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjQTBxNkV2REdrRS9MaUxa
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4Wkd0YnBQRnExeVdUTGFu
|
||||||
YWxNOFBKQlAwOW5qSk9hM1Q5c0tjZTdWUjBjCkM5TmtwR2RBRER3Uzc4dWtGOVM2
|
N3o3MnF2aTY2NlBmdDJYT01zRytWZ2w1dFg0ClAzcnJ0NFYrVWlBM2JQU1B0SEJi
|
||||||
bjZFZVc3V0t0enhyam1DWVM3b0h5WlEKLS0tIGNPUzFJUGRYZStMRTMwV3pWTW1t
|
MGE5aVh6KzNmaEoxaHFOTW90K0VmMGsKLS0tIDNkOGZyVmMzME80TlBWMzI5UVR2
|
||||||
V003cnFtYVNEbERiRDV4bmVXVlBaUTAK7pLGaixTRCg5lKhN8CN95cdr7X8X1oDY
|
djB3Y2FIRDFKWlEwTnRBUnRIT3M2OXcK+SIt/7DRdQi6H1AZooJN2Pt2g1EwVTZe
|
||||||
LX2t+SPvb8hqsssLf/mqVxPsgAXl0L9lfsYtRsuMWONmaJsOleVE4A==
|
Q14cEt0sLyVYzLJugfz2JWRHDZX6wPueYcTSEs7w3wAPVwvJWju8bg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df
|
- recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbDA5U0xnUDNXYUtRVVN3
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzQXhNSFBnNUtMdkpwR0th
|
||||||
YW5aTFg1T0pOZWc4cXFDRDlrRmxZWWw1MUdRCjdlUVg0S0IxTXM4ZXcydGR0aldu
|
M1NmOVorcUdlZTFDM3dVRHZlYWpJcDZiakVnCit6eTFOeW92SzhPYzJxR0VTem9r
|
||||||
WnU3ZnUydUh4em02TWFVamx6a0xpQmMKLS0tIEdpWFg1UEVGNHIzY2VZZk40NlBG
|
MSs4cWxRbzVBQmlWaHIwMjB5RUlJMXcKLS0tIHNSVTloOEVVVndDWkVrWmQrYXlD
|
||||||
WXJpUUxadERyYUExRFMzNzBXaUVET3cKG9ZwWy5YvTr/BAw/i+ZJos5trwRvaW5j
|
NTd1WGFJWHVLTnFNT3hYbDdtSnMzTTAKBmJOayZLbjmBejwVzVtUSYPki+qPkYwG
|
||||||
eV/SHiEteZZtCuCVFAp3iolE/mJyu97nA2yFwWaLN86h+/xkOJsdqA==
|
xdO3L7n0Z8Cv/kVYZpkuG5GqOUL+nCJuYDjF0g4PaLb6WWd0W8ZGFA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2025-05-31T08:08:02Z"
|
lastmodified: "2025-12-01T11:01:54Z"
|
||||||
mac: ENC[AES256_GCM,data:p6FHDa6Xfd66pH4zB8s6nhGGk2Ha2YTC/wUsCrqu+9M01VQ7qv9tha1MpKMj9TUxSPSxPOI++5zkNi5LJbs4Y4q0KH4yd9w/guMmJB2+d2YUwNCTofvmQp3wS1KtaRbaai6mAXZELaVEsRkmwUdkdApNbSZkTZgDc+CMH7OmHbs=,iv:w/kv2wRO6N4k1U7y8efS7LXhrpMxkZ9kTs3lFo23MA8=,tag:F4rZGG00AQZLfGU3djgW8Q==,type:str]
|
mac: ENC[AES256_GCM,data:taGX5HHZCL7Zo4taS2Jz/5WxhvpBNNKZ13ZCtS3x/P17tC1Nrk2UDcxbOZ1pPVbVvvaAHJtDb3owFvBOM4nr2Eve0M9zT4HbXh3hke7AviQ6U7CT1ru6LjY7W8lBjbQ6uCt+Ldxd1PRPPGiyKdK5GAUPKg6avFjpJbhEikh8Gww=,iv:NNs5usVJ5izYvHKnNm1IgjSt4dg0QFQ7cClJ6zh+3wM=,tag:sYYbEWIUgOWthEItdy5PFg==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.10.2
|
version: 3.11.0
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
https://channels.nixos.org/nixos-25.05
|
https://channels.nixos.org/nixos-25.11
|
||||||
|
|||||||
@@ -48,15 +48,11 @@ let
|
|||||||
doveadm -v sync -u $user $SERVER
|
doveadm -v sync -u $user $SERVER
|
||||||
done
|
done
|
||||||
|
|
||||||
doveadm user *@ghetto.at | while read user; do
|
|
||||||
doveadm -v sync -u $user $SERVER
|
|
||||||
done
|
|
||||||
|
|
||||||
doveadm user *@szaku-consulting.at | while read user; do
|
doveadm user *@szaku-consulting.at | while read user; do
|
||||||
doveadm -v sync -u $user $SERVER
|
doveadm -v sync -u $user $SERVER
|
||||||
done
|
done
|
||||||
|
|
||||||
doveadm user *@korean-skin.care | while read user; do
|
doveadm user *@scana11y.com | while read user; do
|
||||||
doveadm -v sync -u $user $SERVER
|
doveadm -v sync -u $user $SERVER
|
||||||
done
|
done
|
||||||
'';
|
'';
|
||||||
@@ -193,10 +189,15 @@ in
|
|||||||
managesieve_logout_format = bytes ( in=%i : out=%o )
|
managesieve_logout_format = bytes ( in=%i : out=%o )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lda_original_recipient_header = X-Original-To
|
||||||
|
|
||||||
plugin {
|
plugin {
|
||||||
sieve_dir = /var/vmail/%d/%n/sieve/scripts/
|
sieve_dir = /var/vmail/%d/%n/sieve/scripts/
|
||||||
sieve = /var/vmail/%d/%n/sieve/active-script.sieve
|
sieve = /var/vmail/%d/%n/sieve/active-script.sieve
|
||||||
sieve_extensions = +vacation-seconds +editheader
|
sieve_extensions = +vacation +vacation-seconds +editheader
|
||||||
|
sieve_vacation_use_original_recipient = yes
|
||||||
|
sieve_vacation_dont_check_recipient = yes
|
||||||
|
sieve_vacation_database = file:/var/vmail/%d/%n/sieve/vacation.db;
|
||||||
sieve_vacation_min_period = 1min
|
sieve_vacation_min_period = 1min
|
||||||
|
|
||||||
fts = lucene
|
fts = lucene
|
||||||
@@ -239,11 +240,11 @@ in
|
|||||||
|
|
||||||
sops.secrets.dovecot-ldap-password = { };
|
sops.secrets.dovecot-ldap-password = { };
|
||||||
|
|
||||||
systemd.services.dovecot2.preStart = ''
|
systemd.services.dovecot.preStart = ''
|
||||||
sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${ldapConfig} > /run/dovecot2/ldap.conf
|
sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${ldapConfig} > /run/dovecot2/ldap.conf
|
||||||
'';
|
'';
|
||||||
|
|
||||||
systemd.services.dovecot2 = {
|
systemd.services.dovecot = {
|
||||||
wants = [ "acme-imap.${domain}.service" ];
|
wants = [ "acme-imap.${domain}.service" ];
|
||||||
after = [ "acme-imap.${domain}.service" ];
|
after = [ "acme-imap.${domain}.service" ];
|
||||||
};
|
};
|
||||||
@@ -256,7 +257,7 @@ in
|
|||||||
"imap-test.${domain}"
|
"imap-test.${domain}"
|
||||||
"imap-02.${domain}"
|
"imap-02.${domain}"
|
||||||
];
|
];
|
||||||
postRun = "systemctl --no-block restart dovecot2.service";
|
postRun = "systemctl --no-block restart dovecot.service";
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ in {
|
|||||||
olcTLSCACertificateFile = "/var/lib/acme/ldap.${domain}/full.pem";
|
olcTLSCACertificateFile = "/var/lib/acme/ldap.${domain}/full.pem";
|
||||||
olcTLSCertificateFile = "/var/lib/acme/ldap.${domain}/cert.pem";
|
olcTLSCertificateFile = "/var/lib/acme/ldap.${domain}/cert.pem";
|
||||||
olcTLSCertificateKeyFile = "/var/lib/acme/ldap.${domain}/key.pem";
|
olcTLSCertificateKeyFile = "/var/lib/acme/ldap.${domain}/key.pem";
|
||||||
olcTLSCipherSuite = "HIGH:MEDIUM:+3DES:+RC4:+aNULL";
|
olcTLSCipherSuite = "HIGH:!aNULL:!MD5:!3DES:!RC4";
|
||||||
olcTLSCRLCheck = "none";
|
olcTLSCRLCheck = "none";
|
||||||
olcTLSVerifyClient = "never";
|
olcTLSVerifyClient = "never";
|
||||||
olcTLSProtocolMin = "3.1";
|
olcTLSProtocolMin = "3.3";
|
||||||
olcSecurity = "tls=1";
|
olcSecurity = "tls=1";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,52 +111,6 @@ in {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
"olcDatabase={3}mdb".attrs = {
|
|
||||||
objectClass = ["olcDatabaseConfig" "olcMdbConfig"];
|
|
||||||
|
|
||||||
olcDatabase = "{3}mdb";
|
|
||||||
olcDbDirectory = "/var/lib/openldap/data";
|
|
||||||
|
|
||||||
olcSuffix = "dc=ghetto,dc=at";
|
|
||||||
|
|
||||||
olcAccess = [
|
|
||||||
''
|
|
||||||
{0}to attrs=userPassword
|
|
||||||
by self write
|
|
||||||
by anonymous auth
|
|
||||||
by dn="cn=owncloud,ou=system,ou=users,dc=cloonar,dc=com" write
|
|
||||||
by dn="cn=authelia,ou=system,ou=users,dc=cloonar,dc=com" write
|
|
||||||
by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read
|
|
||||||
by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write
|
|
||||||
by * none
|
|
||||||
''
|
|
||||||
''
|
|
||||||
{1}to attrs=pgpPublicKey
|
|
||||||
by self write
|
|
||||||
by anonymous read
|
|
||||||
by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read
|
|
||||||
by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write
|
|
||||||
by * read
|
|
||||||
''
|
|
||||||
''
|
|
||||||
{2}to *
|
|
||||||
by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read
|
|
||||||
by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write
|
|
||||||
by * read
|
|
||||||
''
|
|
||||||
];
|
|
||||||
};
|
|
||||||
"olcOverlay=memberof,olcDatabase={3}mdb".attrs = {
|
|
||||||
objectClass = [ "olcOverlayConfig" "olcMemberOf" ];
|
|
||||||
olcOverlay = "memberof";
|
|
||||||
olcMemberOfRefint = "TRUE";
|
|
||||||
};
|
|
||||||
"olcOverlay=ppolicy,olcDatabase={3}mdb".attrs = {
|
|
||||||
objectClass = [ "olcOverlayConfig" "olcPPolicyConfig" ];
|
|
||||||
olcOverlay = "ppolicy";
|
|
||||||
olcPPolicyHashCleartext = "TRUE";
|
|
||||||
};
|
|
||||||
|
|
||||||
"olcDatabase={4}mdb".attrs = {
|
"olcDatabase={4}mdb".attrs = {
|
||||||
objectClass = ["olcDatabaseConfig" "olcMdbConfig"];
|
objectClass = ["olcDatabaseConfig" "olcMdbConfig"];
|
||||||
|
|
||||||
@@ -330,6 +284,42 @@ in {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"olcDatabase={9}mdb".attrs = {
|
||||||
|
objectClass = ["olcDatabaseConfig" "olcMdbConfig"];
|
||||||
|
|
||||||
|
olcDatabase = "{9}mdb";
|
||||||
|
olcDbDirectory = "/var/lib/openldap/data";
|
||||||
|
|
||||||
|
olcSuffix = "dc=scana11y,dc=com";
|
||||||
|
|
||||||
|
olcAccess = [
|
||||||
|
''
|
||||||
|
{0}to attrs=userPassword
|
||||||
|
by self write
|
||||||
|
by anonymous auth
|
||||||
|
by dn="cn=owncloud,ou=system,ou=users,dc=cloonar,dc=com" write
|
||||||
|
by dn="cn=authelia,ou=system,ou=users,dc=cloonar,dc=com" write
|
||||||
|
by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read
|
||||||
|
by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write
|
||||||
|
by * none
|
||||||
|
''
|
||||||
|
''
|
||||||
|
{1}to attrs=pgpPublicKey
|
||||||
|
by self write
|
||||||
|
by anonymous read
|
||||||
|
by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read
|
||||||
|
by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write
|
||||||
|
by * read
|
||||||
|
''
|
||||||
|
''
|
||||||
|
{2}to *
|
||||||
|
by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read
|
||||||
|
by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write
|
||||||
|
by * read
|
||||||
|
''
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
# "cn=module{0},cn=config" = {
|
# "cn=module{0},cn=config" = {
|
||||||
# attrs = {
|
# attrs = {
|
||||||
# objectClass = "olcModuleList";
|
# objectClass = "olcModuleList";
|
||||||
|
|||||||
@@ -128,16 +128,16 @@ in
|
|||||||
compatibility_level = "2";
|
compatibility_level = "2";
|
||||||
|
|
||||||
# bigger attachement size
|
# bigger attachement size
|
||||||
mailbox_size_limit = "202400000";
|
mailbox_size_limit = 202400000;
|
||||||
message_size_limit = "51200000";
|
message_size_limit = 51200000;
|
||||||
smtpd_helo_required = "yes";
|
smtpd_helo_required = "yes";
|
||||||
smtpd_delay_reject = "yes";
|
smtpd_delay_reject = "yes";
|
||||||
strict_rfc821_envelopes = "yes";
|
strict_rfc821_envelopes = "yes";
|
||||||
|
|
||||||
# send Limit
|
# send Limit
|
||||||
smtpd_error_sleep_time = "1s";
|
smtpd_error_sleep_time = "1s";
|
||||||
smtpd_soft_error_limit = "10";
|
smtpd_soft_error_limit = 10;
|
||||||
smtpd_hard_error_limit = "20";
|
smtpd_hard_error_limit = 20;
|
||||||
|
|
||||||
smtpd_use_tls = "yes";
|
smtpd_use_tls = "yes";
|
||||||
smtp_tls_note_starttls_offer = "yes";
|
smtp_tls_note_starttls_offer = "yes";
|
||||||
@@ -151,14 +151,13 @@ in
|
|||||||
smtpd_tls_key_file = "/var/lib/acme/mail.cloonar.com/key.pem";
|
smtpd_tls_key_file = "/var/lib/acme/mail.cloonar.com/key.pem";
|
||||||
smtpd_tls_CAfile = "/var/lib/acme/mail.cloonar.com/fullchain.pem";
|
smtpd_tls_CAfile = "/var/lib/acme/mail.cloonar.com/fullchain.pem";
|
||||||
|
|
||||||
smtpd_tls_dh512_param_file = config.security.dhparams.params.postfix512.path;
|
|
||||||
smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix2048.path;
|
smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix2048.path;
|
||||||
|
|
||||||
smtpd_tls_session_cache_database = ''btree:''${data_directory}/smtpd_scache'';
|
smtpd_tls_session_cache_database = ''btree:''${data_directory}/smtpd_scache'';
|
||||||
smtpd_tls_mandatory_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1";
|
smtpd_tls_mandatory_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1";
|
||||||
smtpd_tls_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1";
|
smtpd_tls_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1";
|
||||||
smtpd_tls_mandatory_ciphers = "medium";
|
smtpd_tls_mandatory_ciphers = "medium";
|
||||||
tls_medium_cipherlist = "AES128+EECDH:AES128+EDH";
|
tls_medium_cipherlist = "ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20:DHE+CHACHA20";
|
||||||
|
|
||||||
# authentication
|
# authentication
|
||||||
smtpd_sasl_auth_enable = "yes";
|
smtpd_sasl_auth_enable = "yes";
|
||||||
@@ -225,8 +224,7 @@ in
|
|||||||
|
|
||||||
security.dhparams = {
|
security.dhparams = {
|
||||||
enable = true;
|
enable = true;
|
||||||
params.postfix512.bits = 512;
|
params.postfix2048.bits = 2048;
|
||||||
params.postfix2048.bits = 1024;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
security.acme.certs."mail.${domain}" = {
|
security.acme.certs."mail.${domain}" = {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ in
|
|||||||
|
|
||||||
# systemd.services.rspamd.serviceConfig.SupplementaryGroups = [ "redis-rspamd" ];
|
# systemd.services.rspamd.serviceConfig.SupplementaryGroups = [ "redis-rspamd" ];
|
||||||
|
|
||||||
systemd.services.dovecot2.preStart = ''
|
systemd.services.dovecot.preStart = ''
|
||||||
mkdir -p /var/lib/dovecot/sieve/
|
mkdir -p /var/lib/dovecot/sieve/
|
||||||
for i in ${sieve-spam-filter}/share/sieve-rspamd-filter/*.sieve; do
|
for i in ${sieve-spam-filter}/share/sieve-rspamd-filter/*.sieve; do
|
||||||
dest="/var/lib/dovecot/sieve/$(basename $i)"
|
dest="/var/lib/dovecot/sieve/$(basename $i)"
|
||||||
|
|||||||
@@ -4,58 +4,44 @@ netdata-claim-token: ENC[AES256_GCM,data:ECx8zLnU/dj08vfA76oVbVzL3JG9MLBoFmxSjtj
|
|||||||
openldap-rootpw: ENC[AES256_GCM,data:W0em1Dffg+IUoynwwPD4NjFksR38ZO4mhWFI83ALvYcwYIplxw/gDRLGCqbSt6TR5C65CKr1sOUiU+4Xq3UWmw==,iv:BHQhISTIYuwSM3KiSb0mEEo3BMNo6FXEDXoIvI3SZrU=,tag:tX8gfnk1JYnaNionk/jrLg==,type:str]
|
openldap-rootpw: ENC[AES256_GCM,data:W0em1Dffg+IUoynwwPD4NjFksR38ZO4mhWFI83ALvYcwYIplxw/gDRLGCqbSt6TR5C65CKr1sOUiU+4Xq3UWmw==,iv:BHQhISTIYuwSM3KiSb0mEEo3BMNo6FXEDXoIvI3SZrU=,tag:tX8gfnk1JYnaNionk/jrLg==,type:str]
|
||||||
dovecot-ldap-password: ENC[AES256_GCM,data:JYAt8/WggwclNEPO9CaWfQsvQBA8DDJCU2km93HpowoVwIdvQ/0lQHeXndPYe1EmJGJ3vLErie+Zn2kDINIMqQ==,iv:HR0QJ0GgQks3NzhfXwjHupCKcPOekkiTcp5Jxbz7CxI=,tag:19m7F6TjGUPOuHQJuUq2pw==,type:str]
|
dovecot-ldap-password: ENC[AES256_GCM,data:JYAt8/WggwclNEPO9CaWfQsvQBA8DDJCU2km93HpowoVwIdvQ/0lQHeXndPYe1EmJGJ3vLErie+Zn2kDINIMqQ==,iv:HR0QJ0GgQks3NzhfXwjHupCKcPOekkiTcp5Jxbz7CxI=,tag:19m7F6TjGUPOuHQJuUq2pw==,type:str]
|
||||||
sops:
|
sops:
|
||||||
kms: []
|
|
||||||
gcp_kms: []
|
|
||||||
azure_kv: []
|
|
||||||
hc_vault: []
|
|
||||||
age:
|
age:
|
||||||
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
|
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAraEttTi84cGd2bkd1RENP
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0NWZPWXltTVNXNGxPd0hZ
|
||||||
bm9zRmlNdWZtSzZJVElVWW5qTXlzS1lreTNBCm9BMnJ6bEJON2Y5aVZvVjFmQlJw
|
R0U4VzN5WlI0WWZrRVVFMmpnckpMMkREaTBvCm54eTZtZlZzRVpwRmg4Ulp0VG5w
|
||||||
VVVpSEVRNDJaa2FadFh2U1gySHFXQmcKLS0tIEhjeG5Wb0FDMlBxWW9aem45aTdF
|
VnJkc29nN0VBRFR1U1J6L0RQeWlLNlkKLS0tIDJ3eTdiUWJzbURvSk1neEhyakJS
|
||||||
N1ZQNlE2aTl5OGhqTUVNa20yelNpcW8KoXud5IID1g/KOvM30wn2cJFWQ5En4M5H
|
Z2MzZi8ybW1PMngyRGk4NHhIMzZsem8KZuy1TWwvkFGsAVMIEk2+bwDcsmYziUjj
|
||||||
kJ/cLDSIBqgOpjtEeEDtMsKG4yW3H91YbXjwQ0UkoPJorauVPWnTYw==
|
Wd4wMK1XuLnJyFYPt6CwzBAPG+1LQzmYWdC9mNI00YZM6XneU3OisQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
|
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFbVcxeTJZM1dDUFhIZ3VE
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBZEhsK0x1QkczeFIvL0JI
|
||||||
ZlBaTU9tQ0Y2V2xlZFUxUXNKcjdadVVMd2w4Ck9TK2UyVFVTVSt1dzNWWUtxYzdw
|
UWY5R252WkZvR0s2SStlWVBMQk9ENFpaRHpRClg3VjhpYW5UbzJkODRFYWF2aGpr
|
||||||
SVZ3R3VjRUxDMDNRWnpRZVBHWXdzN0UKLS0tIHQ0ZW0xZDd4bFVBV0ZjZE9Jcm9F
|
ajE3aUFhZStYY0NJYlg1QTZqVHJsODAKLS0tIGsyRHlXSVQyV2RXVCswRVlsbktV
|
||||||
cVd0aW1qWHFMMjh3SXhTYjJrN1ZEZHcKi9QhittNcxnz+Zzc/pyFutXg3Z8JJjgc
|
c0Z5ZXhtb0wrT0Q3WU1ONjFiNk1WOVkKHxnDqJkGfiqrlAyzJHYVbJlR1/jluFU+
|
||||||
j3rW5N6eNJw0W50qPw0xdI44KEkWOc4vh+QGcPY57yqjSy4+SjWhWA==
|
hM/wENwqtlZ7RCSdG68AssgP9zukO94sV9mAtbfOdeVwXa1LU66Ncw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2TnNnOUtsbFBzS0E0bnFK
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5TG9wT2JHN2pOVjRueUF2
|
||||||
NGk4ZkRjUWdRdG15aTQwU2cwQXdycjhxa3dvCkUwUGdmQ3FPQnFhZC9NcE9LUG1O
|
UGJkM2d5VFpLT0hKVmIwV2Qva25ubk1lK0ZBCkJiNWpuZ3grQ0lkSDlCMDBwYjRR
|
||||||
S0lydjZkdCt2V3R4dWlnUlBUSkp2RXcKLS0tIFJ3UkZhSkhTMlZZSjdXbFBObXNQ
|
cDlPVHhtWlpnaVFYMFJqWWY2ZVFGNncKLS0tIFZQVVRSQXVOZnNDOHVwTHBraUx3
|
||||||
RW40cXUrdFAzb1B1VTUzOGY2RTcveUUKFxxBBioTXTZ3INRykgRPoYwwbbuDMiXH
|
MVRVRlRQMFcyelNvL3FaNjc3U3VYbmsKZ+rJ/EFb3KNyyJ5hqO/wV4AtO1FJCeB/
|
||||||
/Oy5yWE74I9KZJr/2idzd34Dq8PUB28lDyiDdxlISyAS33D4H0cl1w==
|
oazkDDoFBE+uhiLmdCy41eYkqW8Owt/zrO29nITeJ5EtGAXTbACcgg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4
|
- recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4
|
||||||
enc: |
|
enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiZHFvbFMrZ1dTQzBZUkw4
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDZlJYSG51NEE3emlTVDM0
|
||||||
dkl2UUlmcEZmZUVKeHVoSytYRzZVQ3p6T2hzCnJXaUJ4SUVaZFR3dEZtQ2ttZWNN
|
WEE4LzFqazdZQkRZSUlqQ0dzYURkbWc5RWxnCnJobm5LVnkxZkFIeTNWWUJvOUFU
|
||||||
NHo0Znk5TjZzemtmWHdkSGlIZ04zUlkKLS0tIDRvclhTMFlsdERtQUk0azJ4ZVFM
|
SlZhZDBsdHhDRzFVQjhsN3F1dE9SVDAKLS0tIFBlOEwxallncjBxWDZCSkhZdlJN
|
||||||
WDMva0RCTnkzT0RWeWY5V281M0hjQkEK9o9cIFOiEwFeo+77QI9lXqdxlMCNGhOY
|
b21icTBmeFM1cnVkaXAySHFzam1hYmcKULP2EuMGhspSusYPZs/DTksaZb0Asfel
|
||||||
BtowL/7wo0Tfi7+CkBuKP/Bxp2D0x3b4OHDsoCNG0nc+55F/rDtR5A==
|
mVn9Unqe2b9tT5cchGrxLiDJ+2YvfTA0s/JpDtLN+MpiRQQl0vJikg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
- recipient: age1azmxsw5llmp2nnsv3yc2l8paelmq9rfepxd8jvmswgsmax0qyyxqdnsc7t
|
|
||||||
enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkMjIwT3pUcHlkc2N3eWZl
|
|
||||||
cVdtT3NGcDNyMFZ3V1lhWGdJMExyVXYwUTJFCmMrZ3dwZm1ZcVZVMnB6b1NPUDVR
|
|
||||||
UFZUaHdRVWFNKzNrdGE0ZWxUNnVOeWsKLS0tIFhnbklUMkd4ZGFrUjhUcVBKRktX
|
|
||||||
YXlwV28xR2poYnFja0xVdzRPcnZmV2sKDbM77Msos187Du6D7s1wlgEuVxqQ4cw1
|
|
||||||
Rwm64kyiQPwh1W9sPhMOZWyEvUTP4QL2Bs6aB1Javf4BDKka0PeP6A==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2024-07-08T11:20:50Z"
|
lastmodified: "2024-07-08T11:20:50Z"
|
||||||
mac: ENC[AES256_GCM,data:GPUwpSAz6fj7mRxX1ebEb2sLAMLkQLuKPXk+B3+zZmA6+D7gAKrrBGUWHqYA9DMMY0r32OZSccGRmeKqdA7sWmzdIJTcBu8EyER1nJqVFJiXcOOdTkCLdOM4xW969YE0lBKpIAQ40E7YXYYwkI1JINneIBTuXkvIBmSQ3Bt2+ak=,iv:VEPNQxDLzxyTxkn8dI6xNDe9ESk2RojSNYYEwT+Ggas=,tag:cfUEKU3arSJl+lEOa+4iRA==,type:str]
|
mac: ENC[AES256_GCM,data:GPUwpSAz6fj7mRxX1ebEb2sLAMLkQLuKPXk+B3+zZmA6+D7gAKrrBGUWHqYA9DMMY0r32OZSccGRmeKqdA7sWmzdIJTcBu8EyER1nJqVFJiXcOOdTkCLdOM4xW969YE0lBKpIAQ40E7YXYYwkI1JINneIBTuXkvIBmSQ3Bt2+ak=,iv:VEPNQxDLzxyTxkn8dI6xNDe9ESk2RojSNYYEwT+Ggas=,tag:cfUEKU3arSJl+lEOa+4iRA==,type:str]
|
||||||
pgp: []
|
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.8.1
|
version: 3.8.1
|
||||||
|
|||||||
60
hosts/nas/STORAGE.md
Normal file
60
hosts/nas/STORAGE.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# NAS Storage Notes
|
||||||
|
|
||||||
|
## Current Issue: XFS Metadata Overhead
|
||||||
|
|
||||||
|
The XFS filesystem on `/var/lib/multimedia` uses ~100GB more than the actual file data due to metadata overhead.
|
||||||
|
|
||||||
|
### Root Cause
|
||||||
|
|
||||||
|
The filesystem was created with advanced features enabled:
|
||||||
|
|
||||||
|
```
|
||||||
|
rmapbt=1 # Reverse mapping btree - tracks block ownership
|
||||||
|
reflink=1 # Copy-on-write support
|
||||||
|
```
|
||||||
|
|
||||||
|
These features add metadata that scales with **filesystem size**, not file count. On a 5TB filesystem with 700GB of data, this results in ~100GB (~2%) overhead.
|
||||||
|
|
||||||
|
### Diagnostic Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compare file data vs filesystem usage
|
||||||
|
du -sh /var/lib/multimedia # Actual file data
|
||||||
|
df -h /var/lib/multimedia # Filesystem reports
|
||||||
|
|
||||||
|
# Check XFS features
|
||||||
|
xfs_info /var/lib/multimedia
|
||||||
|
|
||||||
|
# Verify block allocation
|
||||||
|
xfs_db -r -c "freesp -s" /dev/mapper/vg--data-lv--multimedia
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommendation: LVM + ext4
|
||||||
|
|
||||||
|
For media storage (write-once, read-many), ext4 with minimal reserved space offers the lowest overhead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create filesystem with 0% reserved blocks
|
||||||
|
mkfs.ext4 -m 0 /dev/vg/lv
|
||||||
|
|
||||||
|
# Or adjust existing ext4
|
||||||
|
tune2fs -m 0 /dev/vg/lv
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why ext4 over XFS for this use case
|
||||||
|
|
||||||
|
| Consideration | ext4 | XFS (current) |
|
||||||
|
|---------------|------|---------------|
|
||||||
|
| Reserved space | 0% with `-m 0` | N/A |
|
||||||
|
| Metadata overhead | ~0.5% | ~2% (with rmapbt) |
|
||||||
|
| Shrink support | Yes | No |
|
||||||
|
| Performance for 4K stream | Identical | Identical |
|
||||||
|
|
||||||
|
A single 4K remux stream requires ~12 MB/s. Any filesystem handles this trivially.
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
1. Backup data from XFS volumes
|
||||||
|
2. Recreate LVs with ext4 (`mkfs.ext4 -m 0`)
|
||||||
|
3. Restore data
|
||||||
|
4. Update `/etc/fstab` or NixOS `fileSystems` config
|
||||||
1
hosts/nas/channel
Normal file
1
hosts/nas/channel
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://channels.nixos.org/nixos-25.11
|
||||||
100
hosts/nas/configuration.nix
Normal file
100
hosts/nas/configuration.nix
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# NAS host configuration
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
impermanence = builtins.fetchTarball "https://github.com/nix-community/impermanence/archive/master.tar.gz";
|
||||||
|
in {
|
||||||
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
"${impermanence}/nixos.nix"
|
||||||
|
./utils/bento.nix
|
||||||
|
./utils/modules/sops.nix
|
||||||
|
./utils/modules/set-nix-channel.nix
|
||||||
|
./utils/modules/victoriametrics
|
||||||
|
./utils/modules/promtail
|
||||||
|
|
||||||
|
# ./modules/cyberghost.nix
|
||||||
|
./modules/pyload.nix
|
||||||
|
./modules/jellyfin.nix
|
||||||
|
./modules/power-management.nix
|
||||||
|
./modules/disk-monitoring.nix
|
||||||
|
./modules/ugreen-leds.nix
|
||||||
|
|
||||||
|
./hardware-configuration.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
nixpkgs.overlays = [
|
||||||
|
(import ./utils/overlays/packages.nix)
|
||||||
|
];
|
||||||
|
|
||||||
|
# Hostname
|
||||||
|
networking.hostName = "nas";
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
time.timeZone = "Europe/Vienna";
|
||||||
|
console.keyMap = "de";
|
||||||
|
|
||||||
|
# SSH server
|
||||||
|
services.openssh.enable = true;
|
||||||
|
users.users.root.openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7"
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRQuPqH5fdX3KEw7DXzWEdO3AlUn1oSmtJtHB71ICoH Generated By Termius"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Firewall
|
||||||
|
networking.firewall.enable = true;
|
||||||
|
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||||
|
|
||||||
|
# SOPS configuration
|
||||||
|
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||||
|
sops.defaultSopsFile = ./secrets.yaml;
|
||||||
|
|
||||||
|
# Btrfs maintenance
|
||||||
|
services.btrfs.autoScrub = {
|
||||||
|
enable = true;
|
||||||
|
interval = "monthly";
|
||||||
|
fileSystems = [ "/nix" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Impermanence - persist important directories
|
||||||
|
# Note: /var/lib/downloads and /var/lib/multimedia are mounted from LVM on RAID
|
||||||
|
environment.persistence."/nix/persist/system" = {
|
||||||
|
hideMounts = true;
|
||||||
|
directories = [
|
||||||
|
"/var/lib/pyload"
|
||||||
|
"/var/lib/jellyfin"
|
||||||
|
"/var/log"
|
||||||
|
"/var/lib/nixos"
|
||||||
|
"/var/bento"
|
||||||
|
"/root/.ssh"
|
||||||
|
];
|
||||||
|
files = [
|
||||||
|
"/etc/machine-id"
|
||||||
|
{ file = "/etc/ssh/ssh_host_ed25519_key"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
|
||||||
|
{ file = "/etc/ssh/ssh_host_ed25519_key.pub"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
|
||||||
|
{ file = "/etc/ssh/ssh_host_rsa_key"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
|
||||||
|
{ file = "/etc/ssh/ssh_host_rsa_key.pub"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# System packages
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
vim
|
||||||
|
screen
|
||||||
|
];
|
||||||
|
|
||||||
|
# Nix settings
|
||||||
|
nix = {
|
||||||
|
settings = {
|
||||||
|
auto-optimise-store = true;
|
||||||
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
|
};
|
||||||
|
gc = {
|
||||||
|
automatic = true;
|
||||||
|
dates = "weekly";
|
||||||
|
options = "--delete-older-than 14d";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "24.05";
|
||||||
|
}
|
||||||
1
hosts/nas/fleet.nix
Symbolic link
1
hosts/nas/fleet.nix
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../fleet.nix
|
||||||
103
hosts/nas/hardware-configuration.nix
Normal file
103
hosts/nas/hardware-configuration.nix
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Hardware configuration for NAS
|
||||||
|
# TODO: Update disk labels/UUIDs after installation
|
||||||
|
{ config, lib, pkgs, modulesPath, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.loader.systemd-boot = {
|
||||||
|
enable = true;
|
||||||
|
configurationLimit = 5;
|
||||||
|
};
|
||||||
|
boot.loader.efi.canTouchEfiVariables = true;
|
||||||
|
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usb_storage" "sd_mod" ];
|
||||||
|
boot.initrd.kernelModules = [ "dm-snapshot" ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
# Power management kernel parameters
|
||||||
|
boot.kernelParams = [
|
||||||
|
"intel_pstate=passive" # Better with powersave governor
|
||||||
|
"i915.enable_rc6=1" # GPU deep sleep states
|
||||||
|
"i915.enable_dc=2" # Display C-states (deepest)
|
||||||
|
"i915.enable_fbc=1" # Frame buffer compression
|
||||||
|
];
|
||||||
|
|
||||||
|
# RAID 1 arrays for data storage
|
||||||
|
boot.swraid = {
|
||||||
|
enable = true;
|
||||||
|
mdadmConf = ''
|
||||||
|
DEVICE /dev/disk/by-id/nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1MAZ0E7-part1
|
||||||
|
DEVICE /dev/disk/by-id/nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1M9Z0E7-part1
|
||||||
|
DEVICE /dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_8582A01SF4MJ-part1
|
||||||
|
DEVICE /dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_75V2A0H3F4MJ-part1
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Tmpfs root filesystem (ephemeral - resets on reboot)
|
||||||
|
fileSystems."/" = {
|
||||||
|
device = "none";
|
||||||
|
fsType = "tmpfs";
|
||||||
|
options = [ "size=8G" "mode=755" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Boot partition - EFI
|
||||||
|
fileSystems."/boot" = {
|
||||||
|
device = "/dev/disk/by-partlabel/BOOT";
|
||||||
|
fsType = "vfat";
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/nix" = {
|
||||||
|
device = "/dev/disk/by-partlabel/NIXOS";
|
||||||
|
fsType = "btrfs";
|
||||||
|
neededForBoot = true;
|
||||||
|
options = [
|
||||||
|
"subvol=@"
|
||||||
|
"compress=zstd:1"
|
||||||
|
"noatime"
|
||||||
|
"commit=120"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/nix/store" = {
|
||||||
|
device = "/dev/disk/by-partlabel/NIXOS";
|
||||||
|
fsType = "btrfs";
|
||||||
|
neededForBoot = true;
|
||||||
|
options = [
|
||||||
|
"subvol=@nix-store"
|
||||||
|
"compress=zstd:1"
|
||||||
|
"noatime"
|
||||||
|
"commit=120"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/nix/persist" = {
|
||||||
|
device = "/dev/disk/by-partlabel/NIXOS";
|
||||||
|
fsType = "btrfs";
|
||||||
|
neededForBoot = true;
|
||||||
|
options = [
|
||||||
|
"subvol=@nix-persist"
|
||||||
|
"compress=zstd:1"
|
||||||
|
"noatime"
|
||||||
|
"commit=120"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# LVM volumes on RAID array
|
||||||
|
fileSystems."/var/lib/downloads" = {
|
||||||
|
device = "/dev/vg-data-fast/downloads";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/var/lib/multimedia" = {
|
||||||
|
device = "/dev/vg-data-slow/multimedia";
|
||||||
|
fsType = "ext4";
|
||||||
|
options = [ "noatime" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# DHCP networking
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
103
hosts/nas/modules/cyberghost.nix
Normal file
103
hosts/nas/modules/cyberghost.nix
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
let
|
||||||
|
localNetwork = "10.42.96.0/20";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# SOPS secrets for CyberGhost credentials
|
||||||
|
sops.secrets.cyberghost-auth = {
|
||||||
|
mode = "0400";
|
||||||
|
owner = "root";
|
||||||
|
};
|
||||||
|
sops.secrets.cyberghost-ca = {
|
||||||
|
mode = "0400";
|
||||||
|
owner = "root";
|
||||||
|
};
|
||||||
|
sops.secrets.cyberghost-cert = {
|
||||||
|
mode = "0400";
|
||||||
|
owner = "root";
|
||||||
|
};
|
||||||
|
sops.secrets.cyberghost-key = {
|
||||||
|
mode = "0400";
|
||||||
|
owner = "root";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = [ pkgs.openvpn ];
|
||||||
|
|
||||||
|
# OpenVPN client service
|
||||||
|
services.openvpn.servers.cyberghost = {
|
||||||
|
autoStart = true;
|
||||||
|
updateResolvConf = true;
|
||||||
|
config = ''
|
||||||
|
client
|
||||||
|
dev tun
|
||||||
|
proto udp
|
||||||
|
remote 87-1-hu.cg-dialup.net 443
|
||||||
|
resolv-retry infinite
|
||||||
|
nobind
|
||||||
|
persist-key
|
||||||
|
persist-tun
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
auth-user-pass ${config.sops.secrets.cyberghost-auth.path}
|
||||||
|
ca ${config.sops.secrets.cyberghost-ca.path}
|
||||||
|
cert ${config.sops.secrets.cyberghost-cert.path}
|
||||||
|
key ${config.sops.secrets.cyberghost-key.path}
|
||||||
|
|
||||||
|
# Security
|
||||||
|
data-ciphers AES-256-GCM:AES-128-GCM:AES-256-CBC
|
||||||
|
data-ciphers-fallback AES-256-CBC
|
||||||
|
auth SHA256
|
||||||
|
remote-cert-tls server
|
||||||
|
script-security 2
|
||||||
|
|
||||||
|
# Connection
|
||||||
|
ping 5
|
||||||
|
explicit-exit-notify 2
|
||||||
|
route-delay 5
|
||||||
|
|
||||||
|
# Split tunnel: Don't pull routes from server, we'll set our own
|
||||||
|
route-nopull
|
||||||
|
|
||||||
|
# Route all traffic through VPN except local network
|
||||||
|
route 0.0.0.0 128.0.0.0 vpn_gateway
|
||||||
|
route 128.0.0.0 128.0.0.0 vpn_gateway
|
||||||
|
|
||||||
|
# Keep local network route direct
|
||||||
|
route ${localNetwork} net_gateway
|
||||||
|
|
||||||
|
verb 4
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Kill switch: Block outgoing traffic if VPN is down
|
||||||
|
networking.firewall = {
|
||||||
|
extraCommands = ''
|
||||||
|
# Allow traffic to local network
|
||||||
|
iptables -A OUTPUT -d ${localNetwork} -j ACCEPT
|
||||||
|
|
||||||
|
# Allow traffic through VPN tunnel
|
||||||
|
iptables -A OUTPUT -o tun+ -j ACCEPT
|
||||||
|
|
||||||
|
# Allow loopback
|
||||||
|
iptables -A OUTPUT -o lo -j ACCEPT
|
||||||
|
|
||||||
|
# Allow established connections (for responses)
|
||||||
|
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
|
||||||
|
# Allow OpenVPN to establish connection (UDP 443)
|
||||||
|
iptables -A OUTPUT -p udp --dport 443 -j ACCEPT
|
||||||
|
|
||||||
|
# Drop all other outgoing internet traffic (kill switch)
|
||||||
|
iptables -A OUTPUT ! -d ${localNetwork} -j DROP
|
||||||
|
'';
|
||||||
|
|
||||||
|
extraStopCommands = ''
|
||||||
|
iptables -D OUTPUT -d ${localNetwork} -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -D OUTPUT -o tun+ -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -D OUTPUT -o lo -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -D OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -D OUTPUT -p udp --dport 443 -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -D OUTPUT ! -d ${localNetwork} -j DROP 2>/dev/null || true
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
199
hosts/nas/modules/disk-monitoring.nix
Normal file
199
hosts/nas/modules/disk-monitoring.nix
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# Disk monitoring for NAS
|
||||||
|
# - S.M.A.R.T. metrics collection (respects disk spindown)
|
||||||
|
# - mdadm RAID array status
|
||||||
|
# - Exports metrics via node_exporter textfile collector
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
# Disk identifiers from hardware-configuration.nix
|
||||||
|
disks = [
|
||||||
|
"/dev/disk/by-id/ata-ST18000NM000J-2TV103_ZR52TBSB"
|
||||||
|
"/dev/disk/by-id/ata-ST18000NM000J-2TV103_ZR52V9QX"
|
||||||
|
"/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_8582A01SF4MJ"
|
||||||
|
"/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_75V2A0H3F4MJ"
|
||||||
|
"/dev/disk/by-id/nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1MAZ0E7"
|
||||||
|
"/dev/disk/by-id/nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1M9Z0E7"
|
||||||
|
];
|
||||||
|
|
||||||
|
textfileDir = "/var/lib/prometheus-node-exporter";
|
||||||
|
|
||||||
|
# Script to collect S.M.A.R.T. and mdadm metrics
|
||||||
|
collectMetricsScript = pkgs.writeShellScript "collect-disk-metrics" ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TEXTFILE_DIR="${textfileDir}"
|
||||||
|
METRICS_FILE="$TEXTFILE_DIR/disk_health.prom"
|
||||||
|
TEMP_FILE="$TEXTFILE_DIR/disk_health.prom.tmp"
|
||||||
|
|
||||||
|
mkdir -p "$TEXTFILE_DIR"
|
||||||
|
: > "$TEMP_FILE"
|
||||||
|
|
||||||
|
# Timestamp of collection
|
||||||
|
echo "# HELP disk_metrics_last_update Unix timestamp of last metrics collection" >> "$TEMP_FILE"
|
||||||
|
echo "# TYPE disk_metrics_last_update gauge" >> "$TEMP_FILE"
|
||||||
|
echo "disk_metrics_last_update $(date +%s)" >> "$TEMP_FILE"
|
||||||
|
|
||||||
|
echo "" >> "$TEMP_FILE"
|
||||||
|
echo "# HELP smart_device_active Whether the disk was active (1) or sleeping (0) when checked" >> "$TEMP_FILE"
|
||||||
|
echo "# TYPE smart_device_active gauge" >> "$TEMP_FILE"
|
||||||
|
|
||||||
|
# S.M.A.R.T. metrics for each disk
|
||||||
|
for disk in ${lib.concatStringsSep " " disks}; do
|
||||||
|
if [[ ! -e "$disk" ]]; then
|
||||||
|
echo "Warning: Disk $disk not found, skipping" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve symlink to get actual device (needed for hdparm/smartctl)
|
||||||
|
device=$(readlink -f "$disk")
|
||||||
|
|
||||||
|
# Extract model+serial from disk-by-id path for stable labeling
|
||||||
|
# ata-ST18000NM000J-2TV103_ZR52TBSB → ST18000NM000J-2TV103-ZR52TBSB
|
||||||
|
# nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1MAZ0E7 → KIOXIA-EXCERIA_PLUS_G3_SSD-7FJKS1MAZ0E7
|
||||||
|
disk_id=$(basename "$disk")
|
||||||
|
serial=$(echo "$disk_id" | sed 's/.*_//')
|
||||||
|
model=$(echo "$disk_id" | sed 's/^[^-]*-//; s/_[^_]*$//')
|
||||||
|
short_name="$model-$serial"
|
||||||
|
|
||||||
|
# Check power state without waking disk
|
||||||
|
power_state=$(${pkgs.hdparm}/bin/hdparm -C "$device" 2>/dev/null | grep -oP '(standby|active/idle|active|idle)' | head -1 || echo "unknown")
|
||||||
|
|
||||||
|
if [[ "$power_state" == "standby" ]]; then
|
||||||
|
# Disk is sleeping - don't wake it, report inactive
|
||||||
|
echo "smart_device_active{device=\"$short_name\",serial=\"$serial\"} 0" >> "$TEMP_FILE"
|
||||||
|
echo "Disk $short_name is in standby, skipping S.M.A.R.T. collection" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disk is active - collect S.M.A.R.T. data
|
||||||
|
echo "smart_device_active{device=\"$short_name\",serial=\"$serial\"} 1" >> "$TEMP_FILE"
|
||||||
|
|
||||||
|
# Get S.M.A.R.T. health status
|
||||||
|
if ${pkgs.smartmontools}/bin/smartctl -H "$device" 2>/dev/null | grep -q "PASSED"; then
|
||||||
|
health=1
|
||||||
|
else
|
||||||
|
health=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get S.M.A.R.T. attributes
|
||||||
|
smartctl_output=$(${pkgs.smartmontools}/bin/smartctl -A "$device" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Parse key attributes
|
||||||
|
# Format: ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE
|
||||||
|
|
||||||
|
get_raw_value() {
|
||||||
|
local attr_id="$1"
|
||||||
|
echo "$smartctl_output" | awk -v id="$attr_id" '$1 == id { print $10 }' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
reallocated=$(get_raw_value "5")
|
||||||
|
power_on_hours=$(get_raw_value "9")
|
||||||
|
temperature=$(get_raw_value "194")
|
||||||
|
reallocated_event=$(get_raw_value "196")
|
||||||
|
pending_sector=$(get_raw_value "197")
|
||||||
|
offline_uncorrectable=$(get_raw_value "198")
|
||||||
|
udma_crc_error=$(get_raw_value "199")
|
||||||
|
|
||||||
|
# Output metrics
|
||||||
|
cat >> "$TEMP_FILE" << EOF
|
||||||
|
|
||||||
|
# S.M.A.R.T. metrics for $short_name
|
||||||
|
smart_health_passed{device="$short_name",serial="$serial"} $health
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[[ -n "$reallocated" ]] && echo "smart_reallocated_sector_ct{device=\"$short_name\",serial=\"$serial\"} $reallocated" >> "$TEMP_FILE"
|
||||||
|
[[ -n "$power_on_hours" ]] && echo "smart_power_on_hours{device=\"$short_name\",serial=\"$serial\"} $power_on_hours" >> "$TEMP_FILE"
|
||||||
|
[[ -n "$temperature" ]] && echo "smart_temperature_celsius{device=\"$short_name\",serial=\"$serial\"} $temperature" >> "$TEMP_FILE"
|
||||||
|
[[ -n "$reallocated_event" ]] && echo "smart_reallocated_event_count{device=\"$short_name\",serial=\"$serial\"} $reallocated_event" >> "$TEMP_FILE"
|
||||||
|
[[ -n "$pending_sector" ]] && echo "smart_current_pending_sector{device=\"$short_name\",serial=\"$serial\"} $pending_sector" >> "$TEMP_FILE"
|
||||||
|
[[ -n "$offline_uncorrectable" ]] && echo "smart_offline_uncorrectable{device=\"$short_name\",serial=\"$serial\"} $offline_uncorrectable" >> "$TEMP_FILE"
|
||||||
|
[[ -n "$udma_crc_error" ]] && echo "smart_udma_crc_error_count{device=\"$short_name\",serial=\"$serial\"} $udma_crc_error" >> "$TEMP_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# mdadm RAID array status (doesn't access disks)
|
||||||
|
echo "" >> "$TEMP_FILE"
|
||||||
|
echo "# HELP mdadm_array_state RAID array state (1=clean/active/resyncing, 0=degraded/other)" >> "$TEMP_FILE"
|
||||||
|
echo "# TYPE mdadm_array_state gauge" >> "$TEMP_FILE"
|
||||||
|
echo "# HELP mdadm_array_devices_total Total devices in RAID array" >> "$TEMP_FILE"
|
||||||
|
echo "# TYPE mdadm_array_devices_total gauge" >> "$TEMP_FILE"
|
||||||
|
echo "# HELP mdadm_array_devices_active Active devices in RAID array" >> "$TEMP_FILE"
|
||||||
|
echo "# TYPE mdadm_array_devices_active gauge" >> "$TEMP_FILE"
|
||||||
|
|
||||||
|
# Find RAID arrays
|
||||||
|
for md_device in /dev/md/*; do
|
||||||
|
[[ -e "$md_device" ]] || continue
|
||||||
|
|
||||||
|
array_name=$(basename "$md_device")
|
||||||
|
|
||||||
|
# Get array details
|
||||||
|
mdadm_output=$(${pkgs.mdadm}/bin/mdadm --detail "$md_device" 2>/dev/null || continue)
|
||||||
|
|
||||||
|
# Parse state
|
||||||
|
state=$(echo "$mdadm_output" | grep "State :" | sed 's/.*State : //' | tr -d ' ')
|
||||||
|
if [[ "$state" == *clean* ]] || [[ "$state" == *active* ]]; then
|
||||||
|
state_value=1
|
||||||
|
else
|
||||||
|
state_value=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse device counts
|
||||||
|
total_devices=$(echo "$mdadm_output" | grep "Raid Devices" | awk '{print $4}')
|
||||||
|
active_devices=$(echo "$mdadm_output" | grep "Active Devices" | awk '{print $4}')
|
||||||
|
|
||||||
|
echo "mdadm_array_state{array=\"$array_name\",state=\"$state\"} $state_value" >> "$TEMP_FILE"
|
||||||
|
[[ -n "$total_devices" ]] && echo "mdadm_array_devices_total{array=\"$array_name\"} $total_devices" >> "$TEMP_FILE"
|
||||||
|
[[ -n "$active_devices" ]] && echo "mdadm_array_devices_active{array=\"$array_name\"} $active_devices" >> "$TEMP_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Atomically replace the metrics file
|
||||||
|
mv "$TEMP_FILE" "$METRICS_FILE"
|
||||||
|
|
||||||
|
echo "Disk metrics collection complete"
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# Required packages
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
smartmontools
|
||||||
|
hdparm
|
||||||
|
mdadm
|
||||||
|
];
|
||||||
|
|
||||||
|
# Node exporter with textfile collector
|
||||||
|
services.prometheus.exporters.node = {
|
||||||
|
enable = true;
|
||||||
|
enabledCollectors = [
|
||||||
|
"textfile"
|
||||||
|
];
|
||||||
|
extraFlags = [
|
||||||
|
"--collector.textfile.directory=${textfileDir}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Systemd service to collect metrics
|
||||||
|
systemd.services.disk-metrics = {
|
||||||
|
description = "Collect S.M.A.R.T. and RAID metrics";
|
||||||
|
path = with pkgs; [ coreutils gawk gnugrep gnused ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = "${collectMetricsScript}";
|
||||||
|
# Run as root to access disk devices
|
||||||
|
User = "root";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Timer to run every 20 minutes (5min buffer for 15min spindown)
|
||||||
|
systemd.timers.disk-metrics = {
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "*:0/20"; # Every 20 minutes
|
||||||
|
RandomizedDelaySec = "1min";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Ensure textfile directory exists and is persisted
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${textfileDir} 0755 root root -"
|
||||||
|
];
|
||||||
|
}
|
||||||
186
hosts/nas/modules/filebot-process.nix
Normal file
186
hosts/nas/modules/filebot-process.nix
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
{ pkgs }:
|
||||||
|
|
||||||
|
pkgs.writeShellScriptBin "filebot-process" ''
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# FileBot AMC script for automated media organization
|
||||||
|
# Called by PyLoad's package_extracted hook with parameters:
|
||||||
|
# $1 = package_id
|
||||||
|
# $2 = package_name
|
||||||
|
# $3 = download_folder (actual path to extracted files)
|
||||||
|
# $4 = password (optional)
|
||||||
|
|
||||||
|
PACKAGE_ID="''${1:-}"
|
||||||
|
PACKAGE_NAME="''${2:-unknown}"
|
||||||
|
DOWNLOAD_DIR="''${3:-/var/lib/downloads}"
|
||||||
|
PASSWORD="''${4:-}"
|
||||||
|
|
||||||
|
OUTPUT_DIR="/var/lib/multimedia"
|
||||||
|
LOG_FILE="/var/lib/pyload/filebot-amc.log"
|
||||||
|
EXCLUDE_LIST="/var/lib/pyload/filebot-exclude-list.txt"
|
||||||
|
PASSWORD_FILE="/var/lib/pyload/extraction-passwords.txt"
|
||||||
|
|
||||||
|
# Ensure FileBot data directory exists
|
||||||
|
mkdir -p /var/lib/pyload/.local/share/filebot/data
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
touch "$EXCLUDE_LIST"
|
||||||
|
|
||||||
|
# Install FileBot license if not already installed
|
||||||
|
if [ ! -f /var/lib/pyload/.local/share/filebot/data/.license ]; then
|
||||||
|
echo "$(date): Installing FileBot license..." >> "$LOG_FILE"
|
||||||
|
${pkgs.filebot}/bin/filebot --license /var/lib/pyload/filebot-license.psm || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===========================================" >> "$LOG_FILE"
|
||||||
|
echo "$(date): PyLoad package hook triggered" >> "$LOG_FILE"
|
||||||
|
echo "Package ID: $PACKAGE_ID" >> "$LOG_FILE"
|
||||||
|
echo "Package Name: $PACKAGE_NAME" >> "$LOG_FILE"
|
||||||
|
echo "Download Directory: $DOWNLOAD_DIR" >> "$LOG_FILE"
|
||||||
|
echo "===========================================" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Check if download directory exists
|
||||||
|
if [ ! -d "$DOWNLOAD_DIR" ]; then
|
||||||
|
echo "$(date): Download directory does not exist: $DOWNLOAD_DIR" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Archive Extraction ---
|
||||||
|
|
||||||
|
# Try extraction with passwords, then without
|
||||||
|
try_extract() {
|
||||||
|
local archive="$1"
|
||||||
|
local outdir
|
||||||
|
outdir="$(dirname "$archive")"
|
||||||
|
|
||||||
|
# Try each password from file
|
||||||
|
if [ -f "$PASSWORD_FILE" ]; then
|
||||||
|
while IFS= read -r pass || [ -n "$pass" ]; do
|
||||||
|
[ -z "$pass" ] && continue
|
||||||
|
case "$archive" in
|
||||||
|
*.rar) ${pkgs.unrar}/bin/unrar x -p"$pass" -o+ "$archive" "$outdir/" >/dev/null 2>&1 && return 0 ;;
|
||||||
|
*.7z) ${pkgs.p7zip}/bin/7z x -p"$pass" -aoa -o"$outdir" "$archive" >/dev/null 2>&1 && return 0 ;;
|
||||||
|
*.zip) ${pkgs.p7zip}/bin/7z x -p"$pass" -aoa -o"$outdir" "$archive" >/dev/null 2>&1 && return 0 ;;
|
||||||
|
esac
|
||||||
|
done < "$PASSWORD_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try without password
|
||||||
|
case "$archive" in
|
||||||
|
*.rar) ${pkgs.unrar}/bin/unrar x -o+ "$archive" "$outdir/" >/dev/null 2>&1 && return 0 ;;
|
||||||
|
*.7z) ${pkgs.p7zip}/bin/7z x -aoa -o"$outdir" "$archive" >/dev/null 2>&1 && return 0 ;;
|
||||||
|
*.zip) ${pkgs.p7zip}/bin/7z x -aoa -o"$outdir" "$archive" >/dev/null 2>&1 && return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Delete all parts of a split RAR archive
|
||||||
|
delete_rar_parts() {
|
||||||
|
local first_part="$1"
|
||||||
|
local base dir
|
||||||
|
|
||||||
|
dir="$(dirname "$first_part")"
|
||||||
|
# Extract base name: "foo.part1.rar" -> "foo", "foo.part01.rar" -> "foo"
|
||||||
|
base="$(basename "$first_part" | ${pkgs.gnused}/bin/sed -E 's/\.part[0-9]+\.rar$//')"
|
||||||
|
|
||||||
|
# Delete all parts matching the pattern
|
||||||
|
find "$dir" -maxdepth 1 -type f -iname "''${base}.part*.rar" -delete
|
||||||
|
echo "$(date): Deleted all parts: ''${base}.part*.rar" >> "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract archives in directory
|
||||||
|
extract_archives() {
|
||||||
|
local dir="$1"
|
||||||
|
local extracted=0
|
||||||
|
|
||||||
|
# 1. Handle split RAR archives (*.part1.rar or *.part01.rar - first part only)
|
||||||
|
while IFS= read -r -d "" archive; do
|
||||||
|
echo "$(date): Extracting split RAR: $archive" >> "$LOG_FILE"
|
||||||
|
if try_extract "$archive"; then
|
||||||
|
echo "$(date): Extraction successful" >> "$LOG_FILE"
|
||||||
|
delete_rar_parts "$archive"
|
||||||
|
extracted=1
|
||||||
|
else
|
||||||
|
echo "$(date): Extraction FAILED: $archive" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
done < <(find "$dir" -type f \( -iname "*.part1.rar" -o -iname "*.part01.rar" \) -print0 2>/dev/null)
|
||||||
|
|
||||||
|
# 2. Handle single RAR files (not part of split archive)
|
||||||
|
while IFS= read -r -d "" archive; do
|
||||||
|
echo "$(date): Extracting RAR: $archive" >> "$LOG_FILE"
|
||||||
|
if try_extract "$archive"; then
|
||||||
|
echo "$(date): Extraction successful, deleting: $archive" >> "$LOG_FILE"
|
||||||
|
rm -f "$archive"
|
||||||
|
extracted=1
|
||||||
|
else
|
||||||
|
echo "$(date): Extraction FAILED: $archive" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
done < <(find "$dir" -type f -iname "*.rar" ! -iname "*.part*.rar" -print0 2>/dev/null)
|
||||||
|
|
||||||
|
# 3. Handle 7z and zip archives
|
||||||
|
while IFS= read -r -d "" archive; do
|
||||||
|
echo "$(date): Extracting: $archive" >> "$LOG_FILE"
|
||||||
|
if try_extract "$archive"; then
|
||||||
|
echo "$(date): Extraction successful, deleting: $archive" >> "$LOG_FILE"
|
||||||
|
rm -f "$archive"
|
||||||
|
extracted=1
|
||||||
|
else
|
||||||
|
echo "$(date): Extraction FAILED: $archive" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
done < <(find "$dir" -type f \( -iname "*.7z" -o -iname "*.zip" \) -print0 2>/dev/null)
|
||||||
|
|
||||||
|
[ "$extracted" -eq 1 ] && return 0 || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run extraction (loop to handle nested archives)
|
||||||
|
echo "$(date): Starting archive extraction..." >> "$LOG_FILE"
|
||||||
|
for i in 1 2 3; do
|
||||||
|
extract_archives "$DOWNLOAD_DIR" || break
|
||||||
|
done
|
||||||
|
echo "$(date): Archive extraction complete" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# --- Media Processing ---
|
||||||
|
|
||||||
|
# Check if there are any video/media files to process
|
||||||
|
if ! find "$DOWNLOAD_DIR" -type f \( -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.m4v" -o -iname "*.mov" \) -print -quit | grep -q .; then
|
||||||
|
echo "$(date): No media files found in: $DOWNLOAD_DIR" >> "$LOG_FILE"
|
||||||
|
echo "$(date): Skipping FileBot processing" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date): Starting FileBot processing" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Run FileBot AMC script
|
||||||
|
set +e # Temporarily disable exit on error to capture exit code
|
||||||
|
${pkgs.filebot}/bin/filebot \
|
||||||
|
-script fn:amc \
|
||||||
|
--output "$OUTPUT_DIR" \
|
||||||
|
--action move \
|
||||||
|
--conflict auto \
|
||||||
|
-non-strict \
|
||||||
|
--log-file "$LOG_FILE" \
|
||||||
|
--def \
|
||||||
|
excludeList="$EXCLUDE_LIST" \
|
||||||
|
movieFormat="$OUTPUT_DIR/movies/{n} ({y})/{n} ({y}) - {vf}" \
|
||||||
|
seriesFormat="$OUTPUT_DIR/tv-shows/{n}/Season {s.pad(2)}/{n} - {s00e00} - {t}" \
|
||||||
|
ut_dir="$DOWNLOAD_DIR" \
|
||||||
|
ut_kind=multi \
|
||||||
|
clean=y \
|
||||||
|
skipExtract=y
|
||||||
|
|
||||||
|
FILEBOT_EXIT_CODE=$?
|
||||||
|
set -e # Re-enable exit on error
|
||||||
|
|
||||||
|
if [ $FILEBOT_EXIT_CODE -ne 0 ]; then
|
||||||
|
echo "$(date): FileBot processing failed with exit code $FILEBOT_EXIT_CODE" >> "$LOG_FILE"
|
||||||
|
exit 0 # Don't fail the hook even if FileBot fails
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date): FileBot processing completed successfully" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Clean up any remaining empty directories
|
||||||
|
find "$DOWNLOAD_DIR" -type d -empty -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "$(date): All processing completed" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
''
|
||||||
51
hosts/nas/modules/jellyfin.nix
Normal file
51
hosts/nas/modules/jellyfin.nix
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{ lib, pkgs, ... }: {
|
||||||
|
# Intel graphics support for hardware transcoding
|
||||||
|
hardware.graphics = {
|
||||||
|
enable = true;
|
||||||
|
extraPackages = with pkgs; [
|
||||||
|
intel-media-driver
|
||||||
|
vpl-gpu-rt
|
||||||
|
intel-compute-runtime
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Set VA-API driver to iHD (modern Intel driver)
|
||||||
|
environment.sessionVariables = {
|
||||||
|
LIBVA_DRIVER_NAME = "iHD";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Jellyfin user with render/video groups for GPU access
|
||||||
|
users.users.jellyfin = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "jellyfin";
|
||||||
|
home = "/var/lib/jellyfin";
|
||||||
|
createHome = true;
|
||||||
|
extraGroups = [ "render" "video" ];
|
||||||
|
};
|
||||||
|
users.groups.jellyfin = {};
|
||||||
|
|
||||||
|
# Create jellyfin directory
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/jellyfin 0755 jellyfin jellyfin - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.jellyfin = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Override systemd hardening for GPU access
|
||||||
|
systemd.services.jellyfin = {
|
||||||
|
serviceConfig = {
|
||||||
|
PrivateUsers = lib.mkForce false; # Disable user namespacing - breaks GPU device access
|
||||||
|
DeviceAllow = [
|
||||||
|
"/dev/dri/card0 rw"
|
||||||
|
"/dev/dri/renderD128 rw"
|
||||||
|
];
|
||||||
|
SupplementaryGroups = [ "render" "video" ]; # Critical: Explicit group membership for GPU access
|
||||||
|
};
|
||||||
|
environment = {
|
||||||
|
LIBVA_DRIVER_NAME = "iHD"; # Ensure service sees this variable
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
24
hosts/nas/modules/power-management.nix
Normal file
24
hosts/nas/modules/power-management.nix
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Power management for NAS
|
||||||
|
# - CPU powersave governor (scales up on demand for transcoding)
|
||||||
|
# - Disk spindown after 15 minutes idle
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# CPU Power Management - powersave scales up on demand for transcoding
|
||||||
|
powerManagement.cpuFreqGovernor = "powersave";
|
||||||
|
|
||||||
|
# Disk spindown - hdparm for Seagate 18TB drives
|
||||||
|
environment.systemPackages = [ pkgs.hdparm ];
|
||||||
|
|
||||||
|
services.udev.extraRules = ''
|
||||||
|
# Seagate 18TB NAS drives - APM 127 allows spindown, -S 180 = 15 min
|
||||||
|
ACTION=="add", KERNEL=="sd[a-z]", SUBSYSTEM=="block", \
|
||||||
|
ATTRS{model}=="ST18000NM000J*", \
|
||||||
|
RUN+="${pkgs.hdparm}/bin/hdparm -B 127 -S 180 /dev/%k"
|
||||||
|
|
||||||
|
# Toshiba 20TB NAS drives - same settings
|
||||||
|
ACTION=="add", KERNEL=="sd[a-z]", SUBSYSTEM=="block", \
|
||||||
|
ATTRS{model}=="TOSHIBA MG10ACA2*", \
|
||||||
|
RUN+="${pkgs.hdparm}/bin/hdparm -B 127 -S 180 /dev/%k"
|
||||||
|
'';
|
||||||
|
}
|
||||||
120
hosts/nas/modules/pyload.nix
Normal file
120
hosts/nas/modules/pyload.nix
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
filebotScript = pkgs.callPackage ./filebot-process.nix {};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
|
||||||
|
"unrar"
|
||||||
|
"filebot"
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
unrar # Required for RAR archive extraction
|
||||||
|
p7zip # Required for 7z and other archive formats
|
||||||
|
];
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/downloads 0755 pyload pyload - -"
|
||||||
|
"d /var/lib/multimedia 0775 root jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/movies 0775 jellyfin jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/tv-shows 0775 jellyfin jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/music 0755 jellyfin jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/audiobooks 0775 jellyfin jellyfin - -"
|
||||||
|
|
||||||
|
# PyLoad hook scripts directory (package_finished triggers after download completes)
|
||||||
|
"d /var/lib/pyload/config 0755 pyload pyload - -"
|
||||||
|
"d /var/lib/pyload/config/scripts 0755 pyload pyload - -"
|
||||||
|
"d /var/lib/pyload/config/scripts/package_finished 0755 pyload pyload - -"
|
||||||
|
"L+ /var/lib/pyload/config/scripts/package_finished/filebot-process.sh - - - - ${filebotScript}/bin/filebot-process"
|
||||||
|
];
|
||||||
|
|
||||||
|
# FileBot license secret (only if secrets.yaml exists)
|
||||||
|
sops.secrets.filebot-license = {
|
||||||
|
mode = "0440";
|
||||||
|
owner = "pyload";
|
||||||
|
group = "pyload";
|
||||||
|
path = "/var/lib/pyload/filebot-license.psm";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Extraction passwords for filebot-process script (one password per line)
|
||||||
|
sops.secrets.pyload-extraction-passwords = {
|
||||||
|
mode = "0440";
|
||||||
|
owner = "pyload";
|
||||||
|
group = "pyload";
|
||||||
|
path = "/var/lib/pyload/extraction-passwords.txt";
|
||||||
|
};
|
||||||
|
|
||||||
|
# PyLoad user with jellyfin group membership for multimedia access
|
||||||
|
users.users.pyload = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "pyload";
|
||||||
|
home = "/var/lib/pyload";
|
||||||
|
createHome = true;
|
||||||
|
extraGroups = [ "jellyfin" ];
|
||||||
|
};
|
||||||
|
users.groups.pyload = {};
|
||||||
|
|
||||||
|
services.pyload = {
|
||||||
|
enable = true;
|
||||||
|
downloadDirectory = "/var/lib/downloads";
|
||||||
|
listenAddress = "0.0.0.0";
|
||||||
|
port = 8000;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Configure pyload service
|
||||||
|
systemd.services.pyload = {
|
||||||
|
# Add extraction tools to service PATH
|
||||||
|
path = with pkgs; [
|
||||||
|
unrar # For RAR extraction
|
||||||
|
p7zip # For 7z extraction
|
||||||
|
];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
# Disable SSL certificate verification
|
||||||
|
PYLOAD__GENERAL__SSL_VERIFY = "0";
|
||||||
|
|
||||||
|
# Download speed limiting (150 Mbit/s = 19200 KiB/s)
|
||||||
|
PYLOAD__DOWNLOAD__LIMIT_SPEED = "1";
|
||||||
|
PYLOAD__DOWNLOAD__MAX_SPEED = "19200";
|
||||||
|
|
||||||
|
# Disable ExtractArchive plugin (extraction handled by filebot-process script)
|
||||||
|
PYLOAD__EXTRACTARCHIVE__ENABLED = "0";
|
||||||
|
|
||||||
|
# Enable ExternalScripts plugin for hooks
|
||||||
|
PYLOAD__EXTERNALSCRIPTS__ENABLED = "1";
|
||||||
|
PYLOAD__EXTERNALSCRIPTS__UNLOCK = "1"; # Run hooks asynchronously
|
||||||
|
|
||||||
|
# DdownloadCom plugin: don't fall back to free if premium fails
|
||||||
|
PYLOAD__DDOWNLOADCOM__FALLBACK = "0";
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
# Bind-mount DNS configuration files into the sandboxed service
|
||||||
|
BindReadOnlyPaths = [
|
||||||
|
"/etc/resolv.conf"
|
||||||
|
"/etc/nsswitch.conf"
|
||||||
|
"/etc/hosts"
|
||||||
|
"/etc/ssl"
|
||||||
|
"/etc/static/ssl"
|
||||||
|
"/run/secrets" # SOPS secrets access for FileBot license
|
||||||
|
];
|
||||||
|
# Bind mount multimedia directory as writable for FileBot hook scripts
|
||||||
|
BindPaths = [ "/var/lib/multimedia" ];
|
||||||
|
|
||||||
|
# Override SystemCallFilter to allow @resources syscalls
|
||||||
|
# FileBot (Java) needs resource management syscalls like setpriority
|
||||||
|
# during cleanup operations. Still block privileged syscalls for security.
|
||||||
|
SystemCallFilter = lib.mkForce [
|
||||||
|
"@system-service"
|
||||||
|
"@resources" # Explicitly allow resource management syscalls
|
||||||
|
"~@privileged" # Still block privileged operations
|
||||||
|
"fchown" # Re-allow fchown for FileBot file operations
|
||||||
|
"fchown32" # 32-bit compatibility
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Open firewall for PyLoad web interface
|
||||||
|
networking.firewall.allowedTCPPorts = [ 8000 ];
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user