main
1{
2 config,
3 globals,
4 lib,
5 libx,
6 pkgs,
7 ...
8}:
9let
10 # Common security headers for Caddy
11 securityHeaders = ''
12 header {
13 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
14 X-Content-Type-Options "nosniff"
15 X-Frame-Options "SAMEORIGIN"
16 Referrer-Policy "strict-origin-when-cross-origin"
17 Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
18 Content-Security-Policy "default-src 'self' *.sbr.pm *.demeester.fr; style-src 'self' 'unsafe-inline'; script-src 'self'"
19 X-XSS-Protection "1; mode=block"
20 Cache-Control "public, max-age=604800, immutable"
21 -Server
22 }
23 '';
24
25 # Security headers for media services (more permissive CSP for multimedia)
26 mediaSecurityHeaders = ''
27 header {
28 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
29 X-Content-Type-Options "nosniff"
30 X-Frame-Options "SAMEORIGIN"
31 Referrer-Policy "strict-origin-when-cross-origin"
32 Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
33 -Server
34 }
35 '';
36
37 # Security headers for git repository viewer (allow inline scripts/styles for gitmal)
38 gitSecurityHeaders = ''
39 header {
40 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
41 X-Content-Type-Options "nosniff"
42 X-Frame-Options "SAMEORIGIN"
43 Referrer-Policy "strict-origin-when-cross-origin"
44 Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
45 Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"
46 X-XSS-Protection "1; mode=block"
47 -Server
48 }
49 '';
50
51 # Robots.txt snippet - polite request to AI scrapers
52 robotsTxtSnippet = ''
53 @robots path /robots.txt
54 handle @robots {
55 respond 200 {
56 body `User-agent: CCBot
57 Disallow: /
58
59 User-agent: ChatGPT-User
60 Disallow: /
61
62 User-agent: GPTBot
63 Disallow: /
64
65 User-agent: Google-Extended
66 Disallow: /
67
68 User-agent: anthropic-ai
69 Disallow: /
70
71 User-agent: Omgilibot
72 Disallow: /
73
74 User-agent: Omgili
75 Disallow: /
76
77 User-agent: FacebookBot
78 Disallow: /`
79 close
80 }
81 }
82 '';
83
84 # AI bot blocking snippet - enforcement via HTTP 403
85 blockAIBotsSnippet = ''
86 @aibots {
87 header User-Agent *CCBot*
88 header User-Agent *ChatGPT-User*
89 header User-Agent *GPTBot*
90 header User-Agent *Google-Extended*
91 header User-Agent *anthropic-ai*
92 header User-Agent *Omgilibot*
93 header User-Agent *Omgili*
94 header User-Agent *FacebookBot*
95 }
96 handle @aibots {
97 respond "AI scraping not permitted" 403
98 }
99 '';
100in
101{
102 imports = [
103
104 ../common/services/openssh.nix
105 ];
106
107 # ── Fail2ban ────────────────────────────────────────────────────────
108 services.fail2ban = {
109 enable = true;
110
111 # Ban for 1 hour, increase on repeat offenders via recidive jail
112 bantime = "1h";
113 bantime-increment = {
114 enable = true;
115 maxtime = "168h"; # Max 1 week ban
116 factor = "4"; # Aggressive escalation
117 };
118
119 maxretry = 5;
120
121 # Ignore VPN and loopback
122 ignoreIP = [
123 "127.0.0.0/8"
124 "::1"
125 "10.100.0.0/24" # WireGuard VPN
126 "176.172.78.0/24" # Home (Bouygues FTTH)
127 "196.176.0.0/16" # Tunisia (Ooredoo)
128 ];
129
130 jails = {
131 # Caddy auth failures (401/403 responses)
132 caddy-auth = ''
133 enabled = true
134 backend = auto
135 filter = caddy-auth
136 logpath = /var/log/caddy/access*.log
137 maxretry = 10
138 findtime = 600
139 bantime = 3600
140 '';
141
142 # Caddy aggressive scanning (404 floods)
143 caddy-scan = ''
144 enabled = true
145 backend = auto
146 filter = caddy-scan
147 logpath = /var/log/caddy/access*.log
148 maxretry = 30
149 findtime = 60
150 bantime = 3600
151 '';
152
153 # Caddy rate abuse (too many requests)
154 caddy-flood = ''
155 enabled = true
156 backend = auto
157 filter = caddy-flood
158 logpath = /var/log/caddy/access*.log
159 maxretry = 200
160 findtime = 60
161 bantime = 7200
162 '';
163 };
164 };
165
166 # Caddy fail2ban filters for JSON access logs
167 environment.etc = {
168 # Ban IPs that get too many 401/403 responses (brute force / unauthorized access)
169 "fail2ban/filter.d/caddy-auth.conf".text = ''
170 [Definition]
171 failregex = ^.*"remote_ip":"<HOST>".*"status":(401|403),.*$
172 ignoreregex = ^.*"host":"ntfy\.sbr\.pm".*$
173 datepattern = "ts":{EPOCH}
174 '';
175
176 # Ban IPs that trigger excessive 404s (scanning for vulnerabilities)
177 "fail2ban/filter.d/caddy-scan.conf".text = ''
178 [Definition]
179 failregex = ^.*"remote_ip":"<HOST>".*"status":404,.*$
180 ignoreregex =
181 datepattern = "ts":{EPOCH}
182 '';
183
184 # Ban IPs with excessive request volume (flood / DDoS)
185 "fail2ban/filter.d/caddy-flood.conf".text = ''
186 [Definition]
187 failregex = ^.*"remote_ip":"<HOST>".*"status":\d+,.*$
188 ignoreregex = ^.*"remote_ip":"10\.100\.0\..*$
189 ^.*"host":"ntfy\.sbr\.pm".*$
190 ^.*"host":"git\.sbr\.pm".*$
191 datepattern = "ts":{EPOCH}
192 '';
193 };
194
195 # Age secrets
196 age.secrets."ntfy-token" = {
197 file = ../../secrets/sakhalin/ntfy-token.age;
198 mode = "400";
199 owner = "root";
200 group = "root";
201 };
202
203 # Allow Caddy to access git repositories in vincent's home
204 users.users.caddy.extraGroups = [ "users" ];
205
206 # Allow vincent to run systemd-run without password (for git hooks)
207 # Use /run/current-system/sw/bin path to avoid hardcoded Nix store paths
208 security.sudo.extraRules = [
209 {
210 users = [ "vincent" ];
211 commands = [
212 {
213 command = "/run/current-system/sw/bin/systemd-run";
214 options = [ "NOPASSWD" ];
215 }
216 ];
217 }
218 ];
219
220 # Install gitmal for self-hosted git web view
221 environment.systemPackages = with pkgs; [
222 gitmal
223 ];
224
225 # Git hook background task execution with notifications
226 systemd.services."git-notify@" = {
227 description = "Git build notification for %i";
228 serviceConfig = {
229 Type = "oneshot";
230 ExecStart = "${pkgs.writeShellScript "git-notify" ''
231 #!/usr/bin/env bash
232 set -euo pipefail
233
234 UNIT_NAME="$1"
235 RESULT=$(${pkgs.systemd}/bin/systemctl show -p Result --value "$UNIT_NAME")
236 EXIT_CODE=$(${pkgs.systemd}/bin/systemctl show -p ExecMainStatus --value "$UNIT_NAME")
237
238 # Get execution timestamps (in microseconds since epoch)
239 START_TIME=$(${pkgs.systemd}/bin/systemctl show -p ExecMainStartTimestamp --value "$UNIT_NAME")
240 EXIT_TIME=$(${pkgs.systemd}/bin/systemctl show -p ExecMainExitTimestamp --value "$UNIT_NAME")
241
242 # Calculate duration in seconds
243 START_EPOCH=$(${pkgs.coreutils}/bin/date -d "$START_TIME" +%s 2>/dev/null || echo "0")
244 EXIT_EPOCH=$(${pkgs.coreutils}/bin/date -d "$EXIT_TIME" +%s 2>/dev/null || echo "0")
245 DURATION=$((EXIT_EPOCH - START_EPOCH))
246
247 # Format duration as human-readable
248 if [ "$DURATION" -ge 60 ]; then
249 MINUTES=$((DURATION / 60))
250 SECONDS=$((DURATION % 60))
251 DURATION_STR="''${MINUTES}m ''${SECONDS}s"
252 else
253 DURATION_STR="''${DURATION}s"
254 fi
255
256 # Parse unit name to extract job type and repo
257 # Format: git-<job>-<repo>-<timestamp>
258 JOB_TYPE=$(echo "$UNIT_NAME" | cut -d'-' -f2)
259 REPO=$(echo "$UNIT_NAME" | cut -d'-' -f3)
260
261 # Only notify on failure
262 if [ "$RESULT" != "success" ]; then
263 ${pkgs.curl}/bin/curl -s \
264 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
265 config.age.secrets."ntfy-token".path
266 })" \
267 -H "Title: ❌ Git $JOB_TYPE Failed: $REPO (after $DURATION_STR)" \
268 -H "Priority: high" \
269 -H "Tags: x,git,$JOB_TYPE,warning" \
270 -d "Job $UNIT_NAME failed after $DURATION_STR (exit code: $EXIT_CODE). Check logs: journalctl -u $UNIT_NAME" \
271 "https://ntfy.sbr.pm/git-builds" || true
272 fi
273 ''} %i";
274 };
275 };
276
277 # Helper script for gitmal generation (called from post-receive hooks)
278 environment.etc."git-hooks/generate-gitmal.sh" = {
279 text = ''
280 #!${pkgs.bash}/bin/bash
281 set -euo pipefail
282
283 # Set PATH to include git and coreutils (gitmal needs git, script needs basename)
284 export PATH="${pkgs.git}/bin:${pkgs.coreutils}/bin:$PATH"
285
286 REPO_PATH="$1"
287 THEME="''${2:-github-dark}" # Default to 'github-dark' theme if not specified
288 REPO_NAME=$(basename "$REPO_PATH" .git)
289 OUTPUT_DIR="/home/vincent/git/public/$REPO_NAME"
290
291 echo "Generating gitmal for repository: $REPO_NAME"
292 echo "Repository path: $REPO_PATH"
293 echo "Output directory: $OUTPUT_DIR"
294 echo "Theme: $THEME"
295
296 # Generate static site with gitmal
297 cd "$REPO_PATH"
298 ${pkgs.gitmal}/bin/gitmal --output "$OUTPUT_DIR" --theme "$THEME"
299
300 echo "Gitmal generation complete: $OUTPUT_DIR"
301 '';
302 mode = "0755";
303 };
304
305 # Remote build forwarding script
306 environment.etc."git-hooks/forward-build.sh" = {
307 text = ''
308 #!${pkgs.bash}/bin/bash
309 set -euo pipefail
310
311 REPO_NAME="$1"
312 BUILD_TYPE="''${2:-auto}"
313 TIMESTAMP=$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S)
314 UNIT_NAME="build-remote-''${REPO_NAME}-''${TIMESTAMP}"
315
316 echo "Forwarding build to aomi: $REPO_NAME ($BUILD_TYPE)..."
317
318 # SSH to aomi and trigger build with systemd-run
319 ${pkgs.openssh}/bin/ssh -o BatchMode=yes builder@10.100.0.17 \
320 "sudo /run/current-system/sw/bin/systemd-run \
321 --unit=\"$UNIT_NAME\" \
322 --description=\"Remote build: $REPO_NAME ($BUILD_TYPE)\" \
323 --property=\"OnSuccess=job-notify@\''${UNIT_NAME}.service\" \
324 --property=\"OnFailure=job-notify@\''${UNIT_NAME}.service\" \
325 --property=\"User=builder\" \
326 --property=\"Group=users\" \
327 --property=\"WorkingDirectory=/var/lib/git-builds\" \
328 /etc/git-builds/execute-build.sh \"$REPO_NAME\" \"$BUILD_TYPE\""
329
330 echo "✓ Build queued on aomi: $UNIT_NAME"
331 echo " View status: ssh aomi 'systemctl status $UNIT_NAME'"
332 echo " View logs: ssh aomi 'journalctl -u $UNIT_NAME'"
333 '';
334 mode = "0755";
335 };
336
337 # Example post-receive hook template
338 environment.etc."git-hooks/post-receive.example" = {
339 text = ''
340 #!${pkgs.bash}/bin/bash
341 # Example post-receive hook for git repositories
342 # Copy this to your repository's hooks/post-receive and make it executable
343 #
344 # This hook uses systemd-run to execute gitmal generation in the background
345 # with automatic notifications via ntfy when the job completes.
346 #
347 # Optionally, it can also trigger remote builds on aomi.
348
349 set -euo pipefail
350
351 # Configuration
352 GITMAL_ENABLED="true"
353 GITMAL_THEME="github-dark" # Options: github-dark, github-light, dark, light, auto
354 REMOTE_BUILD_ENABLED="false" # Set to "true" to enable remote builds on aomi
355 BUILD_TYPE="nixos" # Options: nixos, make, docker, go, custom, auto
356
357 REPO_PATH="$(pwd)"
358 REPO_NAME=$(basename "$REPO_PATH" .git)
359 TIMESTAMP=$(date +%Y%m%d-%H%M%S)
360
361 # 1. Generate gitmal (local static site on kerkouane)
362 if [ "$GITMAL_ENABLED" = "true" ]; then
363 UNIT_NAME="git-gitmal-''${REPO_NAME}-''${TIMESTAMP}"
364 echo "Queuing gitmal generation for $REPO_NAME with theme: $GITMAL_THEME..."
365
366 sudo /run/current-system/sw/bin/systemd-run \
367 --unit="$UNIT_NAME" \
368 --description="Gitmal generation for $REPO_NAME" \
369 --property="OnSuccess=git-notify@''${UNIT_NAME}.service" \
370 --property="OnFailure=git-notify@''${UNIT_NAME}.service" \
371 --property="User=vincent" \
372 --property="Group=users" \
373 --working-directory="$REPO_PATH" \
374 /etc/git-hooks/generate-gitmal.sh "$REPO_PATH" "$GITMAL_THEME"
375
376 echo "✓ Gitmal generation queued as: $UNIT_NAME"
377 echo " View status: systemctl status $UNIT_NAME"
378 echo " View logs: journalctl -u $UNIT_NAME"
379 fi
380
381 # 2. Trigger remote build (on aomi)
382 if [ "$REMOTE_BUILD_ENABLED" = "true" ]; then
383 /etc/git-hooks/forward-build.sh "$REPO_NAME" "$BUILD_TYPE"
384 fi
385 '';
386 mode = "0755";
387 };
388
389 # Setup permissions for git directories (via systemd tmpfiles)
390 systemd.tmpfiles.rules = [
391 "d /home/vincent 0711 vincent users -" # Allow traversal to git directory
392 "d /home/vincent/git 0700 vincent users -" # Private git directory
393 "d /home/vincent/git/public 0755 vincent users -" # Public repositories only
394 "d /var/log/git-builds 0755 vincent users -" # Git build logs
395 ];
396
397 # Disable TPM2 (VPS has no TPM hardware)
398 security.tpm2.enable = lib.mkForce false;
399
400 # Override common SSH config to restrict to VPN network only
401 services.openssh = {
402 listenAddresses = [
403 {
404 addr = builtins.head globals.machines.kerkouane.net.vpn.ips;
405 port = 22;
406 }
407 ];
408 openFirewall = lib.mkForce false;
409 };
410
411 services.wireguard.server = {
412 enable = true;
413 ips = libx.wg-ips globals.machines.kerkouane.net.vpn.ips;
414 peers = libx.generateWireguardPeers globals.machines;
415 };
416
417 # SOCKS5 proxy for SearXNG outgoing requests (VPN-only)
418 # Allows searx on sakhalin to route queries through kerkouane's IP
419 # for round-robin proxy rotation to avoid search engine rate limiting
420 services.microsocks = {
421 enable = true;
422 ip = builtins.head globals.machines.kerkouane.net.vpn.ips;
423 port = 1080;
424 };
425
426 services.gosmee = {
427 enable = true;
428 public-url = "https://webhook.sbr.pm";
429 };
430
431 services.ntfy-sh = {
432 enable = true;
433 settings = {
434 base-url = "https://ntfy.sbr.pm";
435 upstream-base-url = "https://ntfy.sh";
436 listen-http = "localhost:8111";
437 behind-proxy = true;
438 enable-login = true;
439 auth-default-access = "deny-all";
440 };
441 };
442
443 # Firewall configuration
444 # TODO: Migrate to nftables once wireguard server module supports it
445 networking.firewall = {
446 allowPing = true;
447 # Public ports
448 allowedTCPPorts = [
449 80 # HTTP
450 443 # HTTPS
451 ];
452
453 # Additional iptables rules
454 extraCommands = ''
455 # Allow node exporter (9000) only from VPN network
456 iptables -A nixos-fw -p tcp -s 10.100.0.0/24 --dport 9000 -j nixos-fw-accept
457
458 # Allow microsocks SOCKS5 proxy (1080) only from VPN network
459 iptables -A nixos-fw -p tcp -s 10.100.0.0/24 --dport 1080 -j nixos-fw-accept
460
461 # Block known SYN flood source (USBINF INFORMATICA LTDA, Brazil)
462 iptables -I nixos-fw 1 -s 45.233.176.0/22 -j DROP
463 ip6tables -I nixos-fw 1 -s ::ffff:45.233.176.0/118 -j DROP
464
465 # SYN flood protection: limit new connections per /24 subnet
466 iptables -A nixos-fw -p tcp --syn -m connlimit --connlimit-above 30 --connlimit-mask 24 -j DROP
467 ip6tables -A nixos-fw -p tcp --syn -m connlimit --connlimit-above 30 --connlimit-mask 24 -j DROP
468 '';
469 };
470 # Allow Caddy to access public git repositories only (override ProtectHome)
471 systemd.services.caddy.serviceConfig = {
472 ProtectHome = lib.mkForce "tmpfs"; # Allow read access to /home with bind mounts
473 BindReadOnlyPaths = [ "/home/vincent/git/public" ];
474 };
475
476 services.caddy = {
477 enable = true;
478 email = "vincent@sbr.pm";
479
480 # Use Caddy with rate-limit plugin
481 package = pkgs.caddy.withPlugins {
482 plugins = [ "github.com/mholt/caddy-ratelimit@v0.1.1-0.20250915152450-04ea34edc0c4" ];
483 hash = "sha256-x5jeKfjXeUj4t5t6+gRRjpmjF3n5P25T0lVED4EEu54=";
484 };
485
486 # Enable Prometheus metrics on VPN interface only
487 globalConfig = ''
488 admin ${builtins.head globals.machines.kerkouane.net.vpn.ips}:2019
489 metrics
490 '';
491
492 # Enable JSON access logging (NixOS option)
493 logFormat = ''
494 output file /var/log/caddy/access.log {
495 roll_size 100MiB
496 roll_keep 10
497 roll_keep_for 720h
498 }
499 format json
500 '';
501
502 virtualHosts = {
503 # File server with directory browsing (replaces fancyindex)
504 "dl.sbr.pm".extraConfig = ''
505 ${blockAIBotsSnippet}
506 ${robotsTxtSnippet}
507
508 root * /var/www/dl.sbr.pm
509 file_server browse {
510 hide .fancyindex README.md HEADER.md
511 }
512
513 ${securityHeaders}
514 '';
515
516 # Alias for dl.sbr.pm
517 "files.sbr.pm".extraConfig = ''
518 redir https://dl.sbr.pm{uri} permanent
519 '';
520
521 # ntfy - reverse proxy with websockets
522 "ntfy.sbr.pm".extraConfig = ''
523 # Rate limiting for notification service
524 rate_limit {
525 zone ntfy_publish {
526 key {remote_host}
527 events 50
528 window 1m
529 }
530 }
531
532 reverse_proxy localhost:8111
533 '';
534
535 # Static sites
536 "paste.sbr.pm".extraConfig = ''
537 ${blockAIBotsSnippet}
538 ${robotsTxtSnippet}
539
540 root * /var/www/paste.sbr.pm
541 file_server
542 ${securityHeaders}
543 '';
544
545 "sbr.pm".extraConfig = ''
546 ${blockAIBotsSnippet}
547 ${robotsTxtSnippet}
548
549 root * /var/www/sbr.pm
550 file_server
551 ${securityHeaders}
552 '';
553
554 # Go vanity URL service
555 "go.sbr.pm".extraConfig = ''
556 reverse_proxy localhost:8080
557 ${securityHeaders}
558 '';
559
560 # Whoami service (remote)
561 "whoami.sbr.pm".extraConfig = ''
562 reverse_proxy 10.100.0.8:80 {
563 header_up Host {host}
564 }
565 '';
566
567 # Immich photo management (proxied to rhea)
568 "immich.sbr.pm".extraConfig = ''
569 ${blockAIBotsSnippet}
570 ${robotsTxtSnippet}
571
572 # Allow large photo/video uploads (50GB limit)
573 request_body {
574 max_size 50GB
575 }
576
577 # Strict rate limiting for authentication endpoints
578 @auth {
579 path /auth/* /api/auth/*
580 }
581 route @auth {
582 rate_limit {
583 zone immich_auth {
584 key {remote_host}
585 events 10
586 window 1m
587 }
588 }
589 reverse_proxy 10.100.0.50:2283 {
590 header_up Host {host}
591 header_up X-Real-IP {remote_host}
592 }
593 }
594
595 # Moderate rate limiting for API endpoints
596 @api {
597 path /api/*
598 }
599 route @api {
600 rate_limit {
601 zone immich_api {
602 key {remote_host}
603 events 100
604 window 1m
605 }
606 }
607 reverse_proxy 10.100.0.50:2283 {
608 header_up Host {host}
609 header_up X-Real-IP {remote_host}
610 }
611 }
612
613 # Permissive rate limiting for media/general requests
614 rate_limit {
615 zone immich_media {
616 key {remote_host}
617 events 1000
618 window 1m
619 }
620 }
621
622 reverse_proxy 10.100.0.50:2283 {
623 header_up Host {host}
624 header_up X-Real-IP {remote_host}
625 }
626
627 ${mediaSecurityHeaders}
628 '';
629
630 # Navidrome music streaming (proxied to aion)
631 "navidrome.sbr.pm".extraConfig = ''
632 ${blockAIBotsSnippet}
633 ${robotsTxtSnippet}
634
635 # Rate limiting for music streaming
636 rate_limit {
637 zone navidrome_general {
638 key {remote_host}
639 events 500
640 window 1m
641 }
642 }
643
644 reverse_proxy 10.100.0.49:4533 {
645 header_up Host {host}
646 header_up X-Real-IP {remote_host}
647 }
648
649 ${mediaSecurityHeaders}
650 '';
651
652 # Jellyfin media server (proxied to rhea)
653 "jellyfin.sbr.pm".extraConfig = ''
654 ${blockAIBotsSnippet}
655 ${robotsTxtSnippet}
656
657 # Rate limiting for media server
658 rate_limit {
659 zone jellyfin_general {
660 key {remote_host}
661 events 500
662 window 1m
663 }
664 }
665
666 reverse_proxy 10.100.0.50:8096 {
667 header_up Host {host}
668 header_up X-Real-IP {remote_host}
669 }
670
671 ${mediaSecurityHeaders}
672 '';
673
674 # Audiobookshelf audiobook server (proxied to aion)
675 "audiobookshelf.sbr.pm".extraConfig = ''
676 ${blockAIBotsSnippet}
677 ${robotsTxtSnippet}
678
679 # Rate limiting for audiobook streaming
680 rate_limit {
681 zone audiobookshelf_general {
682 key {remote_host}
683 events 500
684 window 1m
685 }
686 }
687
688 reverse_proxy 10.100.0.49:13378 {
689 header_up Host {host}
690 header_up X-Real-IP {remote_host}
691 }
692
693 ${mediaSecurityHeaders}
694 '';
695
696 # Service aliases (user-friendly URLs - transparent proxy)
697 "music.sbr.pm".extraConfig = ''
698 ${blockAIBotsSnippet}
699 ${robotsTxtSnippet}
700
701 # Rate limiting for music streaming
702 rate_limit {
703 zone music_general {
704 key {remote_host}
705 events 500
706 window 1m
707 }
708 }
709
710 reverse_proxy 10.100.0.49:4533 {
711 header_up Host {host}
712 header_up X-Real-IP {remote_host}
713 }
714
715 ${mediaSecurityHeaders}
716 '';
717
718 "photos.sbr.pm".extraConfig = ''
719 ${blockAIBotsSnippet}
720 ${robotsTxtSnippet}
721
722 # Allow large photo/video uploads (50GB limit)
723 request_body {
724 max_size 50GB
725 }
726
727 # Strict rate limiting for authentication endpoints
728 @auth {
729 path /auth/* /api/auth/*
730 }
731 route @auth {
732 rate_limit {
733 zone photos_auth {
734 key {remote_host}
735 events 10
736 window 1m
737 }
738 }
739 reverse_proxy 10.100.0.50:2283 {
740 header_up Host {host}
741 header_up X-Real-IP {remote_host}
742 }
743 }
744
745 # Moderate rate limiting for API endpoints
746 @api {
747 path /api/*
748 }
749 route @api {
750 rate_limit {
751 zone photos_api {
752 key {remote_host}
753 events 100
754 window 1m
755 }
756 }
757 reverse_proxy 10.100.0.50:2283 {
758 header_up Host {host}
759 header_up X-Real-IP {remote_host}
760 }
761 }
762
763 # Permissive rate limiting for media/general requests
764 rate_limit {
765 zone photos_media {
766 key {remote_host}
767 events 1000
768 window 1m
769 }
770 }
771
772 reverse_proxy 10.100.0.50:2283 {
773 header_up Host {host}
774 header_up X-Real-IP {remote_host}
775 }
776
777 ${mediaSecurityHeaders}
778 '';
779
780 "podcasts.sbr.pm".extraConfig = ''
781 ${blockAIBotsSnippet}
782 ${robotsTxtSnippet}
783
784 # Rate limiting for audiobook streaming
785 rate_limit {
786 zone podcasts_general {
787 key {remote_host}
788 events 500
789 window 1m
790 }
791 }
792
793 reverse_proxy 10.100.0.49:13378 {
794 header_up Host {host}
795 header_up X-Real-IP {remote_host}
796 }
797
798 ${mediaSecurityHeaders}
799 '';
800
801 # Webhook/gosmee service with SSE support
802 "webhook.sbr.pm".extraConfig = ''
803 reverse_proxy localhost:3333 {
804 flush_interval -1
805 }
806 '';
807
808 # Personal website with directory browsing
809 "vincent.demeester.fr".extraConfig = ''
810 ${blockAIBotsSnippet}
811 ${robotsTxtSnippet}
812
813 root * /var/www/vincent.demeester.fr
814
815 # Try files with .html extension
816 try_files {path} {path}.html {path}/ /index.html
817
818 file_server browse {
819 hide .fancyindex README.md HEADER.md
820 }
821
822 ${securityHeaders}
823 '';
824
825 # Self-hosted git repositories (public only)
826 "git.sbr.pm".extraConfig = ''
827 ${blockAIBotsSnippet}
828 ${robotsTxtSnippet}
829
830 root * /home/vincent/git/public
831 file_server browse {
832 hide .fancyindex README.md HEADER.md
833 }
834
835 ${gitSecurityHeaders}
836 '';
837 };
838 };
839
840 services.govanityurl = {
841 enable = true;
842 user = "caddy";
843 host = "go.sbr.pm";
844 config = ''
845 paths:
846 /x:
847 repo: https://github.com/vdemeester/x
848 /lord:
849 repo: https://github.com/vdemeester/lord
850 /ape:
851 repo: https://git.sr.ht/~vdemeester/ape
852 /nr:
853 repo: https://git.sr.ht/~vdemeester/nr
854 /ram:
855 repo: https://git.sr.ht/~vdemeester/ram
856 /sec:
857 repo: https://git.sr.ht/~vdemeester/sec
858 '';
859 };
860 security.acme = {
861 acceptTerms = true;
862 defaults.email = "vincent@sbr.pm";
863 };
864}