fedora-csb-system-manager
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"
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 ../common/services/prometheus-exporters-node.nix
104 ../common/services/openssh.nix
105 ];
106
107 # Age secrets
108 age.secrets."ntfy-token" = {
109 file = ../../secrets/sakhalin/ntfy-token.age;
110 mode = "400";
111 owner = "root";
112 group = "root";
113 };
114
115 # TODO make it an option ? (otherwise I'll add it for all)
116 users.users.vincent.linger = true;
117
118 # Allow Caddy to access git repositories in vincent's home
119 users.users.caddy.extraGroups = [ "users" ];
120
121 # Allow vincent to run systemd-run without password (for git hooks)
122 # Use /run/current-system/sw/bin path to avoid hardcoded Nix store paths
123 security.sudo.extraRules = [
124 {
125 users = [ "vincent" ];
126 commands = [
127 {
128 command = "/run/current-system/sw/bin/systemd-run";
129 options = [ "NOPASSWD" ];
130 }
131 ];
132 }
133 ];
134
135 # Install gitmal for self-hosted git web view
136 environment.systemPackages = with pkgs; [
137 gitmal
138 ];
139
140 # Git hook background task execution with notifications
141 systemd.services."git-notify@" = {
142 description = "Git build notification for %i";
143 serviceConfig = {
144 Type = "oneshot";
145 ExecStart = "${pkgs.writeShellScript "git-notify" ''
146 #!/usr/bin/env bash
147 set -euo pipefail
148
149 UNIT_NAME="$1"
150 RESULT=$(${pkgs.systemd}/bin/systemctl show -p Result --value "$UNIT_NAME")
151 EXIT_CODE=$(${pkgs.systemd}/bin/systemctl show -p ExecMainStatus --value "$UNIT_NAME")
152
153 # Get execution timestamps (in microseconds since epoch)
154 START_TIME=$(${pkgs.systemd}/bin/systemctl show -p ExecMainStartTimestamp --value "$UNIT_NAME")
155 EXIT_TIME=$(${pkgs.systemd}/bin/systemctl show -p ExecMainExitTimestamp --value "$UNIT_NAME")
156
157 # Calculate duration in seconds
158 START_EPOCH=$(${pkgs.coreutils}/bin/date -d "$START_TIME" +%s 2>/dev/null || echo "0")
159 EXIT_EPOCH=$(${pkgs.coreutils}/bin/date -d "$EXIT_TIME" +%s 2>/dev/null || echo "0")
160 DURATION=$((EXIT_EPOCH - START_EPOCH))
161
162 # Format duration as human-readable
163 if [ "$DURATION" -ge 60 ]; then
164 MINUTES=$((DURATION / 60))
165 SECONDS=$((DURATION % 60))
166 DURATION_STR="''${MINUTES}m ''${SECONDS}s"
167 else
168 DURATION_STR="''${DURATION}s"
169 fi
170
171 # Parse unit name to extract job type and repo
172 # Format: git-<job>-<repo>-<timestamp>
173 JOB_TYPE=$(echo "$UNIT_NAME" | cut -d'-' -f2)
174 REPO=$(echo "$UNIT_NAME" | cut -d'-' -f3)
175
176 if [ "$RESULT" = "success" ]; then
177 ${pkgs.curl}/bin/curl -s \
178 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
179 config.age.secrets."ntfy-token".path
180 })" \
181 -H "Title: ✅ Git $JOB_TYPE Success: $REPO ($DURATION_STR)" \
182 -H "Tags: white_check_mark,git,$JOB_TYPE" \
183 -H "Priority: default" \
184 -d "Job $UNIT_NAME completed successfully in $DURATION_STR (exit code: $EXIT_CODE)" \
185 "https://ntfy.sbr.pm/git-builds" || true
186 else
187 ${pkgs.curl}/bin/curl -s \
188 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
189 config.age.secrets."ntfy-token".path
190 })" \
191 -H "Title: ❌ Git $JOB_TYPE Failed: $REPO (after $DURATION_STR)" \
192 -H "Priority: high" \
193 -H "Tags: x,git,$JOB_TYPE,warning" \
194 -d "Job $UNIT_NAME failed after $DURATION_STR (exit code: $EXIT_CODE). Check logs: journalctl -u $UNIT_NAME" \
195 "https://ntfy.sbr.pm/git-builds" || true
196 fi
197 ''} %i";
198 };
199 };
200
201 # Helper script for gitmal generation (called from post-receive hooks)
202 environment.etc."git-hooks/generate-gitmal.sh" = {
203 text = ''
204 #!${pkgs.bash}/bin/bash
205 set -euo pipefail
206
207 # Set PATH to include git and coreutils (gitmal needs git, script needs basename)
208 export PATH="${pkgs.git}/bin:${pkgs.coreutils}/bin:$PATH"
209
210 REPO_PATH="$1"
211 THEME="''${2:-github-dark}" # Default to 'github-dark' theme if not specified
212 REPO_NAME=$(basename "$REPO_PATH" .git)
213 OUTPUT_DIR="/home/vincent/git/public/$REPO_NAME"
214
215 echo "Generating gitmal for repository: $REPO_NAME"
216 echo "Repository path: $REPO_PATH"
217 echo "Output directory: $OUTPUT_DIR"
218 echo "Theme: $THEME"
219
220 # Generate static site with gitmal
221 cd "$REPO_PATH"
222 ${pkgs.gitmal}/bin/gitmal --output "$OUTPUT_DIR" --theme "$THEME"
223
224 echo "Gitmal generation complete: $OUTPUT_DIR"
225 '';
226 mode = "0755";
227 };
228
229 # Remote build forwarding script
230 environment.etc."git-hooks/forward-build.sh" = {
231 text = ''
232 #!${pkgs.bash}/bin/bash
233 set -euo pipefail
234
235 REPO_NAME="$1"
236 BUILD_TYPE="''${2:-auto}"
237 TIMESTAMP=$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S)
238 UNIT_NAME="build-remote-''${REPO_NAME}-''${TIMESTAMP}"
239
240 echo "Forwarding build to aomi: $REPO_NAME ($BUILD_TYPE)..."
241
242 # SSH to aomi and trigger build with systemd-run
243 ${pkgs.openssh}/bin/ssh -o BatchMode=yes builder@10.100.0.17 \
244 "sudo /run/current-system/sw/bin/systemd-run \
245 --unit=\"$UNIT_NAME\" \
246 --description=\"Remote build: $REPO_NAME ($BUILD_TYPE)\" \
247 --property=\"OnSuccess=job-notify@\''${UNIT_NAME}.service\" \
248 --property=\"OnFailure=job-notify@\''${UNIT_NAME}.service\" \
249 --property=\"User=builder\" \
250 --property=\"Group=users\" \
251 --property=\"WorkingDirectory=/var/lib/git-builds\" \
252 /etc/git-builds/execute-build.sh \"$REPO_NAME\" \"$BUILD_TYPE\""
253
254 echo "✓ Build queued on aomi: $UNIT_NAME"
255 echo " View status: ssh aomi 'systemctl status $UNIT_NAME'"
256 echo " View logs: ssh aomi 'journalctl -u $UNIT_NAME'"
257 '';
258 mode = "0755";
259 };
260
261 # Example post-receive hook template
262 environment.etc."git-hooks/post-receive.example" = {
263 text = ''
264 #!${pkgs.bash}/bin/bash
265 # Example post-receive hook for git repositories
266 # Copy this to your repository's hooks/post-receive and make it executable
267 #
268 # This hook uses systemd-run to execute gitmal generation in the background
269 # with automatic notifications via ntfy when the job completes.
270 #
271 # Optionally, it can also trigger remote builds on aomi.
272
273 set -euo pipefail
274
275 # Configuration
276 GITMAL_ENABLED="true"
277 GITMAL_THEME="github-dark" # Options: github-dark, github-light, dark, light, auto
278 REMOTE_BUILD_ENABLED="false" # Set to "true" to enable remote builds on aomi
279 BUILD_TYPE="nixos" # Options: nixos, make, docker, go, custom, auto
280
281 REPO_PATH="$(pwd)"
282 REPO_NAME=$(basename "$REPO_PATH" .git)
283 TIMESTAMP=$(date +%Y%m%d-%H%M%S)
284
285 # 1. Generate gitmal (local static site on kerkouane)
286 if [ "$GITMAL_ENABLED" = "true" ]; then
287 UNIT_NAME="git-gitmal-''${REPO_NAME}-''${TIMESTAMP}"
288 echo "Queuing gitmal generation for $REPO_NAME with theme: $GITMAL_THEME..."
289
290 sudo /run/current-system/sw/bin/systemd-run \
291 --unit="$UNIT_NAME" \
292 --description="Gitmal generation for $REPO_NAME" \
293 --property="OnSuccess=git-notify@''${UNIT_NAME}.service" \
294 --property="OnFailure=git-notify@''${UNIT_NAME}.service" \
295 --property="User=vincent" \
296 --property="Group=users" \
297 --working-directory="$REPO_PATH" \
298 /etc/git-hooks/generate-gitmal.sh "$REPO_PATH" "$GITMAL_THEME"
299
300 echo "✓ Gitmal generation queued as: $UNIT_NAME"
301 echo " View status: systemctl status $UNIT_NAME"
302 echo " View logs: journalctl -u $UNIT_NAME"
303 fi
304
305 # 2. Trigger remote build (on aomi)
306 if [ "$REMOTE_BUILD_ENABLED" = "true" ]; then
307 /etc/git-hooks/forward-build.sh "$REPO_NAME" "$BUILD_TYPE"
308 fi
309 '';
310 mode = "0755";
311 };
312
313 # Setup permissions for git directories (via systemd tmpfiles)
314 systemd.tmpfiles.rules = [
315 "d /home/vincent 0711 vincent users -" # Allow traversal to git directory
316 "d /home/vincent/git 0700 vincent users -" # Private git directory
317 "d /home/vincent/git/public 0755 vincent users -" # Public repositories only
318 "d /var/log/git-builds 0755 vincent users -" # Git build logs
319 ];
320
321 # Disable TPM2 (VPS has no TPM hardware)
322 security.tpm2.enable = lib.mkForce false;
323
324 # Override common SSH config to restrict to VPN network only
325 services.openssh = {
326 listenAddresses = [
327 {
328 addr = builtins.head globals.machines.kerkouane.net.vpn.ips;
329 port = 22;
330 }
331 ];
332 openFirewall = lib.mkForce false;
333 };
334
335 services.wireguard.server = {
336 enable = true;
337 ips = libx.wg-ips globals.machines.kerkouane.net.vpn.ips;
338 peers = libx.generateWireguardPeers globals.machines;
339 };
340
341 services.gosmee = {
342 enable = true;
343 public-url = "https://webhook.sbr.pm";
344 };
345
346 services.ntfy-sh = {
347 enable = true;
348 settings = {
349 base-url = "https://ntfy.sbr.pm";
350 upstream-base-url = "https://ntfy.sh";
351 listen-http = "localhost:8111";
352 behind-proxy = true;
353 enable-login = true;
354 auth-default-access = "deny-all";
355 };
356 };
357
358 # Firewall configuration
359 # TODO: Migrate to nftables once wireguard server module supports it
360 networking.firewall = {
361 allowPing = true;
362 # Public ports
363 allowedTCPPorts = [
364 80 # HTTP
365 443 # HTTPS
366 ];
367
368 # Additional iptables rules
369 extraCommands = ''
370 # Allow node exporter (9000) only from VPN network
371 iptables -A nixos-fw -p tcp -s 10.100.0.0/16 --dport 9000 -j nixos-fw-accept
372 '';
373 };
374 # Allow Caddy to access public git repositories only (override ProtectHome)
375 systemd.services.caddy.serviceConfig = {
376 ProtectHome = lib.mkForce "tmpfs"; # Allow read access to /home with bind mounts
377 BindReadOnlyPaths = [ "/home/vincent/git/public" ];
378 };
379
380 services.caddy = {
381 enable = true;
382 email = "vincent@sbr.pm";
383
384 # Use Caddy with rate-limit plugin
385 package = pkgs.caddy.withPlugins {
386 plugins = [ "github.com/mholt/caddy-ratelimit@v0.1.1-0.20250915152450-04ea34edc0c4" ];
387 hash = "sha256-h3FJ2zUJiWbkC6Gop9Xj897Qt5K3px0zgXgLXykekRA=";
388 };
389
390 # Enable Prometheus metrics on VPN interface only
391 globalConfig = ''
392 admin ${builtins.head globals.machines.kerkouane.net.vpn.ips}:2019
393 metrics
394 '';
395
396 # Enable JSON access logging (NixOS option)
397 logFormat = ''
398 output file /var/log/caddy/access.log {
399 roll_size 100MiB
400 roll_keep 10
401 roll_keep_for 720h
402 }
403 format json
404 '';
405
406 virtualHosts = {
407 # File server with directory browsing (replaces fancyindex)
408 "dl.sbr.pm".extraConfig = ''
409 ${blockAIBotsSnippet}
410 ${robotsTxtSnippet}
411
412 root * /var/www/dl.sbr.pm
413 file_server browse {
414 hide .fancyindex README.md HEADER.md
415 }
416
417 ${securityHeaders}
418 '';
419
420 # Alias for dl.sbr.pm
421 "files.sbr.pm".extraConfig = ''
422 redir https://dl.sbr.pm{uri} permanent
423 '';
424
425 # ntfy - reverse proxy with websockets
426 "ntfy.sbr.pm".extraConfig = ''
427 # Rate limiting for notification service
428 rate_limit {
429 zone ntfy_publish {
430 key {remote_host}
431 events 50
432 window 1m
433 }
434 }
435
436 reverse_proxy localhost:8111
437 '';
438
439 # Static sites
440 "paste.sbr.pm".extraConfig = ''
441 ${blockAIBotsSnippet}
442 ${robotsTxtSnippet}
443
444 root * /var/www/paste.sbr.pm
445 file_server
446 ${securityHeaders}
447 '';
448
449 "sbr.pm".extraConfig = ''
450 ${blockAIBotsSnippet}
451 ${robotsTxtSnippet}
452
453 root * /var/www/sbr.pm
454 file_server
455 ${securityHeaders}
456 '';
457
458 # Go vanity URL service
459 "go.sbr.pm".extraConfig = ''
460 reverse_proxy localhost:8080
461 ${securityHeaders}
462 '';
463
464 # Whoami service (remote)
465 "whoami.sbr.pm".extraConfig = ''
466 reverse_proxy 10.100.0.8:80 {
467 header_up Host {host}
468 }
469 '';
470
471 # Immich photo management (proxied to rhea)
472 "immich.sbr.pm".extraConfig = ''
473 ${blockAIBotsSnippet}
474 ${robotsTxtSnippet}
475
476 # Allow large photo/video uploads (50GB limit)
477 request_body {
478 max_size 50GB
479 }
480
481 # Strict rate limiting for authentication endpoints
482 @auth {
483 path /auth/* /api/auth/*
484 }
485 route @auth {
486 rate_limit {
487 zone immich_auth {
488 key {remote_host}
489 events 10
490 window 1m
491 }
492 }
493 reverse_proxy 10.100.0.50:2283 {
494 header_up Host {host}
495 header_up X-Real-IP {remote_host}
496 }
497 }
498
499 # Moderate rate limiting for API endpoints
500 @api {
501 path /api/*
502 }
503 route @api {
504 rate_limit {
505 zone immich_api {
506 key {remote_host}
507 events 100
508 window 1m
509 }
510 }
511 reverse_proxy 10.100.0.50:2283 {
512 header_up Host {host}
513 header_up X-Real-IP {remote_host}
514 }
515 }
516
517 # Permissive rate limiting for media/general requests
518 rate_limit {
519 zone immich_media {
520 key {remote_host}
521 events 1000
522 window 1m
523 }
524 }
525
526 reverse_proxy 10.100.0.50:2283 {
527 header_up Host {host}
528 header_up X-Real-IP {remote_host}
529 }
530
531 ${mediaSecurityHeaders}
532 '';
533
534 # Navidrome music streaming (proxied to aion)
535 "navidrome.sbr.pm".extraConfig = ''
536 ${blockAIBotsSnippet}
537 ${robotsTxtSnippet}
538
539 # Rate limiting for music streaming
540 rate_limit {
541 zone navidrome_general {
542 key {remote_host}
543 events 500
544 window 1m
545 }
546 }
547
548 reverse_proxy 10.100.0.49:4533 {
549 header_up Host {host}
550 header_up X-Real-IP {remote_host}
551 }
552
553 ${mediaSecurityHeaders}
554 '';
555
556 # Jellyfin media server (proxied to rhea)
557 "jellyfin.sbr.pm".extraConfig = ''
558 ${blockAIBotsSnippet}
559 ${robotsTxtSnippet}
560
561 # Rate limiting for media server
562 rate_limit {
563 zone jellyfin_general {
564 key {remote_host}
565 events 500
566 window 1m
567 }
568 }
569
570 reverse_proxy 10.100.0.50:8096 {
571 header_up Host {host}
572 header_up X-Real-IP {remote_host}
573 }
574
575 ${mediaSecurityHeaders}
576 '';
577
578 # Audiobookshelf audiobook server (proxied to aion)
579 "audiobookshelf.sbr.pm".extraConfig = ''
580 ${blockAIBotsSnippet}
581 ${robotsTxtSnippet}
582
583 # Rate limiting for audiobook streaming
584 rate_limit {
585 zone audiobookshelf_general {
586 key {remote_host}
587 events 500
588 window 1m
589 }
590 }
591
592 reverse_proxy 10.100.0.49:13378 {
593 header_up Host {host}
594 header_up X-Real-IP {remote_host}
595 }
596
597 ${mediaSecurityHeaders}
598 '';
599
600 # Service aliases (user-friendly URLs - transparent proxy)
601 "music.sbr.pm".extraConfig = ''
602 ${blockAIBotsSnippet}
603 ${robotsTxtSnippet}
604
605 # Rate limiting for music streaming
606 rate_limit {
607 zone music_general {
608 key {remote_host}
609 events 500
610 window 1m
611 }
612 }
613
614 reverse_proxy 10.100.0.49:4533 {
615 header_up Host {host}
616 header_up X-Real-IP {remote_host}
617 }
618
619 ${mediaSecurityHeaders}
620 '';
621
622 "photos.sbr.pm".extraConfig = ''
623 ${blockAIBotsSnippet}
624 ${robotsTxtSnippet}
625
626 # Allow large photo/video uploads (50GB limit)
627 request_body {
628 max_size 50GB
629 }
630
631 # Strict rate limiting for authentication endpoints
632 @auth {
633 path /auth/* /api/auth/*
634 }
635 route @auth {
636 rate_limit {
637 zone photos_auth {
638 key {remote_host}
639 events 10
640 window 1m
641 }
642 }
643 reverse_proxy 10.100.0.50:2283 {
644 header_up Host {host}
645 header_up X-Real-IP {remote_host}
646 }
647 }
648
649 # Moderate rate limiting for API endpoints
650 @api {
651 path /api/*
652 }
653 route @api {
654 rate_limit {
655 zone photos_api {
656 key {remote_host}
657 events 100
658 window 1m
659 }
660 }
661 reverse_proxy 10.100.0.50:2283 {
662 header_up Host {host}
663 header_up X-Real-IP {remote_host}
664 }
665 }
666
667 # Permissive rate limiting for media/general requests
668 rate_limit {
669 zone photos_media {
670 key {remote_host}
671 events 1000
672 window 1m
673 }
674 }
675
676 reverse_proxy 10.100.0.50:2283 {
677 header_up Host {host}
678 header_up X-Real-IP {remote_host}
679 }
680
681 ${mediaSecurityHeaders}
682 '';
683
684 "podcasts.sbr.pm".extraConfig = ''
685 ${blockAIBotsSnippet}
686 ${robotsTxtSnippet}
687
688 # Rate limiting for audiobook streaming
689 rate_limit {
690 zone podcasts_general {
691 key {remote_host}
692 events 500
693 window 1m
694 }
695 }
696
697 reverse_proxy 10.100.0.49:13378 {
698 header_up Host {host}
699 header_up X-Real-IP {remote_host}
700 }
701
702 ${mediaSecurityHeaders}
703 '';
704
705 # Webhook/gosmee service with SSE support
706 "webhook.sbr.pm".extraConfig = ''
707 reverse_proxy localhost:3333 {
708 flush_interval -1
709 }
710 '';
711
712 # Personal website with directory browsing
713 "vincent.demeester.fr".extraConfig = ''
714 ${blockAIBotsSnippet}
715 ${robotsTxtSnippet}
716
717 root * /var/www/vincent.demeester.fr
718
719 # Try files with .html extension
720 try_files {path} {path}.html {path}/ /index.html
721
722 file_server browse {
723 hide .fancyindex README.md HEADER.md
724 }
725
726 ${securityHeaders}
727 '';
728
729 # Self-hosted git repositories (public only)
730 "git.sbr.pm".extraConfig = ''
731 ${blockAIBotsSnippet}
732 ${robotsTxtSnippet}
733
734 root * /home/vincent/git/public
735 file_server browse {
736 hide .fancyindex README.md HEADER.md
737 }
738
739 ${gitSecurityHeaders}
740 '';
741 };
742 };
743
744 services.govanityurl = {
745 enable = true;
746 user = "caddy";
747 host = "go.sbr.pm";
748 config = ''
749 paths:
750 /x:
751 repo: https://github.com/vdemeester/x
752 /lord:
753 repo: https://github.com/vdemeester/lord
754 /ape:
755 repo: https://git.sr.ht/~vdemeester/ape
756 /nr:
757 repo: https://git.sr.ht/~vdemeester/nr
758 /ram:
759 repo: https://git.sr.ht/~vdemeester/ram
760 /sec:
761 repo: https://git.sr.ht/~vdemeester/sec
762 '';
763 };
764 security.acme = {
765 acceptTerms = true;
766 defaults.email = "vincent@sbr.pm";
767 };
768}