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