Compare commits

...

272 Commits

Author SHA1 Message Date
d728722274 feat: nas add cyberghost 2025-12-11 11:34:57 +01:00
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
d4438c8585 refactor: notebook configration 2025-06-02 01:04:43 +02:00
0df4a4c1ec fix: update Firefox Sync configuration and proxy settings for improved functionality 2025-06-01 22:10:04 +02:00
365d15767b feat: add Firefox Sync module and update DNS settings for sync.cloonar.com 2025-06-01 17:01:12 +02:00
4969520222 feat: enhance Blackbox Exporter configuration with domain blacklist and update Grafana alerting rules 2025-06-01 11:40:04 +02:00
9cfd7f5052 fix: correct syntax for extraScrapeConfigs in VictoriaMetrics configuration 2025-06-01 09:37:16 +02:00
faad280aa0 fix: update Blackbox Exporter scrape config and Grafana alert expressions for improved monitoring accuracy 2025-06-01 09:08:36 +02:00
f1ea4b9b20 feat: implement website alerting plan with Blackbox Exporter and VictoriaMetrics integration 2025-06-01 00:47:43 +02:00
b6b90bca7d refactor: Grafana alerting rules: consolidate and reorganize alert definitions
- Deleted individual alert files for host down, inode usage, and RAM usage.
- Merged service down alerts into a new structure with separate files for each service (Gitea, Postfix, Dovecot, OpenLDAP, WireGuard).
- Introduced a new system alert structure consolidating CPU, disk, host down, inode, and RAM usage alerts.
- Updated alert conditions to use 'D' for thresholds and adjusted expressions accordingly.
- Improved annotations and labels for clarity and consistency across alerts.
2025-05-31 21:14:36 +02:00
39b9726be7 feat: add Loki datasource configuration for Grafana 2025-05-31 19:30:04 +02:00
7fc3c3db63 feat: add VictoriaMetrics module, update Dovecot Sieve extensions, and fix Grafana service expression 2025-05-31 19:21:56 +02:00
89b2a1cf45 feat: add service monitoring alerts for Gitea, Postfix, Dovecot, OpenLDAP, and WireGuard, and consolidate alerting rules in Grafana 2025-05-31 15:53:26 +02:00
94ee6bc9a4 feat: implement comprehensive service monitoring and alerting plan for OpenLDAP, Postfix, Dovecot, Gitea, and WireGuard using Grafana and VictoriaMetrics 2025-05-31 13:37:35 +02:00
81f04c6c51 refactor: remove unused MAC address entry from dnsmasq configuration, update gitea-vm to include network settings, enhance grafana-monitor with internet connectivity check, and clean up web module imports 2025-05-31 12:53:02 +02:00
d0c67baeb8 feat: add Grafana online status monitoring module with Pushover notifications 2025-05-31 11:35:17 +02:00
35fa61ef34 feat: refactor Grafana alerting rules into a consolidated system module and update individual alert files 2025-05-31 09:57:03 +02:00
8b5fb0861d feat: restructure Grafana configuration, migrate alert rules to new format and add VictoriaMetrics datasource 2025-05-31 09:27:25 +02:00
17a3602d3c feat: implement centralized alerting with vmalert and Grafana, add alert rules for CPU, disk, inode, RAM usage, and host status 2025-05-30 21:39:58 +02:00
fa42667c2a fix: update NixOS channel references to version 25.05 and adjust netdata configuration 2025-05-30 18:32:47 +02:00
d161d5f421 fix: correct API calls in lspconfig and utils, ensure proper setup for none-ls 2025-05-30 00:31:36 +02:00
a36b1e8310 fix: update AI Mailer configuration, adjust polling interval, and modify logging level
feat: update NixOS channel to 25.05 and remove unused unstable imports
fix: correct keyboard layout configuration in sway
feat: update ai-mailer package source and hash
2025-05-30 00:21:07 +02:00
640ad93684 fix: update AI Mailer configuration for Gmail and adjust logging level 2025-05-30 00:20:59 +02:00
51a3a10701 feat: add fingerprint reader setup and management instructions 2025-05-29 08:36:25 +02:00
cf340ca277 feat: add set-nix-channel module to manage nix-channel automatically 2025-05-29 00:38:12 +02:00
c0d51ee06d feat: remove unused hosts 2025-05-29 00:37:45 +02:00
53d73142ae Add a11ywatch and related configurations for Podman and Nginx
- Introduced a new module for a11ywatch with Podman support, creating a bridge network and defining backend and frontend containers.
- Configured Nginx to serve the a11ywatch application with SSL and ACME support.
- Added user and group configurations for a11ywatch.
- Created a systemd service to ensure the Podman network exists on boot.

Implement Firefox Container Controller extension and host

- Added a module for the Firefox Container Controller extension, allowing installation via Nix.
- Created a native messaging host for the extension to communicate with the container controller.
- Included CLI helpers to enqueue commands for showing and hiding containers.

Enable fingerprint authentication in PAM

- Configured fingerprint authentication for login, sudo, and swaylock services.

Setup Raspberry Pi OS image creation script

- Developed a script to create a read-only Raspberry Pi OS Lite image with Snapcast client.
- Included configuration for Wi-Fi, hostname, and Snapcast server.
- Implemented user and group setup for Snapcast client and ensured necessary services are enabled.

Document Raspberry Pi Zero W setup instructions

- Added detailed instructions for configuring Raspberry Pi OS on Zero W, including disabling unused services and setting up Snapcast client.

Create test configuration script for NixOS

- Implemented a script to perform dry-builds for NixOS configurations, allowing for easy validation of host configurations.
2025-05-29 00:10:07 +02:00
8e52274edd fix: picoreplayer remove device as no longer needed 2025-05-24 11:18:47 +02:00
cbde498ae8 fix: picoreplayer remove config param 2025-05-24 11:05:10 +02:00
9feace9558 fix: picoreplayer script 2025-05-24 10:12:51 +02:00
2a5496118b feat: add postfix to web server 2025-05-24 10:12:39 +02:00
f362b2ab77 feat: add hs-cloud to collabora 2025-05-24 10:12:23 +02:00
7a8cd490d5 feat: update nextcloud 2025-05-24 10:12:12 +02:00
5a20d97084 feat: add a firefox container 2025-05-24 10:11:48 +02:00
8def0af08f feat: add lms 2025-05-24 10:11:18 +02:00
348d8e1d03 feat: changes to home-assistant 2025-05-24 10:10:51 +02:00
a078503a89 feat: add picoreplayer script 2025-05-24 09:58:22 +02:00
bc57914131 many changes 2025-05-08 22:46:20 +02:00
3b01625f7d feat: changes to esphome 2025-05-08 22:45:53 +02:00
8cf4762a65 feat: kea unbound sync remove old leases 2025-05-01 21:28:58 +02:00
6f9b384caa fix: cloonar assistant config server 2025-04-30 15:54:45 +02:00
7ac54dd987 feat: add test to updns 2025-04-30 15:54:18 +02:00
57684db260 fix: sway display layout 2025-04-30 15:54:01 +02:00
c20998d365 fix: cloonar assistant config server 2025-04-28 22:40:36 +02:00
2f1d88b001 feat: add cloonar-assistant-customers repo 2025-04-28 22:40:17 +02:00
b8453eaf43 fix: home assistant multimedia off automation 2025-04-28 22:39:51 +02:00
87d22fba6d fix: electricity sensor home assistant 2025-04-28 22:39:37 +02:00
e4eb5c80fc fix: create the config files for ca config server the right way 2025-04-28 10:54:03 +02:00
c8e3542fe8 feat: add cloonar assistant config server 2025-04-28 10:41:06 +02:00
0ac30a5190 fix: electricity pricing 2025-04-28 10:40:44 +02:00
c02651e65a feat: add updns 2025-04-25 22:35:42 +02:00
9a5a28098c changes 2025-04-25 20:35:33 +02:00
9cfc423a38 add ai mailer 2025-03-02 03:41:42 +01:00
3b043eaf6d add vscode server microvm and update user configurations 2025-03-01 15:31:17 +01:00
386b70314d many changes 2025-02-23 16:00:33 +01:00
e2add63337 add vscode 2025-02-04 11:52:44 +01:00
7de9b583d5 add local mysql and postgresql server 2025-02-04 11:52:38 +01:00
406c0f539e add secrets for cyberghost vp 2025-02-04 11:52:23 +01:00
1651b8a550 add cyberghost vpn for chatgpt 2025-02-04 11:52:12 +01:00
ff2fdd3c08 add new projects 2025-02-04 11:52:02 +01:00
35ad68fbbe add go to lsp and update chatgpt.vim 2025-02-04 11:51:54 +01:00
bd4503c035 fix ldap logging and add sleep to certificate renewal postRun 2025-02-04 11:51:38 +01:00
c423af5498 add host to wireguard 2025-02-04 11:51:20 +01:00
a2d482e16d many changes 2025-01-26 10:55:38 +01:00
12ef36af33 changes 2024-12-29 22:04:44 +01:00
6ae6c5e0e5 some changes regarding rustdesk and a new project 2024-12-26 14:13:40 +01:00
44b47ce18c changes 2024-12-21 13:47:00 +01:00
c96c24f864 many changes and more modularizing 2024-12-12 22:30:24 +01:00
df50e70f3e refactor folder structure 2024-12-01 12:35:59 +01:00
506 changed files with 17405 additions and 33503 deletions

106
.chatgpt_config.yaml Normal file
View File

@@ -0,0 +1,106 @@
project_name: "cloonar-nixos"
default_prompt_blocks:
- "basic-prompt"
- "secure-coding"
initial_prompt: |
You are helping me build or refine a NixOS configuration (potentially with Nix Flakes). Please keep the following points in mind when generating or explaining code:
1. **Project & Directory Structure**
- For single-host configurations, you may have a simple structure like:
```
/etc/nixos/
├── configuration.nix
├── hardware-configuration.nix
└── other-module.nix
```
- For multi-host setups or more complex deployments, consider **modules** in a dedicated folder:
```
my-nix-config/
├── flake.nix # (if using Flakes)
├── hosts/
│ ├── hostname1/
│ │ └── configuration.nix
│ └── hostname2/
│ └── configuration.nix
├── modules/
│ ├── networking.nix
│ ├── services.nix
│ ├── users.nix
│ └── ...
└── hardware/
└── hardware-configuration-<machine>.nix
```
- Split large configurations into multiple `.nix` files or modules for clarity. Import them in a top-level `configuration.nix` or `flake.nix`.
2. **Nix Flakes (Optional)**
- If using Flakes, include a top-level `flake.nix` defining your outputs:
- `outputs.nixosConfigurations.<hostname> = { ... }`
- Reference your system with something like `nixos-rebuild switch --flake .#<hostname>`.
- Keep pinned inputs (e.g., `nixpkgs` at a particular commit) in your `flake.lock` to ensure reproducibility.
3. **System Configuration & Modules**
- Place typical NixOS settings (e.g., `networking.hostName`, `time.timeZone`, `environment.systemPackages`, etc.) in `configuration.nix` or a modular file structure.
- Use [NixOS modules](https://nixos.org/manual/nixos/stable/index.html#sec-writing-modules) to separate concerns. For example:
- `networking.nix` for network settings,
- `users.nix` for user/group management,
- `services.nix` for enabling/configuring system services.
- If you have custom logic or package overlays, keep them in separate files (e.g., `overlays.nix`).
4. **Home Manager Integration (Optional)**
- For user-level configuration (e.g., dotfiles, user-specific packages), consider integrating [Home Manager](https://nix-community.github.io/home-manager/) either as a standalone or via Flakes.
- Keep Home Manager configs in a separate `home.nix` file, referencing it in your main configuration or flake outputs.
5. **Security & Secrets Management**
- Avoid committing plain-text secrets (passwords, tokens) to version control.
- Consider using [sops-nix](https://github.com/Mic92/sops-nix) or other secret management solutions to encrypt sensitive files.
- Enable recommended security settings, such as:
- `security.sudo.wheelNeedsPassword = true`
- `security.rtkit.enable = true`
- `users.users.<name>.extraGroups` to limit privileges.
- Regularly update your `nixpkgs` channel or flake inputs for the latest security patches.
6. **System Services & Daemons**
- Use built-in NixOS modules for services (e.g., `services.nginx`, `services.postgresql`, etc.) instead of manual configuration whenever possible.
- For each service, ensure you:
- Set `enable = true;` if its needed,
- Provide configuration in the same module file or a dedicated file if its complex.
- Keep service-specific secrets (e.g., database passwords) out of the main config by referencing environment variables or a secret management solution.
7. **Package Management & Overlays**
- Place packages you need system-wide into `environment.systemPackages`.
- For overriding or extending packages from `nixpkgs`, use the [overlays](https://nixos.wiki/wiki/Overlays) mechanism:
```nix
self: super: {
myPackage = super.callPackage ./pkgs/my-package { };
}
```
- Maintain a dedicated `overlays/` folder if you have multiple custom overlays.
8. **Customization & Extensions**
- Use `environment.etc` or NixOS options to create or manage custom config files in `/etc/`.
- For advanced use cases, you can define your own modules to unify logic for related settings or services.
- Document each module with comments about what it configures and why.
9. **Testing & Deployment**
- Use the `nixos-rebuild test` command to evaluate changes without fully switching.
- If using Flakes, run `nixos-rebuild test --flake .#<hostname>`.
- Test critical services after switching (e.g., `systemctl status service-name`).
- Consider building virtual machines via `nixos-rebuild build-vm` or [NixOS tests](https://nixos.org/manual/nixos/stable/index.html#sec-nixos-tests) to validate complex changes.
10. **Output Format**
- Present any generated Nix configuration as well-structured `.nix` files, referencing them in a central place (`configuration.nix` or `flake.nix`).
- When explaining your reasoning, describe which modules or options you chose and why (e.g., “I separated `networking.nix` to isolate network settings from system services.”).
- If you modify existing files, specify precisely which lines or sections have changed, and why you made those changes.
Please follow these guidelines to ensure the generated or explained NixOS configuration adheres to best practices for maintainability, modularity, and security.
debug: false
improved_debug: false
preview_changes: false
interactive_file_selection: false
partial_acceptance: false
enable_debug_commands: false
prompt_char_limit: 300000
enable_step_by_step: true

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

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

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ raspberry/.env
raspberry/result
esphome/trash
esphome/.esphome

8
.mcp.json Normal file
View File

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

84
.roo/rules/rules.md Normal file
View File

@@ -0,0 +1,84 @@
# RULES.md
## Overview
This repository manages NixOS configurations for multiple systems, structured to promote modularity, security, and maintainability.
### Directory Structure
Each host has its own directory under `hosts/`, containing:
```
hosts/
└── hostname/
├── configuration.nix
├── modules/
└── secrets.yaml
```
* `configuration.nix`: Main configuration file for the host.
* `modules/`: Custom NixOS modules specific to the host.
* `secrets.yaml`: Encrypted secrets file (see [Secrets Management](#secrets-management)).
## Configuration Management
### Modularization
* Break down configurations into reusable modules placed in the `modules/` directory.
* Use the `imports` directive in `configuration.nix` to include necessary modules.
* Avoid monolithic configurations; modularity enhances clarity and reusability.
### Version Control
* Track all configuration files using Git.
* Exclude sensitive files like `secrets.yaml` from version control.
* Use descriptive commit messages to document changes.
## Deployment with Bento
Bento is utilized for deploying configurations across systems.
* Centralize configurations on a management server.
* Ensure each host accesses only its specific configuration files.
* Leverage Bento's features to manage deployments efficiently.([NixOS Discourse][1], [Reddit][2], [cbiit.github.io][3])
## Security Best Practices
### Secrets Management
* Never store plain-text secrets in the Nix store or configuration files.
* Use tools like [sops-nix](https://github.com/Mic92/sops-nix) to encrypt `secrets.yaml`.
* Restrict access to decrypted secrets using appropriate file permissions.([Reddit][4], [dade][5])
### System Hardening
* Disable unnecessary services to minimize attack surfaces.
* Configure firewalls to allow only essential traffic.
* Regularly update systems to apply security patches.
### User Management
* Implement the principle of least privilege for user accounts.
* Use SSH keys for authentication; disable password-based logins.
* Monitor user activities and access logs for suspicious behavior.
## Maintenance Guidelines
* 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.
---
Adhering to these guidelines will help maintain a secure, organized, and efficient NixOS configuration across multiple systems.
[1]: https://discourse.nixos.org/t/introducing-bento-a-nixos-deployment-framework/21446?utm_source=chatgpt.com "Introducing bento, a NixOS deployment framework"
[2]: https://www.reddit.com/r/NixOS/comments/1e95b69/how_do_you_guys_organize_your_nix_config_files_i/?utm_source=chatgpt.com "How do you guys organize your .nix config files? I have a ... - Reddit"
[3]: https://cbiit.github.io/bento-docs/master/installation/bento-quick-start.html?utm_source=chatgpt.com "1. Quick Start Tutorial — Bento release-4.1.0 documentation"
[4]: https://www.reddit.com/r/NixOS/comments/1cnhx6z/best_security_practices_for_nixos_devices_exposed/?utm_source=chatgpt.com "Best Security practices for NixOS devices exposed to the Internet"
[5]: https://0xda.de/blog/2024/07/framework-and-nixos-sops-nix-secrets-management/?utm_source=chatgpt.com "Framework and NixOS - Sops-nix Secrets Management - dade"
[6]: https://nixos-and-flakes.thiscute.world/nixos-with-flakes/modularize-the-configuration?utm_source=chatgpt.com "Modularize Your NixOS Configuration | NixOS & Flakes Book"

View File

@@ -4,22 +4,20 @@
# for a more complex example.
keys:
- &bitwarden age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 # nixos age key
- &dominik age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
- &dominik age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
- &dominik2 age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
- &git-server age106n5n3rrrss45eqqzz8pq90la3kqdtnw63uw0sfa2mahk5xpe30sxs5x58
- &web-01-server age1y6lvl5jkwc47p5ae9yz9j9kuwhy7rtttua5xhygrgmr7ehd49svsszyt42
- &web-02 age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw
- &web-arm age1ylrpaytkm0k5kcecsxvyv5xd9ts4md0uap48g6wsmj9pwm4lf5esffu0gw
- &home-assistant-server age1ezq2j34qngky22enhnslx6hzh4ekwk8dtmn6c9us0uqxqpn7hgpsspjz58
- &ldap-server-test age1azmxsw5llmp2nnsv3yc2l8paelmq9rfepxd8jvmswgsmax0qyyxqdnsc7t
- &testmodules age1zkzpnfeakyvg3fqtyay32sushjx2hqe28y6hs6ss7plemzqjqa5s6s5yu3
- &ldap-server-arm age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4
- &fw age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df
- &fw-new age12msc2c6drsaw0yk2hjlaw0q0lyq0emjx5e8rq7qc7ql689k593kqfmhss2
- &netboot age14uarclad0ty5supc8ep09793xrnwkv8a4h9j0fq8d8lc92n2dadqkf64vw
- &gpd-win4 age1ceg548u5ma6rgu3xgvd254y5xefqrdqfqhcjsjp3255q976fgd2qaua53d
- &nb age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
- &amzebs-01 age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
- &nas age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek
- &mail-social-grow-tech age1gtulvdj4aclpfhk3mmzvpz9xysccxhvu99x6ayaqlj8m44ehffgq6zuc5u
- &web-social-grow-tech age1md4kkdf08zmagqv0yzza8h75f80c9j8np2p6eqea6fpa94szd5lsltz9va
creation_rules:
- path_regex: ^[^/]+\.yaml$
key_groups:
@@ -27,24 +25,29 @@ creation_rules:
- *bitwarden
- *dominik
- *dominik2
- path_regex: hosts/nb-01.cloonar.com/[^/]+\.yaml$
- *nb
- path_regex: hosts/nb/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- path_regex: hosts/nb-new.cloonar.com/[^/]+\.yaml$
- *nb
- path_regex: hosts/gpd-win4/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- path_regex: hosts/fw.cloonar.com/[^/]+\.yaml$
- *gpd-win4
- *nb
- path_regex: hosts/fw/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *fw
- path_regex: hosts/fw-new/[^/]+\.yaml$
key_groups:
@@ -52,118 +55,102 @@ creation_rules:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *fw
- *fw-new
- path_regex: hosts/fw.cloonar.com/modules/web/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *web-02
- path_regex: hosts/web-01.cloonar.com/[^/]+\.yaml$
- path_regex: hosts/fw-new/modules/web/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *web-01-server
- *web-02
- path_regex: hosts/web-arm/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *web-arm
- path_regex: hosts/mail.cloonar.com/[^/]+\.yaml$
- 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
- *ldap-server-test
- path_regex: hosts/mail.social-grow.tech/[^/]+\.yaml$
- path_regex: hosts/fw/modules/web/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *mail-social-grow-tech
- path_regex: hosts/web.social-grow.tech/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *web-social-grow-tech
- *nb
- *web-02
- path_regex: utils/modules/lego/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *git-server
- *web-01-server
- *web-02
- *web-arm
- *home-assistant-server
- *ldap-server-arm
- *ldap-server-test
- *testmodules
- *netboot
- *fw
- *fw-new
- *mail-social-grow-tech
- *web-social-grow-tech
- path_regex: hosts/web-01.cloonar.com/modules/bitwarden/[^/]+\.yaml$
- path_regex: utils/modules/attic-cache/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *web-01-server
- path_regex: hosts/web-01.cloonar.com/modules/zammad/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *web-01-server
- path_regex: utils/modules/plausible/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *web-01-server
- *nb
- path_regex: utils/modules/promtail/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *git-server
- *web-01-server
- *nb
- *web-arm
- *home-assistant-server
- *ldap-server-arm
- *ldap-server-test
- *testmodules
- *netboot
- *fw
- *fw-new
- *nas
- *amzebs-01
- path_regex: utils/modules/victoriametrics/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *git-server
- *web-01-server
- *nb
- *web-arm
- *home-assistant-server
- *ldap-server-arm
- *ldap-server-test
- *testmodules
- *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,8 +2,8 @@
- 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
nix-shell -p ssh-to-age --run 'ssh-keyscan example.com | ssh-to-age'
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
```console
@@ -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
@@ -58,3 +58,62 @@ umask 0077; wg genpsk > psk
```console
nix hash to-sri --type sha256 $(nix-prefetch-url https://tar.gz)
```
# 8. Fingerprint Reader Setup (e.g., on Framework Laptop with Goodix reader)
This section assumes you have configured fingerprint support in your NixOS configuration, for example, by creating and importing a module like `hosts/nb/modules/fingerprint.nix` with the following content:
```nix
# hosts/nb/modules/fingerprint.nix
{ config, pkgs, ... }:
{
services.fprintd.enable = true;
security.pam.services.login.fprintAuth = true;
security.pam.services.sudo.fprintAuth = true;
# Add other services like swaylock if needed
# security.pam.services.swaylock.fprintAuth = true;
}
```
After rebuilding your NixOS configuration (`sudo nixos-rebuild switch`), you can enroll fingerprints for a user.
## Enrolling Fingerprints
To enroll a fingerprint for the current user:
```console
fprintd-enroll
```
Or for a specific user (e.g., `dominik`):
```console
fprintd-enroll dominik
```
Follow the on-screen prompts to scan your fingerprint multiple times.
## Verifying Enrollment
You can verify enrolled fingerprints:
```console
fprintd-verify
```
## Listing Enrolled Fingerprints
To see which fingers are enrolled for the current user:
```console
fprintd-list $(whoami)
```
Or for a specific user:
```console
fprintd-list dominik
```
## Deleting Fingerprints
To delete all fingerprints for the current user:
```console
fprintd-delete $(whoami)
```
Or for a specific user:
```console
fprintd-delete dominik
```
You can also delete specific fingerprints by their ID if you know it.

2
buchhaltung.md Normal file
View File

@@ -0,0 +1,2 @@
Bei EU Rechnungen das Hakerl machen bei "Nicht im Inland steuerbare Leistung (außerhalb EU, z.B. Schweiz)"
VXEhGveIHdSj7JKq6zof48vLhKaCo0RJea6DhVqopA8=

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

@@ -0,0 +1,340 @@
substitutions:
# Default name
name: "hallway-light-switch"
# Default friendly name
friendly_name: "Hallway Light Switch"
# Allows ESP device to be automatically linked to an 'Area' in Home Assistant. Typically used for areas such as 'Lounge Room', 'Kitchen' etc
room: "Hallway"
# Description as appears in ESPHome & top of webserver page
device_description: "Hallway Light Switch"
# Project Name
project_name: "Athom Technology.Mini Relay V2"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v2.0.4"
# Restore the relay (GPO switch) upon reboot to state:
light_restore_mode: RESTORE_DEFAULT_OFF
# Set the update interval for sensors
sensor_update_interval: 10s
# Current Limit in Amps.
current_limit : "10"
# Define a domain for this device to use. i.e. iot.home.lan (so device will appear as athom-smart-plug-v2.iot.home.lan in DNS/DHCP logs)
dns_domain: ".cloonar.smart"
# Set timezone of the smart plug. Useful if the plug is in a location different to the HA server. Can be entered in unix Country/Area format (i.e. "Australia/Sydney")
timezone: ""
# Set the duration between the sntp service polling ntp.org servers for an update
sntp_update_interval: 6h
# Network time servers for your region, enter from lowest to highest priority. To use local servers update as per zones or countries at: https://www.ntppool.org/zone/@
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.pool.ntp.org"
sntp_server_3: "2.pool.ntp.org"
# Enables faster network connections, with last connected SSID being connected to and no full scan for SSID being undertaken
wifi_fast_connect: "false"
# Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
log_level: "WARN"
# Hide the ENERGY sensor that shows kWh consumed, but with no time period associated with it. Resets when device restarted and reflashed.
hide_energy_sensor: "true"
# Enable or disable the use of IPv6 networking on the device
ipv6_enable: "false"
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: false
min_version: 2024.5.0
project:
name: "${project_name}"
version: "${project_version}"
platformio_options:
board_build.mcu: esp32c3
board_build.variant: esp32c3
board_build.flash_mode: dio
esp32:
board: esp32-c3-devkitm-1
flash_size: 4MB
variant: ESP32C3
framework:
type: arduino
version: recommended
preferences:
flash_write_interval: 5min
api:
ota:
- platform: esphome
mdns:
disabled: false
web_server:
port: 80
network:
enable_ipv6: ${ipv6_enable}
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: True
domain: ${dns_domain}
esp32_improv:
authorizer: none
uart:
rx_pin: GPIO20
baud_rate: 4800
data_bits: 8
stop_bits: 1
parity: EVEN
globals:
- id: total_energy
type: float
restore_value: yes
initial_value: '0.0'
binary_sensor:
- platform: status
name: "Status"
entity_category: diagnostic
- platform: gpio
pin:
number: GPIO3
mode: INPUT_PULLUP
inverted: true
name: "Power Button"
disabled_by_default: true
on_multi_click:
- timing:
- ON for at most 1s
- OFF for at least 0.2s
then:
- light.toggle: mini_relay
- timing:
- ON for at least 4s
then:
- button.press: Reset
- platform: gpio
id: the_switch
name: "Power Switch"
pin:
number: GPIO4
mode: INPUT_PULLUP
inverted: true
on_multi_click:
- timing:
- ON for at most 1s
then:
- light.toggle: mini_relay
sensor:
- platform: uptime
name: "Uptime Sensor"
id: uptime_sensor
entity_category: diagnostic
internal: true
- platform: wifi_signal
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: "diagnostic"
- platform: copy
source_id: wifi_signal_db
name: "WiFi Signal Percent"
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: "diagnostic"
device_class: ""
- platform: cse7766
id: athom_cse7766
current:
name: "Current"
filters:
- throttle_average: ${sensor_update_interval}
- lambda: if (x < 0.060) return 0.0; else return x; #For the chip will report less than 3w power when no load is connected
on_value_range:
- above: ${current_limit}
then:
- light.turn_off: mini_relay
voltage:
name: "Voltage"
filters:
- throttle_average: ${sensor_update_interval}
power:
name: "Power"
id: power_sensor
filters:
- throttle_average: ${sensor_update_interval}
- lambda: if (x < 3.0) return 0.0; else return x; #For the chip will report less than 3w power when no load is connected
energy:
name: "Energy"
id: energy
unit_of_measurement: kWh
filters:
- throttle: ${sensor_update_interval}
# Multiplication factor from W to kW is 0.001
- multiply: 0.001
on_value:
then:
- lambda: |-
static float previous_energy_value = 0.0;
float current_energy_value = id(energy).state;
id(total_energy) += current_energy_value - previous_energy_value;
previous_energy_value = current_energy_value;
id(total_energy_sensor).update();
apparent_power:
name: "Apparent Power"
filters:
- throttle_average: ${sensor_update_interval}
reactive_power:
name: "Reactive Power"
filters:
- throttle_average: ${sensor_update_interval}
power_factor:
name: "Power Factor"
filters:
- throttle_average: ${sensor_update_interval}
- platform: template
name: "Total Energy"
id: total_energy_sensor
unit_of_measurement: kWh
device_class: "energy"
state_class: "total_increasing"
icon: "mdi:lightning-bolt"
accuracy_decimals: 3
lambda: |-
return id(total_energy);
update_interval: ${sensor_update_interval}
- platform: total_daily_energy
name: "Total Energy Today"
restore: true
power_id: power_sensor
unit_of_measurement: kWh
accuracy_decimals: 3
filters:
- multiply: 0.001
button:
- platform: restart
name: "Restart"
entity_category: config
- platform: factory_reset
name: "Factory Reset"
id: Reset
entity_category: config
- platform: safe_mode
name: "Safe Mode"
internal: false
entity_category: config
output:
- platform: gpio
id: relay_output
pin: GPIO6
light:
- platform: status_led
id: led
name: "Blue LED"
disabled_by_default: true
pin:
number: GPIO7
inverted: true
- platform: binary
id: mini_relay
output: relay_output
name: "Mini Switch"
restore_mode: ${light_restore_mode}
on_turn_on:
- light.turn_on: led
on_turn_off:
- light.turn_off: led
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
entity_category: diagnostic
ssid:
name: "Connected SSID"
entity_category: diagnostic
mac_address:
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);
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
seconds = 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 {
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");'

View File

@@ -1,78 +1,214 @@
substitutions:
device_name: "livingroom-bulb-1"
name: "livingroom-bulb-1"
friendly_name: "Living Room Bulb 1"
room: "Living Room"
device_description: "athom 7w rgbcw light bulb"
project_name: "Athom Technology.Athom RGBCW Bulb"
dns_domain: ".cloonar.smart"
timezone: ""
sntp_update_interval: 6h
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.pool.ntp.org"
sntp_server_3: "2.pool.ntp.org"
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
name: "${name}"
friendly_name: "${friendly_name}"
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: false
min_version: 2024.6.0
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
priority: 300
then:
- light.turn_on:
id: my_light
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: my_light
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: my_light
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: my_light
id: rgbww_light
brightness: 100%
# Enable Home Assistant API
esp8266:
board: esp8285
restore_from_flash: true
preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
platform: esphome
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: .cloonar.smart
domain: "${dns_domain}"
light:
- platform: rgbw
id: my_light
name: ${friendly_name}
red: pwm_r
green: pwm_g
blue: pwm_b
white: pwm_w
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
channel: 1
hidden: true
manual_ip:
static_ip: 10.42.100.11
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
name: "Status"
entity_category: diagnostic
sensor:
- platform: uptime
name: "Uptime Sensor"
id: uptime_sensor
entity_category: diagnostic
internal: true
- platform: wifi_signal
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
name: "WiFi Signal Percent"
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: diagnostic
output:
- platform: esp8266_pwm
pin: GPIO13
frequency: 1000 Hz
id: pwm_r
id: red_output
pin: GPIO4
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: green_output
pin: GPIO12
frequency: 1000 Hz
id: pwm_g
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: blue_output
pin: GPIO14
frequency: 1000 Hz
id: pwm_b
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: warm_white_output
pin: GPIO13
min_power: 0.000499
max_power: 0.9
- platform: esp8266_pwm
id: white_output
pin: GPIO5
frequency: 1000 Hz
id: pwm_w
min_power: 0.000499
max_power: 0.9
light:
- platform: rgbww
id: rgbww_light
name: "RGBCW_Bulb"
red: red_output
green: green_output
blue: blue_output
warm_white: warm_white_output
cold_white: white_output
cold_white_color_temperature: 6000 K
warm_white_color_temperature: 3000 K
color_interlock: true
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
entity_category: diagnostic
ssid:
name: "Connected SSID"
entity_category: diagnostic
mac_address:
name: "Mac Address"
entity_category: diagnostic
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
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 {
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
timezone: "${timezone}"
update_interval: ${sntp_update_interval}
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
on_time_sync:
then:
- 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");'

View File

@@ -1,78 +1,214 @@
substitutions:
device_name: "livingroom-bulb-2"
name: "livingroom-bulb-2"
friendly_name: "Living Room Bulb 2"
room: "Living Room"
device_description: "athom 7w rgbcw light bulb"
project_name: "Athom Technology.Athom RGBCW Bulb"
dns_domain: ".cloonar.smart"
timezone: ""
sntp_update_interval: 6h
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.pool.ntp.org"
sntp_server_3: "2.pool.ntp.org"
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
name: "${name}"
friendly_name: "${friendly_name}"
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: false
min_version: 2024.6.0
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
priority: 300
then:
- light.turn_on:
id: my_light
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: my_light
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: my_light
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: my_light
id: rgbww_light
brightness: 100%
# Enable Home Assistant API
esp8266:
board: esp8285
restore_from_flash: true
preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
platform: esphome
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: .cloonar.smart
domain: "${dns_domain}"
light:
- platform: rgbw
id: my_light
name: ${friendly_name}
red: pwm_r
green: pwm_g
blue: pwm_b
white: pwm_w
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
channel: 1
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
name: "Status"
entity_category: diagnostic
sensor:
- platform: uptime
name: "Uptime Sensor"
id: uptime_sensor
entity_category: diagnostic
internal: true
- platform: wifi_signal
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
name: "WiFi Signal Percent"
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: diagnostic
output:
- platform: esp8266_pwm
pin: GPIO13
frequency: 1000 Hz
id: pwm_r
id: red_output
pin: GPIO4
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: green_output
pin: GPIO12
frequency: 1000 Hz
id: pwm_g
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: blue_output
pin: GPIO14
frequency: 1000 Hz
id: pwm_b
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: warm_white_output
pin: GPIO13
min_power: 0.000499
max_power: 0.9
- platform: esp8266_pwm
id: white_output
pin: GPIO5
frequency: 1000 Hz
id: pwm_w
min_power: 0.000499
max_power: 0.9
light:
- platform: rgbww
id: rgbww_light
name: "RGBCW_Bulb"
red: red_output
green: green_output
blue: blue_output
warm_white: warm_white_output
cold_white: white_output
cold_white_color_temperature: 6000 K
warm_white_color_temperature: 3000 K
color_interlock: true
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
entity_category: diagnostic
ssid:
name: "Connected SSID"
entity_category: diagnostic
mac_address:
name: "Mac Address"
entity_category: diagnostic
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
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 {
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
timezone: "${timezone}"
update_interval: ${sntp_update_interval}
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
on_time_sync:
then:
- 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");'

View File

@@ -1,78 +1,214 @@
substitutions:
device_name: "livingroom-bulb-3"
name: "livingroom-bulb-3"
friendly_name: "Living Room Bulb 3"
room: "Living Room"
device_description: "athom 7w rgbcw light bulb"
project_name: "Athom Technology.Athom RGBCW Bulb"
dns_domain: ".cloonar.smart"
timezone: ""
sntp_update_interval: 6h
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.pool.ntp.org"
sntp_server_3: "2.pool.ntp.org"
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
name: "${name}"
friendly_name: "${friendly_name}"
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: false
min_version: 2024.6.0
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
priority: 300
then:
- light.turn_on:
id: my_light
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: my_light
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: my_light
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: my_light
id: rgbww_light
brightness: 100%
# Enable Home Assistant API
esp8266:
board: esp8285
restore_from_flash: true
preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
platform: esphome
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: .cloonar.smart
domain: "${dns_domain}"
light:
- platform: rgbw
id: my_light
name: ${friendly_name}
red: pwm_r
green: pwm_g
blue: pwm_b
white: pwm_w
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
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
name: "Status"
entity_category: diagnostic
sensor:
- platform: uptime
name: "Uptime Sensor"
id: uptime_sensor
entity_category: diagnostic
internal: true
- platform: wifi_signal
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
name: "WiFi Signal Percent"
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: diagnostic
output:
- platform: esp8266_pwm
pin: GPIO13
frequency: 1000 Hz
id: pwm_r
id: red_output
pin: GPIO4
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: green_output
pin: GPIO12
frequency: 1000 Hz
id: pwm_g
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: blue_output
pin: GPIO14
frequency: 1000 Hz
id: pwm_b
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: warm_white_output
pin: GPIO13
min_power: 0.000499
max_power: 0.9
- platform: esp8266_pwm
id: white_output
pin: GPIO5
frequency: 1000 Hz
id: pwm_w
min_power: 0.000499
max_power: 0.9
light:
- platform: rgbww
id: rgbww_light
name: "RGBCW_Bulb"
red: red_output
green: green_output
blue: blue_output
warm_white: warm_white_output
cold_white: white_output
cold_white_color_temperature: 6000 K
warm_white_color_temperature: 3000 K
color_interlock: true
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
entity_category: diagnostic
ssid:
name: "Connected SSID"
entity_category: diagnostic
mac_address:
name: "Mac Address"
entity_category: diagnostic
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
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 {
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
timezone: "${timezone}"
update_interval: ${sntp_update_interval}
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
on_time_sync:
then:
- 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");'

View File

@@ -1,78 +1,214 @@
substitutions:
device_name: "livingroom-bulb-4"
name: "livingroom-bulb-4"
friendly_name: "Living Room Bulb 4"
room: "Living Room"
device_description: "athom 7w rgbcw light bulb"
project_name: "Athom Technology.Athom RGBCW Bulb"
dns_domain: ".cloonar.smart"
timezone: ""
sntp_update_interval: 6h
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.pool.ntp.org"
sntp_server_3: "2.pool.ntp.org"
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
name: "${name}"
friendly_name: "${friendly_name}"
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: false
min_version: 2024.6.0
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
priority: 300
then:
- light.turn_on:
id: my_light
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: my_light
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: my_light
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: my_light
id: rgbww_light
brightness: 100%
# Enable Home Assistant API
esp8266:
board: esp8285
restore_from_flash: true
preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
platform: esphome
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: .cloonar.smart
domain: "${dns_domain}"
light:
- platform: rgbw
id: my_light
name: ${friendly_name}
red: pwm_r
green: pwm_g
blue: pwm_b
white: pwm_w
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
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
name: "Status"
entity_category: diagnostic
sensor:
- platform: uptime
name: "Uptime Sensor"
id: uptime_sensor
entity_category: diagnostic
internal: true
- platform: wifi_signal
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
name: "WiFi Signal Percent"
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: diagnostic
output:
- platform: esp8266_pwm
pin: GPIO13
frequency: 1000 Hz
id: pwm_r
id: red_output
pin: GPIO4
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: green_output
pin: GPIO12
frequency: 1000 Hz
id: pwm_g
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: blue_output
pin: GPIO14
frequency: 1000 Hz
id: pwm_b
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: warm_white_output
pin: GPIO13
min_power: 0.000499
max_power: 0.9
- platform: esp8266_pwm
id: white_output
pin: GPIO5
frequency: 1000 Hz
id: pwm_w
min_power: 0.000499
max_power: 0.9
light:
- platform: rgbww
id: rgbww_light
name: "RGBCW_Bulb"
red: red_output
green: green_output
blue: blue_output
warm_white: warm_white_output
cold_white: white_output
cold_white_color_temperature: 6000 K
warm_white_color_temperature: 3000 K
color_interlock: true
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
entity_category: diagnostic
ssid:
name: "Connected SSID"
entity_category: diagnostic
mac_address:
name: "Mac Address"
entity_category: diagnostic
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
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 {
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
timezone: "${timezone}"
update_interval: ${sntp_update_interval}
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
on_time_sync:
then:
- 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");'

View File

@@ -1,78 +1,214 @@
substitutions:
device_name: "livingroom-bulb-5"
name: "livingroom-bulb-5"
friendly_name: "Living Room Bulb 5"
room: "Living Room"
device_description: "athom 7w rgbcw light bulb"
project_name: "Athom Technology.Athom RGBCW Bulb"
dns_domain: ".cloonar.smart"
timezone: ""
sntp_update_interval: 6h
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.pool.ntp.org"
sntp_server_3: "2.pool.ntp.org"
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
name: "${name}"
friendly_name: "${friendly_name}"
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: false
min_version: 2024.6.0
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
priority: 300
then:
- light.turn_on:
id: my_light
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: my_light
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: my_light
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: my_light
id: rgbww_light
brightness: 100%
# Enable Home Assistant API
esp8266:
board: esp8285
restore_from_flash: true
preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
platform: esphome
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: .cloonar.smart
domain: "${dns_domain}"
light:
- platform: rgbw
id: my_light
name: ${friendly_name}
red: pwm_r
green: pwm_g
blue: pwm_b
white: pwm_w
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
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
name: "Status"
entity_category: diagnostic
sensor:
- platform: uptime
name: "Uptime Sensor"
id: uptime_sensor
entity_category: diagnostic
internal: true
- platform: wifi_signal
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
name: "WiFi Signal Percent"
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: diagnostic
output:
- platform: esp8266_pwm
pin: GPIO13
frequency: 1000 Hz
id: pwm_r
id: red_output
pin: GPIO4
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: green_output
pin: GPIO12
frequency: 1000 Hz
id: pwm_g
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: blue_output
pin: GPIO14
frequency: 1000 Hz
id: pwm_b
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: warm_white_output
pin: GPIO13
min_power: 0.000499
max_power: 0.9
- platform: esp8266_pwm
id: white_output
pin: GPIO5
frequency: 1000 Hz
id: pwm_w
min_power: 0.000499
max_power: 0.9
light:
- platform: rgbww
id: rgbww_light
name: "RGBCW_Bulb"
red: red_output
green: green_output
blue: blue_output
warm_white: warm_white_output
cold_white: white_output
cold_white_color_temperature: 6000 K
warm_white_color_temperature: 3000 K
color_interlock: true
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
entity_category: diagnostic
ssid:
name: "Connected SSID"
entity_category: diagnostic
mac_address:
name: "Mac Address"
entity_category: diagnostic
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
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 {
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
timezone: "${timezone}"
update_interval: ${sntp_update_interval}
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
on_time_sync:
then:
- 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");'

View File

@@ -1,78 +1,214 @@
substitutions:
device_name: "livingroom-bulb-6"
name: "livingroom-bulb-6"
friendly_name: "Living Room Bulb 6"
room: "Living Room"
device_description: "athom 7w rgbcw light bulb"
project_name: "Athom Technology.Athom RGBCW Bulb"
dns_domain: ".cloonar.smart"
timezone: ""
sntp_update_interval: 6h
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.pool.ntp.org"
sntp_server_3: "2.pool.ntp.org"
esphome:
name: ${device_name}
comment: ${friendly_name}
platform: ESP8266
board: esp01_1m
name: "${name}"
friendly_name: "${friendly_name}"
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: false
min_version: 2024.6.0
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
priority: 300
then:
- light.turn_on:
id: my_light
id: rgbww_light
- delay: 100ms
- light.turn_on:
id: my_light
id: rgbww_light
brightness: 20%
- delay: 100ms
- light.turn_on:
id: my_light
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: my_light
id: rgbww_light
brightness: 100%
# Enable Home Assistant API
esp8266:
board: esp8285
restore_from_flash: true
preferences:
flash_write_interval: 1min
api:
batch_delay: 0ms
ota:
platform: esphome
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Disable fast_connect so we do a full scan (required for hidden SSIDs)
fast_connect: True
domain: .cloonar.smart
domain: "${dns_domain}"
light:
- platform: rgbw
id: my_light
name: ${friendly_name}
red: pwm_r
green: pwm_g
blue: pwm_b
white: pwm_w
# Your hidden network
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
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
name: "Status"
entity_category: diagnostic
sensor:
- platform: uptime
name: "Uptime Sensor"
id: uptime_sensor
entity_category: diagnostic
internal: true
- platform: wifi_signal
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 60s
entity_category: diagnostic
- platform: copy
source_id: wifi_signal_db
name: "WiFi Signal Percent"
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: diagnostic
output:
- platform: esp8266_pwm
pin: GPIO13
frequency: 1000 Hz
id: pwm_r
id: red_output
pin: GPIO4
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: green_output
pin: GPIO12
frequency: 1000 Hz
id: pwm_g
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: blue_output
pin: GPIO14
frequency: 1000 Hz
id: pwm_b
min_power: 0.000499
max_power: 1
- platform: esp8266_pwm
id: warm_white_output
pin: GPIO13
min_power: 0.000499
max_power: 0.9
- platform: esp8266_pwm
id: white_output
pin: GPIO5
frequency: 1000 Hz
id: pwm_w
min_power: 0.000499
max_power: 0.9
light:
- platform: rgbww
id: rgbww_light
name: "RGBCW_Bulb"
red: red_output
green: green_output
blue: blue_output
warm_white: warm_white_output
cold_white: white_output
cold_white_color_temperature: 6000 K
warm_white_color_temperature: 3000 K
color_interlock: true
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
entity_category: diagnostic
ssid:
name: "Connected SSID"
entity_category: diagnostic
mac_address:
name: "Mac Address"
entity_category: diagnostic
- platform: template
name: 'Last Restart'
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
- platform: template
name: "Uptime"
entity_category: diagnostic
lambda: |-
int seconds = (id(uptime_sensor).state);
int days = seconds / (24 * 3600);
seconds %= (24 * 3600);
int hours = seconds / 3600;
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 {
return { (String(seconds) + "s").c_str() };
}
icon: mdi:clock-start
time:
- platform: sntp
id: sntp_time
timezone: "${timezone}"
update_interval: ${sntp_update_interval}
servers:
- "${sntp_server_1}"
- "${sntp_server_2}"
- "${sntp_server_3}"
on_time_sync:
then:
- 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");'

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

@@ -19,42 +19,42 @@
};
users = [
{
username = "web-01.cloonar.com";
key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCl7cvUGMmtpVfJ3PB4Rco7V8z83nivst77SgBn+Z3cHgcDJDu9l3L4Q6rv9b6thmEX+Xf0ri6UwDI8UuJro4F9qpCXsTkHres3f/pDZokgfO7bvU2l7ujq6NnAx0qJWdB6oku36x3t2wBnvkDijXLtGPeQbd6c33hECEwA7QszvoBbGi0yFiGsqR5W7o0kiju/LMzCkExeaspFV6DBtEW0qZVMYx+lBIK5Hi/g3vBjbhFdWGz8T2AITcAnGI9n6f+dg3dlMPEHXnF9KRod1EVDnYMxbEp49i98m65F1xAFwOo35WSg48LlV1PK1VusboE3pHgE2VEFmW1J+PVQZ+z0JAaRBv/wSVN0YzuCLfLtUr10K1W23YbT1UVm7FusKpT1KElZ9adfbk6SXVhXnru40VcwqgYfw7naQJzT8aDI9Tnci+z4xCCxrdUF/psDBPD5sfjMPbjdPbt6Jnx1H9ZodiC/sQUtbn6MMbenMSf/AmuUC9xzpXlqCtPmN1dSC+8= root@web-01";
}
{
username = "web-arm";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzJRWe8hsqAVnGSjPrcheloteWMzORoQ5Gj4IfhCROF";
}
{
username = "mail.cloonar.com";
username = "mail";
key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCfEuRazRv8zKWJSq+T3SssgOrkBFu6y/t6uoMNrD3P9WHowRDejo2rBsWFgPszhfgxLpWHiuSZFMG8z+07k5fVTdmbUwx0vXI1lmQ7AxB/CPwBef2Vpb7b8Rq6geejvP8X6UjQWP0rsCMtoX2SeBDTG8bDlyq1U3vYxVY4hery6a9Wu57OI5VbSIHhqQvExo7euz8V7ORsLyT8gi9x3r8gNaKJmvssB6QXXZ7U2sJaAUjhV/BmrZJD5qR9EwqwiMPJ2+SkZ0Vz6CFG6GLyB/ngXPEfclLKK7AzookJy7WepqojjFTzmOBMH903oR+MIpjDECKxgaFtW4xY0A/tj8ZDCBPtP8AKjediOASkAi7eUMPseQKDE0BNLSidC0hlQUe0aPaMeA8b1U86PblzpgF8ntkUPbxhO0AgHKq9fPN+f58f75fryNbhgPRRkeLet1q3hxguEMg2MIg/EqIw862YPWPtGRk0wJHwQU7jx+9BbjdptAVTJo/Cj9vM7mpZphE= root@mail";
}
{
username = "nb-01.cloonar.com";
key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7";
username = "nb";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6g/lXONzSW1JbyXnj+/0QPWtaiNxu9A0GOCbi96603";
}
{
username = "nb-new.cloonar.com";
username = "nb-new";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC1dDoAJUY58I+4SSfDAkO5kInsMcJT/r/mW+MYXLQVR";
}
{
username = "fw.cloonar.com";
username = "fw";
key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDtxpJAFohRtBaET9e7EE4I6UmeUT/h1ZTD1zeOHFiWB/AT71ooDT4/QukJOA3LqklDjtDQHH+qjGY50Wa8/oGTA/X3aBDPg5GAHN+U+kYO2UTC69VVjh4TTS35ijg+AdgegtMI4c0VIUMZB24tthV9KEbD20w6XnTzy2Q6PjbBrwsOeHYr9pkygJZDU65ZeKmLyR6yLaadHzXX1I7V2SwiakPEebhQaGipm540d+tAbirKCHcmiORkpd++e3dfwi25hC9bCQ7b3bdaFPAmuhhFEid4jpCt79X+l0qqpClgRLziBjYykNJDFKAljFBJA11/3ofPCuaBCDUuJVhAH044gtT3sbvJq1prd8ElZy6L1yc5YbfFgDMwi71Y2hef780NmDs5Opk9xUCKqdl1YfLyUDgdiiaZ8uhUMd2Ai9BAxJAXtcz/V41ngt3YkUVyGTZdTAODIKk44blGIkgs7JO4yam4UB1curbD0faIZnWLyS5pdFQ+FI05YVjoHXJdme8=";
}
{
username = "fw-new";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILnb9todh2b+c3iCmEz72smRwL37aZf3Xs3voT7+PLTP";
}
{
username = "mail.social-grow.tech";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH1K4mhBji1kMGnO55OOFaDknBf2Q6wgm7DaMYKip+S5";
username = "gpd-win4";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjfS2DtS8PQgkf86dU+EVu5t+r/QlCWmY7+RPYprQrO";
}
{
username = "web.social-grow.tech";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIw4lHUd/+rHIWP2WBAj9smo2CkeHEOHhTqZzacmxMcC";
username = "nas";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICS6b97LPUpr7/kWvOcI40s5e+gfbfz0I2/hAPL6zTmU";
}
{
username = "amzebs-01";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMkFZ60SPl8pzEtGrFq1+n6ZkDuNe3xJaccJMjr3y/q";
}
];
in {

57
gpd-win-4.md Normal file
View File

@@ -0,0 +1,57 @@
I want a wall-mounted docking solution for my GPD Win 4, designed in OpenSCAD 2021.1. Here are the requirements and clarifications:
Orientation & Fit
The GPD Win 4 should be inserted upside down (top facing down), with the screen facing the wall.
It slides in from the top and is guided by side rails.
There should be a small clearance so the GPD Win 4 can be easily inserted/removed without excessive friction.
Front Rail (Lip)
Side Rails:
The dock should have two side rails that run from top to bottom, guiding the GPD Win 4.
The front is open for airflow.
However, there should be a small lip (front rail) on each side, running from top to bottom and connected to the side rails. This lip prevents the GPD Win 4 from falling out forward.
Back Plate / Wall Mount
The dock has a solid back plate that mounts to the wall with two countersunk screws.
The default spacing and size of these screws can be parameterized (e.g., an M4 or M3 countersunk hole).
The back plate thickness should be sufficient for strength (e.g., 34 mm).
No special side or back vents are needed.
Cable Brackets
At the bottom, back, inside the dock, there are two brackets, one for a 90° USB-C cable (standard USB-C power) and one for a 90° Oculink flat cable.
The back plate should be open where these two brackets are, so the cables can exit the dock.
Each bracket should have:
An opening on the side facing the wall, to allow the cable to pass behind (i.e., into or through the wall).
A hole for an M3 screw that presses against the cable from the side to lock it in place.
Enough space to seat a 90° connector so it points upwards to plug into the GPD Win 4.
Parametric Design
The design should be fully parameterized in OpenSCAD, including (but not limited to) the following parameters:
device_width, device_thickness, device_length (for the GPD Win 4)
clearance_x, clearance_y, clearance_z (how much extra space around the device)
wall_plate_thickness
rail_thickness
front_rail_lip_width or front_rail_lip_thickness
wall_mount_screw_hole_diameter, wall_mount_screw_spacing (for countersunk screws)
bracket_inner_width_usbC, bracket_inner_height_usbC (for the USB-C connector dimensions)
bracket_inner_width_oculink, bracket_inner_height_oculink (for the Oculink connector dimensions)
m3_side_screw_hole_diameter (the hole that lets an M3 screw clamp the cable from the side)
Any other geometry parameters (openings for cables, bracket thickness, etc.)
Defaults
Please choose default dimensions that accurately reflect:
Approximate GPD Win 4 size (if not exact, then close estimates).
Standard 90° USB-C and 90° Oculink connector sizes.
Typical M3 screws for cable clamps.
Countersunk holes for M3 or M4 wall screws (whichever you prefer).
Version
This must render successfully in OpenSCAD 2021.1.
Summary
The final output should be an OpenSCAD file that, when the parameters are set to their defaults, produces the described wall-mounted docking station for the GPD Win 4 with side rails, minimal front lip, bracket cutouts for cables, and properly sized holes for screws.
If any additional measurements or details are needed, please ask.

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

@@ -53,6 +53,7 @@ let
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;
@@ -138,12 +139,12 @@ in
BindPaths = "BindPaths=/var/www/${domain}:/var/www/${domain}";
};
}
) cfg.instances;
) (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
user = if instanceOpts.user != null
then instanceOps.user
else builtins.replaceStrings ["." "-"] ["_" "_"] domain;
in
@@ -166,7 +167,7 @@ in
phpPackage = instanceOpts.phpPackage;
phpEnv."PATH" = pkgs.lib.makeBinPath [ instanceOpts.phpPackage ];
}
) cfg.instances;
) (lib.filterAttrs (name: opts: opts.enablePhp) cfg.instances);
};
@@ -216,7 +217,7 @@ in
'';
# Cache Media: images, icons, video, audio, HTC
"~* \\.(?:jpg|jpeg|gif|png|webp|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff2)$".extraConfig = ''
"~* \\.(?: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";
@@ -228,19 +229,12 @@ in
add_header Cache-Control "public";
'';
# Cache CSS, Javascript, Images, Icons, Video, Audio, HTC, Fonts
"~* \\.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff2)$".extraConfig = ''
expires 1y;
access_log off;
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) {
@@ -256,7 +250,7 @@ in
fastcgi_pass unix:${config.services.phpfpm.pools."${domain}".socket};
fastcgi_index index.php;
'';
}
})
];
extraConfig = instanceOpts.extraConfig;
@@ -325,4 +319,3 @@ config.users.groups = mapAttrs' (instance: instanceOpts:
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

@@ -1,16 +1,16 @@
{ pkgs, lib, config, ... }:
{
services.webstack.instances."api.optiprot.eu" = {
services.webstack.instances."api.ebs.amz.at" = {
enableDefaultLocations = false;
enableMysql = true;
authorizedKeys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDDBGlzrg6NP5ezRFVu1CV8r0uCcS2oIgYG2/u6Cit++ARWQRO5Y0+9qC1Y2RNUaLPbvTmXg7ShskolUeuLryqvp10K2kXQ4E9NlmJ3BNLiAfWCzfe6gAgr6u5unVlXHttnP0leYpGGMUCKuiJpzy/bR6rMIUrCQC6W/MeXkwysNWKvL+ZD0IeQbogtfMFZmag9PO04RKZZvuUn9YvlgkTEK97g5dtyP1NxdtE9dDYf0G+0HcHITcw+lVmGNNwi43nAoUHieQd1kWc8YmxFB+y5O+vRH2O6pZBSdr0tdK6bPcezxd3Gk6i3a54yZfbvSislWA+o7s6uw/qExocpZb7xWa5ymPrGlEPbpYdT1y3hFO25+L1lR4QdG9oUNtJ974bL+EmYmHU+j32K3f8fxDg6BRo8FuriLtAzP7/2/7W8K4nIdMoosS+Ond2JE6XFkg1kSrXCivDBQoetZLO2y+ZPYcsQwIZsdjOnZqVr76nTepqCGIKYCuNM/9sl4AWCsyU="
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTsA1z6/vOshSqmEUGO6vFbAYCrucgNORMKyoQ5/9/l"
];
extraConfig = ''
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php
index index.php;
charset utf-8;
@@ -31,4 +31,7 @@
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

@@ -1,16 +1,16 @@
{ pkgs, lib, config, ... }:
{
services.webstack.instances."api.paraclub.at" = {
services.webstack.instances."api.ebs.cloonar.dev" = {
enableDefaultLocations = false;
enableMysql = true;
authorizedKeys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCmLPJoHwL+d7dnc3aFLbRCDshxRSQ0dtAVv/LYBn2/PBlZcIyVO9drjr702GL9QuS5DQyjtoZjSOvv1ykBKedUwY3XDyyZgtqjleojKIFMXkdXtD5iG+RUraUfzcFCZU12BYXSeAXK1HmIjSDUtDOlp6lVVWxNpz1vWSRtA/+PULhP+n5Cj7232Wf372+EPfQPntOlcMbyrDLFtj7cUz+E6BH0qdX0l3QtIVnK/C1iagPAwLcwPJd9Sfs8lj5C4g8T9uBJa6OX+87lE4ySYY+Cik9BN59S0ctjXvWCFsPO3udQSC1mf33XdDenc2mbi+lZWTfrN8S2K5CsbxRsVBlbapFBRwufEpN4iQnaTu1QmzDrmktBFAPJ2jvjBJPIx6W3KOy3kUwh9WNhzd/ubf9dFTHzkTzgluo/Zk6/S8fTJiA4rbYKSkLw9Y265bvtR1kfUBLKSa/Axe5dkKysX1RNKfTJEwbh2TfIS3apQPZZc5kIEWfeK/6kbQX7WJZFtTs="
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqpF703JmLTBpBjTSvC0bnYu+lSYdmaGPHxMnHEbMmp"
];
extraConfig = ''
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php
index index.php;
charset utf-8;
@@ -31,4 +31,7 @@
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

@@ -1,16 +1,16 @@
{ pkgs, lib, config, ... }:
{
services.webstack.instances."api.optiprot.cloonar.dev" = {
services.webstack.instances."api.stage.ebs.amz.at" = {
enableDefaultLocations = false;
enableMysql = true;
authorizedKeys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDDBGlzrg6NP5ezRFVu1CV8r0uCcS2oIgYG2/u6Cit++ARWQRO5Y0+9qC1Y2RNUaLPbvTmXg7ShskolUeuLryqvp10K2kXQ4E9NlmJ3BNLiAfWCzfe6gAgr6u5unVlXHttnP0leYpGGMUCKuiJpzy/bR6rMIUrCQC6W/MeXkwysNWKvL+ZD0IeQbogtfMFZmag9PO04RKZZvuUn9YvlgkTEK97g5dtyP1NxdtE9dDYf0G+0HcHITcw+lVmGNNwi43nAoUHieQd1kWc8YmxFB+y5O+vRH2O6pZBSdr0tdK6bPcezxd3Gk6i3a54yZfbvSislWA+o7s6uw/qExocpZb7xWa5ymPrGlEPbpYdT1y3hFO25+L1lR4QdG9oUNtJ974bL+EmYmHU+j32K3f8fxDg6BRo8FuriLtAzP7/2/7W8K4nIdMoosS+Ond2JE6XFkg1kSrXCivDBQoetZLO2y+ZPYcsQwIZsdjOnZqVr76nTepqCGIKYCuNM/9sl4AWCsyU="
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqpF703JmLTBpBjTSvC0bnYu+lSYdmaGPHxMnHEbMmp"
];
extraConfig = ''
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php
index index.php;
charset utf-8;
@@ -31,4 +31,7 @@
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} = {};
}

View File

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

View File

@@ -1,83 +0,0 @@
{ config, pkgs, ... }:
let
domain = "sync.cloonar.com";
in {
sops.secrets.firefox-sync = { };
security.acme.certs."${domain}" = {
group = "nginx";
};
containers."firefox-sync" = {
autoStart = true;
ephemeral = false; # because of ssh key
privateNetwork = true;
hostBridge = "server";
hostAddress = "10.42.97.1";
localAddress = "10.42.97.51/24";
bindMounts = {
"/run/secrets/firefox-sync" = {
hostPath = "/run/secrets/firefox-sync";
isReadOnly = true;
};
"/var/lib/acme/${domain}/" = {
hostPath = "${config.security.acme.certs.${domain}.directory}";
isReadOnly = true;
};
};
config = { lib, config, pkgs, ... }: {
networking = {
hostName = "firefox-sync";
useHostResolvConf = false;
defaultGateway = {
address = "10.42.97.1";
interface = "eth0";
};
firewall.enable = false;
nameservers = [ "10.42.97.1" ];
};
services.nginx.enable = true;
services.nginx.virtualHosts."${domain}" = {
sslCertificate = "/var/lib/acme/${domain}/fullchain.pem";
sslCertificateKey = "/var/lib/acme/${domain}/key.pem";
sslTrustedCertificate = "/var/lib/acme/${domain}/chain.pem";
listen = [
{
addr = "0.0.0.0";
ssl = true;
port = 5000;
}
];
locations."/" = {
proxyPass = "http://localhost:5001/";
recommendedProxySettings = true;
};
};
services.mysql.package = pkgs.mariadb;
services.firefox-syncserver = {
enable = true;
singleNode = {
enable = true;
enableNginx = false;
hostname = domain;
};
settings = {
port = 5001;
tokenserver.enable = true;
};
secrets = "/run/secrets/firefox-sync";
logLevel = "trace";
};
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRQuPqH5fdX3KEw7DXzWEdO3AlUn1oSmtJtHB71ICoH Generated By Termius"
];
system.stateVersion = "23.05";
};
};
}

View File

@@ -1,77 +0,0 @@
{ config, pkgs, ... }:
let
foundry-vtt = pkgs.callPackage ../pkgs/foundry-vtt {};
cids = import ../modules/staticids.nix;
in {
users.users.foundry-vtt = {
isSystemUser = true;
uid = cids.uids.foundry-vtt;
home = "/var/lib/foundry-vtt";
group = "foundry-vtt";
createHome = true;
};
users.groups.foundry-vtt = {
gid = cids.gids.foundry-vtt;
};
containers.foundry-vtt = {
autoStart = true;
ephemeral = true;
privateNetwork = true;
hostBridge = "server";
hostAddress = "10.42.97.1";
localAddress = "10.42.97.21/24";
bindMounts = {
"/var/lib/foundry-vtt" = {
hostPath = "/var/lib/foundry-vtt";
isReadOnly = false;
};
};
config = { lib, config, pkgs, ... }: {
networking = {
hostName = "foundry-vtt";
useHostResolvConf = false;
defaultGateway = {
address = "10.42.97.1";
interface = "eth0";
};
nameservers = [ "10.42.97.1" ];
};
systemd.services.foundry-vtt = {
description = "Foundry VTT Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
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}";
Restart = "always";
User = "foundry-vtt";
WorkingDirectory = "${config.users.users.foundry-vtt.home}";
};
};
users.users.foundry-vtt = {
isSystemUser = true;
uid = cids.uids.foundry-vtt;
home = "/var/lib/foundry-vtt";
group = "foundry-vtt";
};
users.groups.foundry-vtt = {
gid = cids.gids.foundry-vtt;
};
networking.firewall = {
enable = true;
allowedTCPPorts = [ 30000 ];
};
system.stateVersion = "24.05";
};
};
}

View File

@@ -1,28 +0,0 @@
{ config, ... }:
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; [
epex_spot
];
services.home-assistant.config = {
sensor = [
{
platform = "template";
sensors = {
electricity_price = {
friendly_name = "Current Price of electricity";
unit_of_measurement = "EUR/kWh";
value_template = ''
{{ (((states('sensor.epex_spot_data_price') | int ) / 1000) + (0.0149 + 0.053 + 0.00866)) | float }}
'';
};
};
}
];
};
}

View File

@@ -1,22 +0,0 @@
{
services.home-assistant.config = {
"automation toilet music" = {
alias = "toilet music";
trigger = {
platform = "state";
entity_id = "light.toilett_lights";
};
action = [
{
service = "media_player.volume_mute";
target = {
entity_id = "media_player.music_toilet_snapcast_client";
};
data = {
is_volume_muted = "{{ trigger.to_state.state == 'off' }}";
};
}
];
};
};
}

View File

@@ -1,31 +0,0 @@
{
services.home-assistant = {
extraComponents = [ "snapcast" ];
config = {
"automation piano" = {
alias = "piano";
trigger = {
platform = "state";
entity_id = "media_player.music_piano_snapcast_client";
attribute = "is_volume_muted";
};
condition = [
{
condition = "template";
value_template = "{{ trigger.from_state.state != 'unavailable' }}";
}
{
condition = "template";
value_template = "{{ state_attr('media_player.music_piano_snapcast_client', 'is_volume_muted') == true or state_attr('media_player.music_piano_snapcast_client', 'is_volume_muted') == false }}";
}
];
action = {
service = "switch.turn_on";
target = {
entity_id = "switch.piano_switch_power";
};
};
};
};
};
}

View File

@@ -1,133 +0,0 @@
{ pkgs, config, python3Packages, ... }:
let
domain = "snapcast.cloonar.com";
snapweb = pkgs.stdenv.mkDerivation {
pname = "snapweb";
version = "0.8";
src = pkgs.fetchzip {
url = "https://github.com/badaix/snapweb/releases/download/v0.8.0/snapweb.zip";
sha256 = "sha256-IpT1pcuzcM8kqWJUX3xxpRQHlfPNsrwhemLmY0PyzjI=";
stripRoot = false;
};
installPhase = ''
mkdir -p $out
cp -r $src/* $out/
'';
};
in
{
security.acme.certs."${domain}" = {
group = "nginx";
};
containers.snapcast = {
autoStart = true;
ephemeral = false; # because of ssh key
privateNetwork = true;
hostBridge = "server";
hostAddress = "10.42.97.1";
localAddress = "10.42.97.21/24";
bindMounts = {
"/var/lib/acme/snapcast/" = {
hostPath = "${config.security.acme.certs.${domain}.directory}";
isReadOnly = true;
};
};
config = { lib, config, pkgs, python3Packages, ... }:
let
shairport-sync = pkgs.shairport-sync.overrideAttrs (_: {
configureFlags = [
"--with-alsa" "--with-pipe" "--with-pa" "--with-stdout"
"--with-avahi" "--with-ssl=openssl" "--with-soxr"
"--without-configfiles"
"--sysconfdir=/etc"
"--with-metadata"
];
});
in
{
networking = {
hostName = "snapcast";
useHostResolvConf = false;
defaultGateway = {
address = "10.42.96.1";
interface = "eth0";
};
nameservers = [ "10.42.97.1" ];
firewall.enable = false;
};
environment.etc = {
# Creates /etc/nanorc
shairport = {
text = ''
whatever you want to put in the file goes here.
metadata =
{
enabled = "yes"; // set this to yes to get Shairport Sync to solicit metadata from the source and to pass it on via a pipe
include_cover_art = "yes"; // set to "yes" to get Shairport Sync to solicit cover art from the source and pass it via the pipe. You must also set "enabled" to "yes".
cover_art_cache_directory = "/tmp/shairport-sync/.cache/coverart"; // artwork will be stored in this directory if the dbus or MPRIS interfaces are enabled or if the MQTT client is in use. Set it to "" to prevent caching, which may be useful on some systems
pipe_name = "/tmp/shairport-sync-metadata";
pipe_timeout = 5000; // wait for this number of milliseconds for a blocked pipe to unblock before giving up
};
'';
# The UNIX file mode bits
mode = "0440";
};
};
services.snapserver = {
enable = true;
codec = "flac";
http.enable = true;
http.docRoot = "${snapweb}/";
streams.mopidy = {
type = "pipe";
location = "/run/snapserver/mopidy";
};
streams.airplay = {
type = "airplay";
location = "${shairport-sync}/bin/shairport-sync";
query = {
devicename = "Multi Room New";
port = "5000";
params = "--mdns=avahi";
};
};
streams.mixed = {
type = "meta";
location = "/airplay/mopidy";
};
};
services.avahi.enable = true;
services.avahi.publish.enable = true;
services.avahi.publish.userServices = true;
services.nginx.enable = true;
services.nginx.virtualHosts."snapcast.cloonar.com" = {
sslCertificate = "/var/lib/acme/snapcast/fullchain.pem";
sslCertificateKey = "/var/lib/acme/snapcast/key.pem";
sslTrustedCertificate = "/var/lib/acme/snapcast/chain.pem";
forceSSL = true;
extraConfig = ''
proxy_buffering off;
'';
locations."/".extraConfig = ''
proxy_pass http://127.0.0.1:1780;
proxy_set_header Host $host;
proxy_redirect http:// https://;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
'';
};
system.stateVersion = "23.05";
};
};
}

View File

@@ -1,19 +0,0 @@
{ ... }: {
services.nginx.virtualHosts."git.cloonar.com" = {
forceSSL = true;
enableACME = true;
acmeRoot = null;
locations."/" = {
proxyPass = "https://git.cloonar.com/";
};
};
services.nginx.virtualHosts."foundry-vtt.cloonar.com" = {
forceSSL = true;
enableACME = true;
acmeRoot = null;
locations."/" = {
proxyPass = "http://10.42.97.21:30000";
proxyWebsockets = true;
};
};
}

View File

@@ -1,34 +0,0 @@
borg-passphrase: ENC[AES256_GCM,data:2WjoqMRmXvW9EGMmpMYhrC0Qt0Dk7QWlbEncZPdK2SxVljEoFibjVEr6jeYdAx6UkaXdjk9pD3PBbls2tWt0TiNQdh8=,iv:bHzASNjqqfPsQ/1w/oM7x0FubAzzRkn+iWrZlenU9rs=,tag:ektqi0rqEywg9YGybPQesw==,type:str]
borg-ssh-key: ENC[AES256_GCM,data:b/xZnUTfi85IG1s897CBF1HD7BTswQUatbotyZfLmbhxXxEyffUeaiGsT9Gh9yQqOKTstTihA48nVk/4ekAPD/ZGDQ189V1BwKkQ5chN9TSULofekfmemhUhVGjnx8OFl6hYYpTttQSTLHtczmfE2iX1JyrZy2Z+H+w6dbZjkYDayRUt/4+5wCtQJ1Nt7bjzwLWhjdVtwDeBLm/kCywVguZLCgyiuqmXMr1h9jpUS7URZegGz1lFs34Ismu1LtaRjFGRyd8aKaTU6PSxDbjE4dQ3Lh1Hm3nhtOrSkswBZLp8OTP6emrQ7c3oJp1zqO5zQHXxD2V5hkPw6ln0Ee1aQp1rvLD8shRXzRbHG+mySvjKLJvLypnNuYfQklqlnhbG+M1/NN13oVF13nHpKwP5q33sRr49mfHw8YHdRhHuhYHVrpy8ep0AmPXiDYCDM4cnlOMnzlH/toF0fq0YRny6QoqKNpaYhmA61MXRPTZCqoAcE1N+oo7HymjJetzL9b2FkPCoDOx989IJ8SUaBJpzR+agNsFi87htVllRp4ozms/m56dI0AdwqeAre00iMBzpVS0hXURE7fqvAnLHQD1goW9XB2mztqcJ09YafrOgTA3oyazWcAjxgV33GupxxIDmwRdLmavvr4qrHfddYctYLPI7VolqT9JmKN6iVG9vYsDutgoyRlhzbGASKPLgcYn9sGG+LBgTHfZyABnYOaUetVP72mhSN30ZZixcCskVlGg5C53wrW5o6mBv+PyG8PimxLmQylbvHUdGGVLQfMpJaaXgpUjBX1MWdQAVa+Nyjm7QwYdRKoCb3suQ6bOq5O9eotel3GPB8gpKzInhNA/0xiB4UyCGp1i21iRS9+Rc7yufo5s3t56k0643K2DhBUVgssiTsG15BbQdX4c1O28i9zwEZ+wVci1yvLX38M0a3tDDt9iW1BIOWehShS7dpyJR2/OgWLFagw9hYP5h24t5k6Gz2ODhPouaFccYDRUBR6UECxA+gDS+trN8iNSX1oWa0ys0XvgwWpJ2CrdSArNqe1BdhM47BQwudiA3RwaEN3wRh5PeykSk/3BUXK+ZdAr0BZ8ij2q4F8zQexLxnrV6xRqofNcVs62iJAjx6g86InSv0nNjLQ9U/fBTL66u1iRZFJhuxPjDNfLJZqT0TvRR7KBcNWTwTuMCGNp5s9TngMUF4uhHx8qGxtjfH58WjixOhC9lgUt7cYEFIeefcwIO9VVnKoiXK5sPIvIsjtLRzGvejYSd0ZwSF3Ly9FkWLkr+o5rs5bXtGMsSQ+BUFg5nM1BqrHIGv9M+F4kPxhnqm9/JXuMSQ+JUzix5N0vHuSTphCayDpMHJYRUEkDEmwPXMyB9zWVmvMb0ByUnfs/n/jmL4WRuggYqchIR3/xuco5HUqLbEKXiJ39wVgy+i3/biWOOEu5BmMx3qbgQ1+6nlxY+f1qpXZ8br0RlXLOQ6L/O9Qa9gKZaxLm/5GCiFZ+SeU/c5OgUndYqTk6FsbDlNurA69IqjwubG345lpdB9VPoGP7dLsx3VaGKW0bvr06oRaeasMx90SN5bGQJH+0iQFkGPhp0m2v31zpBk1IibXi5Qb1OWGXGYd+iNt1ZQF0HVuEqQEXI62x92QkaR7eHowR4tCRF1xH1ZrBkyjtdofUU2wPqsRrOWqGIZWUh/JpfXkSAZQo9yJKnHcp9d3BPEvWpLWS9g1Jfej5XG497aP6crWw5XawOyzi+PEgz2Y3Q0R/MM3S1W2R7Z+21nekbCfghpNylwIX4UYkeX8YorheiumkUfFXjktPSkFCTuUrYAA89WZjIIqd4/gt3tS7keCsjEiTkW2KdDPlzNItKnC8xWnpRc+Wh6ghA/nt3j4POb880j3scFoDjgOv5lNk2Q84S/IW+DQ3U8o4JrKiXsxchDvmgGbU4FbXZTGLXeM1CybmbZKogIHdwJkhC425oqA1PMiq5tDPLKpl2214JuaV4Xd8R0bwCSHYjQp9gqJT9j1Wg/3P0M3/VGZGoJEVriiBl6PBHP2CcvxK1NADDmMHgGQwwfROoSijAzzPKCy9sgzsquTkqzq8q4aChjGKShxs+52dpnmmuygSlxjyVQCEW9kLERf1Nm1arsLkHJ4ZsWgSrskGvjsPEvyEnpY33gGB7fpy90NW0GtELgGzEw/1nfLcFbRBJ7gH+4Dby2fBTxoV2ks9m0Fv6OWsfIe6H54zWLmqB1RkQaskb1wDKU3HATOmuYo/fByLIsMyR5l3P7LXWF5CJOprzp41rGts/ybJEG1EUtmVCs2epTwbeG/Waq1DB3TFa639ETjxOfGQ65PXp5aT1d5v+ko87LiR+0us6xwlfZ6NMRRZuPt4wycFgPUAAmpdmguwDKifHKA258g9kzotT25JeFFEMVhsMi1PoXEqA+sFomdsLt+Vtpr2aGMUWyHD/E2fgAtybLwxbjqDINi8vXWJxv/UZdH8wBOlWLtaeGg5/jRsMuL/hSSZ84Q2zfRVvV7/BZ7wnxfoXmAwRdTZijAvc9TxWszP6E5mAix7s/znU+1vnseJdxWa4Ff1wOGVL/Tem2K0J/mp75XuzSP7nCYDMgqhnvfzlD8vv6QpxtDUAbdTBDyPkQ4U9L6+y5ul5Aegpui+p0G9/0UHdBYhJiFd90omnhSmyHx2pvgUTfbL/Kv/pk7nwTv89a87NXNA9K6AATwx0kUPgIWs/5FGi8leCXGSsgBbJogL1htC72pKzVH6ckEzKeBzRADmwFLhnPIvp37ZkQPj0rrWRhkd5RqsFcN0166N+M4lPD0hzPd2+nEXDAOHoCK7U+BcRcJ3GUlyPU91dbWfo9otPd3naTvGVZuFDxOihLtBXaLTsxmS4STk6DVRjwNmX8YC9FwXkED19xEeH6KkaFs1nVXnmDqpvi2BcueT96t6TOeu5HcA9fAgFTpOKVT6cK2PcHTtJhjrPkfSYr0/ksJdV7r9N4JgAEfiASMMHS5uQWJlyJKWo92rJ2IvSCQx4lcK3gasgcTsVaYmuRORM+6263r4NKS8W8r55XvVyW/C7vvsVq6wF3xUkQadBkxIUQUVWxxCc1pWOlfWwMs0i+ZssoaWopbs7x45z86i+3HsHmfS6GuXUpQfgvXe9Bn7mOj7VQWaG9NIFUpIxisGfdY9L8+RXobo7etD3da7TNMs40BT+34tijcX53FzKwvG3ESNPB2hjOAITDta6LDOHhJrlVqn90p1DicThHOaT3fxt6ST287EhWqK9S1gpkLrp0gNSA9v+K9mBvWaWYNDXY7sGxOIMzCEIdFT18Pra92NhGTJtC0XizHDMUfGx5WAaard1Iy/PYXvavoAwp30qDCQGF+PgwSProa+JtQQPzoEgtSXNVhUWIzz10TACuo+vHt8sHvFG3VuU7jSOr9sqVrN36KMDUlwo0gavHKsjRxHf2OGh552q7AP+sM6Y5WhA4KhmQSUKCVxYVQ==,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]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoUWdTYlRjWDJvemF5Q2sr
VCtrS2dTTGRwUlNIWHd0WkVCRkRMcGhuTzE0ClNic1FmQ05UNWQwbGc4TUFMNGlI
K0RhK2pqUGY3UElmK1pNUEkxV2xGUTQKLS0tIFRORE9JTDRZK0MwZUJoc2xlcHFH
bmp3ZW14TVdCMHhkSi84NE5neDdrY3cKYfgu7aqvG6wQmEFhmzieXFGoQpyffPXj
jiHrAPjBBFy21wdYf0nQXNMzekqOMJwOj0oNA2b5omprPxjB9uns4Q==
-----END AGE ENCRYPTED FILE-----
- recipient: age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUUjQxWnBMQXo3QmF1STUw
bHh1NDhvQXZIQ2RiOUx5OU5Wc3BVSEJDUEZVCmVzeFk5SWpMbVV4VUdsRmhiaWwz
bTJDY1pJRXJvNUdCSXJqQ3Byd3lWN2sKLS0tIHRKdXRNc1BYcURBRVNlenk1OEl3
Q05BN0VnQ0haeHBobWhRV0EzL3dLSEkKWlALiX5mvG8y0WUc8yFWMbcpSRrSGoQx
SHaOlDCjYvViZ7GPRLqnSwDGZ1clC6JsTbwKXrMsWdZBKvSO/VIWQw==
-----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: []
unencrypted_suffix: _unencrypted
version: 3.8.1

View File

@@ -1,64 +0,0 @@
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:ir25XfzLBb/H/YWzxP501hCaLBB4jpiLW7WUcnvguzosT9QeOtBdJ0WB1IndEMtiEgQyE9kyGOJ3QJwzbQNkX6CG96Uzt2mKw8gw8ayUqC+B9zR8eIRYiDKOYs+YREVo7nA5pLLzIc/9jaRicDFMmw1Thmk7UUJKB1DNV49nU9K+nAfrCzk7ZQieY8oaasFD0cvNb4Ndj6f9PWSXkNBwKK52ig4hDeNBs1bdy8nDE8VqlwOo8H2DcYMzdMjKCZDBRccy8NofHEhakCW5OdliFyIHsLkcBHca3Bp46JN7wbo8avPPd9bXGuRiOSWYq50RcyZUovnB3g7Dk3swCyuiFztnStN63+g7ZnGFdYLYDYfuDSPN1W2HCkknmaoT910VNE8sEAMyfXk4tqJv4eW4qmFk2UwPlRCrsk9GtdRQ5wm8muNPHEZ8s2dGkn4WDcjy7SUpgF4UJJZV8iJe74W9BK1Ef+AWWNsNjYfZde3iw1+8Fz1u65u4seFWqQMok/noADpszbpk+YYRoM+5D/YVMx+KeDtoFqnZfULM/BqvAqdYYZtRzojndeNW6Ea4sxDE+XQ5b1OwGFlNAlnuS1fYYPvKojrKNgT9KMwbsvPijU5vFddY8Qpz2h6GKEv/OW87j5UeyDW4l32lvyawBuzczBfiFgCElggGSZHM5rjE4Deb06eQleTioZ79EDXTv5UsPQ6Bc1v5Wvnu8DvxJe4B10vxH70JIGIlmjwo0yhMkxDTN7BkAGQC0QAPhwtURDq+XVufQNjlTUjjH1Q1E4u0Vy19clMs8SStqFeMN02BfWZdS9mbueF5Ehc+8wTfAs43CQFublJ4wfG1PzEbqj9LZdimFe4hCnE2y6Gbf591shugVSAMA3UXQUuvFQmm69i9gz88YSYrkLlVStM+dtXCugZho72xgHtnI+5o19wuoZPRoxe47W0T2kJZZeomtqoAsSo5yr5JeYzYdaHYcK2fgRY0HWgWzOxnVEfX/gRPR3b20Tko6yp9lIDECkXVDQSxptxqIYk+VuETnD9YF2OpYeHZLGoo9OLdEHVZRcuy1S74aAOJGO9SAHLw3eukxG//AZlwcOYjOsYDVt3BjhYZEkYCLg8GkAqV/7bGsxT7pgckNEB2NRYQI9ckqEcEw9CdkYre67HwfPCvAble68VnRzgp+v5s0koVjTURF9FTxvVOXQEbvSpY828idyx6nOaAIHoqpIOFz4jsGE9L4FKamqnlnjzj2Ri/MboT9JQBj8bnIF/ej+dQGpfqZo7zqtu3d0B/9e0xuVTcqI9Bxlqn3D4108I8R37Ctr5OFKloeOZ8HHMsHcBUAzZC6/fWrOspru14YHW2YNj8nBxHve/P3oiTQ/nlXLcBGLoFfI+hOpofccQB8FnkKfTbLSRUGrGY6NJt9RCnZgm2+RUgel77XpsCsT/Q5ZGclBdyk8mSaqVjiNyHCbCV5tF/tWnuvf859S0tcmqbJ0FhIRAvwxFucmfi6FSPX5HEMdRbNV7szrHKSX60u7YA2DBBzv3c/+C2bxq70vhwFelqz7FqpVKwebbE4/a59lZpibzefCoji/TPDJB62/ox5NHHE5qenv7IPcEj3dEmdasbrApAw1UFsFlRCnlg4JIYley/AQx7OzUSImqkG8JWvSJ4JXijhsr9dPFR/cb0srUO88aFNh/ZUQhELZCVnzAsF81Y4w6LTGApMfUVN/yx9MqENGvObywzMls1UJphvzDZzvb+Ue6eqELogN1QcEI/WOirwVtJO6E7IevEtK4xxWsLfRHVjtbLc4QjCWuiyszAPTTttKJ+iC2h14Wj1XoiMpWRiVnj+jI9iWRen96P4glYEfuCYQS6vbGkNDEoZt/FnkLJDbLdjXatmhUoRpvExOtp26ULR/f1lwzLMJBt1qPvhuGur1ru2B1e8+AVte1Cfjmk+xrnxNwkTFLGe89Qjd77wPyQv9h0YrhZ6uDi2zLemhZs2LjW5ZvzV5P4thMDxkhezJHatPHAGa8OfclJOyrRTyW2azdz2A45MNzZtCQcnQdQxBXf+XRskLnhquZfgv66hFITjuF/HeI9cq4HJcrgaOcVj+tBdK1bTCyL2kqKkCpSCbh/Pv6FuAlDXgLjsWwZgOKz8gfTIfXMapPLDYVTbS/PPPABylZflN98FFyeFDHB3Fwn1a6qAJ0mC7+4sowVZ1DIAoflaHqNs5TXyb3KeZGgXj5ZQwhv1z6NySvOS6cHxx0PvkFo99T1NHztxCRERNvBdWSwsr32DTwEvZo5iNPy3lvKI5A+rXc7jlQkUbufbddtLw2iPtt29XyMDOysK010fXzzQRjaz4R8ZaDtHNjqPrynvqFPXRB0VSIrwXS2utU7bmD+0dGX26t9k5qRBi7Gm+iZNKGMnSRsm17bVk5o8q0tb1P1eGL9mexZJJvxolfXVFJJtR8m6vLmUX1LSht/JhoWFElrINl0hviwd1dehmTqdQqWz5/imjF+pVOasrt7XVZ+7T/rDpuwNl375qSZptM1pMUExJ3CvzigpnarXXQxEBYkf0haGvQwPWNVHe/bR/1VooSQkH/mGg1g+rcTqp4yB5hsFu1lNK4ph04WQOqaafg40HBv6e5cOjLkFdEtYNpjyd6sRS+WHk7zzFlfPVlzijq8f+oDH9ALRzNnL1Y2DrX53wx4dBBWvxE1Yhb6Kj6Er4ZDiRLLXo+wJOGCpnNTPJMVaYskZ+LN2e9nS2/ZwbsNBnPHxSqCc1oP4d3yXH0j90VKnWg79aIEOagRvTF/9F6SkkGL9zVuUnoVSPwq97etWWtjGoEORMGY7jkGOK+U391p7Z69Hrv2AejS1BoSDeGcxXasFvINpmc+Hl2c+zOlFBySu2zA39cVlcStUFICA5GCmE5Eum4ED9DXP6RAuicD7YE0qSKbMkfLxIWMCZ6wBcwVUjdt43SI/ZqdpDm3E1kTRg07dE0R091rtfzEiIwBM4xFPJBafOx0L/Do61YMOHGzi6wgIQO7P7wIslv62M8MD1KKa/eH0tE2vhG/GyEGtKkg3P9vZRJwioifyshS1hvrt5pLinuCaDYyqMAl8Ro0OOm8di7+mBvXib0nRLfW7wBGDA4ADTipizNWAmbspQQl89kH5gdxgXO5U+N/qc0zXbpB+qeHVkPIK1DmrJ8pHLOE8mOpLy7eHUsSku/WtTt/RP4pcDbBU/43MCbk7NXKu/LjKjkQBjAL49LxnYmhEU7X//jtwSPE3gdx0x+wRJxzlbehM6rpfDRV5WQGSFf7yjLc/Ga1KwsgVdAstJEzDdv2vWSsjNzfJvHVBLrQPIC9fggi3DeLiHTAryCUcLUhNj4xtZWhSS1qmx07E4VzfjDJLMOsLY0vlimgngZ3YYCjC3Sw0frfQH2SZvmbLd3XfBdud67ZaMUobcRhnKzQnilldyD1jWVWLdVTup4RVxT4GYek9nmYflzpWWmwbXatz9Sgcw==,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]
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]
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]
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]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsNzNjZ1o1dXFxalFiRXUx
U3NQK0gvQWVRbnAxam8yZmJTTmRTaVVZdkdrCnQ0R1ZBWEVmcE12NWNuaDFtRGlj
UFRManh2VFgwUFJaNFpVZFNqc01oSkEKLS0tIHA5UDlHY1lDWUtwTk10RHZoQWQ1
bzZ6MzhQQmYrZ3JKUDZoa1lDZXRHRDAKHtzHnt+zHgMsuyX0vP6xapvJ8796/vkn
u9U56OdFlqthTy870vMMoJWW3wAFfj/QV124bG63lJ02gAHEr/PGJw==
-----END AGE ENCRYPTED FILE-----
- recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLc0ZsVlNzQ0d1dGJlSzN6
bzB0bnhHTzlodWJveFBmdVVCdjJ5c2V0dkM4Cmt1cHhJa2U4NmJZSUFGYzhCQmdH
eVJDUjc0LzdIOHo4TWlCeVEvQUg1b1EKLS0tIGRpTFA4TkgvU2ZLOXM3NktMbjRP
aGM2aVdRSUpsRXRCZE02MXJ3MVpxK00KO2dZUNZ1KQFg4bnNp1PEntL2fY1h+JCK
l7CnGwotydc9NybwYtisv9XVrz3QoiD09OiLvg7VkmfzEaGmqmja/g==
-----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjQTBxNkV2REdrRS9MaUxa
YWxNOFBKQlAwOW5qSk9hM1Q5c0tjZTdWUjBjCkM5TmtwR2RBRER3Uzc4dWtGOVM2
bjZFZVc3V0t0enhyam1DWVM3b0h5WlEKLS0tIGNPUzFJUGRYZStMRTMwV3pWTW1t
V003cnFtYVNEbERiRDV4bmVXVlBaUTAK7pLGaixTRCg5lKhN8CN95cdr7X8X1oDY
LX2t+SPvb8hqsssLf/mqVxPsgAXl0L9lfsYtRsuMWONmaJsOleVE4A==
-----END AGE ENCRYPTED FILE-----
- recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbDA5U0xnUDNXYUtRVVN3
YW5aTFg1T0pOZWc4cXFDRDlrRmxZWWw1MUdRCjdlUVg0S0IxTXM4ZXcydGR0aldu
WnU3ZnUydUh4em02TWFVamx6a0xpQmMKLS0tIEdpWFg1UEVGNHIzY2VZZk40NlBG
WXJpUUxadERyYUExRFMzNzBXaUVET3cKG9ZwWy5YvTr/BAw/i+ZJos5trwRvaW5j
eV/SHiEteZZtCuCVFAp3iolE/mJyu97nA2yFwWaLN86h+/xkOJsdqA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-11-20T21:39:00Z"
mac: ENC[AES256_GCM,data:JCFvFwSqnAQCOB76n5pfQsdsaod8bBiVZ2VY+WWBDWi84gQByhqy808E2ZZJSJ1/amUi8dNBeOPNWZIGdieuWJyatrqjWziAl7gXx5u35i77sS6hAD+G/Fc/elgRbjc0VIbplZ7UxBmwo3vkVpI4RqQiQv63MvKHI+TkoY8vFUM=,iv:uy50x8FqqDW7hCLZeHfhFB/dxa3N6kM2Vj9waAZJngg=,tag:Wt1FG0kW4VFZ2fvvAC0T4A==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.8.1

1
hosts/fw/channel Normal file
View File

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

View File

@@ -4,19 +4,23 @@
./utils/bento.nix
./utils/modules/sops.nix
./utils/modules/lego/lego.nix
./utils/modules/nginx.nix
./utils/modules/autoupgrade.nix
./utils/modules/victoriametrics
./utils/modules/promtail
./utils/modules/borgbackup.nix
# ./utils/modules/netdata.nix
./utils/modules/set-nix-channel.nix
# fw
./modules/network-prefix.nix
./modules/networking.nix
./modules/setupnetwork.nix
./modules/firewall.nix
./modules/dhcp4.nix
./modules/unbound.nix
# ./modules/dhcp4.nix
# ./modules/unbound.nix
./modules/dnsmasq.nix
./modules/avahi.nix
./modules/openconnect.nix
./modules/wireguard.nix
@@ -25,25 +29,36 @@
./modules/ddclient.nix
# ./modules/wol.nix
# microvm
./modules/microvm.nix
./modules/gitea-vm.nix
# ./modules/vscode-server.nix # Add VS Code Server microvm
./modules/ai-mailer.nix
# ./modules/wazuh.nix
# web
./modules/web
# git
./modules/gitea.nix
./modules/fwmetrics.nix
# ./modules/fwmetrics.nix
# ha customers
./modules/ha-customers
./modules/firefox-sync.nix
./modules/fivefilters.nix
# ./modules/pyload
# ./modules/firefox-sync.nix
# home assistant
./modules/home-assistant
./modules/deconz.nix
# ./modules/mopidy.nix
# ./modules/mosquitto.nix
./modules/snapserver.nix
# ./modules/snapserver.nix
./modules/lms.nix
# gaming
# ./modules/palworld.nix
@@ -52,11 +67,15 @@
# setup network
./modules/setupnetwork.nix
./modules/set-nix-channel.nix # Automatically manage nix-channel from /var/bento/channel
./modules/grafana-monitor.nix # Grafana online status monitor
./hardware-configuration.nix
];
networkPrefix = "10.42";
nixpkgs.overlays = [
(import ./utils/overlays/packages.nix)
];
@@ -67,11 +86,25 @@
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;
@@ -89,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";
@@ -105,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
};
@@ -137,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

@@ -0,0 +1,111 @@
{ config, pkgs, ... }:
{
users.users.ai-mailer = {
isSystemUser = true;
group = "ai-mailer";
home = "/var/lib/ai-mailer";
createHome = true;
description = "AI Mailer service user";
};
users.groups.ai-mailer = { };
environment.etc."ai-mailer/config.yaml" = {
mode = "0400";
user = "ai-mailer";
group = "ai-mailer";
text = ''
imap:
server: "imap.gmail.com"
port: 993
username: "it@paraclub.at"
password: "file://${config.sops.secrets.ai-mailer-imap-password.path}"
mailbox_in: "INBOX"
draft_box: "[Gmail]/Entwürfe"
processed_box: "INBOX/Done"
use_tls: true
ai:
openrouter_api_key: "file://${config.sops.secrets.ai-mailer-openrouter-key.path}"
model: "openai/gpt-5-mini"
temperature: 0.3
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/"
- "https://paraclub.at/de/ueber-uns/anfahrt/"
- "https://paraclub.at/de/tandemfallschirmspringen/faq/"
- "https://paraclub.at/de/ausbildung/uebersicht/"
- "https://paraclub.at/de/ausbildung/aff-ablauf/"
- "https://paraclub.at/de/ausbildung/kurstermine/"
- "https://paraclub.at/de/ausbildung/anmeldung/"
- "https://paraclub.at/de/ausbildung/kosten/"
polling:
interval: "300s"
processing:
max_tokens: 30000
skip_junk_emails: false
logging:
level: "info"
file_path: "/var/log/ai-mailer/ai-mailer.log"
'';
};
sops.secrets.ai-mailer-imap-password = {
owner = "ai-mailer";
};
sops.secrets.ai-mailer-openrouter-key = {
owner = "ai-mailer";
};
systemd.services.ai-mailer = {
description = "AI Mail Assistant Service";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = "ai-mailer";
Group = "ai-mailer";
WorkingDirectory = "/var/lib/ai-mailer";
ExecStart = "${pkgs.ai-mailer}/bin/ai-mailer -config /etc/ai-mailer/config.yaml";
Restart = "always";
RestartSec = "10s";
StateDirectory = "ai-mailer";
LogsDirectory = "ai-mailer";
RuntimeDirectory = "ai-mailer";
# Security settings
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
CapabilityBoundingSet = "";
};
restartTriggers = [
"/etc/ai-mailer/config.yaml"
config.sops.secrets.ai-mailer-imap-password.path
config.sops.secrets.ai-mailer-openrouter-key.path
];
};
}

View File

@@ -0,0 +1,94 @@
{ config, pkgs, lib, ... }:
let
domain = "a11ywatch.cloonar.com";
confDir = "/var/lib/a11ywatch";
json = pkgs.formats.json { };
in {
# 1) Enable Podman (daemonless, drop-in for docker)
virtualisation.podman.enable = true; # :contentReference[oaicite:0]{index=0}
virtualisation.podman.dockerCompat = true; # :contentReference[oaicite:1]{index=1}
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;# :contentReference[oaicite:2]{index=2}
services.nginx.virtualHosts."${domain}" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:3000/";
};
};
environment.etc."containers/networks/a11ywatch-net.json" = {
source = json.generate "a11ywatch-net.json" ({
name = "a11ywatch-net";
id = "ccb4b7fb90d2df26db27ef0995765b04f52d318db752c9474b470c5ef4d7978d";
driver = "bridge";
network_interface = "podman1";
subnets = [
{
subnet = "10.89.0.0/24";
gateway = "10.89.0.1";
}
];
ipv6_enabled = false;
internal = false;
dns_enabled = true;
ipam_options = {
driver = "host-local";
};
});
};
users.users.a11ywatch = {
isSystemUser = true;
group = "a11ywatch";
home = "/var/lib/a11ywatch";
createHome = true;
};
users.groups.a11ywatch = { };
users.groups.docker.members = [ "a11ywatch" ];
# 2) Create the bridge network on boot via a oneshot systemd service
systemd.services.a11ywatch-net = {
description = "Ensure a11ywatch-net Podman network exists";
wants = [ "podman.service" ];
after = [ "podman.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = ''
${pkgs.podman}/bin/podman network inspect a11ywatch-net >/dev/null 2>&1 \
|| ${pkgs.podman}/bin/podman network create a11ywatch-net
'';
RemainAfterExit = true;
};
wantedBy = [
"multi-user.target"
];
};
# 3) Declare your two containers using the podman backend
virtualisation.oci-containers = {
backend = "podman"; # :contentReference[oaicite:3]{index=3}
containers = {
a11ywatch-backend = {
image = "docker.io/a11ywatch/a11ywatch:latest";
autoStart = true;
ports = [ "3280:3280" ];
volumes = [ "${confDir}:/a11ywatch/conf" ];
environment = { SUPER_MODE = "true"; };
extraOptions = [ "--network=a11ywatch-net" ];
};
a11ywatch-frontend = {
image = "docker.io/a11ywatch/web:latest";
autoStart = true;
ports = [ "3000:3000" ];
volumes = [ "${confDir}:/a11ywatch/conf" ];
environment = { SUPER_MODE = "true"; };
extraOptions = [
"--network=a11ywatch-net"
];
};
};
};
}

View File

@@ -15,7 +15,7 @@
];
extraOptions = [
"--network=server"
"--ip=10.42.97.201"
"--ip=${config.networkPrefix}.97.201"
];
};
};

View File

@@ -0,0 +1,54 @@
{
lib,
pkgs,
...
}: let
users = [
{
username = "ca-test";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDglSLU9AUtbU0fCN0eByi/EHyo1QiPPLiscN5RAR+wq";
}
];
userList = lib.concatStringsSep "," (map (u: u.username) users);
in {
environment.etc = {
# our single user+key file
"cloonar_assistant_ssh/sftp_users_keys" = {
text = lib.concatStringsSep "\n"
(map (u: "${u.username} ${u.key}") users);
mode = "0600";
user = "root";
group = "root";
};
# the little awk script to extract the key for $1
"cloonar_assistant_ssh/sftp-fetch-key.sh" = {
text = ''
#!/usr/bin/env bash
awk -v u="$1" '$1==u { $1=""; sub(/^ +/, ""); print }' /etc/cloonar_assistant_ssh/sftp_users_keys
'';
mode = "0700";
user = "root";
group = "root";
};
};
systemd.tmpfiles.rules = map (u:
# Type 'd' = create directory if missing
# Mode 0755, owner root:root
"d /home/cloonar-assistant-configs/${u.username} 0755 root root -"
) users;
services.openssh.extraConfig = ''
Match User ${userList}
X11Forwarding no
AllowTcpForwarding no
ChrootDirectory /home/cloonar-assistant-configs/%u
ForceCommand internal-sftp
# only for those matched users:
AuthorizedKeysCommand /etc/cloonar_assistant_ssh/sftp-fetch-key.sh %u
AuthorizedKeysCommandUser root
'';
}

View File

@@ -2,7 +2,7 @@
{
services.ddclient = {
enable = true;
use = "if, if=wan";
usev4 = "if, if=wan";
protocol = "hetzner";
# server = "https://dns.hetzner.com/api/v1/";
username = "dominik.polakovics@cloonar.com";
@@ -15,6 +15,7 @@
"palworld.cloonar.com"
"matrix.cloonar.com"
"element.cloonar.com"
"tinder.cloonar.com"
];
};

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=10.42.97.22"
"--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

@@ -1,4 +1,5 @@
{ ... }: {
{ config, ... }:
{
services.kea.dhcp4 = {
enable = true;
settings = {
@@ -21,17 +22,18 @@
renew-timer = 1000;
subnet4 = [
{
id = 96;
pools = [
{
pool = "10.42.96.100 - 10.42.96.240";
pool = "${config.networkPrefix}.96.100 - ${config.networkPrefix}.96.240";
}
];
subnet = "10.42.96.0/24";
subnet = "${config.networkPrefix}.96.0/24";
interface = "lan";
option-data = [
{
name = "routers";
data = "10.42.96.1";
data = "${config.networkPrefix}.96.1";
}
{
name = "domain-name";
@@ -43,40 +45,41 @@
}
{
name = "domain-name-servers";
data = "10.42.96.1";
data = "${config.networkPrefix}.96.1";
}
];
reservations = [
{
hw-address = "04:7c:16:d5:63:5e";
ip-address = "10.42.96.5";
ip-address = "${config.networkPrefix}.96.5";
server-hostname = "omada.cloonar.com";
}
{
hw-address = "30:05:5c:56:62:37";
ip-address = "10.42.96.100";
ip-address = "${config.networkPrefix}.96.100";
server-hostname = "brn30055c566237.cloonar.com";
}
{
hw-address = "24:df:a7:b1:1b:74";
ip-address = "10.42.96.101";
ip-address = "${config.networkPrefix}.96.101";
server-hostname = "rmproplus-b1-1b-74.cloonar.com";
}
];
}
{
id = 97;
pools = [
{
pool = "10.42.97.100 - 10.42.97.240";
pool = "${config.networkPrefix}.97.100 - ${config.networkPrefix}.97.240";
}
];
subnet = "10.42.97.0/24";
subnet = "${config.networkPrefix}.97.0/24";
interface = "server";
option-data = [
{
name = "routers";
data = "10.42.97.1";
data = "${config.networkPrefix}.97.1";
}
{
name = "domain-name";
@@ -84,54 +87,55 @@
}
{
name = "domain-name-servers";
data = "10.42.97.1";
data = "${config.networkPrefix}.97.1";
}
];
reservations = [
{
hw-address = "1a:c4:04:6e:29:bd";
ip-address = "10.42.97.2";
ip-address = "${config.networkPrefix}.97.2";
server-hostname = "omada.cloonar.com";
}
{
hw-address = "02:00:00:00:00:03";
ip-address = "10.42.97.5";
ip-address = "${config.networkPrefix}.97.5";
server-hostname = "web-02.cloonar.com";
}
{
hw-address = "02:00:00:00:00:04";
ip-address = "10.42.97.6";
ip-address = "${config.networkPrefix}.97.6";
server-hostname = "matrix.cloonar.com";
}
{
hw-address = "ea:db:d4:c1:18:ba";
ip-address = "10.42.97.50";
ip-address = "${config.networkPrefix}.97.50";
server-hostname = "git.cloonar.com";
}
{
hw-address = "c2:4f:64:dd:13:0c";
ip-address = "10.42.97.20";
ip-address = "${config.networkPrefix}.97.20";
server-hostname = "home-assistant.cloonar.com";
}
{
hw-address = "1a:c4:04:6e:29:02";
ip-address = "10.42.97.25";
ip-address = "${config.networkPrefix}.97.25";
server-hostname = "deconz.cloonar.com";
}
];
}
{
id = 101;
pools = [
{
pool = "10.42.101.100 - 10.42.101.240";
pool = "${config.networkPrefix}.101.100 - ${config.networkPrefix}.101.240";
}
];
subnet = "10.42.101.0/24";
subnet = "${config.networkPrefix}.101.0/24";
interface = "infrastructure";
option-data = [
{
name = "routers";
data = "10.42.101.1";
data = "${config.networkPrefix}.101.1";
}
{
name = "domain-name";
@@ -139,29 +143,30 @@
}
{
name = "domain-name-servers";
data = "10.42.101.1";
data = "${config.networkPrefix}.101.1";
}
{
name = "capwap-ac-v4";
code = 138;
data = "10.42.97.2";
data = "${config.networkPrefix}.97.2";
}
];
reservations = [
];
}
{
id = 99;
pools = [
{
pool = "10.42.99.100 - 10.42.99.240";
pool = "${config.networkPrefix}.99.100 - ${config.networkPrefix}.99.240";
}
];
subnet = "10.42.99.0/24";
subnet = "${config.networkPrefix}.99.0/24";
interface = "multimedia";
option-data = [
{
name = "routers";
data = "10.42.99.1";
data = "${config.networkPrefix}.99.1";
}
{
name = "domain-name";
@@ -169,59 +174,60 @@
}
{
name = "domain-name-servers";
data = "10.42.99.1";
data = "${config.networkPrefix}.99.1";
}
];
reservations = [
{
hw-address = "c4:a7:2b:c7:ea:30";
ip-address = "10.42.99.10";
ip-address = "${config.networkPrefix}.99.10";
hostname = "metz.cloonar.multimedia";
}
{
hw-address = "f0:2f:9e:d4:3b:21";
ip-address = "10.42.99.11";
ip-address = "${config.networkPrefix}.99.11";
hostname = "firetv-living";
}
{
hw-address = "bc:33:29:ed:24:f0";
ip-address = "10.42.99.12";
ip-address = "${config.networkPrefix}.99.12";
hostname = "ps5";
}
{
hw-address = "e4:2a:ac:32:3f:79";
ip-address = "10.42.99.13";
ip-address = "${config.networkPrefix}.99.13";
hostname = "xbox";
}
{
hw-address = "98:b6:e9:b6:ef:f4";
ip-address = "10.42.99.14";
ip-address = "${config.networkPrefix}.99.14";
hostname = "switch";
}
{
hw-address = "f0:2f:9e:c1:74:72";
ip-address = "10.42.99.21";
ip-address = "${config.networkPrefix}.99.21";
hostname = "firetv-bedroom";
}
{
hw-address = "30:05:5c:56:62:37";
ip-address = "10.42.99.100";
ip-address = "${config.networkPrefix}.99.100";
server-hostname = "brn30055c566237";
}
];
}
{
id = 254;
pools = [
{
pool = "10.42.254.10 - 10.42.254.254";
pool = "${config.networkPrefix}.254.10 - ${config.networkPrefix}.254.254";
}
];
subnet = "10.42.254.0/24";
subnet = "${config.networkPrefix}.254.0/24";
interface = "guest";
option-data = [
{
name = "routers";
data = "10.42.254.1";
data = "${config.networkPrefix}.254.1";
}
{
name = "domain-name-servers";
@@ -230,17 +236,18 @@
];
}
{
id = 100;
pools = [
{
pool = "10.42.100.100 - 10.42.100.240";
pool = "${config.networkPrefix}.100.100 - ${config.networkPrefix}.100.240";
}
];
subnet = "10.42.100.0/24";
subnet = "${config.networkPrefix}.100.0/24";
interface = "smart";
option-data = [
{
name = "routers";
data = "10.42.100.1";
data = "${config.networkPrefix}.100.1";
}
{
name = "domain-name";
@@ -248,29 +255,29 @@
}
{
name = "domain-name-servers";
data = "10.42.100.1";
data = "${config.networkPrefix}.100.1";
}
];
reservations = [
{
hw-address = "fc:ee:28:03:63:e9";
ip-address = "10.42.100.148";
ip-address = "${config.networkPrefix}.100.148";
server-hostname = "k1c";
}
{
hw-address = "cc:50:e3:bc:27:64";
ip-address = "10.42.100.112";
ip-address = "${config.networkPrefix}.100.112";
server-hostname = "Nuki_Bridge_1A753F72";
}
{
hw-address = "34:6f:24:f3:af:ad";
ip-address = "10.42.100.137";
ip-address = "${config.networkPrefix}.100.137";
server-hostname = "daikin86604";
}
{
hw-address = "34:6f:24:c1:f8:54";
ip-address = "10.42.100.139";
ip-address = "${config.networkPrefix}.100.139";
server-hostname = "daikin53800";
}
];

View File

@@ -0,0 +1,176 @@
{ config, ... }: {
services.resolved.enable = false;
services.dnsmasq = {
enable = true;
settings = {
port = "53";
bind-interfaces = true; # force dnsmasq to bind immediately
expand-hosts = true;
log-dhcp = true;
server = [
"/epicenter.works/10.50.60.1"
"/akvorrat.at/10.50.60.1"
"9.9.9.9"
"149.112.112.11"
];
interface = [
"lan"
"server"
"infrastructure"
"multimedia"
"guest"
"smart"
];
domain = [
"cloonar.com,lan"
"cloonar.com,server"
"cloonar.com,infrastructure"
"cloonar.multimedia,multimedia"
"cloonar.smart,smart"
"cloonar.guest,guest"
];
dhcp-option = [
"lan,15,cloonar.com" # domain name
"lan,3,${config.networkPrefix}.96.1" # Gateway
"lan,6,${config.networkPrefix}.96.1" # DNS
"server,15,cloonar.com"
"server,3,${config.networkPrefix}.97.1"
"server,6,${config.networkPrefix}.97.1"
"infrastructure,15,cloonar.com"
"infrastructure,3,${config.networkPrefix}.101.1"
"infrastructure,6,${config.networkPrefix}.101.1"
"multimedia,15,cloonar.multimedia"
"multimedia,3,${config.networkPrefix}.99.1"
"multimedia,6,${config.networkPrefix}.99.1"
"smart,15,cloonar.smart"
"smart,3,${config.networkPrefix}.100.1"
"smart,6,${config.networkPrefix}.100.1"
"guest,15,cloonar.guest"
"guest,3,${config.networkPrefix}.254.1"
"guest,6,9.9.9.9"
];
dhcp-range = [
"lan,${config.networkPrefix}.96.100,${config.networkPrefix}.96.200,24h"
"server,${config.networkPrefix}.97.100,${config.networkPrefix}.97.200,24h"
"infrastructure,${config.networkPrefix}.101.100,${config.networkPrefix}.101.200,24h"
"multimedia,${config.networkPrefix}.99.100,${config.networkPrefix}.99.200,24h"
"smart,${config.networkPrefix}.100.100,${config.networkPrefix}.100.200,24h"
"guest,${config.networkPrefix}.254.100,${config.networkPrefix}.254.200,24h"
];
dhcp-host = [
"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"
"fc:ee:28:03:63:e9,${config.networkPrefix}.100.148,k1c"
"cc:50:e3:bc:27:64,${config.networkPrefix}.100.112,Nuki_Bridge_1A753F72"
"34:6f:24:f3:af:ad,${config.networkPrefix}.100.137,daikin86604"
"34:6f:24:c1:f8:54,${config.networkPrefix}.100.139,daikin53800"
];
address = [
"/fw.cloonar.com/${config.networkPrefix}.97.1"
"/omada.cloonar.com/${config.networkPrefix}.97.2"
"/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"
"/lms.cloonar.com/${config.networkPrefix}.97.21"
"/git.cloonar.com/${config.networkPrefix}.97.50"
"/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"
"/piwik.wohnservice-wien.at/10.254.240.109"
"/wohnberatung-wien.at/10.254.240.109"
"/wohnpartner-wien.at/10.254.240.109"
"/wohnservice-wien.at/10.254.240.109"
"/mieterhilfe.at/10.254.240.109"
"/wienbautvor.at/10.254.240.109"
"/wienwohntbesser.at/10.254.240.109"
"/a.stage.wohnberatung-wien.at/10.254.240.110"
"/a.stage.wohnpartner-wien.at/10.254.240.110"
"/a.stage.wohnservice-wien.at/10.254.240.110"
"/a.stage.mieterhilfe.at/10.254.240.110"
"/a.stage.wienbautvor.at/10.254.240.110"
"/a.stage.wienwohntbesser.at/10.254.240.110"
"/b.stage.wohnberatung-wien.at/10.254.240.110"
"/b.stage.wohnpartner-wien.at/10.254.240.110"
"/b.stage.wohnservice-wien.at/10.254.240.110"
"/b.stage.mieterhilfe.at/10.254.240.110"
"/b.stage.wienbautvor.at/10.254.240.110"
"/b.stage.wienwohntbesser.at/10.254.240.110"
"/web.hilgenberg-gmbh.de/91.107.197.169"
# gaming
"/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"
"/cdnjs.cloudflare.com/104.17.24.14"
# esphome devices
"/livingroom-bulb-1.cloonar.smart/${config.networkPrefix}.100.11"
"/livingroom-bulb-2.cloonar.smart/${config.networkPrefix}.100.12"
"/livingroom-bulb-3.cloonar.smart/${config.networkPrefix}.100.13"
"/livingroom-bulb-4.cloonar.smart/${config.networkPrefix}.100.14"
"/livingroom-bulb-5.cloonar.smart/${config.networkPrefix}.100.15"
"/livingroom-bulb-6.cloonar.smart/${config.networkPrefix}.100.16"
"/bedroom-bulb-0.cloonar.smart/${config.networkPrefix}.100.21"
"/bedroom-bulb-0.cloonar.smart/${config.networkPrefix}.100.22"
"/bedroom-bulb-0.cloonar.smart/${config.networkPrefix}.100.23"
"/bedroom-bulb-0.cloonar.smart/${config.networkPrefix}.100.24"
"/hallway-bulb-0.cloonar.smart/${config.networkPrefix}.100.31"
"/hallway-bulb-0.cloonar.smart/${config.networkPrefix}.100.32"
"/bath-bulb-0.cloonar.smart/${config.networkPrefix}.100.41"
"/bath-bulb-0.cloonar.smart/${config.networkPrefix}.100.42"
"/paraclub.at/188.34.191.144"
];
};
};
systemd.services.dnsmasq = {
requires = [ "network-online.target" ];
after = [ "network-online.target" ];
};
networking.firewall.allowedUDPPorts = [ 53 67 ];
}

View File

@@ -0,0 +1,59 @@
{ config, pkgs, ... }:
let
domain = "sync.cloonar.com";
networkPrefix = config.networkPrefix;
in {
sops.secrets.firefox-sync = {
mode = "0777";
};
security.acme.certs."${domain}" = {
group = "nginx";
};
containers."firefox-sync" = {
autoStart = true;
ephemeral = false; # because of ssh key
privateNetwork = true;
hostBridge = "server";
hostAddress = "${config.networkPrefix}.97.1";
localAddress = "${config.networkPrefix}.97.6/24";
bindMounts = {
"/run/secrets/firefox-sync" = {
hostPath = "/run/secrets/firefox-sync";
isReadOnly = true;
};
};
config = { lib, config, pkgs, ... }: {
networking = {
hostName = "firefox-sync";
useHostResolvConf = false;
defaultGateway = {
address = "${networkPrefix}.97.1";
interface = "eth0";
};
nameservers = [ "${networkPrefix}.97.1" ];
};
services.mysql.package = pkgs.mariadb;
services.firefox-syncserver = {
enable = true;
settings.host = "0.0.0.0";
singleNode = {
enable = true;
hostname = "0.0.0.0";
url = "https://${domain}";
};
secrets = "/run/secrets/firefox-sync";
logLevel = "debug";
};
networking.firewall = {
enable = true;
allowedTCPPorts = [ 5000 ];
};
system.stateVersion = "23.05";
};
};
}

View File

@@ -1,4 +1,4 @@
{ pkgs, ... }: {
{ config, pkgs, ... }: {
networking = {
firewall.checkReversePath = false;
nat.enable = false;
@@ -8,6 +8,18 @@
"cloonar-fw" = {
family = "inet";
content = ''
chain snap-qos-raw {
type filter hook prerouting priority raw; policy accept;
tcp dport 1704 counter mark set 10 comment "Mark Snapcast traffic"
tcp dport 3483 counter mark set 10 comment "Mark Squezelite traffic"
udp dport 3483 counter mark set 10 comment "Mark Squezelite traffic"
}
chain snap-qos-mangle {
type filter hook postrouting priority mangle + 10; policy accept;
mark 10 counter ip dscp set cs3 comment "Tag Snapcast with CS3"
}
chain output {
type filter hook output priority 100; policy accept;
}
@@ -22,6 +34,7 @@
type filter hook input priority filter; policy drop;
iifname "lo" accept comment "trusted interfaces"
iifname "lan" counter accept comment "Spice"
iifname { "server", "vserver", "vm-*", "lan", "wg_cloonar" } counter accept comment "allow trusted to router"
ct state vmap { invalid : drop, established : accept, related : accept, new : jump input-allow, untracked : jump input-allow }
tcp flags syn / fin,syn,rst,ack log prefix "refused connection: " level info
}
@@ -34,11 +47,14 @@
iifname "lan" tcp dport 5931 counter accept comment "Spice"
iifname { "server", "vserver", "vm-*", "lan", "wg_cloonar" } counter accept comment "allow trusted to router"
iifname { "multimedia", "smart", "infrastructure", "podman0", "setup" } udp dport { 53, 5353 } counter accept comment "DNS"
iifname { "multimedia", "smart", "infrastructure", "server", "lan", "guest" } udp dport { 67 } counter accept comment "DHCP"
iifname { "wan", "multimedia" } icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP"
# Accept mDNS for avahi reflection
iifname "server" ip saddr 10.42.97.20/32 tcp dport { llmnr } counter accept
iifname "server" ip saddr 10.42.97.20/32 udp dport { mdns, llmnr } counter accept
iifname "server" ip saddr ${config.networkPrefix}.97.20/32 tcp dport { llmnr } counter accept
iifname "server" ip saddr ${config.networkPrefix}.97.20/32 udp dport { mdns, llmnr } counter accept
iifname "server" udp dport 5353 ip daddr 224.0.0.251 counter accept comment "Avahi mDNS"
iifname "lan" udp dport 5353 ip daddr 224.0.0.251 counter accept comment "Avahi mDNS"
# Allow all returning traffic
ct state { established, related } counter accept
@@ -79,17 +95,31 @@
# multimedia airplay
iifname "multimedia" oifname { "lan" } counter accept
iifname "multimedia" oifname "server" tcp dport { 1704, 1705 } counter accept
iifname "multimedia" oifname "server" tcp dport { 3483, 9000 } counter accept
iifname "multimedia" oifname "server" udp dport { 3483 } counter accept
iifname "multimedia" oifname "server" icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP"
iifname "lan" oifname "server" udp dport { 5000, 5353, 6001 - 6011 } counter accept
# avahi
iifname "server" ip saddr 10.42.97.20/32 oifname { "lan" } counter accept
iifname "server" ip saddr ${config.networkPrefix}.97.20/32 oifname { "lan" } counter accept
# Allow Chromecast
iifname "lan" oifname "server" udp dport 5353 ip daddr 224.0.0.251 counter accept comment "mDNS query LANServer"
iifname "server" oifname "lan" udp sport 5353 ip saddr 224.0.0.251 counter accept comment "mDNS response ServerLAN"
iifname "lan" oifname "server" tcp dport 9881 counter accept comment "chromecast"
# SSDP / UPnP discovery if needed
iifname { "lan", "server" } oifname { "server", "lan" } \
udp dport 1900 ip daddr 239.255.255.250 counter accept comment "SSDP query"
iifname { "lan", "server" } oifname { "server", "lan" } \
udp sport 1900 ip saddr 239.255.255.250 counter accept comment "SSDP response"
# smart home coap
iifname "smart" oifname "server" ip daddr 10.42.97.20/32 udp dport { 5683 } counter accept
iifname "smart" oifname "server" ip daddr 10.42.97.20/32 tcp dport { 1883 } counter accept
iifname "smart" oifname "server" ip daddr ${config.networkPrefix}.97.20/32 udp dport { 5683 } counter accept
iifname "smart" oifname "server" ip daddr ${config.networkPrefix}.97.20/32 tcp dport { 1883 } counter accept
# Forward to git server
oifname "server" ip daddr 10.42.97.50 tcp dport { 22 } counter accept
oifname "server" ip daddr 10.42.97.5 tcp dport { 80, 443 } counter accept
oifname "server" ip daddr ${config.networkPrefix}.97.50 tcp dport { 22 } counter accept
oifname "server" ip daddr ${config.networkPrefix}.97.5 tcp dport { 80, 443 } counter accept
# lan and vpn to any
iifname { "lan", "server", "vserver", "wg_cloonar" } oifname { "lan", "vb-*", "vm-*", "server", "vserver", "infrastructure", "multimedia", "smart", "wg_cloonar", "guest", "setup" } counter accept
@@ -100,11 +130,11 @@
# accept palword server
iifname { "wan", "lan" } oifname "podman0" udp dport { 8211, 27015 } counter accept comment "palworld"
# forward to ark server
oifname "server" ip daddr 10.42.97.201 tcp dport { 27020 } counter accept comment "ark survival evolved"
oifname "server" ip daddr 10.42.97.201 udp dport { 7777, 7778, 27015 } counter accept comment "ark survival evolved"
oifname "server" ip daddr ${config.networkPrefix}.97.201 tcp dport { 27020 } counter accept comment "ark survival evolved"
oifname "server" ip daddr ${config.networkPrefix}.97.201 udp dport { 7777, 7778, 27015 } counter accept comment "ark survival evolved"
# firefox-sync
oifname "server" ip daddr 10.42.97.51 tcp dport { 5000 } counter accept comment "firefox-sync"
oifname "server" ip daddr ${config.networkPrefix}.97.51 tcp dport { 5000 } counter accept comment "firefox-sync"
# allow all established, related
ct state { established, related } accept comment "Allow established traffic"
@@ -136,21 +166,22 @@
content = ''
chain prerouting {
type nat hook prerouting priority filter; policy accept;
iifname "server" ip daddr 10.42.96.255 udp dport { 9 } dnat to 10.42.96.255
iifname "wan" tcp dport { 22 } dnat to 10.42.97.50
iifname "wan" tcp dport { 80, 443 } dnat to 10.42.97.5
iifname "wan" tcp dport { 5000 } dnat to 10.42.97.51
iifname { "wan", "lan" } udp dport { 7777, 7778, 27015 } dnat to 10.42.97.201
iifname { "wan", "lan" } tcp dport { 27020 } dnat to 10.42.97.201
iifname "server" ip daddr ${config.networkPrefix}.96.255 udp dport { 9 } dnat to ${config.networkPrefix}.96.255
iifname "wan" tcp dport { 22 } dnat to ${config.networkPrefix}.97.50
iifname "wan" tcp dport { 80, 443 } dnat to ${config.networkPrefix}.97.5
iifname "wan" tcp dport { 5000 } dnat to ${config.networkPrefix}.97.51
iifname { "wan", "lan" } udp dport { 7777, 7778, 27015 } dnat to ${config.networkPrefix}.97.201
iifname { "wan", "lan" } tcp dport { 27020 } dnat to ${config.networkPrefix}.97.201
}
# Setup NAT masquerading on external interfaces
chain postrouting {
type nat hook postrouting priority filter; policy accept;
oifname { "wan", "wg_cloonar", "wrwks", "wg_epicenter", "wg_ghetto_at" } masquerade
iifname { "wan", "wg_cloonar" } ip daddr 10.42.97.50 masquerade
iifname { "wan", "wg_cloonar" } ip daddr 10.42.97.51 masquerade
iifname { "wan", "wg_cloonar" } ip daddr 10.42.97.201 masquerade
iifname { "lan", "wg_cloonar" } ip daddr ${config.networkPrefix}.110.101 masquerade
iifname { "wan", "wg_cloonar" } ip daddr ${config.networkPrefix}.97.50 masquerade
iifname { "wan", "wg_cloonar" } ip daddr ${config.networkPrefix}.97.51 masquerade
iifname { "wan", "wg_cloonar" } ip daddr ${config.networkPrefix}.97.201 masquerade
}
'';
};

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

@@ -0,0 +1,162 @@
{ config, pkgs, ... }:
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;
uid = cids.uids.foundry-vtt;
home = "/var/lib/foundry-vtt";
group = "foundry-vtt";
createHome = true;
};
users.groups.foundry-vtt = {
gid = cids.gids.foundry-vtt;
};
containers.foundry-vtt = {
autoStart = true;
ephemeral = true;
privateNetwork = true;
hostBridge = "server";
hostAddress = "${hostConfig.networkPrefix}.97.1";
localAddress = "${hostConfig.networkPrefix}.97.21/24";
bindMounts = {
"/var/lib/foundry-vtt" = {
hostPath = "/var/lib/foundry-vtt";
isReadOnly = false;
};
};
config = { lib, config, pkgs, ... }: {
networking = {
hostName = "foundry-vtt";
useHostResolvConf = false;
defaultGateway = {
address = "${hostConfig.networkPrefix}.96.1";
interface = "eth0";
};
firewall.enable = false;
nameservers = [ "${hostConfig.networkPrefix}.97.1" ];
};
systemd.services.foundry-vtt = {
description = "Foundry VTT Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
NODE_ENV = "production";
};
serviceConfig = {
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}";
};
};
users.users.foundry-vtt = {
isSystemUser = true;
uid = cids.uids.foundry-vtt;
home = "/var/lib/foundry-vtt";
group = "foundry-vtt";
};
users.groups.foundry-vtt = {
gid = cids.gids.foundry-vtt;
};
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

@@ -1,4 +1,4 @@
{ lib, nixpkgs, pkgs, ... }: let
{ config, lib, nixpkgs, pkgs, ... }: let
# hostname = "git-02";
# json = pkgs.formats.json { };
runners = ["git-runner-1" "git-runner-2"];
@@ -38,6 +38,13 @@ in {
];
};
systemd.network.networks."10-lan" = {
matchConfig.PermanentMACAddress = "02:00:00:00:00:0${toString idx}";
address = [ "${config.networkPrefix}.97.5${toString idx}/24" ];
gateway = [ "${config.networkPrefix}.97.1" ];
dns = [ "${config.networkPrefix}.97.1" ];
};
networking.hostName = runner;
virtualisation.podman.enable = true;
@@ -48,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
};
};
};
@@ -62,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

@@ -2,6 +2,7 @@
let
cids = import ../modules/staticids.nix;
domain = "git.cloonar.com";
networkPrefix = config.networkPrefix;
user = {
isSystemUser = true;
@@ -27,8 +28,8 @@ in
ephemeral = false; # because of ssh key
privateNetwork = true;
hostBridge = "server";
hostAddress = "10.42.97.1";
localAddress = "10.42.97.50/24";
hostAddress = "${networkPrefix}.97.1";
localAddress = "${networkPrefix}.97.50/24";
bindMounts = {
"/var/lib/gitea" = {
hostPath = "/var/lib/gitea/";
@@ -45,6 +46,7 @@ in
config = { lib, config, pkgs, ... }: {
imports = [
../fleet.nix
../modules/cloonar-assistant-config-server.nix
];
environment.systemPackages = with pkgs; [
@@ -55,11 +57,11 @@ in
hostName = "git";
useHostResolvConf = false;
defaultGateway = {
address = "10.42.96.1";
address = "${networkPrefix}.96.1";
interface = "eth0";
};
firewall.enable = false;
nameservers = [ "10.42.97.1" ];
nameservers = [ "${networkPrefix}.97.1" ];
};
services.nginx.enable = true;
@@ -68,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/";
};
@@ -83,6 +88,9 @@ in
HTTP_PORT = 3001;
DOMAIN = domain;
};
repository = {
DEFAULT_BRANCH = "main";
};
openid = {
ENABLE_OPENID_SIGNIN = false;
ENABLE_OPENID_SIGNUP = true;
@@ -93,6 +101,7 @@ in
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
SHOW_REGISTRATION_BUTTON = false;
ENABLE_NOTIFY_MAIL = true;
REQUIRE_SIGNIN_VIEW = false;
};
mailer = {
ENABLED = true;
@@ -103,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

@@ -0,0 +1,193 @@
{ config, pkgs, lib, ... }:
let
grafanaMonitorUser = "grafana-monitor";
grafanaMonitorGroup = "grafana-monitor";
stateDir = "/var/lib/${grafanaMonitorUser}";
# Monitoring script will be defined here later
monitorScript = pkgs.writeShellScriptBin "grafana-online-check" ''
#!${pkgs.bash}/bin/bash
set -euo pipefail
GRAFANA_URL="https://grafana.cloonar.com/api/health"
STATE_FILE="${stateDir}/status.env"
PUSHOVER_API_TOKEN_FILE="/run/secrets/pushover-api-token"
PUSHOVER_USER_KEY_FILE="/run/secrets/pushover-user-key"
MAX_FAILURES=5
# Ensure state directory exists (NixOS creates $HOME for the user, which is stateDir)
# The script runs as grafanaMonitorUser, so $HOME will be /var/lib/grafana-monitor
mkdir -p "''${HOME}"
# Load current state or initialize
CONSECUTIVE_FAILURES=0
ALERT_SENT="false"
LAST_KNOWN_STATUS="UP" # Assume UP initially if no state file
# Note: STATE_FILE uses $stateDir which is /var/lib/grafana-monitor.
# The script will run with HOME=/var/lib/grafana-monitor.
# So, using ''${HOME}/status.env or ''${STATE_FILE} should resolve to the same path.
# Let's stick to ''${STATE_FILE} for consistency with its definition.
if [[ -f "''${STATE_FILE}" ]]; then
source "''${STATE_FILE}"
fi
# Check secrets
if [[ ! -f "''${PUSHOVER_API_TOKEN_FILE}" ]] || [[ ! -r "''${PUSHOVER_API_TOKEN_FILE}" ]]; then
echo "Error: Pushover API token file (''${PUSHOVER_API_TOKEN_FILE}) not found or not readable." >&2
exit 1
fi
PUSHOVER_API_TOKEN=$(cat "''${PUSHOVER_API_TOKEN_FILE}")
if [[ ! -f "''${PUSHOVER_USER_KEY_FILE}" ]] || [[ ! -r "''${PUSHOVER_USER_KEY_FILE}" ]]; then
echo "Error: Pushover user key file (''${PUSHOVER_USER_KEY_FILE}) not found or not readable." >&2
exit 1
fi
PUSHOVER_USER_KEY=$(cat "''${PUSHOVER_USER_KEY_FILE}")
# Internet connectivity check
INTERNET_CHECK_URL="https://1.1.1.1" # Using a reliable IP to bypass potential DNS issues for the check itself
echo "Performing internet connectivity check to ''${INTERNET_CHECK_URL}..."
if ! ${pkgs.curl}/bin/curl --head --silent --fail --connect-timeout 3 --max-time 5 "''${INTERNET_CHECK_URL}" > /dev/null 2>&1; then
echo "Internet connectivity check failed. Cannot reach ''${INTERNET_CHECK_URL}. Skipping Grafana check and exiting successfully."
exit 0
else
echo "Internet connectivity check successful. Proceeding with Grafana check."
fi
echo "" # Add a blank line for readability before Grafana check logs
echo "Checking Grafana at ''${GRAFANA_URL}..."
ACTUAL_HTTP_CODE="000" # Default if curl doesn't provide one
CURL_ERROR_MESSAGE=""
CURL_STDERR_OUTPUT=$(mktemp)
# Ensure temp file is cleaned up on exit, error, or interrupt
trap 'rm -f "''${CURL_STDERR_OUTPUT}"' EXIT TERM INT HUP
# -L: follow redirects
# -sS: silent mode, but show errors
# --fail: curl exits with 22 on server errors (4xx, 5xx)
# --connect-timeout 5: max time to connect
# --max-time 10: max total time for operation
# --stderr: redirect stderr to a file to capture detailed errors
# -o /dev/null: discard response body
# --write-out "%{http_code}": output the HTTP status code
if ACTUAL_HTTP_CODE=$(${pkgs.curl}/bin/curl -L -sS --fail --connect-timeout 5 --max-time 10 \
--stderr "''${CURL_STDERR_OUTPUT}" \
-o /dev/null --write-out "%{http_code}" "''${GRAFANA_URL}"); then
# Curl exited with 0. With --fail, this means HTTP status was 2xx.
echo "Grafana is UP (HTTP ''${ACTUAL_HTTP_CODE})."
CURRENT_STATUS="UP"
if [[ "''${LAST_KNOWN_STATUS}" == "DOWN" && "''${ALERT_SENT}" == "true" ]]; then
echo "Grafana recovered. Sending recovery notification."
${pkgs.curl}/bin/curl -sS -X POST \
-F "token=''${PUSHOVER_API_TOKEN}" \
-F "user=''${PUSHOVER_USER_KEY}" \
-F "message=Grafana at ''${GRAFANA_URL} is back online (HTTP ''${ACTUAL_HTTP_CODE})." \
-F "title=Grafana Recovered (fw)" \
-F "priority=0" \
https://api.pushover.net/1/messages.json
ALERT_SENT="false"
fi
CONSECUTIVE_FAILURES=0
else
# Curl exited with a non-zero status.
CURL_EXIT_CODE=$?
CURL_ERROR_MESSAGE=$(cat "''${CURL_STDERR_OUTPUT}" | tr -d '\n' | sed 's/"/\\"/g') # Read, remove newlines, escape quotes for JSON
echo "Grafana check failed. Curl Exit Code: ''${CURL_EXIT_CODE}. HTTP Code reported: ''${ACTUAL_HTTP_CODE}."
echo "Curl Stderr: ''${CURL_ERROR_MESSAGE}"
CURRENT_STATUS="DOWN"
CONSECUTIVE_FAILURES=$(( ''${CONSECUTIVE_FAILURES} + 1 ))
echo "Consecutive failures: ''${CONSECUTIVE_FAILURES}"
if [[ ''${CONSECUTIVE_FAILURES} -ge ''${MAX_FAILURES} && "''${ALERT_SENT}" == "false" ]]; then
echo "Grafana has been offline for ''${CONSECUTIVE_FAILURES} checks (>= ''${MAX_FAILURES}). Sending alert."
PUSHOVER_TITLE="Grafana OFFLINE (fw)"
PUSHOVER_MSG="Grafana ''${GRAFANA_URL} offline for ''${MAX_FAILURES}+ min. HTTP:''${ACTUAL_HTTP_CODE}, CurlExit:''${CURL_EXIT_CODE}."
if [[ -n "''${CURL_ERROR_MESSAGE}" ]]; then
PUSHOVER_MSG+=" Err: ''${CURL_ERROR_MESSAGE}"
fi
# Truncate message if too long for Pushover (1024 chars)
PUSHOVER_MSG=$(echo "''${PUSHOVER_MSG}" | cut -c 1-1024)
${pkgs.curl}/bin/curl -sS -X POST \
-F "token=''${PUSHOVER_API_TOKEN}" \
-F "user=''${PUSHOVER_USER_KEY}" \
-F "message=''${PUSHOVER_MSG}" \
-F "title=''${PUSHOVER_TITLE}" \
-F "priority=1" \
https://api.pushover.net/1/messages.json
ALERT_SENT="true"
fi
fi
# Temp file is removed by trap
# Save current state
echo "Saving state: CONSECUTIVE_FAILURES=''${CONSECUTIVE_FAILURES}, ALERT_SENT=''${ALERT_SENT}, LAST_KNOWN_STATUS=''${CURRENT_STATUS}"
(
echo "CONSECUTIVE_FAILURES=''${CONSECUTIVE_FAILURES}"
echo "ALERT_SENT=''${ALERT_SENT}"
echo "LAST_KNOWN_STATUS=''${CURRENT_STATUS}"
) > "''${STATE_FILE}" # Using STATE_FILE which is ${stateDir}/status.env
chmod 600 "''${STATE_FILE}"
echo "Grafana check finished."
'';
in
{
# Module is now implicitly enabled when imported
config = {
users.users.${grafanaMonitorUser} = {
isSystemUser = true;
group = grafanaMonitorGroup;
home = stateDir; # Home directory for state
createHome = true; # NixOS will create this directory
description = "User for Grafana online monitoring service";
};
users.groups.${grafanaMonitorGroup} = {};
# Sops secrets for Pushover
sops.secrets."pushover-api-token" = {
owner = grafanaMonitorUser;
group = grafanaMonitorGroup;
mode = "0400"; # Read-only for the user
};
sops.secrets."pushover-user-key" = {
owner = grafanaMonitorUser;
group = grafanaMonitorGroup;
mode = "0400"; # Read-only for the user
};
environment.systemPackages = [
pkgs.curl
pkgs.coreutils # for mkdir, cat, echo, rm used in script (though bash builtins are often used)
];
systemd.services.grafana-online-check = {
description = "Grafana Online Check Service";
wantedBy = [ "multi-user.target" ]; # Or timers.target if only started by timer
after = [ "network-online.target" ]; # Ensure network is up and secrets are available
requires = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
User = grafanaMonitorUser;
Group = grafanaMonitorGroup;
ExecStart = "${monitorScript}/bin/grafana-online-check";
# Permissions to write to its own home directory (stateDir) are implicit
# If using StateDirectory= in systemd, it would be different.
# For home directory usage, ensure the user has rights. `createHome = true` helps.
};
};
systemd.timers.grafana-online-check = {
description = "Timer to periodically check Grafana's online status";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min"; # Wait a bit after boot
OnUnitActiveSec = "1min"; # Run every 1 minute after the last run
Unit = "grafana-online-check.service";
};
};
};
}

View File

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

View File

@@ -0,0 +1,29 @@
{
lib,
pkgs,
...
}: let
create_users = host: {
users.users."${host.username}.ghetto.at" = {
createHome = true;
home = "/home/customers/ghetto/" + host.username;
isNormalUser = false;
isSystemUser = true;
group = "sftp_users";
openssh.authorizedKeys.keys = [
host.key
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7"
];
shell = null;
};
};
users = [
{
username = "fw";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzJRWe8hsqAVnGSjPrcheloteWMzORoQ5Gj4IfhCROF";
}
];
in {
imports = builtins.map create_users users;
}

View File

@@ -4,8 +4,9 @@ let
pkgs-with-home-assistant = import (builtins.fetchGit {
name = "new-home-assistant";
url = "https://github.com/nixos/nixpkgs/";
rev = "41dea55321e5a999b17033296ac05fe8a8b5a257";
rev = "18dd725c29603f582cf1900e0d25f9f1063dbf11";
}) {};
networkPrefix = config.networkPrefix;
in
{
users.users.hass = {
@@ -35,8 +36,8 @@ in
ephemeral = false;
privateNetwork = true;
hostBridge = "server";
hostAddress = "10.42.97.1";
localAddress = "10.42.97.20/24";
hostAddress = "${networkPrefix}.97.1";
localAddress = "${networkPrefix}.97.20/24";
extraFlags = [
"--capability=CAP_NET_ADMIN"
"--capability=CAP_MKNOD"
@@ -74,7 +75,9 @@ in
};
};
config = { lib, config, pkgs, ... }: {
networkPrefix = networkPrefix;
imports = [
../network-prefix.nix
./3dprinter.nix
./ac.nix
# ./aeg.nix
@@ -93,6 +96,7 @@ in
./presense.nix
./remote.nix
./roborock.nix
./scenes
./scene-switch.nix
./shelly.nix
./sleep.nix
@@ -103,11 +107,11 @@ in
hostName = "home-assistant";
useHostResolvConf = false;
defaultGateway = {
address = "10.42.96.1";
address = "${networkPrefix}.96.1";
interface = "eth0";
};
firewall.enable = false;
nameservers = [ "10.42.97.1" ];
nameservers = [ "${networkPrefix}.97.1" ];
};
environment.systemPackages = [
@@ -158,7 +162,7 @@ in
};
script = ''
set -e
HACS_VERSION="2.0.1" # Replace with the latest version
HACS_VERSION="2.0.5" # Replace with the latest version
HACS_DIR="/var/lib/hass/custom_components/hacs"
mkdir -p "$HACS_DIR"

View File

@@ -0,0 +1,28 @@
{ config, pkgs, ... }:
let
in {
services.home-assistant.customComponents = with pkgs.home-assistant-custom-components; [
epex_spot
];
services.home-assistant.config = {
sensor = [
{
platform = "template";
sensors = {
electricity_price = {
friendly_name = "Current Price of electricity";
unit_of_measurement = "EUR/kWh";
value_template = ''
{{ ((states('sensor.epex_spot_data_price') | float ) + (0.0149 + 0.074 + 0.007 + 0.0074 + 0.0006)) | float }}
'';
entity_id = [
"sensor.epex_spot_data_price"
"sensor.time"
];
};
};
}
];
};
}

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

@@ -71,6 +71,21 @@
action = [
{
choose = [
{
conditions = [ "{{ is_state('automation.light_sunset', 'off') }}" ];
sequence = [
{
service = "light.turn_on";
target = {
entity_id = "{{ trigger.entity_id }}";
};
data = {
brightness_pct = 100;
color_temp = 250;
};
}
];
}
{
conditions = [ "{{ state_attr('sun.sun', 'elevation') < 5 and trigger.entity_id == 'light.toilet_lights' }}" ];
sequence = [
@@ -125,8 +140,8 @@
entity_id = "{{ trigger.entity_id }}";
};
data = {
brightness_pct = 20;
rgbw_color = [ 255 126 0 255 ];
brightness_pct = 30;
color_temp = 450;
};
}
];
@@ -259,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";
};
}
];
};
@@ -323,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";
}
];
}
];
}
];
@@ -357,12 +396,13 @@
all = true;
entities = [
"light.livingroom_switch"
"light.living_room_bulb_1"
"light.living_room_bulb_2"
"light.living_room_bulb_3"
"light.living_room_bulb_4"
"light.living_room_bulb_5"
"light.living_room_bulb_6"
"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"
];
}
{
@@ -376,6 +416,7 @@
all = true;
entities = [
"light.kitchen_switch"
"light.kitchen_bulb_1"
"light.kitchen"
];
}
@@ -406,21 +447,62 @@
"light.bathroom_bulb_2"
];
}
{
platform = "switch";
name = "Hallway Switch";
entity_id = "switch.hallway";
}
{
platform = "group";
name = "Hallway Lights";
all = true;
entities = [
"light.hallway_switch"
"light.hallway_light_switch_mini_switch"
"light.hallway_bulb_1"
"light.hallway_bulb_2"
];
}
{
platform = "template";
lights = {
hallway_group_proxy = {
friendly_name = "Hallway Lights (Proxy)";
# follow the real groups on/off state
value_template = "{{ is_state('light.hallway_lights','on') }}";
turn_on = {
service = "light.turn_on";
data = { entity_id = "light.hallway_lights"; };
};
turn_off = {
service = "light.turn_off";
data = { entity_id = "light.hallway_lights"; };
};
# brightness support
set_level = {
service = "light.turn_on";
data_template = {
entity_id = "light.hallway_lights";
brightness = "{{ brightness }}";
};
};
# color temperature support (if you have CT-capable bulbs)
set_temperature = {
service = "light.turn_on";
data_template = {
entity_id = "light.hallway_lights";
color_temp = "{{ color_temp }}";
};
};
# RGB color support
set_color = {
service = "light.turn_on";
data_template = {
entity_id = "light.hallway_lights";
rgb_color = [ "{{ red }}" "{{ green }}" "{{ blue }}" ];
};
};
# always report as “available”
availability_template = "true";
# declare which color modes you need
supported_color_modes = [ "brightness" "color_temp" "rgb" ];
};
};
}
{
platform = "switch";
name = "Toilet Switch";

View File

@@ -1,4 +1,9 @@
{
let
devices = [
"device_tracker.dominiks_iphone"
"device_tracker.dominiks_mp01"
];
in {
services.home-assistant.extraComponents = [
"nuki"
];
@@ -9,9 +14,7 @@
mode = "restart";
trigger = {
platform = "state";
entity_id = [
"device_tracker.dominiks_iphone"
];
entity_id = devices;
from = "not_home";
to = "home";
};

View File

@@ -76,7 +76,7 @@
{
service = "automation.turn_off";
target = {
entity_id = "automation.all_multimedia_off"; # Replace with your target automation ID
entity_id = "automation.all_multimedia_off";
};
}
{
@@ -115,7 +115,7 @@
{
service = "automation.turn_on";
target = {
entity_id = "automation.all_multimedia_off"; # Replace with your target automation ID
entity_id = "automation.all_multimedia_off";
};
}
];
@@ -246,6 +246,7 @@
platform = "state";
entity_id = "binary_sensor.multimedia_device_on";
to = "off";
for = "00:00:30";
};
action = [
{
@@ -289,13 +290,6 @@
command = "b64:JgDaAAABKZMUERMSExITEhMSExETEhMSExITEhMSExETNxQ2ExITEhMSEzcTNxM3ExITEhM3ExITNxMSEhITEhM3EzcTEhM3EwAFyAABKJQUERMSEhITEhMSExITEhMSEhITEhMSExITNxM3ExITEhMREzcTNxQ3EhITEhM3ExITNxMSExITEhM3EzcTEhM3EwAFyAABKJQUERMSExETEhMSExITEhMSExETEhMSExITNxM3ExITEhMREzcTOBI4ExETEhM3ExITNxMSExITEhM3EzcTEhM3E5IGAA0FAAAAAAAAAAAAAAAAAAA=";
};
}
# turn off tv switch
{
service = "switch.turn_off";
target = {
entity_id = "switch.tv_switch";
};
}
];
};
"automation all_multimedia_on" = {
@@ -490,6 +484,12 @@
entity_id = "script.turn_on_tv";
};
}
{
service = "media_player.turn_off";
target = {
entity_id = "media_player.marantz_sr6015";
};
}
];
}
];

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