Compare commits

...

204 Commits

Author SHA1 Message Date
10fb8e88ce fix: change click and load package name 2025-12-10 13:36:33 +01:00
5f300d9e7b feat: nb initial click and load 2025-12-10 12:49:30 +01:00
99ac2ea3b0 feat: nas move archive extraction to filebot script 2025-12-10 11:40:19 +01:00
2caa36c0ab fix: nas filebot and add extraction passwords 2025-12-10 11:29:05 +01:00
55c15c790d fix: fw lightning 2025-12-10 11:28:08 +01:00
450d9d6457 fix: claude.md update 2025-12-07 12:59:43 +01:00
e08bf42eaa fix: nas leds and disks 2025-12-07 12:59:27 +01:00
8e0e5c0d16 feat: add disks to monitoring 2025-12-05 21:57:58 +01:00
ada9db7942 feat: add disks to nas 2025-12-04 15:01:47 +01:00
5995612407 fix: amzebs add mysql port 2025-12-04 12:46:28 +01:00
5762916970 feat: add mcp server 2025-12-04 11:39:17 +01:00
dd456eab69 feat: update pyload 2025-12-04 11:39:08 +01:00
18a8fde66e feat: add uv for mcp 2025-12-04 11:38:22 +01:00
f97c9185c1 docs: update claude 2025-12-02 11:28:05 +01:00
8bf4b185a1 feat(nas): update to 25.11, add software, add storage plan 2025-12-02 11:27:52 +01:00
8424d771f6 feat(fw): update to 25.11 2025-12-02 08:34:57 +01:00
840f99a7e9 feat(amzebs-01): update to 25.11 2025-12-01 23:02:35 +01:00
1b27bafd41 feat(web-arm): update to 25.11
- Migrate logind.extraConfig to logind.settings.Login
- Update dovecot alert for service rename (dovecot2 → dovecot)
- Fix sa-core buildGoModule env attribute for CGO_ENABLED
2025-12-01 22:48:02 +01:00
4770d671c0 docs: add commit footer convention to CLAUDE.md 2025-12-01 22:25:08 +01:00
28a7bed3b9 feat(mail): update to 25.11 with TLS hardening
- Upgrade NixOS channel from 25.05 to 25.11
- Fix dovecot systemd service rename (dovecot2 -> dovecot)
- Convert postfix numeric settings to integers (25.11 requirement)
- Remove insecure 512-bit DH params, fix 2048-bit DH params
- Update postfix ciphers to modern ECDHE/DHE+AESGCM/CHACHA20
- Require TLS 1.2 minimum for OpenLDAP
- Remove weak ciphers (3DES, RC4, aNULL) from OpenLDAP
2025-12-01 22:24:57 +01:00
170becceb0 fix: nvim 2025-12-01 22:05:24 +01:00
6e8f530537 feat: amz add cron job 2025-12-01 16:17:45 +01:00
209bafd70f feat: test-configuration script get real errors 2025-12-01 16:17:28 +01:00
1d182437db feat: nb update to 25.11 2025-12-01 16:17:10 +01:00
6c046a549e feat: change pushover emergency on alerts 2025-12-01 13:29:37 +01:00
0a30a2ac23 fix: ai-mailer restart on secret change 2025-12-01 13:29:26 +01:00
82c15e8d26 feat: pyload config change, cyberghost change 2025-11-30 19:53:13 +01:00
f277d089bd feat: add cyberghost module 2025-11-30 19:29:33 +01:00
7ed345b8e8 feat: add pyload extraction passwords 2025-11-30 19:29:24 +01:00
bd6b15b617 changes 2025-11-29 22:42:09 +01:00
3282b7d634 fix: monitoring 2025-11-29 22:42:00 +01:00
21ed381d18 fix: pyload 2025-11-29 22:41:48 +01:00
4500f41983 feat: add ugreen nas leds 2025-11-29 13:12:18 +01:00
1d30eeb939 feat: update victoriametrics secret 2025-11-28 23:51:33 +01:00
537f144885 feat: add smart alerting and noatime to disks 2025-11-28 23:50:24 +01:00
dbada3c509 feat: change harddisk nas 2025-11-28 21:17:07 +01:00
c8be707420 fix: nas handling 2025-11-28 20:54:12 +01:00
fdba2c75c7 feat: add nas host 2025-11-28 20:53:47 +01:00
55d600c0c0 feat: add nas to fleet 2025-11-28 18:49:56 +01:00
71c5bd5e6c change claude 2025-11-28 01:57:30 +01:00
998f04713f fix(nb): additional sops.lua bug fixes
- Use nvim_buf_get_name(args.buf) instead of vim.fn.expand("%:p") in BufReadPost
- Add timeout to encrypt operation to prevent infinite hangs
- Add microsecond precision to temp file names to prevent collisions
- Strip trailing newline before vim.split to avoid extra empty lines
- Add trailing newline when writing temp file for POSIX compliance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 01:54:20 +01:00
6935fbea8b fix: sops implementation 2025-11-28 01:32:23 +01:00
301e090251 feat: add Claude.md 2025-11-28 01:00:55 +01:00
58d8ef050c feat: install own claude-code 2025-11-27 22:10:05 +01:00
41cb2ec791 fix: update script for claude-code 2025-11-27 22:09:48 +01:00
1faec5b2d1 feat: add updated claude code 2025-11-27 21:49:24 +01:00
111b8cec97 feat: change rustdesk for epicenter 2025-11-27 21:49:11 +01:00
3aaebdb1c4 fix: filebot 2025-11-27 12:50:00 +01:00
3e7b8c93e3 feat: split pyload 2025-11-26 22:39:07 +01:00
3e2f46377e fix: pyload and filebot 2025-11-26 21:08:58 +01:00
38bead3dc8 fix: pyload 2025-11-26 12:48:24 +01:00
351d36b217 fix: changes to pyload 2025-11-26 00:26:21 +01:00
59a37c9b46 feat: add jellyfin and hardware acceleration for transcoding 2025-11-25 19:48:52 +01:00
d7d3722ce7 fix: pyload 2025-11-25 17:03:03 +01:00
6475524d23 fix: amz postfix setup 2025-11-23 11:29:07 +01:00
1a70ca9564 feat: add pyload 2025-11-23 11:28:57 +01:00
d6f206f0bb feat: add email 2025-11-21 14:00:47 +01:00
b3c5366f31 feat: nb change building speed 2025-11-19 00:00:58 +01:00
fab06ca4d5 feat: change livingroom to hue 2025-11-19 00:00:43 +01:00
bd1d04943d fix: change deconz 2025-11-19 00:00:34 +01:00
8305d1b0c5 fix: nb chromium 2025-11-18 22:31:07 +01:00
2d812c03eb feat: ai-mailer add faq page 2025-11-18 22:30:54 +01:00
156e63fd6c feat: amz changes 2025-11-18 22:30:41 +01:00
a912c4dc55 feat: amz enable all hosts 2025-11-15 21:56:40 +01:00
8a2a68a91c feat: add alerting for amz ebs server and websites blackbox 2025-11-14 23:08:27 +01:00
01d3ab1357 feat: amzebs-01 update deployment key 2025-11-14 22:08:56 +01:00
20c5af7a69 feat: add amzebs-01 host 2025-11-14 20:06:01 +01:00
865311bf49 feat: initial amzebs config 2025-11-14 09:30:19 +01:00
9fab06795a update esphome readme 2025-11-13 19:32:49 +01:00
038fb7ae76 feat: update ai-mailer 2025-11-13 11:53:14 +01:00
3775e0dd7b feat: add ai-image-alt 2025-11-12 22:50:09 +01:00
66a5d69846 feat: add php 2025-11-12 22:48:54 +01:00
8747f887f8 fix: invidious 2025-11-12 22:48:48 +01:00
39f4460e0a feat: upgrade foundry to v13 2025-11-12 22:48:31 +01:00
6f8626ca8a feat: update ai-mailer 2025-11-12 14:30:35 +01:00
04c08bf419 fix: invidious 2025-11-03 14:43:28 +01:00
709a24366a fix: piped 2025-11-03 12:12:14 +01:00
63dad8c626 fix: invidious password 2025-11-03 01:38:16 +01:00
b57342f53e feat: add invidious 2025-11-03 00:59:18 +01:00
7cefa3a650 add bghelper to piped 2025-11-02 20:36:20 +01:00
5d54ae898e fix: piped cors header 2025-11-02 15:27:35 +01:00
794d5c2dad feat: move piped to fw host 2025-11-02 14:34:30 +01:00
04cdf1bd2f feat: remove korean-skin.care site 2025-11-02 13:42:54 +01:00
b4c4e31437 feat: backup web-02 databases 2025-11-02 12:30:35 +01:00
56bb321e4a fix: n8n 2025-11-02 10:46:50 +01:00
c0d868088e fix: n8n 2025-11-02 10:46:36 +01:00
df5c89f071 feat: add n8n 2025-11-02 00:29:43 +01:00
b73bc3e80a feat: initial n8n config 2025-11-01 23:44:03 +01:00
db25b2bfbb feat: add cleanup for grafana alerting rules 2025-11-01 11:09:05 +01:00
819bfc1531 feat: adjusted the gitea runner dockerfile to include chrome for puppeteer 2025-11-01 09:43:27 +01:00
cfdb8d8474 fix: nvim sops error 2025-10-28 16:31:20 +01:00
d50ed9858c fix: nvim sops, remove lsp 2025-10-28 14:10:04 +01:00
7af4b6a5d1 feat: web stack make php optional 2025-10-27 16:38:12 +01:00
ca04f5d8c3 feat: nb add ownstash api project 2025-10-27 16:37:54 +01:00
a02cefc62a feat: make cloonar website use the web stack module 2025-10-23 19:27:17 +02:00
28974e9688 feat: gitea runner open cache port 2025-10-23 03:06:36 +02:00
aaf5f79895 feat: fw remove unstable packages 2025-10-23 02:53:58 +02:00
eccac4d4a2 feat: gitea runner cache config 2025-10-23 02:30:09 +02:00
399f67ba25 feat: fw speed up builds 2025-10-23 02:30:00 +02:00
439a580dfe feat: fw update gitea to use a docker image with puppeteer, webp and avif deps 2025-10-23 02:15:34 +02:00
bfae290927 feat(web-arm): add AVIF image support to cloonar.dev
Implement AVIF image content negotiation with WebP fallback for
cloonar.dev website. Browser will receive AVIF if supported and
available, otherwise WebP, falling back to original JPEG/PNG.

- Add AVIF-first content negotiation in image location block
- Maintain existing WebP fallback logic
- Include .avif in long-term cache headers (365d)
- Add Vary: Accept header for proper CDN/browser caching

AVIF files should be placed at /avif/$request_uri.avif to be served.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 23:55:08 +02:00
1eeb0b7102 fix ssh key for website 2025-10-22 23:49:16 +02:00
f49ac19af1 fix identityfile for host 2025-10-22 23:49:03 +02:00
27c85ff9d0 fix: nvim sops double save issue 2025-10-22 19:19:40 +02:00
b6d44b5a20 fix: nvim sops lua 2025-10-22 19:14:36 +02:00
b8b7574536 fix: nb nvim auto open claude 2025-10-22 15:25:39 +02:00
5ee2cb2b56 feat: nb update signal desktop files 2025-10-22 15:25:19 +02:00
6c88bc1790 feat: update readme to new nixos-infect channel 2025-10-22 15:24:34 +02:00
6be832b012 feat: nb add ssh config for whoidentifies.me 2025-10-22 14:30:35 +02:00
fc9ef6b9ff feat: nb add sway launcher cleanup 2025-10-22 14:29:53 +02:00
ec19103a81 fix: nb change open mapping for the terminal 2025-10-22 13:23:50 +02:00
7499a21cbd feat: nb add wim-api project 2025-10-22 12:31:53 +02:00
5758b3a320 fix(nvim): enable yaml syntax highlighting for sops files
Add BufReadPre autocmd to set filetype=yaml before buffer is loaded,
ensuring syntax highlighting works immediately when opening encrypted
sops files. Also updated BufReadPost to unconditionally set yaml
filetype after decryption.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 12:29:10 +02:00
7d2f818fca feat: nb add env variable for sops 2025-10-22 11:58:58 +02:00
19d0946e06 feat: add sops to vim 2025-10-22 09:52:40 +02:00
7d5294e7b9 fix: hibernate resume for nb 2025-10-19 18:14:40 +02:00
28ed3fcf74 fix(nb): resolve keyboard/touchpad and btrfs read-only issues after suspend
This commit addresses two critical suspend/resume issues on the nb host:

1. Keyboard and touchpad not working after suspend
   - Added i2c_hid_acpi kernel module
   - Created systemd service to reload the module after resume
   - Excluded input devices from TLP USB autosuspend

2. /nix/persist becoming read-only after suspend
   - Moved swap from /nix/persist to dedicated @swap subvolume
   - Added systemd service to remount /nix/persist if needed
   - Separated swap from persistent data to prevent btrfs corruption

Changes:
- Created hosts/nb/modules/suspend-fixes.nix with resume hooks
- Updated swap path from /nix/persist/swapfile to /swap/swapfile
- Added /swap filesystem mount for @swap btrfs subvolume
- Added USB_EXCLUDE_INPUT=1 to TLP configuration

Note: Manual step required before deployment - create @swap subvolume.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 16:43:02 +02:00
5a35cd04a6 feat: nvim update terminal keybindings 2025-10-19 16:03:29 +02:00
5648224062 fix(nb): force signal-desktop to use X11 with --ozone-platform=x11
Electron 38 has built-in Wayland auto-detection. Even without
ELECTRON_OZONE_PLATFORM_HINT, it detects WAYLAND_DISPLAY and tries
to use Wayland, triggering the empty window bug in Signal Desktop.

Explicitly force X11/XWayland mode with --ozone-platform=x11 flag
to prevent auto-detection and fix the empty window issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 15:56:24 +02:00
bbb9cacd71 fix(nb): remove wayland flags from signal-desktop to fix empty window
Signal Desktop has a known Electron bug where the window never appears
when using Wayland Ozone platform flags. The ready-to-show event doesn't
fire properly on Wayland. Running in XWayland mode resolves this issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 15:28:42 +02:00
40743442e9 fix: use autoupgrade only on AC 2025-10-19 13:31:31 +02:00
7564c5d740 fix: nb change signal flags 2025-10-19 13:25:48 +02:00
3a6d5bb8c4 fix: nb add /boot mount 2025-10-19 12:17:04 +02:00
f256ca7fad feat: nvim add another terminal for ai agents 2025-10-19 12:03:57 +02:00
cb18e436ca feat: nb battery improvement 2025-10-19 12:01:45 +02:00
019b1166ec fix: nb right repo for soundscape 2025-10-18 22:14:13 +02:00
cc15f27205 feat: nb performance tweaks 2025-10-16 21:48:21 +02:00
356c049aaf feat: nb performance tweaks 2025-10-15 11:39:29 +02:00
df6465fa8a feat: nb performance tweaks 2025-10-15 11:25:46 +02:00
a05c33ad87 fix: nb disable attic cache 2025-10-15 10:09:39 +02:00
67906cbf16 fix: attic cache 2025-10-15 10:09:26 +02:00
09e381ecc4 feat: add attic cache 2025-10-14 22:30:20 +02:00
7fd35b79c4 fix: blacklist attic website exporter 2025-10-14 22:29:44 +02:00
c9900e4314 fix: atticd server 2025-10-14 22:24:35 +02:00
5ea3bac570 feat: add update-keys script 2025-10-14 20:02:42 +02:00
eae7bb0e09 feat: web-arm add atticd 2025-10-14 20:01:45 +02:00
465daec0ab feat: change authelia 2025-10-14 19:54:45 +02:00
f516f46b06 feat: update secrets 2025-10-14 19:54:24 +02:00
742d0172cf feat: web-arm install atticd 2025-10-14 19:14:46 +02:00
e0568ddfdc fix: chromium extension installation 2025-10-14 14:14:04 +02:00
9941dfa61f feat: add adb 2025-10-14 14:13:47 +02:00
aac9e9f38f fix: fivefilters https 2025-10-14 14:13:37 +02:00
bdda87778c feat: add android studio 2025-10-13 13:23:51 +02:00
fccec6d87c fix: chrome dev tools mcp 2025-10-13 13:23:37 +02:00
5e259e0b42 feat: add fivefilters 2025-10-13 13:23:13 +02:00
496c483050 feat: web-arm cloonar.dev new key 2025-10-11 21:52:42 +02:00
1433f88d53 feat: nb changes for claude code 2025-10-11 21:52:32 +02:00
506c4f9357 fix: nb flatpak installation 2025-10-10 13:18:31 +02:00
a4ed475237 feat: nb add flatpak iptv package 2025-10-10 12:23:51 +02:00
de43e917c5 feat: nb dominik add project and clean up projects 2025-10-10 10:01:58 +02:00
be515979cf feat: nb add claude code 2025-10-10 10:01:42 +02:00
cc03069d57 feat: add nssTools 2025-10-08 22:19:51 +02:00
fe7aaadf64 feat: add AGENTS.md 2025-10-08 22:14:09 +02:00
0b6549a359 fix: nb codex-cli wrapper 2025-10-08 22:05:19 +02:00
af60555eea update secrets 2025-10-08 21:48:34 +02:00
64334192de fix: nb update hashes for browser extensions 2025-10-08 19:37:27 +02:00
4751fb5582 fix: change to btrfs and fix an error 2025-10-08 19:37:10 +02:00
34e56a13ea Update fleet.nix 2025-10-08 13:53:13 +02:00
c3f2603702 feat: nb add scana11y address and change firefox config 2025-10-02 19:46:19 +02:00
15f6b2edd0 feat: nb change sway config to nautilus 2025-10-02 19:45:58 +02:00
6339b733c4 feat: nb update coding config 2025-10-02 19:45:37 +02:00
305ce21e41 feat: add modularity to scana11y 2025-10-02 19:45:08 +02:00
8ab1c91b38 feat: scana11y changes 2025-09-29 15:59:48 +02:00
bf5c7a74cb feat: esphome updates 2025-09-29 15:59:12 +02:00
b48ec98cb3 feat: web-arm change to docker and install scana11y 2025-09-09 17:55:43 +02:00
58089e558e feat: auto restart foundry-vtt 2025-09-09 10:39:45 +02:00
97b6874258 feat: nb add scana11y repo 2025-09-09 10:39:20 +02:00
8ad0c4d336 feat: web change site handling, add php to scana11y, add ssh deploy key for gitea 2025-09-09 10:39:00 +02:00
536fc2b463 feat: change dovecot2 sieve 2025-09-08 17:15:37 +02:00
b7287b0d51 feat: change gpd win 4 wireguard 2025-09-08 17:15:20 +02:00
a0ffb52f98 feat: add foundry vtt to allerting 2025-09-08 17:13:02 +02:00
eb40b7ff06 feat: add webmail to webhost 2025-09-08 17:12:53 +02:00
b3a71cb9bc feat: nb remove old stuff and add cursor 2025-08-12 12:20:28 +02:00
16594b3e7d feat: remove ghetto.at domain 2025-08-12 12:20:06 +02:00
7937e00018 feat: remove and add dovecot domains 2025-08-07 13:07:37 +02:00
0e91e1e7f5 feat: add scana11y to ldap 2025-08-07 12:08:47 +02:00
99b387fe8b feat: install swayimg 2025-08-07 12:08:39 +02:00
fe53ea7551 add nb-new to fleet 2025-08-07 12:08:28 +02:00
541f9b3776 feat: change iso to btrfs 2025-08-07 12:08:19 +02:00
1c9302c773 feat: add scana11y website 2025-08-07 12:08:09 +02:00
ba9ef3913d feat: change iso to btrfs 2025-08-05 19:52:58 +02:00
79b4a615f0 fix: ldap auth 2025-08-05 18:31:16 +02:00
7225a5e787 fix: ldap auth 2025-08-01 23:11:42 +02:00
467ade9340 fix: ldap auth 2025-08-01 22:16:01 +02:00
619136674e feat: updata phpldapadmin, add linuxbind secret 2025-08-01 20:24:40 +02:00
3990566fe5 feat: many changes 2025-08-01 19:48:49 +02:00
7f01dc4cac feat: many changes 2025-07-11 11:19:42 +02:00
da95b2fa71 feat: add dialog-relations.at website 2025-06-25 08:19:32 +02:00
9b628caaef feat: add copilot instructions symlink 2025-06-22 14:26:57 +02:00
4ec924b736 fix: disable not working and not needed mcps 2025-06-22 14:26:07 +02:00
2712bd2197 feat: add new bed switch and a new scene 2025-06-22 14:25:50 +02:00
03d3ff5712 feat: refactor mcp configuration to separate programs and custom servers 2025-06-20 14:37:53 +02:00
6aeb0c9f89 many changes 2025-06-17 16:46:01 +02:00
91394ef68a feat: add support for pgpPublicKey in OpenLDAP configuration 2025-06-08 13:08:22 +02:00
a7d304cc5b feat: add ldap2vcard repository to project history and clone configuration 2025-06-08 12:05:54 +02:00
0b8619bf64 feat: update configuration files to streamline imports and enhance package management 2025-06-08 09:36:44 +02:00
30e75d0ad5 feat: add different rustdesks for configs 2025-06-05 19:29:00 +02:00
a18a0e913d feat: update MCP configuration to include additional permissions for nixos and puppeteer modules 2025-06-05 15:06:18 +02:00
ecf3e03e81 feat: add Ollama and Qdrant service modules to configuration 2025-06-04 16:13:22 +02:00
934471bd88 feat: add MCP global configuration module, manage Brave Search API key, and set up systemd service for deployment 2025-06-04 15:47:05 +02:00
0fff2f87a5 feat: update environment variables for Wayland support, adjust font sizes in Sway and Waybar configurations, and refine Thunderbird settings 2025-06-04 08:07:27 +02:00
e8bf13275e feat: add metrics exporters for Dovecot and Postfix, update Signal execution command, and improve configuration management 2025-06-03 23:06:40 +02:00
436903543b feat: update Signal desktop execution command for hardware acceleration and modify Sway terminal configuration 2025-06-02 21:04:51 +02:00
c47f678220 feat: add Firefox to system packages in desktop configuration 2025-06-02 12:10:14 +02:00
221 changed files with 11123 additions and 1746 deletions

1
.github/copilot-instructions.md vendored Symbolic link
View File

@@ -0,0 +1 @@
../.roo/rules/rules.md

8
.mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"nixos": {
"command": "uvx",
"args": ["mcp-nixos"]
}
}
}

View File

@@ -70,6 +70,7 @@ Bento is utilized for deploying configurations across systems.
* Regularly review and refactor modules for efficiency and clarity.
* Document all modules and configurations for future reference.
* Test configurations in a controlled environment before deploying to production systems.([NixOS & Flakes][6])
* After developing a feature, delete the corresponding development plan.
---

View File

@@ -4,7 +4,7 @@
# for a more complex example.
keys:
- &bitwarden age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 # nixos age key
- &dominik age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
- &dominik age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
- &dominik2 age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
- &git-server age106n5n3rrrss45eqqzz8pq90la3kqdtnw63uw0sfa2mahk5xpe30sxs5x58
- &web-02 age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw
@@ -14,6 +14,9 @@ keys:
- &fw-new age12msc2c6drsaw0yk2hjlaw0q0lyq0emjx5e8rq7qc7ql689k593kqfmhss2
- &netboot age14uarclad0ty5supc8ep09793xrnwkv8a4h9j0fq8d8lc92n2dadqkf64vw
- &gpd-win4 age1ceg548u5ma6rgu3xgvd254y5xefqrdqfqhcjsjp3255q976fgd2qaua53d
- &nb age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
- &amzebs-01 age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
- &nas age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek
creation_rules:
- path_regex: ^[^/]+\.yaml$
@@ -22,12 +25,14 @@ creation_rules:
- *bitwarden
- *dominik
- *dominik2
- *nb
- path_regex: hosts/nb/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- path_regex: hosts/gpd-win4/[^/]+\.yaml$
key_groups:
- age:
@@ -35,12 +40,14 @@ creation_rules:
- *dominik
- *dominik2
- *gpd-win4
- *nb
- path_regex: hosts/fw/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *fw
- path_regex: hosts/fw-new/[^/]+\.yaml$
key_groups:
@@ -48,28 +55,63 @@ creation_rules:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *fw
- *fw-new
- path_regex: hosts/fw-new/modules/web/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *web-02
- path_regex: hosts/web-arm/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *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$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *ldap-server-arm
- path_regex: hosts/fw/modules/web/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *web-02
- path_regex: utils/modules/lego/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *git-server
- *web-02
- *web-arm
@@ -77,31 +119,38 @@ creation_rules:
- *netboot
- *fw
- *fw-new
- path_regex: utils/modules/plausible/[^/]+\.yaml$
- path_regex: utils/modules/attic-cache/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- path_regex: utils/modules/promtail/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *web-arm
- *ldap-server-arm
- *netboot
- *fw
- *fw-new
- *nas
- *amzebs-01
- path_regex: utils/modules/victoriametrics/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *web-arm
- *ldap-server-arm
- *netboot
- *fw
- *fw-new
- *nas
- *amzebs-01

31
AGENTS.md Normal file
View 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
View 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.

View File

@@ -2,7 +2,7 @@
- install ubuntu 20.04
- get age key from SSH
```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'
```
- 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
```console
sftp host.cloonar.com@git.cloonar.com:/config/bootstrap.sh ./
sftp host@git.cloonar.com:/config/bootstrap.sh ./
```
# 5. Yubikey

View File

@@ -6,4 +6,4 @@ https://github.com/tasmota/mgos32-to-tasmota32/releases
In Tasmota make OTA Update to minimal:
http://ota.tasmota.com/tasmota/release/tasmota-minimal.bin.gz
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

View 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

View File

@@ -5,8 +5,6 @@ substitutions:
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
on_boot:
priority: 300
then:
@@ -23,6 +21,9 @@ esphome:
green: 50%
blue: 0%
white: 100%
esp8266:
board: esp01_1m
interval:
- interval: 15s
@@ -37,6 +38,7 @@ interval:
# Enable Home Assistant API
api:
batch_delay: 0ms
ota:
platform: esphome

View File

@@ -5,8 +5,6 @@ substitutions:
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
on_boot:
priority: 300
then:
@@ -23,6 +21,9 @@ esphome:
green: 50%
blue: 0%
white: 100%
esp8266:
board: esp01_1m
interval:
- interval: 15s
@@ -37,6 +38,7 @@ interval:
# Enable Home Assistant API
api:
batch_delay: 0ms
ota:
platform: esphome

View File

@@ -5,8 +5,6 @@ substitutions:
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
on_boot:
priority: 300
then:
@@ -23,6 +21,9 @@ esphome:
green: 50%
blue: 0%
white: 100%
esp8266:
board: esp01_1m
interval:
- interval: 15s
@@ -37,6 +38,7 @@ interval:
# Enable Home Assistant API
api:
batch_delay: 0ms
ota:
platform: esphome

View File

@@ -5,8 +5,6 @@ substitutions:
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
on_boot:
priority: 300
then:
@@ -23,6 +21,9 @@ esphome:
green: 50%
blue: 0%
white: 100%
esp8266:
board: esp01_1m
interval:
- interval: 15s
@@ -37,6 +38,7 @@ interval:
# Enable Home Assistant API
api:
batch_delay: 0ms
ota:
platform: esphome

View File

@@ -5,8 +5,6 @@ substitutions:
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
on_boot:
priority: 300
then:
@@ -23,6 +21,9 @@ esphome:
green: 50%
blue: 0%
white: 100%
esp8266:
board: esp01_1m
interval:
- interval: 15s
@@ -37,6 +38,7 @@ interval:
# Enable Home Assistant API
api:
batch_delay: 0ms
ota:
platform: esphome

View File

@@ -5,8 +5,6 @@ substitutions:
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
on_boot:
priority: 300
then:
@@ -23,6 +21,9 @@ esphome:
green: 50%
blue: 0%
white: 100%
esp8266:
board: esp01_1m
interval:
- interval: 15s
@@ -37,6 +38,7 @@ interval:
# Enable Home Assistant API
api:
batch_delay: 0ms
ota:
platform: esphome

View File

@@ -5,8 +5,6 @@ substitutions:
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
on_boot:
priority: 300
then:
@@ -23,6 +21,9 @@ esphome:
green: 50%
blue: 0%
white: 100%
esp8266:
board: esp01_1m
interval:
- interval: 15s
@@ -37,6 +38,7 @@ interval:
# Enable Home Assistant API
api:
batch_delay: 0ms
ota:
platform: esphome

View File

@@ -5,8 +5,6 @@ substitutions:
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
on_boot:
priority: 300
then:
@@ -23,6 +21,9 @@ esphome:
green: 50%
blue: 0%
white: 100%
esp8266:
board: esp01_1m
interval:
- interval: 15s
@@ -37,6 +38,7 @@ interval:
# Enable Home Assistant API
api:
batch_delay: 0ms
ota:
platform: esphome

View File

@@ -56,13 +56,14 @@ preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
- platform: esphome
wifi:
# 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
@@ -84,7 +85,6 @@ wifi:
ssid: "${name}_AP"
password: "bulb_fallback_pw"
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:
- platform: status

View File

@@ -10,7 +10,6 @@ substitutions:
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.pool.ntp.org"
sntp_server_3: "2.pool.ntp.org"
log_level: "WARN"
esphome:
name: "${name}"
@@ -23,27 +22,27 @@ esphome:
name: "${project_name}"
version: "${project_version}"
on_boot:
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
interval:
- interval: 15s
then:
- if:
condition:
api.connected: # check if api connected
api.connected:
else:
- light.turn_on:
id: rgbww_light
@@ -57,22 +56,35 @@ preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
- platform: esphome
wifi:
domain: .cloonar.smart
fast_connect: False
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: "${dns_domain}"
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
channel: 1
hidden: True
hidden: true
manual_ip:
static_ip: 10.42.100.12
gateway: 10.42.100.1
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:
- platform: status
@@ -90,7 +102,7 @@ sensor:
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: "diagnostic"
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
@@ -98,8 +110,7 @@ sensor:
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: "diagnostic"
device_class: ""
entity_category: diagnostic
output:
- platform: esp8266_pwm
@@ -153,59 +164,51 @@ text_sensor:
name: "Mac Address"
entity_category: diagnostic
# Creates a sensor showing when the device was last restarted
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
# device_class: timestamp
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds = seconds % (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
seconds = seconds % 60;
if ( days > 3650 ) {
seconds %= 3600;
int minutes = seconds / 60;
seconds %= 60;
if (days > 3650) {
return { "Starting up" };
} else if ( days ) {
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( hours ) {
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( minutes ) {
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if (days) {
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (hours) {
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (minutes) {
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
} else {
return { (String(seconds) +"s").c_str() };
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
# Define the timezone of the device
timezone: "${timezone}"
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
update_interval: ${sntp_update_interval}
# Set specific sntp servers to use
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
# Publish the time the device was last restarted
on_time_sync:
then:
# Update last restart time, but only once.
- if:
condition:
lambda: 'return id(device_last_restart).state == "";'
then:
- text_sensor.template.publish:
id: device_last_restart
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'

View File

@@ -10,18 +10,6 @@ substitutions:
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.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:
name: "${name}"
@@ -34,27 +22,27 @@ esphome:
name: "${project_name}"
version: "${project_version}"
on_boot:
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
interval:
- interval: 15s
then:
- if:
condition:
api.connected: # check if api connected
api.connected:
else:
- light.turn_on:
id: rgbww_light
@@ -68,22 +56,35 @@ preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
- platform: esphome
wifi:
domain: .cloonar.smart
fast_connect: False
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: "${dns_domain}"
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
hidden: True
channel: 1
hidden: true
manual_ip:
static_ip: 10.42.100.13
gateway: 10.42.100.1
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:
- platform: status
@@ -101,7 +102,7 @@ sensor:
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: "diagnostic"
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
@@ -109,8 +110,7 @@ sensor:
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: "diagnostic"
device_class: ""
entity_category: diagnostic
output:
- platform: esp8266_pwm
@@ -164,59 +164,51 @@ text_sensor:
name: "Mac Address"
entity_category: diagnostic
# Creates a sensor showing when the device was last restarted
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
# device_class: timestamp
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds = seconds % (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
seconds = seconds % 60;
if ( days > 3650 ) {
seconds %= 3600;
int minutes = seconds / 60;
seconds %= 60;
if (days > 3650) {
return { "Starting up" };
} else if ( days ) {
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( hours ) {
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( minutes ) {
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if (days) {
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (hours) {
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (minutes) {
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
} else {
return { (String(seconds) +"s").c_str() };
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
# Define the timezone of the device
timezone: "${timezone}"
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
update_interval: ${sntp_update_interval}
# Set specific sntp servers to use
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
# Publish the time the device was last restarted
on_time_sync:
then:
# Update last restart time, but only once.
- if:
condition:
lambda: 'return id(device_last_restart).state == "";'
then:
- text_sensor.template.publish:
id: device_last_restart
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'

View File

@@ -10,18 +10,6 @@ substitutions:
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.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:
name: "${name}"
@@ -34,27 +22,27 @@ esphome:
name: "${project_name}"
version: "${project_version}"
on_boot:
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
interval:
- interval: 15s
then:
- if:
condition:
api.connected: # check if api connected
api.connected:
else:
- light.turn_on:
id: rgbww_light
@@ -68,21 +56,35 @@ preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
- platform: esphome
wifi:
domain: .cloonar.smart
fast_connect: False
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: "${dns_domain}"
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
hidden: True
channel: 1
hidden: true
manual_ip:
static_ip: 10.42.100.14
gateway: 10.42.100.1
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:
- platform: status
@@ -100,7 +102,7 @@ sensor:
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: "diagnostic"
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
@@ -108,8 +110,7 @@ sensor:
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: "diagnostic"
device_class: ""
entity_category: diagnostic
output:
- platform: esp8266_pwm
@@ -163,59 +164,51 @@ text_sensor:
name: "Mac Address"
entity_category: diagnostic
# Creates a sensor showing when the device was last restarted
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
# device_class: timestamp
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds = seconds % (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
seconds = seconds % 60;
if ( days > 3650 ) {
seconds %= 3600;
int minutes = seconds / 60;
seconds %= 60;
if (days > 3650) {
return { "Starting up" };
} else if ( days ) {
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( hours ) {
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( minutes ) {
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if (days) {
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (hours) {
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (minutes) {
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
} else {
return { (String(seconds) +"s").c_str() };
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
# Define the timezone of the device
timezone: "${timezone}"
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
update_interval: ${sntp_update_interval}
# Set specific sntp servers to use
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
# Publish the time the device was last restarted
on_time_sync:
then:
# Update last restart time, but only once.
- if:
condition:
lambda: 'return id(device_last_restart).state == "";'
then:
- text_sensor.template.publish:
id: device_last_restart
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'

View File

@@ -10,18 +10,6 @@ substitutions:
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.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:
name: "${name}"
@@ -34,27 +22,27 @@ esphome:
name: "${project_name}"
version: "${project_version}"
on_boot:
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
interval:
- interval: 15s
then:
- if:
condition:
api.connected: # check if api connected
api.connected:
else:
- light.turn_on:
id: rgbww_light
@@ -68,21 +56,35 @@ preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
- platform: esphome
wifi:
domain: .cloonar.smart
fast_connect: False
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: "${dns_domain}"
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
hidden: True
channel: 1
hidden: true
manual_ip:
static_ip: 10.42.100.15
gateway: 10.42.100.1
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:
- platform: status
@@ -100,7 +102,7 @@ sensor:
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: "diagnostic"
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
@@ -108,8 +110,7 @@ sensor:
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: "diagnostic"
device_class: ""
entity_category: diagnostic
output:
- platform: esp8266_pwm
@@ -163,59 +164,51 @@ text_sensor:
name: "Mac Address"
entity_category: diagnostic
# Creates a sensor showing when the device was last restarted
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
# device_class: timestamp
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds = seconds % (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
seconds = seconds % 60;
if ( days > 3650 ) {
seconds %= 3600;
int minutes = seconds / 60;
seconds %= 60;
if (days > 3650) {
return { "Starting up" };
} else if ( days ) {
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( hours ) {
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( minutes ) {
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if (days) {
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (hours) {
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (minutes) {
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
} else {
return { (String(seconds) +"s").c_str() };
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
# Define the timezone of the device
timezone: "${timezone}"
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
update_interval: ${sntp_update_interval}
# Set specific sntp servers to use
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
# Publish the time the device was last restarted
on_time_sync:
then:
# Update last restart time, but only once.
- if:
condition:
lambda: 'return id(device_last_restart).state == "";'
then:
- text_sensor.template.publish:
id: device_last_restart
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'

View File

@@ -10,18 +10,6 @@ substitutions:
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.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:
name: "${name}"
@@ -34,27 +22,27 @@ esphome:
name: "${project_name}"
version: "${project_version}"
on_boot:
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
then:
- light.turn_on:
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: rgbww_light
red: 100%
green: 50%
blue: 0%
white: 100%
interval:
- interval: 15s
then:
- if:
condition:
api.connected: # check if api connected
api.connected:
else:
- light.turn_on:
id: rgbww_light
@@ -68,22 +56,35 @@ preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
- platform: esphome
wifi:
domain: .cloonar.smart
fast_connect: False
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: "${dns_domain}"
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
hidden: True
channel: 1
hidden: true
manual_ip:
static_ip: 10.42.100.16
gateway: 10.42.100.1
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:
- platform: status
@@ -101,7 +102,7 @@ sensor:
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: "diagnostic"
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
@@ -109,8 +110,7 @@ sensor:
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: "diagnostic"
device_class: ""
entity_category: diagnostic
output:
- platform: esp8266_pwm
@@ -164,59 +164,51 @@ text_sensor:
name: "Mac Address"
entity_category: diagnostic
# Creates a sensor showing when the device was last restarted
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
# device_class: timestamp
# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds = seconds % (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
seconds = seconds % 60;
if ( days > 3650 ) {
seconds %= 3600;
int minutes = seconds / 60;
seconds %= 60;
if (days > 3650) {
return { "Starting up" };
} else if ( days ) {
return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( hours ) {
return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if ( minutes ) {
return { (String(minutes) +"m "+ String(seconds) +"s").c_str() };
} else if (days) {
return { (String(days) + "d " + String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (hours) {
return { (String(hours) + "h " + String(minutes) + "m " + String(seconds) + "s").c_str() };
} else if (minutes) {
return { (String(minutes) + "m " + String(seconds) + "s").c_str() };
} else {
return { (String(seconds) +"s").c_str() };
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
# Define the timezone of the device
timezone: "${timezone}"
# Change sync interval from default 5min to 6 hours (or as set in substitutions)
update_interval: ${sntp_update_interval}
# Set specific sntp servers to use
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
# Publish the time the device was last restarted
on_time_sync:
then:
# Update last restart time, but only once.
- if:
condition:
lambda: 'return id(device_last_restart).state == "";'
then:
- text_sensor.template.publish:
id: device_last_restart
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'
state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");'

View File

@@ -5,8 +5,6 @@ substitutions:
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
on_boot:
then:
- light.turn_on:
@@ -20,7 +18,8 @@ esphome:
id: my_light
color_temperature: 2700 K
esp8266:
board: esp01_1m
interval:
- interval: 15s
@@ -40,6 +39,7 @@ interval:
# Enable Home Assistant API
api:
batch_delay: 0ms
ota:
platform: esphome

View File

@@ -29,6 +29,10 @@
}
{
username = "nb";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6g/lXONzSW1JbyXnj+/0QPWtaiNxu9A0GOCbi96603";
}
{
username = "nb-new";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC1dDoAJUY58I+4SSfDAkO5kInsMcJT/r/mW+MYXLQVR";
}
{
@@ -43,6 +47,15 @@
username = "gpd-win4";
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 {
imports = builtins.map create_users users;

View 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
View File

@@ -0,0 +1 @@
https://channels.nixos.org/nixos-25.11

View 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";
}

View 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" ];
}

View 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"
''
];
};
}

View 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);
}

View 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 -"
];
}

View 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";
};
}

View 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
};
};
}

View 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";
};
}

View 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;
}

View 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

View 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";
}

View 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";
}

View 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";
}

View 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
];
}

View 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} = {};
}

View 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} = {};
}

View 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} = {};
}

View 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
View File

@@ -0,0 +1 @@
../../utils

View File

@@ -1 +1 @@
https://channels.nixos.org/nixos-25.05
https://channels.nixos.org/nixos-25.11

View File

@@ -7,8 +7,10 @@
./utils/modules/nginx.nix
./utils/modules/autoupgrade.nix
./utils/modules/victoriametrics
./utils/modules/promtail
./utils/modules/borgbackup.nix
./utils/modules/set-nix-channel.nix
# fw
./modules/network-prefix.nix
@@ -25,7 +27,6 @@
./modules/podman.nix
./modules/omada.nix
./modules/ddclient.nix
./utils/modules/victoriametrics
# ./modules/wol.nix
@@ -48,7 +49,9 @@
./modules/ha-customers
./modules/firefox-sync.nix
./modules/fivefilters.nix
# ./modules/pyload
# home assistant
./modules/home-assistant
./modules/deconz.nix
@@ -84,11 +87,24 @@
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
"mongodb"
"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";
services.logind.extraConfig = "RuntimeDirectorySize=2G";
services.logind.settings.Login.RuntimeDirectorySize = "2G";
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
sops.defaultSopsFile = ./secrets.yaml;
@@ -106,7 +122,21 @@
];
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 = {
automatic = true;
dates = "weekly";
@@ -122,8 +152,8 @@
services.tlp = {
enable = true;
settings = {
CPU_SCALING_GOVERNOR_ON_AC = "powersave"; # powersave or performance
CPU_ENERGY_PERF_POLICY_ON_AC = "power"; # power or performance
CPU_SCALING_GOVERNOR_ON_AC = "performance"; # powersave or performance
CPU_ENERGY_PERF_POLICY_ON_AC = "performance"; # power or performance
# CPU_MIN_PERF_ON_AC = 0;
# CPU_MAX_PERF_ON_AC = 100; # max 100
};
@@ -154,6 +184,9 @@
# backups
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";
}

View File

@@ -27,12 +27,13 @@
ai:
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
max_tokens: 100000
max_tokens: 200000
context:
urls:
- "https://paraclub.cloonar.dev/de/tandemfallschirmspringen/faq/"
- "https://paraclub.at/de/"
- "https://paraclub.at/de/tandemfallschirmspringen/alle-infos/"
- "https://paraclub.at/de/tandemfallschirmspringen/kosten-tandemsprung/"
@@ -47,6 +48,10 @@
polling:
interval: "300s"
processing:
max_tokens: 30000
skip_junk_emails: false
logging:
level: "info"
file_path: "/var/log/ai-mailer/ai-mailer.log"
@@ -99,6 +104,8 @@
restartTriggers = [
"/etc/ai-mailer/config.yaml"
config.sops.secrets.ai-mailer-imap-password.path
config.sops.secrets.ai-mailer-openrouter-key.path
];
};
}

View File

@@ -2,21 +2,33 @@
virtualisation = {
oci-containers.containers = {
deconz = {
autoStart = false;
autoStart = true;
image = "marthoc/deconz";
volumes = [
"/etc/localtime:/etc/localtime:ro"
"/var/lib/deconz:/root/.local/share/dresden-elektronik/deCONZ"
"/dev/bus/usb:/dev/bus/usb:ro"
"/run/udev:/run/udev:ro"
];
environment = {
DECONZ_DEVICE = "/dev/ttyACM0";
TZ = "Europe/Vienna";
DECONZ_UID = "0";
DECONZ_GID = "0";
DECONZ_START_VERBOSE = "1";
};
extraOptions = [
"--network=server"
"--ip=${config.networkPrefix}.97.22"
"--device=/dev/ttyACM0"
"--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"
];
};
};

View File

@@ -66,20 +66,20 @@
];
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"
"30:05:5c:56:62:37,${config.networkPrefix}.99.100,brn30055c566237"
"1a:c4:04:6e:29:bd,${config.networkPrefix}.97.2,omada"
"02:00:00:00:00:04,${config.networkPrefix}.97.6,matrix"
"ea:db:d4:c1:18:ba,${config.networkPrefix}.97.50,git"
"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"
"c4:a7:2b:c7:ea:30,${config.networkPrefix}.99.10,metz"
"f0:2f:9e:d4:3b:21,${config.networkPrefix}.99.11,firetv-living"
"e4:2a:ac:32:3f:79,${config.networkPrefix}.99.13,xbox"
"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"
"cc:50:e3:bc:27:64,${config.networkPrefix}.100.112,Nuki_Bridge_1A753F72"
@@ -90,7 +90,13 @@
address = [
"/fw.cloonar.com/${config.networkPrefix}.97.1"
"/omada.cloonar.com/${config.networkPrefix}.97.2"
"/pc.cloonar.com/${config.networkPrefix}.96.5"
"/web-02.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"
"/mopidy.cloonar.com/${config.networkPrefix}.97.21"
"/snapcast.cloonar.com/${config.networkPrefix}.97.21"
@@ -99,6 +105,7 @@
"/feeds.cloonar.com/188.34.191.144"
"/nukibridge1a753f72.cloonar.smart/${config.networkPrefix}.100.112"
"/allywatch.cloonar.com/${config.networkPrefix}.97.5"
"/brn30055c566237.cloonar.multimedia/${config.networkPrefix}.99.100"
"/stage.wsw.at/10.254.235.22"
"/prod.wsw.at/10.254.217.23"
@@ -127,6 +134,10 @@
"/foundry-vtt.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"
"/ddl-warez.to/172.67.184.30"

View 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"
];
};
};
};
}

View File

@@ -3,6 +3,54 @@ let
foundry-vtt = pkgs.callPackage ../pkgs/foundry-vtt {};
cids = import ../modules/staticids.nix;
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 {
users.users.foundry-vtt = {
isSystemUser = true;
@@ -35,9 +83,10 @@ in {
hostName = "foundry-vtt";
useHostResolvConf = false;
defaultGateway = {
address = "${hostConfig.networkPrefix}.97.1";
address = "${hostConfig.networkPrefix}.96.1";
interface = "eth0";
};
firewall.enable = false;
nameservers = [ "${hostConfig.networkPrefix}.97.1" ];
};
systemd.services.foundry-vtt = {
@@ -48,7 +97,7 @@ in {
NODE_ENV = "production";
};
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";
User = "foundry-vtt";
WorkingDirectory = "${config.users.users.foundry-vtt.home}";
@@ -66,13 +115,48 @@ in {
gid = cids.gids.foundry-vtt;
};
networking.firewall = {
enable = true;
allowedTCPPorts = [ 30000 ];
};
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";
};
};
}

View 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`

View 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

View File

@@ -55,12 +55,18 @@ in {
name = runner;
tokenFile = "/run/secrets/gitea-runner-token";
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 = {
container = {
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"
];
networking.firewall = {
enable = true; # default, but being explicit is fine
allowedTCPPorts = [ 8088 ];
};
system.stateVersion = "22.05";
};
}) (lib.listToAttrs (lib.lists.imap1 (i: v: { name=v; value=i; }) runners));

View File

@@ -70,6 +70,9 @@ in
sslCertificateKey = "/var/lib/acme/gitea/key.pem";
sslTrustedCertificate = "/var/lib/acme/gitea/chain.pem";
forceSSL = true;
extraConfig = ''
client_max_body_size 2048M;
'';
locations."/" = {
proxyPass = "http://localhost:3001/";
};
@@ -109,6 +112,12 @@ in
USER = "gitea@cloonar.com";
};
actions.ENABLED=true;
attachment = {
MAX_SIZE = 2048; # 2GB in MB for general attachments
};
packages = {
ENABLED = true;
};
};
};

View File

@@ -96,6 +96,7 @@ in
./presense.nix
./remote.nix
./roborock.nix
./scenes
./scene-switch.nix
./shelly.nix
./sleep.nix

View File

@@ -1,11 +1,7 @@
{ config, ... }:
{ config, pkgs, ... }:
let
unstable = import
(builtins.fetchTarball https://github.com/nixos/nixpkgs/tarball/nixpkgs-unstable)
# reuse the current configuration
{ config = config.nixpkgs.config; };
in {
services.home-assistant.customComponents = with unstable.home-assistant-custom-components; [
services.home-assistant.customComponents = with pkgs.home-assistant-custom-components; [
epex_spot
];

View File

@@ -7,6 +7,13 @@
name = "enocean_switch_pc";
}
];
"binary_sensor bed_1" = [
{
platform = "enocean";
id = [ 254 207 162 105 ];
name = "enocean_switch_bed_1";
}
];
sensor = [
{
name = "Bathroom HT";

View File

@@ -274,46 +274,88 @@
};
};
};
"automation bed_button_1" = {
alias = "bed_button_1";
trigger = {
platform = "event";
event_type = "shelly.click";
event_data = {
device = "shellybutton1-E8DB84AA196D";
};
};
"automation bedroom light" = {
alias = "bedroom light";
trigger = [
{
platform = "event";
event_type = "button_pressed";
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 = [
{
choose = [
{
conditions = [ "{{ trigger.event.data.click_type == \"single\" }}" ];
sequence = [
{
service = "light.toggle";
entity_id = "light.bed_reading_1";
}
];
}
{
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";
}
];
}
];
service = "light.toggle";
target = {
entity_id = "light.bedroom_lights";
};
}
];
};
"automation bed light" = {
alias = "bed light";
trigger = [
{
platform = "event";
event_type = "button_pressed";
event_data = {
id = [ 254 207 162 105 ];
which = 0;
onoff = 1;
pushed = 1;
};
}
{
platform = "event";
event_type = "shelly.click";
event_data = {
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;
entities = [
"light.livingroom_switch"
"light.livingroom_bulb_1_rgbcw_bulb"
"light.livingroom_bulb_2_rgbcw_bulb"
"light.livingroom_bulb_3_rgbcw_bulb"
"light.livingroom_bulb_4_rgbcw_bulb"
"light.livingroom_bulb_5_rgbcw_bulb"
"light.livingroom_bulb_6_rgbcw_bulb"
"light.living_bulb_1"
"light.living_bulb_2"
"light.living_bulb_3"
"light.living_bulb_4"
"light.living_bulb_5"
"light.living_bulb_6"
# "light.living_room"
];
}
{
@@ -391,6 +416,7 @@
all = true;
entities = [
"light.kitchen_switch"
"light.kitchen_bulb_1"
"light.kitchen"
];
}

View File

@@ -290,16 +290,6 @@
command = "b64:JgDaAAABKZMUERMSExITEhMSExETEhMSExITEhMSExETNxQ2ExITEhMSEzcTNxM3ExITEhM3ExITNxMSEhITEhM3EzcTEhM3EwAFyAABKJQUERMSEhITEhMSExITEhMSEhITEhMSExITNxM3ExITEhMREzcTNxQ3EhITEhM3ExITNxMSExITEhM3EzcTEhM3EwAFyAABKJQUERMSExETEhMSExITEhMSExETEhMSExITNxM3ExITEhMREzcTOBI4ExETEhM3ExITNxMSExITEhM3EzcTEhM3E5IGAA0FAAAAAAAAAAAAAAAAAAA=";
};
}
{
delay = 30;
}
# turn off tv switch
{
service = "switch.turn_off";
target = {
entity_id = "switch.tv_switch";
};
}
];
};
"automation all_multimedia_on" = {

View File

@@ -41,8 +41,6 @@
service = "wake_on_lan.send_magic_packet";
data = {
mac = "04:7c:16:d5:63:5e";
broadcast_address = "${config.networkPrefix}.96.5";
broadcast_port = 9;
};
}
];

View File

@@ -0,0 +1,28 @@
{ pkgs, ... }:
{
services.home-assistant.config = {
scene = [
{
name = "Date Night";
icon = "mdi:heart";
entities = {
"light.livingroom_showcase" = {
state = "on";
brightness = 255;
rgb_color = [255 110 84];
};
"light.bar_led" = {
state = "on";
brightness = 255;
rgb_color = [255 110 84];
};
"light.shapes_1e51" = {
state = "on";
brightness = 124;
effect = "Date Night";
};
};
}
];
};
}

View File

@@ -0,0 +1,6 @@
{ pkgs, ... }:
{
imports = [
./date.nix
];
}

View File

@@ -7,10 +7,6 @@
at = "input_datetime.wakeup";
};
action = [
{
service = "switch.turn_on";
entity_id = "switch.coffee";
}
{
delay = 1700;
}
@@ -27,34 +23,21 @@
trigger = [
{
platform = "event";
event_type = "shelly.click";
event_type = "button_pressed";
event_data = {
device = "shellybutton1-E8DB84AA196D";
};
}
{
platform = "event";
event_type = "shelly.click";
event_data = {
device = "shellybutton1-E8DB84AA136D";
id = [ 254 207 162 105 ];
which = 1;
onoff = 0;
pushed = 1;
};
}
];
action = [
{
choose = [
{
conditions = [ "{{ trigger.event.data.click_type == \"long\" }}" ];
sequence = [
{
service = "script.turn_on";
target = {
entity_id = "script.turn_off_everything";
};
}
];
}
];
service = "script.turn_on";
target = {
entity_id = "script.turn_off_everything";
};
}
];
};
@@ -65,22 +48,18 @@
service = "light.turn_off";
entity_id = "all";
}
{
service = "switch.turn_off";
entity_id = "switch.coffee";
}
{
service = "switch.turn_off";
entity_id = "switch.78_8c_b5_fe_41_62_port_2_poe";
}
{
service = "switch.turn_off";
entity_id = "switch.78_8c_b5_fe_41_62_port_3_poe";
}
{
service = "switch.turn_off";
entity_id = "switch.hallway_circuit";
}
# {
# service = "switch.turn_off";
# entity_id = "switch.78_8c_b5_fe_41_62_port_2_poe";
# }
# {
# service = "switch.turn_off";
# entity_id = "switch.78_8c_b5_fe_41_62_port_3_poe";
# }
# {
# service = "switch.turn_off";
# entity_id = "switch.hallway_circuit";
# }
# TODO: needs to stay on because phone is not loading otherwise
# {
# service = "switch.turn_off";

View File

@@ -2,6 +2,8 @@
{
imports = [ (builtins.fetchGit {
url = "https://github.com/astro/microvm.nix";
ref = "main";
rev = "42628f7c61b02d385ce2cb1f66f9be333ac20140";
} + "/nixos-modules/host") ];
systemd.network.networks."31-server".matchConfig.Name = [ "vm-*" ];

View 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 = {};
}

View 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";
};
};
}

View 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
''

View 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
};
};
}

View 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
];
};
};
}

View File

@@ -5,6 +5,9 @@
gitea-runner = 10003;
podman = 10004;
foundry-vtt = 10005;
pyload = 10006;
jellyfin = 10007;
filebot = 10008;
};
gids = {
unbound = 10001;
@@ -12,5 +15,8 @@
gitea-runner = 10003;
podman = 10004;
foundry-vtt = 10005;
pyload = 10006;
jellyfin = 10007;
filebot = 10008;
};
}

View File

@@ -2,10 +2,6 @@
let
hostname = "vscode-server";
unstable = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz") {
config = config.nixpkgs.config;
system = pkgs.system;
};
in {
microvm.vms.${hostname} = {
autostart = true;
@@ -24,7 +20,7 @@ in {
};
environment.systemPackages = [
unstable.ddev
pkgs.ddev
];
# Docker is required for ddev
@@ -37,4 +33,4 @@ in {
mac = "02:00:00:00:01:01";
}];
};
}
}

View File

@@ -11,6 +11,9 @@ in {
# needed for matrix
"olm-3.2.16"
];
allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
"n8n"
];
};
};
config = {
@@ -54,13 +57,20 @@ in {
../../utils/modules/lego/lego.nix
# ../../utils/modules/borgbackup.nix
./zammad.nix
./phpldapadmin.nix
./proxies.nix
./matrix.nix
# ./matrix.nix
./n8n.nix
# ./piped.nix # Replaced by Invidious
./invidious.nix
./invidious-init-user.nix
];
networkPrefix = config.networkPrefix;
sops.age.sshKeyPaths = [ "/persist/etc/ssh/ssh_host_ed25519_key" ];
sops.defaultSopsFile = ./secrets.yaml;
time.timeZone = "Europe/Vienna";
systemd.network.networks."10-lan" = {
@@ -75,8 +85,10 @@ in {
directories = [
"/var/lib/zammad"
"/var/lib/postgresql"
"/var/lib/n8n"
"/var/log"
"/var/lib/systemd/coredump"
"/var/backup"
];
};
@@ -116,10 +128,6 @@ in {
# backups
# borgbackup.repo = "u149513-sub2@u149513-sub2.your-backup.de:borg";
sops.age.sshKeyPaths = [ "/persist/etc/ssh/ssh_host_ed25519_key" ];
sops.defaultSopsFile = ./secrets.yaml;
networking.firewall = {
enable = true;
allowedTCPPorts = [ 22 80 443 ];

View 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
'';
};
}

View 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" ];
};
}

View 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;
};
};
}

View File

@@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
with lib;
{
virtualisation.oci-containers.backend = "podman";
virtualisation.oci-containers.containers = {
phpldapadmin = {
image = "phpldapadmin/phpldapadmin:2.2.2";
autoStart = true;
ports = [
"8087:8080/tcp"
];
environmentFiles = [
config.sops.secrets.phpldapadmin.path
];
};
};
systemd.timers."restart-phpldapadmin" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 3:00:00";
Unit = "restart-phpldapadmin.service";
};
};
services.nginx.virtualHosts."pla.cloonar.com" = {
forceSSL = true;
enableACME = true;
acmeRoot = null;
locations."/" = {
proxyPass = "http://localhost:8087";
proxyWebsockets = true;
};
};
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 = {};
}

View 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;
'';
};
};
}

View File

@@ -25,4 +25,61 @@
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;
'';
};
};
}

View File

@@ -1,34 +1,54 @@
borg-passphrase: ENC[AES256_GCM,data:2WjoqMRmXvW9EGMmpMYhrC0Qt0Dk7QWlbEncZPdK2SxVljEoFibjVEr6jeYdAx6UkaXdjk9pD3PBbls2tWt0TiNQdh8=,iv:bHzASNjqqfPsQ/1w/oM7x0FubAzzRkn+iWrZlenU9rs=,tag:ektqi0rqEywg9YGybPQesw==,type:str]
borg-ssh-key: ENC[AES256_GCM,data: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,iv:U3+fjacm8+gZAjPQNz2mjFYTUbLyltTaPiSKb3lvCmk=,tag:ZR6zI1UijDayIvH3v35Hqg==,type:str]
zammad-key-base: ENC[AES256_GCM,data:HO9MuwcwjryuXr5No8sCPfso5bpLtQCoczrC/R214ecVIFwwH1uhMeNO8Tlh6EjRLPo7aVTSz87Vx5yaNVezvHCs55G6TT9mcNS/v/V7sbFz9dNIgbFblY3gFIAa4cViioYc71wdb7d4Tta7qhse5zQ41KhAqCWuGDgFErQA4Oc=,iv:b1wY8fW0psircSlNXwDjPzNWK8NyAMNqegitNcqV6U4=,tag:oQ7nyO9TKOOu6IF7ODzpPA==,type:str]
dendrite-private-key: ENC[AES256_GCM,data:ZHDIa/iYSZGofE67JU63fHRdKbs/ZyEJY45tV6H8WZAOcduGafPYBo2NCZ7nqLbc2Z9dUUgsrpzvkQ3+VaWqFUv7YsE+CbCx4CeiLGMkj8EAGzX4rkJGHMzkkc2UT7v9znCnKACS3fZtU69trqVMcf1PzgqepOHMBku37dzpwOQC/Tc3UTuO72M=,iv:Ljun1/ruY9cDBm9vu62riUrpGjrWtFFx90GeE7uc3Yo=,tag:FF4xPb1SDhK/4ITr/idvYg==,type:str]
matrix-shared-secret: ENC[AES256_GCM,data:HeS4PT0R+TRU6Htwa5TChjK1VAjAdgSS8tSnva+ga3f+mEfJPTQ02pEvS2WFvcnchmEjNYy39zL/rbtX,iv:4yR+VgdJY3VcvLg18v+5jbJDSkFzaeyLNAZ0k8ivjdQ=,tag:RA96iSFDUdlXq30c/vkvpA==,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:3O+SAjX/D4k9SUmGKAfriyOAKaH2Jm4tAbfKDOoZts0=,tag:3yeNyl9TjlENfV+IxZkj/w==,type:str]
zammad-key-base: ENC[AES256_GCM,data:62Gj7zyDGGMTVOv2YvrNVDIX+fxt94KVQ/EJBIqXssM6nrKN7veh4sIoLy3+/KwEMpCL3cnb3x7BKXDndnjulfVuF6pTDUEaiH/8YC5YPp+N3imWRTFYDsCJEkB7AsXkuVCH7f0MoMO3v+56BBYFEAp9E7wVT4Jdid7h56zfzaY=,iv:MMvHSUhNaIZb6XBBXBJlqolmXzPKuiFH0jQxlKnK7GY=,tag:2PPZtcD9wWBI1mokXAfMxw==,type:str]
invidious-hmac-key: ENC[AES256_GCM,data:UZM6COUUHgLu/OVjkkp7rvzhiXBo5O4V/X+8ig==,iv:VX46ainev8JfGNic9FpnYlP7ZQMpTrMwDH0kW/l776s=,tag:HkQwPt23lHw7TQj9HqFIhw==,type:str]
invidious-admin-password: ENC[AES256_GCM,data:YEdvUckgHhq23fa0ZDLvZM9/yRiClqT5LsoX6tPhNTi9rKTFUHwNrCKLZAr8dafbaw==,iv:romRuJxhQqSQMNepVS04Fu4e3SpA0yl7P8LUI1R76Iw=,tag:puncGz4EH5qkNfHRzjEdBg==,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:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoUWdTYlRjWDJvemF5Q2sr
VCtrS2dTTGRwUlNIWHd0WkVCRkRMcGhuTzE0ClNic1FmQ05UNWQwbGc4TUFMNGlI
K0RhK2pqUGY3UElmK1pNUEkxV2xGUTQKLS0tIFRORE9JTDRZK0MwZUJoc2xlcHFH
bmp3ZW14TVdCMHhkSi84NE5neDdrY3cKYfgu7aqvG6wQmEFhmzieXFGoQpyffPXj
jiHrAPjBBFy21wdYf0nQXNMzekqOMJwOj0oNA2b5omprPxjB9uns4Q==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYNVBOdGZHbmc2b2UyVGNj
aVJkV2RwYVlvTzBURndiVTVhOFFJRDVEckJ3CndyVk50UGUxNDE1dS9rblQ5VkZh
aGtYZitMNDhDUi9xczQ4cGNXK1NvOGsKLS0tIFVHSWErVHhma09BcFlzd2x1ZTBj
eXN6NlpjdDNVci9oQTZGRTJ3ZHNHencK53kJSr3udGgPUsaDxYny6gkWXRCldSfM
kGYpeGMh2CGc9x5x2L3JlS3EbGPerblva/6wvmoszI2uL/hZzU/g8g==
-----END AGE ENCRYPTED FILE-----
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhZDJUVXo3bkY0Mzc1RDNG
RXZBRXVSVXpDKzJFdWNpS1RqcEdQUEF5d3hrCkNqcC9CY1I0RzRWdGcwREpzbEt5
d1liOVZEWUEzTWxzZU8vaTZFQzNWQ2sKLS0tIDNYeEt1RlozaGdYNnhpWmZwMGVE
c256c284cmFhUWhQeE5rY3JDY3liekUKIPl8/qYgp2JlVjw5t5PnvS4II+YU1V+C
K7WOVpqIGi5F4Taa3SOtNBzRs7jbdTEE231C4zwZnBucZoX9gGVC+w==
-----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTTERxOHJxTW5Gdk45Ukxx
MjkzWFpuSmdBT1psbWFsSENBVURnb2pObzBFCkZNY3dEeHVSbWpKUVlRSnVEWk9r
cUs4NHNRSWd2OGZ3R2tqcDZqR3I1SmMKLS0tIC8wTkpBUW5PLzFidVJkcFVIL3pP
elhqbCtTN0FyYVdBNEhyTkVacHEyY0kKJam6XZgN7INkIThBPyZ+vi4xhknY7GVJ
57aIYLI6xMvs5E+120qVjXxoo29kzs2uwnKzlbAqMJIY/eoDWW33XQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUUjQxWnBMQXo3QmF1STUw
bHh1NDhvQXZIQ2RiOUx5OU5Wc3BVSEJDUEZVCmVzeFk5SWpMbVV4VUdsRmhiaWwz
bTJDY1pJRXJvNUdCSXJqQ3Byd3lWN2sKLS0tIHRKdXRNc1BYcURBRVNlenk1OEl3
Q05BN0VnQ0haeHBobWhRV0EzL3dLSEkKWlALiX5mvG8y0WUc8yFWMbcpSRrSGoQx
SHaOlDCjYvViZ7GPRLqnSwDGZ1clC6JsTbwKXrMsWdZBKvSO/VIWQw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlTUJHTnJOL2pJM0ViVEZh
SzdNdjlScG1OMDlTSTlLSXdOcTJwOUpWeFJBCjA4UEpFaEZNRGRMSENHWXdyd2RE
akE3T00wUC84Y0JSSldkaGh3TU5CL2cKLS0tIHpURHdSMnRBVHNrSk44ZzBUQTRC
QlRRcFVzby9namhkOGJ6Zk9TVGxMbDQKEyd9Tf67JclHM+kWZxpXl+g3+cMimfHI
VfeF0z7zBcPuLb6xIzEHmXDn6Z3EeCYOq975nQde2JSpmKIZagerhw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-10-14T16:53:41Z"
mac: ENC[AES256_GCM,data:DUi6zUrZBMVaYZ/BvWny7RwPgXe+vQ+odO30fGe8iZHj9d3gzB95F75CqIgENi4gVOA4CQDADE+p45z/mtl04HAh7RiT0/k21RSdQcH2W9AX525fOzeqbxbPA/tXJOctwGrytFwlK9UdJULXkJCwYrJnwNc0XPnBk1FodTykXWs=,iv:q/eapgTVL/rifrrZeIcXT5VO9bEoS4EmmEhYJ2xHvQ4=,tag:xb0Qj/wu17cLTkvefsDqiw==,type:str]
pgp: []
lastmodified: "2025-11-03T12:45:54Z"
mac: ENC[AES256_GCM,data:1+D1VGQ19gAfEL30hUN6BeBxVnYLvkQ1lV48WHeTcM0mlWl9z1KI6eencwNfHw04fnJzt9VNOClw+p8ekRTygUnUOMSEh9QQCGuCaFU7s+vRwtafO4t5Ip00b5P+TM3HEbSFNBRO19+Btqk1sfYZv1u3YemST/v57Y9tk/yx09c=,iv:Ea1KnGony2CvGHB5x0PBqLSb8faxTVbjDPo1F1Sf9bo=,tag:hduJprCw11FJB0bpF8dSHA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.8.1
version: 3.11.0

View File

@@ -22,7 +22,7 @@
allowedIPs = [ "${config.networkPrefix}.98.202/32" ];
}
{ # GPD Win 4
publicKey = "HE4eX4IMKG8eRDzcriy6XdIPV71uBY5VTqjKzfHPsFI=";
publicKey = "p3wnxXK7hurOKxruFCRoefj6gCoQeD5XXxD/ogMpew8=";
allowedIPs = [ "${config.networkPrefix}.98.203/32" ];
}
{

View File

@@ -0,0 +1,7 @@
# default.nix
let
pkgs = import <nixpkgs> {};
in
{
phpLDAPadmin = pkgs.callPackage ./phpldapadmin.nix { };
}

View File

@@ -2,9 +2,9 @@
stdenv.mkDerivation rec {
pname = "foundry-vtt";
version = "12.331";
version = "13.350";
src = ./FoundryVTT-12.331.zip;
src = ./FoundryVTT-Node-13.350.zip;
nativeBuildInputs = [ unzip ];

View File

@@ -0,0 +1,23 @@
{ fetchurl, lib, stdenv }:
stdenv.mkDerivation rec {
pname = "phpLDAPadmin";
version = "2.1.4";
src = fetchurl {
url = "https://github.com/leenooks/phpLDAPadmin/archive/${version}.tar.gz";
sha256 = "hkigC458YSgAZVCzVznix8ktDBuQm+UH3ujXn9Umylc=";
};
installPhase = ''
mkdir -p $out
cp -r . $out/
ln -sf /etc/phpldapadmin/env $out/.env
'';
meta = {
description = "phpLDAPadmin";
license = lib.licenses.gpl3;
platforms = lib.platforms.all;
};
}

View File

@@ -1,66 +1,69 @@
ai-mailer-imap-password: ENC[AES256_GCM,data:kMxDPUK9rk7mbel5JDT03m3Y2w==,iv:cbnkNIVRXd7OLqueSrfYRzfaW9TzI+FauuQD8lgYIy0=,tag:63W7seIgt5TPVFQc84semQ==,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]
borg-passphrase: ENC[AES256_GCM,data:jHb+yXK0RqNdVYtWiueztZFlHC/xQ6ZiAOUcLt6BxmZQewuL3mh4AZ+lQdmA/4EaaTTIhVMR3xFx5fU6b2CtNLiGb/0=,iv:IW09B1EE1OupMCOvv13MXRYiMsD4VmIfyYONUyrPX1c=,tag:3ankeLOaDJkwRUGCd72DuA==,type:str]
borg-ssh-key: ENC[AES256_GCM,data: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,iv:9E1uiPqM3Hh4KWtL8haxm6PRm2VPc+DggrA135FvfB8=,tag:QSOgzVH9IBMgZxJvUhvY2w==,type:str]
ddclient: ENC[AES256_GCM,data:EaXjXS/bwL3S/Fr+rzQ7dXA1eIzeFpHH7H+SvoNhVSg=,iv:3BzjnJG5yT1W8ob2nm0oUlr+sSJ73W/ctl48xyxeeWM=,tag:TqKSwfxF0V1v5T8VT/qblw==,type:str]
gitea-mailer-password: ENC[AES256_GCM,data:M4qCWNt1oQVJzxThIjocm2frwuVMyx+69TBpke25RwxJxEQnvHL1CM579OVroTm7+gGE/oOJqAwDIepfiDtyM1xm,iv:jayFZMbu3uDimS/rIKZSeoU0MsYwWp880iEMs1oQE4k=,tag:qGDncRkyuCWaELhcxUrqtQ==,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-runner-token: ENC[AES256_GCM,data:HpBjLS10w78ihbnAUrlCRGvwrXLBYKH5v/P7XggoUSWLoAazSVQArABxaK7PJas=,iv:q3Y6jV0gmug06O0EYqGVyIJ4AvMGr2ydwY17YKxo0Qw=,tag:Ws5HLbdaeYGGXzDZW/FX4w==,type:str]
home-assistant-ldap: ENC[AES256_GCM,data:uZEPbSnkgQYSd8ev6FD8TRHWWr+vusadtMcvP7KKL2AZAV0h1hga5fODN6I5u0DNL9hq2pNM+FwU0E/svWLRww==,iv:IhmUgSu34NaAY+kUZehx40uymydUYYAyte1aGqQ33/8=,tag:BKFCJPr7Vz4EG78ry/ZD7g==,type:str]
home-assistant-secrets.yaml: ENC[AES256_GCM,data:m7uOVo7hPk/RmqqRS6y7NKoMKsR9Bdi1ntatsZdDOAbJMjZmZL2FgPEHi/zF73zCfRfTOca3dwpulR3WXZ9Ic1sbUIggmusJMg4Gellw1CUhx7SbQN5nieAbPbB9GVxMuV4OakD1u7Swz8JggDT6IwojSnuD5omCRCyUH1wvKB+Re59q6EStderlm5MJNVFlVrbKVbLKLcw4yRgTh34BGnTTjcJmgSlQjO1ciu2B7YQmdl0Fw6d8AdbEzgB5TFG5ONc85UhJDE8Wlw==,iv:GCtpcVChN2UMWtfnWURozCfVj2YbRPqp/bH4Jjntybs=,tag:pcxP7gTBtXMNT5iyW5YXTw==,type:str]
pushover-api-token: ENC[AES256_GCM,data:W2ILPksaNeDvbSlSJztu1vu23kQKLDRHYKoUIvyd,iv:RYFAN6AU+DALphpqpiifhOoEQ8++6DEgo2wETSwxBCg=,tag:pRfaNuz4564LvRuaLggatg==,type:str]
pushover-user-key: ENC[AES256_GCM,data:mh3u3FAdFkGD1d4UKcTwLOsCB2vfhEADI5cd1aT4,iv:4bkR7ZNJwWAYBdu435SPZUovGsfb8qivuDOQdGkPd/U=,tag:5UO4vGt75CCFEM5jxTGkGg==,type:str]
wrwks_vpn_key: ENC[AES256_GCM,data:gGipXC8JJO59b4KWMSo0+r761raQl7RzgBuUbXmPEKlZR21bs5XRAQalzDCFNtjcpNkXiGqAHCLkDTtjPagMsw==,iv:MH1EBJEOdQDEgm9E0F884fynhsH8KiS5QSc605XbASQ=,tag:FUM1eptHS0rpt6ILyQjGOg==,type:str]
wg_cloonar_key: ENC[AES256_GCM,data:Dtp6I5J0jU5LLVwEFU4DFCpUngPRmFMebGXnk2oSwsKtsir/DtRBFG7ictM=,iv:1Abx/EAZRJrRQURljofzUYDgJpuREriX0nSrFbH5Npw=,tag:l4uFl9Uc+W0XeLVfLGmgZA==,type:str]
wg_epicenter_works_key: ENC[AES256_GCM,data:LeLjfwfaz+loWyHYRgIMIPzHzlOnhl9tluKcQFgdes6r+deft1JfnUzDuF0=,iv:DKrc3I+U2hWDH8nnc8ZQeaVtA1eVXu7SXdTn1fxHoH4=,tag:V0PL0GrL2NEPVslAZa801A==,type:str]
wg_epicenter_works_psk: ENC[AES256_GCM,data:Den3NDWdP013Or6/2Vll1igUahuRSNW4hu+nDa5vkr93bbveQTaWFT4TD4U=,iv:r3UsD3+3lUIP2X3Grti7wpXTQBXtu1/MdrycEmpZfsI=,tag:ghbAcxmjGVOe9jCZsmFzjA==,type:str]
wg_ghetto_at_key: ENC[AES256_GCM,data:OIHmoy3SpIi9aefZnZ1PzpyHbEso18ceoTULf2eQkx1rJbaxC6PD1lma7eQ=,iv:u0eFjHHOBzPTmBvBEQsYY5flcBayiAQKd6e7RyiPwJI=,tag:731C9wvv8bA5fuuQq+weVQ==,type:str]
matrix-shared-secret: ENC[AES256_GCM,data:67imd3m6WBeGP/5Msmjy8B6sP983jMyWzRIzWgNVV5jZslX+GBJyEYzm3OTDs1iTZf4ScvuYheTH0QFPfw==,iv:7ElCpESWumbIHmmFaedcpkFm5M58ZT3vW9wb9e1Sbh4=,tag:wr4FIymtJBtCerVqae+Xlw==,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]
ark: ENC[AES256_GCM,data:YYGyzoVIKI9Ac1zGOr0BEpd3fgBsvp1hSwAvfO07/EQdg8ufMWUkNvqNHDKN62ZK5A1NnY3JTA1p4gyZ4ryQeAOsbwqU1GSk2YKHFyPeEnpLz/Ml82KMsv7XPGXuKRXZ4v3UcLu0R8k1Q0gQsMWo4FjCs3FF5mVtJG/YWxxbCYHoBLJ/di5p0DgjuFgJBQknYBpuLzr+yIoeqEyN7XcGYAJO53trEJuOOxLILULifkqISHjZ66i5F1fHW0iUdRbmeWV4aOAeOrsQqXYv,iv:gJwV5ip84zHqpU0l0uESfWWOtcgihMvEEdLaeI+twcU=,tag:sy8udVQsKxV/jOqwhJmWAg==,type:str]
firefox-sync: ENC[AES256_GCM,data:uAJAdyKAuXRuqCFl8742vIejU5RnAPpUxUFCC0s0QeXZR5oH2YOrDh+3vKUmckW4V1cIhSHoe+4+I4HuU5E73DDrJThfIzBEw+spo4HXwZf5KBtu3ujgX6/fSTlPWV7pEsDDsZ0y6ziKPADBDym8yEk0bU9nRedvTBUhVryo3aolzF/c+gJvdeDvKUYa8+8=,iv:yuvE4KG7z7Rp9ZNlLiJ2rh0keed3DuvrELzsfJu4+bs=,tag:HFo1A53Eva31NJ8fRE7TlA==,type:str]
knot-tsig-key: ENC[AES256_GCM,data:H2jEkRSVSIJl1dSolAXj9uUmzD6eEh9zPpoajZLxfuuFt7/LJF8aCEHyk+Q=,iv:9aqywuaILYtejuZGd+Cy8oErrHIoL2XhL1g9HtcUn/o=,tag:K3SnVEXGC/NhlchU7OyA6Q==,type:str]
mopidy-spotify: ENC[AES256_GCM,data:O3s6UvTP8z5KZPCq10GaaEQntWAEoxGFMnTkeUz9AfobrpsGZJcQgyazFX2u4DgAaIjNb34032MISotmuVQDJ14mi8xI5vC9w/Vf16v3TFu/dSKGZNb5ZPQwTUQ+iMJf7chgwOV9guThhutVJokb6pLxzt7fSht7,iv:j8+X1AmuWzIJdafzgrE7WBIlZ7coNNi0/Zn6JObR6rw=,tag:fiw6M2/6nfEPqEgV2YOWLg==,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]
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:EvI0BuCBA1uYOderjAVcB8RSk7un7tiKmgsSe70KQcmfu3CxmQerP/2kQsRTJ0/6pWf4QqNpaes691O3nf+UG1qgG2CUcIaYRQ==,iv:OYEy0xMs+vkGa0qMtY4UP/iol5JPQ0eFVyPpPXLAmUE=,tag:5PeXZcI8TRSUOyuKs0STWg==,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:f7rBK8aNqX8dGyzjoeRX6yl20XsnLU8b4gitaw9+O+0=,tag:WvfUw1JgFBAtS3vsVIvM6Q==,type:str]
ddclient: ENC[AES256_GCM,data:dS6TVVNb6R7EE1JVMDfSnRYCZyHHqEPvwaYpkTSj+VA=,iv:9uMo+9X7dFdVW4wuSgrqIAaQelXuA4cek2oif0GRHow=,tag:ncQq4UeUzWtjPNxEUOlqNA==,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-mailer-password: ENC[AES256_GCM,data:lEv5euTCHG6pyNqrVtKK7oE8wLvk+q8ABXOzFSizQ2TVFi35lyGPzOTel/dCCC0Je5GAHE1KQQ4Y4/iHghZgb5Ft,iv:gt/mCzLbDrHFNqW+Lkd2dy9nRIBKO+rqsVuXM45zJ8k=,tag:gCxTSzY7GZ+jQP9SCsdUtw==,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]
gitea-runner-token: ENC[AES256_GCM,data:pzJp7j1Ktz+27oU+qtESk7D32w7+BSEUkPSX4xuFml0i10z12Gzu0QHXL9s3734=,iv:U77b5515H1URfz5BCdzuY03zVkhSRsL9d+HdHUJFx9U=,tag:QvooaT4TS/X5R5KGdaVpVQ==,type:str]
home-assistant-ldap: ENC[AES256_GCM,data:4kofJzPbiLXILxjuAZWiTb9hu2Gver/IHBCXDnrmrKuCSII6SJ9FrSi67nl7SHdoA6xe22GSMfmPrKzy5sGiow==,iv:F8mIHhWHpaI6kzRV9du6uW/Fj07PbEIU1goSDmeSD5E=,tag:6NIC6sN8OclinribZhrLLw==,type:str]
home-assistant-secrets.yaml: ENC[AES256_GCM,data:rns9heAmVMxB6WWlGMXvF/ianFUnja3FObiLTEKJmodePNsJ8ah3OhuCAX5jON+/7NZ+3JN/hIJjXsORC5WYhr01DvO9meykf0aMpbmAnYI+cmPEPvcunF4NNInl96rpcI519nMiHDSh5J7pD74CxHZcXSV4c9ZR5UBymchrwmHyZMF6dVrD9Jbr9yph1r7iq6S5wlI2ZImWRjaoGDZ1x+ZU8XnsUmYcP4pa1Yt8JBxSnyUw5gxgBkVCh4eSZBsUCt0cd9P0i7qWVg==,iv:YXQsawXZsQb9ZUt1/lkpfTa4tfKIQrLkkyShFtBRaIQ=,tag:/vSnipGiMntdMqHLePSEQw==,type:str]
piped-db-password: ENC[AES256_GCM,data:5atQccdHYDEf638bpiON9VO14jqNDtzZ8nnXVW0/cqtWkZJc8RYn9N7QhAw=,iv:Gwyf1R+mpmX+TFuoYLPHjXwSDwzJhSEpnj5ZsJgmrtk=,tag:zm4zNkzbqbCyTN6o3lQQfg==,type:str]
pushover-api-token: ENC[AES256_GCM,data:cMBDdySEBQ7vS7FUC2DsCcSvEMpapWvMFmnuCsY6,iv:SVDrrDm2pcAfwUVAC5j47YwF4s/FWNARlZdIZ1Wgwgw=,tag:w7ZeNMPXWc9j+zVaSxq1cQ==,type:str]
pushover-user-key: ENC[AES256_GCM,data:fjoA2YQxmeWEbSKWWE5iyi+CUh1vtW9usVCm5EGk,iv:p4YwYIhpgn/bY9t61//CDrDmZrsj9B/naZit62lCpwo=,tag:pqEw3pDlX7i87tE0Nsy0/Q==,type:str]
wrwks_vpn_key: ENC[AES256_GCM,data:VEHqnr/bDtmyLzs0wnmZ0jCWS0BGJWu6Wjq0ZHJuEz8PH3j/E54S9NUe6WRIo+BJCsh1PlRqw/PD9xSqlW5uPg==,iv:OMP0s8Lc2CmFgwRuwB3UWJVuQFqvpy+BiyhnIKbVIb8=,tag:x1LvSf6i8khd8jKgv/284g==,type:str]
wg_cloonar_key: ENC[AES256_GCM,data:1OfHD8yX+pgCXqqxn7cddnnCA9HBjGra4eht7uLxdcbdG9vDvxUoE1x6aWg=,iv:/NBEbmA3wP/zwrqCeBKDzaoSMqz3f4ZeMlWbu81R5Pg=,tag:Apt8x/j0qiJAKR4UEVSkrA==,type:str]
wg_epicenter_works_key: ENC[AES256_GCM,data:CTZkVGEVRlCdt6W0BGPmX0SZbuBBH5IIlUsi44SGXi7gdmrZNwv2zDv6zjA=,iv:4ZDDKqR6pBq8cjX763tBxOvWFaS2IiGaBxJu6L2JYig=,tag:H8p63BvXSx1SKPFw5gnptw==,type:str]
wg_epicenter_works_psk: ENC[AES256_GCM,data:K0SDlDWfUk9vIGP5U1j8p6TJ9GsydJTuKPb4kMgde1CILOia0S9/+4AkMWY=,iv:ITwLoWZXR6NxRFF3eBvOogiWHLmXnf7S1e2FW0ofr/M=,tag:2OVi3OBFYT0nlCx8gf2AdA==,type:str]
wg_ghetto_at_key: ENC[AES256_GCM,data:+bonpVjV1hxwaqtR7ywshmoDxCnFPD11q0OiNLzxUJIaYrDeS1srpyo6rlE=,iv:Djn16kuXTWqJZy/AT77GpH8RcNtUMZ6zcIdKIMHv+PM=,tag:LP2JCaPKpzeOKvBc2bMr4w==,type:str]
matrix-shared-secret: ENC[AES256_GCM,data:nVSHwPa8xYUaDCxL+5neFtzc11DDNzJtoDCSHYXZ+bZXVAAbp6/Pjx6UkTdAA8B2GOM09nFAsBuLnQfJ3w==,iv:WU3hnRlWVwx7Qin3ejw7V4VhAmYLf6oXzVk6xQgZPgA=,tag:O2hJ2q8XDxYF+rHPNgATgA==,type:str]
phpldapadmin: ENC[AES256_GCM,data:94jCcgGJ89Er5ENLqhFZ1qY44Qp709SuUhBUuED6v/a7mPPjrJGDmi0Gm3r1Hb4CDPGkWf+x4NStY7LSQ2bHEzjyMPMS23wvSLTmC5b2TVca1UI8vZRTD1R7OvdWo8d1oNweSpYEnAXGv3USYF0NZo8DrPLM5G8lG5Tk/rKS/mxU5ZRhPyA60rbmIiy3Mk4yNcs1tvTEckxU/zMVl7zUPAsOOlmYGuwJrHmmh9p7YIWHGIgZNiLs3U0BvSKzN7WktmlwqjfWpeLn4dusqgov4SSQ2otAkxLHIH8mGhyotd1wgXJDZc6tilMe+WPHQDz9db7FT0VdeKggQ94FD+8rP0OsIjR4AdjZ,iv:C8X10wtA9jPgS41pxasaZJTO/XFcRymOyTDZCWJlhmg=,tag:xkMJsGubny+Di+GucAqypQ==,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]
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]
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:
age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsNzNjZ1o1dXFxalFiRXUx
U3NQK0gvQWVRbnAxam8yZmJTTmRTaVVZdkdrCnQ0R1ZBWEVmcE12NWNuaDFtRGlj
UFRManh2VFgwUFJaNFpVZFNqc01oSkEKLS0tIHA5UDlHY1lDWUtwTk10RHZoQWQ1
bzZ6MzhQQmYrZ3JKUDZoa1lDZXRHRDAKHtzHnt+zHgMsuyX0vP6xapvJ8796/vkn
u9U56OdFlqthTy870vMMoJWW3wAFfj/QV124bG63lJ02gAHEr/PGJw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrTDFvM2l3Tm5lU0paWXpF
cjVBSFhENW5mNG9DSFM1NXh3UHdaKzlKMGlZCnRmNFBFVWY4N0FqLzF1bUMyUDdL
U091VENiVFhYeEJ5K0xodXlHVkhHKzgKLS0tIGxta3A2TjJiMUtiR2RzcU02Rys5
U1c0SjRKK2UwbTVIQUMrT1pOOVFmOVkKY3UyGNIPZJLE8GG124y0pLgqGub9SMCq
plK5H+kASOB1X6pK+3PBFuDYT1AbsRxXvWgAEMvVI7eBcxQlSrrB4Q==
-----END AGE ENCRYPTED FILE-----
- recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLc0ZsVlNzQ0d1dGJlSzN6
bzB0bnhHTzlodWJveFBmdVVCdjJ5c2V0dkM4Cmt1cHhJa2U4NmJZSUFGYzhCQmdH
eVJDUjc0LzdIOHo4TWlCeVEvQUg1b1EKLS0tIGRpTFA4TkgvU2ZLOXM3NktMbjRP
aGM2aVdRSUpsRXRCZE02MXJ3MVpxK00KO2dZUNZ1KQFg4bnNp1PEntL2fY1h+JCK
l7CnGwotydc9NybwYtisv9XVrz3QoiD09OiLvg7VkmfzEaGmqmja/g==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVaXBqMGl1UytNL3BkZEhQ
S3RFL3lZRVZKTGVRTGFMNlFlWFRCNDNvRTM4CnpWZWovSDZaclQvN2Vwa0dWZGgz
Q1ZLM0sveXBxOVpvNHkycWJWWXdmVE0KLS0tIHl2bFk3RE03N01IdDJPWk5HT1Np
Qm82Sit3Q0haaDdnbzFjendMUm04Wk0KYp09dxXjzvC4IlH6Ilip8YjTz0mFeu/0
5IDMYjT1BuW5YiKgIJVd+UgOd6ysZLFFwk+Us2AcV7z110xk/askqQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjQTBxNkV2REdrRS9MaUxa
YWxNOFBKQlAwOW5qSk9hM1Q5c0tjZTdWUjBjCkM5TmtwR2RBRER3Uzc4dWtGOVM2
bjZFZVc3V0t0enhyam1DWVM3b0h5WlEKLS0tIGNPUzFJUGRYZStMRTMwV3pWTW1t
V003cnFtYVNEbERiRDV4bmVXVlBaUTAK7pLGaixTRCg5lKhN8CN95cdr7X8X1oDY
LX2t+SPvb8hqsssLf/mqVxPsgAXl0L9lfsYtRsuMWONmaJsOleVE4A==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4Wkd0YnBQRnExeVdUTGFu
N3o3MnF2aTY2NlBmdDJYT01zRytWZ2w1dFg0ClAzcnJ0NFYrVWlBM2JQU1B0SEJi
MGE5aVh6KzNmaEoxaHFOTW90K0VmMGsKLS0tIDNkOGZyVmMzME80TlBWMzI5UVR2
djB3Y2FIRDFKWlEwTnRBUnRIT3M2OXcK+SIt/7DRdQi6H1AZooJN2Pt2g1EwVTZe
Q14cEt0sLyVYzLJugfz2JWRHDZX6wPueYcTSEs7w3wAPVwvJWju8bg==
-----END AGE ENCRYPTED FILE-----
- recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbDA5U0xnUDNXYUtRVVN3
YW5aTFg1T0pOZWc4cXFDRDlrRmxZWWw1MUdRCjdlUVg0S0IxTXM4ZXcydGR0aldu
WnU3ZnUydUh4em02TWFVamx6a0xpQmMKLS0tIEdpWFg1UEVGNHIzY2VZZk40NlBG
WXJpUUxadERyYUExRFMzNzBXaUVET3cKG9ZwWy5YvTr/BAw/i+ZJos5trwRvaW5j
eV/SHiEteZZtCuCVFAp3iolE/mJyu97nA2yFwWaLN86h+/xkOJsdqA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzQXhNSFBnNUtMdkpwR0th
M1NmOVorcUdlZTFDM3dVRHZlYWpJcDZiakVnCit6eTFOeW92SzhPYzJxR0VTem9r
MSs4cWxRbzVBQmlWaHIwMjB5RUlJMXcKLS0tIHNSVTloOEVVVndDWkVrWmQrYXlD
NTd1WGFJWHVLTnFNT3hYbDdtSnMzTTAKBmJOayZLbjmBejwVzVtUSYPki+qPkYwG
xdO3L7n0Z8Cv/kVYZpkuG5GqOUL+nCJuYDjF0g4PaLb6WWd0W8ZGFA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-05-31T08:08:02Z"
mac: ENC[AES256_GCM,data:p6FHDa6Xfd66pH4zB8s6nhGGk2Ha2YTC/wUsCrqu+9M01VQ7qv9tha1MpKMj9TUxSPSxPOI++5zkNi5LJbs4Y4q0KH4yd9w/guMmJB2+d2YUwNCTofvmQp3wS1KtaRbaai6mAXZELaVEsRkmwUdkdApNbSZkTZgDc+CMH7OmHbs=,iv:w/kv2wRO6N4k1U7y8efS7LXhrpMxkZ9kTs3lFo23MA8=,tag:F4rZGG00AQZLfGU3djgW8Q==,type:str]
lastmodified: "2025-12-01T11:01:54Z"
mac: ENC[AES256_GCM,data:taGX5HHZCL7Zo4taS2Jz/5WxhvpBNNKZ13ZCtS3x/P17tC1Nrk2UDcxbOZ1pPVbVvvaAHJtDb3owFvBOM4nr2Eve0M9zT4HbXh3hke7AviQ6U7CT1ru6LjY7W8lBjbQ6uCt+Ldxd1PRPPGiyKdK5GAUPKg6avFjpJbhEikh8Gww=,iv:NNs5usVJ5izYvHKnNm1IgjSt4dg0QFQ7cClJ6zh+3wM=,tag:sYYbEWIUgOWthEItdy5PFg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.10.2
version: 3.11.0

View File

@@ -1 +1 @@
https://channels.nixos.org/nixos-25.05
https://channels.nixos.org/nixos-25.11

View File

@@ -13,7 +13,7 @@
./utils/modules/borgbackup.nix
./utils/modules/promtail
./utils/modules/victoriametrics
./modules/metrics
./modules/set-nix-channel.nix # Automatically manage nix-channel from /var/bento/channel
./hardware-configuration.nix

View File

@@ -48,15 +48,11 @@ let
doveadm -v sync -u $user $SERVER
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 -v sync -u $user $SERVER
done
doveadm user *@korean-skin.care | while read user; do
doveadm user *@scana11y.com | while read user; do
doveadm -v sync -u $user $SERVER
done
'';
@@ -193,10 +189,15 @@ in
managesieve_logout_format = bytes ( in=%i : out=%o )
}
lda_original_recipient_header = X-Original-To
plugin {
sieve_dir = /var/vmail/%d/%n/sieve/scripts/
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
fts = lucene
@@ -239,11 +240,11 @@ in
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
'';
systemd.services.dovecot2 = {
systemd.services.dovecot = {
wants = [ "acme-imap.${domain}.service" ];
after = [ "acme-imap.${domain}.service" ];
};
@@ -256,7 +257,7 @@ in
"imap-test.${domain}"
"imap-02.${domain}"
];
postRun = "systemctl --no-block restart dovecot2.service";
postRun = "systemctl --no-block restart dovecot.service";
};
networking.firewall.allowedTCPPorts = [

View File

@@ -0,0 +1,8 @@
{ config, pkgs, ... }:
{
imports = [
../../utils/modules/victoriametrics/default.nix
./postfix-exporter.nix
./dovecot-exporter.nix
];
}

View File

@@ -0,0 +1,15 @@
{ config, pkgs, lib, ... }:
{
services.prometheus.exporters.dovecot = {
enable = true;
};
services.victoriametrics.extraScrapeConfigs = [
''
- job_name: "dovecot-exporter"
static_configs:
- targets: ['localhost:9166']
''
];
}

View File

@@ -0,0 +1,16 @@
{ config, pkgs, lib, ... }:
{
services.prometheus.exporters.postfix = {
enable = true;
};
services.victoriametrics.extraScrapeConfigs = [
''
- job_name: "postfix-exporter"
static_configs:
- targets: ['localhost:9154']
''
];
}

View File

@@ -17,10 +17,10 @@ in {
olcTLSCACertificateFile = "/var/lib/acme/ldap.${domain}/full.pem";
olcTLSCertificateFile = "/var/lib/acme/ldap.${domain}/cert.pem";
olcTLSCertificateKeyFile = "/var/lib/acme/ldap.${domain}/key.pem";
olcTLSCipherSuite = "HIGH:MEDIUM:+3DES:+RC4:+aNULL";
olcTLSCipherSuite = "HIGH:!aNULL:!MD5:!3DES:!RC4";
olcTLSCRLCheck = "none";
olcTLSVerifyClient = "never";
olcTLSProtocolMin = "3.1";
olcTLSProtocolMin = "3.3";
olcSecurity = "tls=1";
};
@@ -55,20 +55,28 @@ in {
by * none
''
''
{1}to attrs=loginShell
{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 attrs=loginShell
by self 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
''
''
{2}to dn.subtree="ou=system,ou=users,dc=cloonar,dc=com"
{3}to dn.subtree="ou=system,ou=users,dc=cloonar,dc=com"
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
''
''
{3}to *
{4}to *
by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read
by dn="cn=admin,dc=cloonar,dc=com" write
by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write
@@ -103,44 +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 *
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 = {
objectClass = ["olcDatabaseConfig" "olcMdbConfig"];
@@ -160,7 +130,15 @@ in {
by * none
''
''
{1}to *
{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
@@ -198,7 +176,15 @@ in {
by * none
''
''
{1}to *
{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
@@ -236,7 +222,15 @@ in {
by * none
''
''
{1}to *
{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
@@ -274,7 +268,51 @@ in {
by * none
''
''
{1}to *
{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
''
];
};
"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
@@ -299,7 +337,7 @@ in {
(1.3.6.1.4.1.28298.1.2.4 NAME 'cloonarUser'
SUP (mailAccount) AUXILIARY
DESC 'Cloonar Account'
MAY (sshPublicKey $ ownCloudQuota $ quota))
MAY (sshPublicKey $ pgpPublicKey $ ownCloudQuota $ quota))
''
];
};
@@ -374,14 +412,22 @@ in {
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
''
''
(1.3.6.1.4.1.24552.500.1.1.1.14
NAME 'pgpPublicKey'
DESC 'PGP/GPG Public key'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
''
];
olcObjectClasses = [
''
(1.3.6.1.4.1.24552.500.1.1.2.0
NAME 'ldapPublicKey'
SUP top AUXILIARY
DESC 'MANDATORY: OpenSSH LPK objectclass'
MUST ( sshPublicKey $ uid ))
DESC 'SSH and PGP Public Key Support'
MUST ( uid )
MAY ( sshPublicKey $ pgpPublicKey ))
''
];
};

View File

@@ -128,16 +128,16 @@ in
compatibility_level = "2";
# bigger attachement size
mailbox_size_limit = "202400000";
message_size_limit = "51200000";
mailbox_size_limit = 202400000;
message_size_limit = 51200000;
smtpd_helo_required = "yes";
smtpd_delay_reject = "yes";
strict_rfc821_envelopes = "yes";
# send Limit
smtpd_error_sleep_time = "1s";
smtpd_soft_error_limit = "10";
smtpd_hard_error_limit = "20";
smtpd_soft_error_limit = 10;
smtpd_hard_error_limit = 20;
smtpd_use_tls = "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_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_session_cache_database = ''btree:''${data_directory}/smtpd_scache'';
smtpd_tls_mandatory_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1";
smtpd_tls_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1";
smtpd_tls_mandatory_ciphers = "medium";
tls_medium_cipherlist = "AES128+EECDH:AES128+EDH";
tls_medium_cipherlist = "ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20:DHE+CHACHA20";
# authentication
smtpd_sasl_auth_enable = "yes";
@@ -225,8 +224,7 @@ in
security.dhparams = {
enable = true;
params.postfix512.bits = 512;
params.postfix2048.bits = 1024;
params.postfix2048.bits = 2048;
};
security.acme.certs."mail.${domain}" = {

View File

@@ -119,7 +119,7 @@ in
# systemd.services.rspamd.serviceConfig.SupplementaryGroups = [ "redis-rspamd" ];
systemd.services.dovecot2.preStart = ''
systemd.services.dovecot.preStart = ''
mkdir -p /var/lib/dovecot/sieve/
for i in ${sieve-spam-filter}/share/sieve-rspamd-filter/*.sieve; do
dest="/var/lib/dovecot/sieve/$(basename $i)"

View File

@@ -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]
dovecot-ldap-password: ENC[AES256_GCM,data:JYAt8/WggwclNEPO9CaWfQsvQBA8DDJCU2km93HpowoVwIdvQ/0lQHeXndPYe1EmJGJ3vLErie+Zn2kDINIMqQ==,iv:HR0QJ0GgQks3NzhfXwjHupCKcPOekkiTcp5Jxbz7CxI=,tag:19m7F6TjGUPOuHQJuUq2pw==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAraEttTi84cGd2bkd1RENP
bm9zRmlNdWZtSzZJVElVWW5qTXlzS1lreTNBCm9BMnJ6bEJON2Y5aVZvVjFmQlJw
VVVpSEVRNDJaa2FadFh2U1gySHFXQmcKLS0tIEhjeG5Wb0FDMlBxWW9aem45aTdF
N1ZQNlE2aTl5OGhqTUVNa20yelNpcW8KoXud5IID1g/KOvM30wn2cJFWQ5En4M5H
kJ/cLDSIBqgOpjtEeEDtMsKG4yW3H91YbXjwQ0UkoPJorauVPWnTYw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0NWZPWXltTVNXNGxPd0hZ
R0U4VzN5WlI0WWZrRVVFMmpnckpMMkREaTBvCm54eTZtZlZzRVpwRmg4Ulp0VG5w
VnJkc29nN0VBRFR1U1J6L0RQeWlLNlkKLS0tIDJ3eTdiUWJzbURvSk1neEhyakJS
Z2MzZi8ybW1PMngyRGk4NHhIMzZsem8KZuy1TWwvkFGsAVMIEk2+bwDcsmYziUjj
Wd4wMK1XuLnJyFYPt6CwzBAPG+1LQzmYWdC9mNI00YZM6XneU3OisQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFbVcxeTJZM1dDUFhIZ3VE
ZlBaTU9tQ0Y2V2xlZFUxUXNKcjdadVVMd2w4Ck9TK2UyVFVTVSt1dzNWWUtxYzdw
SVZ3R3VjRUxDMDNRWnpRZVBHWXdzN0UKLS0tIHQ0ZW0xZDd4bFVBV0ZjZE9Jcm9F
cVd0aW1qWHFMMjh3SXhTYjJrN1ZEZHcKi9QhittNcxnz+Zzc/pyFutXg3Z8JJjgc
j3rW5N6eNJw0W50qPw0xdI44KEkWOc4vh+QGcPY57yqjSy4+SjWhWA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBZEhsK0x1QkczeFIvL0JI
UWY5R252WkZvR0s2SStlWVBMQk9ENFpaRHpRClg3VjhpYW5UbzJkODRFYWF2aGpr
ajE3aUFhZStYY0NJYlg1QTZqVHJsODAKLS0tIGsyRHlXSVQyV2RXVCswRVlsbktV
c0Z5ZXhtb0wrT0Q3WU1ONjFiNk1WOVkKHxnDqJkGfiqrlAyzJHYVbJlR1/jluFU+
hM/wENwqtlZ7RCSdG68AssgP9zukO94sV9mAtbfOdeVwXa1LU66Ncw==
-----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2TnNnOUtsbFBzS0E0bnFK
NGk4ZkRjUWdRdG15aTQwU2cwQXdycjhxa3dvCkUwUGdmQ3FPQnFhZC9NcE9LUG1O
S0lydjZkdCt2V3R4dWlnUlBUSkp2RXcKLS0tIFJ3UkZhSkhTMlZZSjdXbFBObXNQ
RW40cXUrdFAzb1B1VTUzOGY2RTcveUUKFxxBBioTXTZ3INRykgRPoYwwbbuDMiXH
/Oy5yWE74I9KZJr/2idzd34Dq8PUB28lDyiDdxlISyAS33D4H0cl1w==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5TG9wT2JHN2pOVjRueUF2
UGJkM2d5VFpLT0hKVmIwV2Qva25ubk1lK0ZBCkJiNWpuZ3grQ0lkSDlCMDBwYjRR
cDlPVHhtWlpnaVFYMFJqWWY2ZVFGNncKLS0tIFZQVVRSQXVOZnNDOHVwTHBraUx3
MVRVRlRQMFcyelNvL3FaNjc3U3VYbmsKZ+rJ/EFb3KNyyJ5hqO/wV4AtO1FJCeB/
oazkDDoFBE+uhiLmdCy41eYkqW8Owt/zrO29nITeJ5EtGAXTbACcgg==
-----END AGE ENCRYPTED FILE-----
- recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiZHFvbFMrZ1dTQzBZUkw4
dkl2UUlmcEZmZUVKeHVoSytYRzZVQ3p6T2hzCnJXaUJ4SUVaZFR3dEZtQ2ttZWNN
NHo0Znk5TjZzemtmWHdkSGlIZ04zUlkKLS0tIDRvclhTMFlsdERtQUk0azJ4ZVFM
WDMva0RCTnkzT0RWeWY5V281M0hjQkEK9o9cIFOiEwFeo+77QI9lXqdxlMCNGhOY
BtowL/7wo0Tfi7+CkBuKP/Bxp2D0x3b4OHDsoCNG0nc+55F/rDtR5A==
-----END AGE ENCRYPTED FILE-----
- recipient: age1azmxsw5llmp2nnsv3yc2l8paelmq9rfepxd8jvmswgsmax0qyyxqdnsc7t
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkMjIwT3pUcHlkc2N3eWZl
cVdtT3NGcDNyMFZ3V1lhWGdJMExyVXYwUTJFCmMrZ3dwZm1ZcVZVMnB6b1NPUDVR
UFZUaHdRVWFNKzNrdGE0ZWxUNnVOeWsKLS0tIFhnbklUMkd4ZGFrUjhUcVBKRktX
YXlwV28xR2poYnFja0xVdzRPcnZmV2sKDbM77Msos187Du6D7s1wlgEuVxqQ4cw1
Rwm64kyiQPwh1W9sPhMOZWyEvUTP4QL2Bs6aB1Javf4BDKka0PeP6A==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDZlJYSG51NEE3emlTVDM0
WEE4LzFqazdZQkRZSUlqQ0dzYURkbWc5RWxnCnJobm5LVnkxZkFIeTNWWUJvOUFU
SlZhZDBsdHhDRzFVQjhsN3F1dE9SVDAKLS0tIFBlOEwxallncjBxWDZCSkhZdlJN
b21icTBmeFM1cnVkaXAySHFzam1hYmcKULP2EuMGhspSusYPZs/DTksaZb0Asfel
mVn9Unqe2b9tT5cchGrxLiDJ+2YvfTA0s/JpDtLN+MpiRQQl0vJikg==
-----END AGE ENCRYPTED FILE-----
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]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.8.1

60
hosts/nas/STORAGE.md Normal file
View 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
View File

@@ -0,0 +1 @@
https://channels.nixos.org/nixos-25.11

Some files were not shown because too many files have changed in this diff Show More