Commit cdd1ec51081e

Vincent Demeester <vincent@sbr.pm>
2026-06-10 23:00:01
feat: decommission kerkouane from homelab repo
Removed all kerkouane references after migration to carthage (Hetzner) completed in April. Cleaned up globals, flake, secrets, wireguard, DNS, Makefile, android SSH configs, and deleted the systems/kerkouane directory.
1 parent 7f477eb
android/common/ssh_config
@@ -99,14 +99,6 @@ Host hokkaido.home
     HostName 192.168.1.115
     User vincent
 
-Host kerkouane.vpn
-    HostName 10.100.0.1
-    User vincent
-
-Host kerkouane.sbr.pm
-    HostName kerkouane.sbr.pm
-    User vincent
-
 Host kyushu.vpn
     HostName 10.100.0.19
     User vincent
android/osaka/zshrc
@@ -61,7 +61,6 @@ alias athena='ssh athena.vpn'
 alias demeter='ssh demeter.vpn'
 alias rhea='ssh rhea.vpn'
 alias aion='ssh aion.vpn'
-alias kerkouane='ssh kerkouane.vpn'
 alias shikoku='ssh shikoku.vpn'
 alias nagoya='ssh nagoya.vpn'
 alias kyushu='ssh kyushu.vpn'
android/suzu/zshrc
@@ -68,7 +68,6 @@ alias athena='ssh athena.vpn'
 alias demeter='ssh demeter.vpn'
 alias rhea='ssh rhea.vpn'
 alias aion='ssh aion.vpn'
-alias kerkouane='ssh kerkouane.vpn'
 alias shikoku='ssh shikoku.vpn'
 alias nagoya='ssh nagoya.vpn'
 alias kyushu='ssh kyushu.vpn'
lib/functions.nix
@@ -201,8 +201,7 @@ let
         })
         (
           lib.attrsets.filterAttrs (
-            name: value:
-            name != "kerkouane" && name != "carthage" && (hasVPNPublicKey value) && (hasVPNips value)
+            name: value: name != "carthage" && (hasVPNPublicKey value) && (hasVPNips value)
           ) machines
         )
     );
systems/common/services/dns/sbr.pm-common.nix
@@ -42,7 +42,6 @@ let
     "demeter"
     "athena"
     "nagoya"
-    "kerkouane"
     "aomi"
     "kyushu"
     "okinawa"
systems/common/services/dns/sbr.pm-gandi.nix
@@ -4,7 +4,7 @@ let
   dnsHelpers = import ../../../../lib/dns-helpers.nix { inherit globals; };
   inherit (dnsHelpers) getMachineIP;
 
-  # Import the common zone and override immich to point to kerkouane's public IP
+  # Import the common zone
   baseZone = import ./sbr.pm-common.nix {
     inherit dns globals;
     getIPForMachine = getMachineIP;
systems/common/services/wireguard.nix
@@ -1,6 +1,5 @@
 # Auto-derive WireGuard client config from hostname + globals.
-# VPN servers (kerkouane/carthage) are excluded — they keep their own server config.
-# MIGRATION: Cutover done 2026-04-02 — vpnServer is now "carthage" (Hetzner).
+# VPN server (carthage) is excluded — it keeps its own server config.
 {
   hostname,
   globals,
@@ -14,7 +13,7 @@ let
   vpnServer = "carthage";
 
   machine = globals.machines.${hostname};
-  isServer = hostname == "kerkouane" || hostname == "carthage";
+  isServer = hostname == "carthage";
   hasVpn = machine ? net && machine.net ? vpn;
 in
 {
systems/kerkouane/boot.nix
@@ -1,38 +0,0 @@
-{ lib, ... }:
-{
-  console.keyMap = lib.mkForce "us";
-  boot.loader.grub.device = "/dev/vda";
-  boot.loader.grub.enable = lib.mkForce true;
-  boot.loader.systemd-boot.enable = lib.mkForce false;
-  boot.initrd.systemd.enable = lib.mkForce false;
-
-  ## From qemu-quest.nix
-  boot.initrd.availableKernelModules = [
-    "virtio_net"
-    "virtio_pci"
-    "virtio_mmio"
-    "virtio_blk"
-    "virtio_scsi"
-    "9p"
-    "9pnet_virtio"
-  ];
-  boot.initrd.kernelModules = [
-    "virtio_balloon"
-    "virtio_console"
-    "virtio_rng"
-  ];
-
-  boot.initrd.postDeviceCommands = ''
-    # Set the system time from the hardware clock to work around a
-    # bug in qemu-kvm > 1.5.2 (where the VM clock is initialised
-    # to the *boot time* of the host).
-    hwclock -s
-  '';
-
-  # VPS optimization: No physical hardware, no firmware needed
-  hardware.enableRedistributableFirmware = lib.mkForce false;
-  hardware.enableAllFirmware = lib.mkForce false;
-
-  # VPS optimization: No firmware update service needed
-  services.fwupd.enable = lib.mkForce false;
-}
systems/kerkouane/extra.nix
@@ -1,864 +0,0 @@
-{
-  config,
-  globals,
-  lib,
-  libx,
-  pkgs,
-  ...
-}:
-let
-  # Common security headers for Caddy
-  securityHeaders = ''
-    header {
-      Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
-      X-Content-Type-Options "nosniff"
-      X-Frame-Options "SAMEORIGIN"
-      Referrer-Policy "strict-origin-when-cross-origin"
-      Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
-      Content-Security-Policy "default-src 'self' *.sbr.pm *.demeester.fr; style-src 'self' 'unsafe-inline'; script-src 'self'"
-      X-XSS-Protection "1; mode=block"
-      Cache-Control "public, max-age=604800, immutable"
-      -Server
-    }
-  '';
-
-  # Security headers for media services (more permissive CSP for multimedia)
-  mediaSecurityHeaders = ''
-    header {
-      Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
-      X-Content-Type-Options "nosniff"
-      X-Frame-Options "SAMEORIGIN"
-      Referrer-Policy "strict-origin-when-cross-origin"
-      Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
-      -Server
-    }
-  '';
-
-  # Security headers for git repository viewer (allow inline scripts/styles for gitmal)
-  gitSecurityHeaders = ''
-    header {
-      Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
-      X-Content-Type-Options "nosniff"
-      X-Frame-Options "SAMEORIGIN"
-      Referrer-Policy "strict-origin-when-cross-origin"
-      Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
-      Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"
-      X-XSS-Protection "1; mode=block"
-      -Server
-    }
-  '';
-
-  # Robots.txt snippet - polite request to AI scrapers
-  robotsTxtSnippet = ''
-        @robots path /robots.txt
-        handle @robots {
-          respond 200 {
-            body `User-agent: CCBot
-    Disallow: /
-
-    User-agent: ChatGPT-User
-    Disallow: /
-
-    User-agent: GPTBot
-    Disallow: /
-
-    User-agent: Google-Extended
-    Disallow: /
-
-    User-agent: anthropic-ai
-    Disallow: /
-
-    User-agent: Omgilibot
-    Disallow: /
-
-    User-agent: Omgili
-    Disallow: /
-
-    User-agent: FacebookBot
-    Disallow: /`
-            close
-          }
-        }
-  '';
-
-  # AI bot blocking snippet - enforcement via HTTP 403
-  blockAIBotsSnippet = ''
-    @aibots {
-      header User-Agent *CCBot*
-      header User-Agent *ChatGPT-User*
-      header User-Agent *GPTBot*
-      header User-Agent *Google-Extended*
-      header User-Agent *anthropic-ai*
-      header User-Agent *Omgilibot*
-      header User-Agent *Omgili*
-      header User-Agent *FacebookBot*
-    }
-    handle @aibots {
-      respond "AI scraping not permitted" 403
-    }
-  '';
-in
-{
-  imports = [
-
-    ../common/services/openssh.nix
-  ];
-
-  # ── Fail2ban ────────────────────────────────────────────────────────
-  services.fail2ban = {
-    enable = true;
-
-    # Ban for 1 hour, increase on repeat offenders via recidive jail
-    bantime = "1h";
-    bantime-increment = {
-      enable = true;
-      maxtime = "168h"; # Max 1 week ban
-      factor = "4"; # Aggressive escalation
-    };
-
-    maxretry = 5;
-
-    # Ignore VPN and loopback
-    ignoreIP = [
-      "127.0.0.0/8"
-      "::1"
-      "10.100.0.0/24" # WireGuard VPN
-      "176.172.78.0/24" # Home (Bouygues FTTH)
-      "196.176.0.0/16" # Tunisia (Ooredoo)
-    ];
-
-    jails = {
-      # Caddy auth failures (401/403 responses)
-      caddy-auth = ''
-        enabled = true
-        backend = auto
-        filter = caddy-auth
-        logpath = /var/log/caddy/access*.log
-        maxretry = 10
-        findtime = 600
-        bantime = 3600
-      '';
-
-      # Caddy aggressive scanning (404 floods)
-      caddy-scan = ''
-        enabled = true
-        backend = auto
-        filter = caddy-scan
-        logpath = /var/log/caddy/access*.log
-        maxretry = 30
-        findtime = 60
-        bantime = 3600
-      '';
-
-      # Caddy rate abuse (too many requests)
-      caddy-flood = ''
-        enabled = true
-        backend = auto
-        filter = caddy-flood
-        logpath = /var/log/caddy/access*.log
-        maxretry = 200
-        findtime = 60
-        bantime = 7200
-      '';
-    };
-  };
-
-  # Caddy fail2ban filters for JSON access logs
-  environment.etc = {
-    # Ban IPs that get too many 401/403 responses (brute force / unauthorized access)
-    "fail2ban/filter.d/caddy-auth.conf".text = ''
-      [Definition]
-      failregex = ^.*"remote_ip":"<HOST>".*"status":(401|403),.*$
-      ignoreregex = ^.*"host":"ntfy\.sbr\.pm".*$
-      datepattern = "ts":{EPOCH}
-    '';
-
-    # Ban IPs that trigger excessive 404s (scanning for vulnerabilities)
-    "fail2ban/filter.d/caddy-scan.conf".text = ''
-      [Definition]
-      failregex = ^.*"remote_ip":"<HOST>".*"status":404,.*$
-      ignoreregex =
-      datepattern = "ts":{EPOCH}
-    '';
-
-    # Ban IPs with excessive request volume (flood / DDoS)
-    "fail2ban/filter.d/caddy-flood.conf".text = ''
-      [Definition]
-      failregex = ^.*"remote_ip":"<HOST>".*"status":\d+,.*$
-      ignoreregex = ^.*"remote_ip":"10\.100\.0\..*$
-                    ^.*"host":"ntfy\.sbr\.pm".*$
-                    ^.*"host":"git\.sbr\.pm".*$
-      datepattern = "ts":{EPOCH}
-    '';
-  };
-
-  # Age secrets
-  age.secrets."ntfy-token" = {
-    file = ../../secrets/sakhalin/ntfy-token.age;
-    mode = "400";
-    owner = "root";
-    group = "root";
-  };
-
-  # Allow Caddy to access git repositories in vincent's home
-  users.users.caddy.extraGroups = [ "users" ];
-
-  # Allow vincent to run systemd-run without password (for git hooks)
-  # Use /run/current-system/sw/bin path to avoid hardcoded Nix store paths
-  security.sudo.extraRules = [
-    {
-      users = [ "vincent" ];
-      commands = [
-        {
-          command = "/run/current-system/sw/bin/systemd-run";
-          options = [ "NOPASSWD" ];
-        }
-      ];
-    }
-  ];
-
-  # Install gitmal for self-hosted git web view
-  environment.systemPackages = with pkgs; [
-    gitmal
-  ];
-
-  # Git hook background task execution with notifications
-  systemd.services."git-notify@" = {
-    description = "Git build notification for %i";
-    serviceConfig = {
-      Type = "oneshot";
-      ExecStart = "${pkgs.writeShellScript "git-notify" ''
-        #!/usr/bin/env bash
-        set -euo pipefail
-
-        UNIT_NAME="$1"
-        RESULT=$(${pkgs.systemd}/bin/systemctl show -p Result --value "$UNIT_NAME")
-        EXIT_CODE=$(${pkgs.systemd}/bin/systemctl show -p ExecMainStatus --value "$UNIT_NAME")
-
-        # Get execution timestamps (in microseconds since epoch)
-        START_TIME=$(${pkgs.systemd}/bin/systemctl show -p ExecMainStartTimestamp --value "$UNIT_NAME")
-        EXIT_TIME=$(${pkgs.systemd}/bin/systemctl show -p ExecMainExitTimestamp --value "$UNIT_NAME")
-
-        # Calculate duration in seconds
-        START_EPOCH=$(${pkgs.coreutils}/bin/date -d "$START_TIME" +%s 2>/dev/null || echo "0")
-        EXIT_EPOCH=$(${pkgs.coreutils}/bin/date -d "$EXIT_TIME" +%s 2>/dev/null || echo "0")
-        DURATION=$((EXIT_EPOCH - START_EPOCH))
-
-        # Format duration as human-readable
-        if [ "$DURATION" -ge 60 ]; then
-          MINUTES=$((DURATION / 60))
-          SECONDS=$((DURATION % 60))
-          DURATION_STR="''${MINUTES}m ''${SECONDS}s"
-        else
-          DURATION_STR="''${DURATION}s"
-        fi
-
-        # Parse unit name to extract job type and repo
-        # Format: git-<job>-<repo>-<timestamp>
-        JOB_TYPE=$(echo "$UNIT_NAME" | cut -d'-' -f2)
-        REPO=$(echo "$UNIT_NAME" | cut -d'-' -f3)
-
-        # Only notify on failure
-        if [ "$RESULT" != "success" ]; then
-          ${pkgs.curl}/bin/curl -s \
-            -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
-              config.age.secrets."ntfy-token".path
-            })" \
-            -H "Title: ❌ Git $JOB_TYPE Failed: $REPO (after $DURATION_STR)" \
-            -H "Priority: high" \
-            -H "Tags: x,git,$JOB_TYPE,warning" \
-            -d "Job $UNIT_NAME failed after $DURATION_STR (exit code: $EXIT_CODE). Check logs: journalctl -u $UNIT_NAME" \
-            "https://ntfy.sbr.pm/git-builds" || true
-        fi
-      ''} %i";
-    };
-  };
-
-  # Helper script for gitmal generation (called from post-receive hooks)
-  environment.etc."git-hooks/generate-gitmal.sh" = {
-    text = ''
-      #!${pkgs.bash}/bin/bash
-      set -euo pipefail
-
-      # Set PATH to include git and coreutils (gitmal needs git, script needs basename)
-      export PATH="${pkgs.git}/bin:${pkgs.coreutils}/bin:$PATH"
-
-      REPO_PATH="$1"
-      THEME="''${2:-github-dark}"  # Default to 'github-dark' theme if not specified
-      REPO_NAME=$(basename "$REPO_PATH" .git)
-      OUTPUT_DIR="/home/vincent/git/public/$REPO_NAME"
-
-      echo "Generating gitmal for repository: $REPO_NAME"
-      echo "Repository path: $REPO_PATH"
-      echo "Output directory: $OUTPUT_DIR"
-      echo "Theme: $THEME"
-
-      # Generate static site with gitmal
-      cd "$REPO_PATH"
-      ${pkgs.gitmal}/bin/gitmal --output "$OUTPUT_DIR" --theme "$THEME"
-
-      echo "Gitmal generation complete: $OUTPUT_DIR"
-    '';
-    mode = "0755";
-  };
-
-  # Remote build forwarding script
-  environment.etc."git-hooks/forward-build.sh" = {
-    text = ''
-      #!${pkgs.bash}/bin/bash
-      set -euo pipefail
-
-      REPO_NAME="$1"
-      BUILD_TYPE="''${2:-auto}"
-      TIMESTAMP=$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S)
-      UNIT_NAME="build-remote-''${REPO_NAME}-''${TIMESTAMP}"
-
-      echo "Forwarding build to aomi: $REPO_NAME ($BUILD_TYPE)..."
-
-      # SSH to aomi and trigger build with systemd-run
-      ${pkgs.openssh}/bin/ssh -o BatchMode=yes builder@10.100.0.17 \
-        "sudo /run/current-system/sw/bin/systemd-run \
-          --unit=\"$UNIT_NAME\" \
-          --description=\"Remote build: $REPO_NAME ($BUILD_TYPE)\" \
-          --property=\"OnSuccess=job-notify@\''${UNIT_NAME}.service\" \
-          --property=\"OnFailure=job-notify@\''${UNIT_NAME}.service\" \
-          --property=\"User=builder\" \
-          --property=\"Group=users\" \
-          --property=\"WorkingDirectory=/var/lib/git-builds\" \
-          /etc/git-builds/execute-build.sh \"$REPO_NAME\" \"$BUILD_TYPE\""
-
-      echo "✓ Build queued on aomi: $UNIT_NAME"
-      echo "  View status: ssh aomi 'systemctl status $UNIT_NAME'"
-      echo "  View logs:   ssh aomi 'journalctl -u $UNIT_NAME'"
-    '';
-    mode = "0755";
-  };
-
-  # Example post-receive hook template
-  environment.etc."git-hooks/post-receive.example" = {
-    text = ''
-      #!${pkgs.bash}/bin/bash
-      # Example post-receive hook for git repositories
-      # Copy this to your repository's hooks/post-receive and make it executable
-      #
-      # This hook uses systemd-run to execute gitmal generation in the background
-      # with automatic notifications via ntfy when the job completes.
-      #
-      # Optionally, it can also trigger remote builds on aomi.
-
-      set -euo pipefail
-
-      # Configuration
-      GITMAL_ENABLED="true"
-      GITMAL_THEME="github-dark"  # Options: github-dark, github-light, dark, light, auto
-      REMOTE_BUILD_ENABLED="false"  # Set to "true" to enable remote builds on aomi
-      BUILD_TYPE="nixos"             # Options: nixos, make, docker, go, custom, auto
-
-      REPO_PATH="$(pwd)"
-      REPO_NAME=$(basename "$REPO_PATH" .git)
-      TIMESTAMP=$(date +%Y%m%d-%H%M%S)
-
-      # 1. Generate gitmal (local static site on kerkouane)
-      if [ "$GITMAL_ENABLED" = "true" ]; then
-        UNIT_NAME="git-gitmal-''${REPO_NAME}-''${TIMESTAMP}"
-        echo "Queuing gitmal generation for $REPO_NAME with theme: $GITMAL_THEME..."
-
-        sudo /run/current-system/sw/bin/systemd-run \
-          --unit="$UNIT_NAME" \
-          --description="Gitmal generation for $REPO_NAME" \
-          --property="OnSuccess=git-notify@''${UNIT_NAME}.service" \
-          --property="OnFailure=git-notify@''${UNIT_NAME}.service" \
-          --property="User=vincent" \
-          --property="Group=users" \
-          --working-directory="$REPO_PATH" \
-          /etc/git-hooks/generate-gitmal.sh "$REPO_PATH" "$GITMAL_THEME"
-
-        echo "✓ Gitmal generation queued as: $UNIT_NAME"
-        echo "  View status: systemctl status $UNIT_NAME"
-        echo "  View logs:   journalctl -u $UNIT_NAME"
-      fi
-
-      # 2. Trigger remote build (on aomi)
-      if [ "$REMOTE_BUILD_ENABLED" = "true" ]; then
-        /etc/git-hooks/forward-build.sh "$REPO_NAME" "$BUILD_TYPE"
-      fi
-    '';
-    mode = "0755";
-  };
-
-  # Setup permissions for git directories (via systemd tmpfiles)
-  systemd.tmpfiles.rules = [
-    "d /home/vincent 0711 vincent users -" # Allow traversal to git directory
-    "d /home/vincent/git 0700 vincent users -" # Private git directory
-    "d /home/vincent/git/public 0755 vincent users -" # Public repositories only
-    "d /var/log/git-builds 0755 vincent users -" # Git build logs
-  ];
-
-  # Disable TPM2 (VPS has no TPM hardware)
-  security.tpm2.enable = lib.mkForce false;
-
-  # Override common SSH config to restrict to VPN network only
-  services.openssh = {
-    listenAddresses = [
-      {
-        addr = builtins.head globals.machines.kerkouane.net.vpn.ips;
-        port = 22;
-      }
-    ];
-    openFirewall = lib.mkForce false;
-  };
-
-  services.wireguard.server = {
-    enable = true;
-    ips = libx.wg-ips globals.machines.kerkouane.net.vpn.ips;
-    peers = libx.generateWireguardPeers globals.machines;
-  };
-
-  # SOCKS5 proxy for SearXNG outgoing requests (VPN-only)
-  # Allows searx on sakhalin to route queries through kerkouane's IP
-  # for round-robin proxy rotation to avoid search engine rate limiting
-  services.microsocks = {
-    enable = true;
-    ip = builtins.head globals.machines.kerkouane.net.vpn.ips;
-    port = 1080;
-  };
-
-  services.gosmee = {
-    enable = true;
-    public-url = "https://webhook.sbr.pm";
-  };
-
-  services.ntfy-sh = {
-    enable = true;
-    settings = {
-      base-url = "https://ntfy.sbr.pm";
-      upstream-base-url = "https://ntfy.sh";
-      listen-http = "localhost:8111";
-      behind-proxy = true;
-      enable-login = true;
-      auth-default-access = "deny-all";
-    };
-  };
-
-  # Firewall configuration
-  # TODO: Migrate to nftables once wireguard server module supports it
-  networking.firewall = {
-    allowPing = true;
-    # Public ports
-    allowedTCPPorts = [
-      80 # HTTP
-      443 # HTTPS
-    ];
-
-    # Additional iptables rules
-    extraCommands = ''
-      # Allow node exporter (9000) only from VPN network
-      iptables -A nixos-fw -p tcp -s 10.100.0.0/24 --dport 9000 -j nixos-fw-accept
-
-      # Allow microsocks SOCKS5 proxy (1080) only from VPN network
-      iptables -A nixos-fw -p tcp -s 10.100.0.0/24 --dport 1080 -j nixos-fw-accept
-
-      # Block known SYN flood source (USBINF INFORMATICA LTDA, Brazil)
-      iptables -I nixos-fw 1 -s 45.233.176.0/22 -j DROP
-      ip6tables -I nixos-fw 1 -s ::ffff:45.233.176.0/118 -j DROP
-
-      # SYN flood protection: limit new connections per /24 subnet
-      iptables -A nixos-fw -p tcp --syn -m connlimit --connlimit-above 30 --connlimit-mask 24 -j DROP
-      ip6tables -A nixos-fw -p tcp --syn -m connlimit --connlimit-above 30 --connlimit-mask 24 -j DROP
-    '';
-  };
-  # Allow Caddy to access public git repositories only (override ProtectHome)
-  systemd.services.caddy.serviceConfig = {
-    ProtectHome = lib.mkForce "tmpfs"; # Allow read access to /home with bind mounts
-    BindReadOnlyPaths = [ "/home/vincent/git/public" ];
-  };
-
-  services.caddy = {
-    enable = true;
-    email = "vincent@sbr.pm";
-
-    # Use Caddy with rate-limit plugin
-    package = pkgs.caddy.withPlugins {
-      plugins = [ "github.com/mholt/caddy-ratelimit@v0.1.1-0.20250915152450-04ea34edc0c4" ];
-      hash = "sha256-x5jeKfjXeUj4t5t6+gRRjpmjF3n5P25T0lVED4EEu54=";
-    };
-
-    # Enable Prometheus metrics on VPN interface only
-    globalConfig = ''
-      admin ${builtins.head globals.machines.kerkouane.net.vpn.ips}:2019
-      metrics
-    '';
-
-    # Enable JSON access logging (NixOS option)
-    logFormat = ''
-      output file /var/log/caddy/access.log {
-        roll_size 100MiB
-        roll_keep 10
-        roll_keep_for 720h
-      }
-      format json
-    '';
-
-    virtualHosts = {
-      # File server with directory browsing (replaces fancyindex)
-      "dl.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        root * /var/www/dl.sbr.pm
-        file_server browse {
-          hide .fancyindex README.md HEADER.md
-        }
-
-        ${securityHeaders}
-      '';
-
-      # Alias for dl.sbr.pm
-      "files.sbr.pm".extraConfig = ''
-        redir https://dl.sbr.pm{uri} permanent
-      '';
-
-      # ntfy - reverse proxy with websockets
-      "ntfy.sbr.pm".extraConfig = ''
-        # Rate limiting for notification service
-        rate_limit {
-          zone ntfy_publish {
-            key {remote_host}
-            events 50
-            window 1m
-          }
-        }
-
-        reverse_proxy localhost:8111
-      '';
-
-      # Static sites
-      "paste.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        root * /var/www/paste.sbr.pm
-        file_server
-        ${securityHeaders}
-      '';
-
-      "sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        root * /var/www/sbr.pm
-        file_server
-        ${securityHeaders}
-      '';
-
-      # Go vanity URL service
-      "go.sbr.pm".extraConfig = ''
-        reverse_proxy localhost:8080
-        ${securityHeaders}
-      '';
-
-      # Whoami service (remote)
-      "whoami.sbr.pm".extraConfig = ''
-        reverse_proxy 10.100.0.8:80 {
-          header_up Host {host}
-        }
-      '';
-
-      # Immich photo management (proxied to rhea)
-      "immich.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        # Allow large photo/video uploads (50GB limit)
-        request_body {
-          max_size 50GB
-        }
-
-        # Strict rate limiting for authentication endpoints
-        @auth {
-          path /auth/* /api/auth/*
-        }
-        route @auth {
-          rate_limit {
-            zone immich_auth {
-              key {remote_host}
-              events 10
-              window 1m
-            }
-          }
-          reverse_proxy 10.100.0.50:2283 {
-            header_up Host {host}
-            header_up X-Real-IP {remote_host}
-          }
-        }
-
-        # Moderate rate limiting for API endpoints
-        @api {
-          path /api/*
-        }
-        route @api {
-          rate_limit {
-            zone immich_api {
-              key {remote_host}
-              events 100
-              window 1m
-            }
-          }
-          reverse_proxy 10.100.0.50:2283 {
-            header_up Host {host}
-            header_up X-Real-IP {remote_host}
-          }
-        }
-
-        # Permissive rate limiting for media/general requests
-        rate_limit {
-          zone immich_media {
-            key {remote_host}
-            events 1000
-            window 1m
-          }
-        }
-
-        reverse_proxy 10.100.0.50:2283 {
-          header_up Host {host}
-          header_up X-Real-IP {remote_host}
-        }
-
-        ${mediaSecurityHeaders}
-      '';
-
-      # Navidrome music streaming (proxied to aion)
-      "navidrome.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        # Rate limiting for music streaming
-        rate_limit {
-          zone navidrome_general {
-            key {remote_host}
-            events 500
-            window 1m
-          }
-        }
-
-        reverse_proxy 10.100.0.49:4533 {
-          header_up Host {host}
-          header_up X-Real-IP {remote_host}
-        }
-
-        ${mediaSecurityHeaders}
-      '';
-
-      # Jellyfin media server (proxied to rhea)
-      "jellyfin.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        # Rate limiting for media server
-        rate_limit {
-          zone jellyfin_general {
-            key {remote_host}
-            events 500
-            window 1m
-          }
-        }
-
-        reverse_proxy 10.100.0.50:8096 {
-          header_up Host {host}
-          header_up X-Real-IP {remote_host}
-        }
-
-        ${mediaSecurityHeaders}
-      '';
-
-      # Audiobookshelf audiobook server (proxied to aion)
-      "audiobookshelf.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        # Rate limiting for audiobook streaming
-        rate_limit {
-          zone audiobookshelf_general {
-            key {remote_host}
-            events 500
-            window 1m
-          }
-        }
-
-        reverse_proxy 10.100.0.49:13378 {
-          header_up Host {host}
-          header_up X-Real-IP {remote_host}
-        }
-
-        ${mediaSecurityHeaders}
-      '';
-
-      # Service aliases (user-friendly URLs - transparent proxy)
-      "music.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        # Rate limiting for music streaming
-        rate_limit {
-          zone music_general {
-            key {remote_host}
-            events 500
-            window 1m
-          }
-        }
-
-        reverse_proxy 10.100.0.49:4533 {
-          header_up Host {host}
-          header_up X-Real-IP {remote_host}
-        }
-
-        ${mediaSecurityHeaders}
-      '';
-
-      "photos.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        # Allow large photo/video uploads (50GB limit)
-        request_body {
-          max_size 50GB
-        }
-
-        # Strict rate limiting for authentication endpoints
-        @auth {
-          path /auth/* /api/auth/*
-        }
-        route @auth {
-          rate_limit {
-            zone photos_auth {
-              key {remote_host}
-              events 10
-              window 1m
-            }
-          }
-          reverse_proxy 10.100.0.50:2283 {
-            header_up Host {host}
-            header_up X-Real-IP {remote_host}
-          }
-        }
-
-        # Moderate rate limiting for API endpoints
-        @api {
-          path /api/*
-        }
-        route @api {
-          rate_limit {
-            zone photos_api {
-              key {remote_host}
-              events 100
-              window 1m
-            }
-          }
-          reverse_proxy 10.100.0.50:2283 {
-            header_up Host {host}
-            header_up X-Real-IP {remote_host}
-          }
-        }
-
-        # Permissive rate limiting for media/general requests
-        rate_limit {
-          zone photos_media {
-            key {remote_host}
-            events 1000
-            window 1m
-          }
-        }
-
-        reverse_proxy 10.100.0.50:2283 {
-          header_up Host {host}
-          header_up X-Real-IP {remote_host}
-        }
-
-        ${mediaSecurityHeaders}
-      '';
-
-      "podcasts.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        # Rate limiting for audiobook streaming
-        rate_limit {
-          zone podcasts_general {
-            key {remote_host}
-            events 500
-            window 1m
-          }
-        }
-
-        reverse_proxy 10.100.0.49:13378 {
-          header_up Host {host}
-          header_up X-Real-IP {remote_host}
-        }
-
-        ${mediaSecurityHeaders}
-      '';
-
-      # Webhook/gosmee service with SSE support
-      "webhook.sbr.pm".extraConfig = ''
-        reverse_proxy localhost:3333 {
-          flush_interval -1
-        }
-      '';
-
-      # Personal website with directory browsing
-      "vincent.demeester.fr".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        root * /var/www/vincent.demeester.fr
-
-        # Try files with .html extension
-        try_files {path} {path}.html {path}/ /index.html
-
-        file_server browse {
-          hide .fancyindex README.md HEADER.md
-        }
-
-        ${securityHeaders}
-      '';
-
-      # Self-hosted git repositories (public only)
-      "git.sbr.pm".extraConfig = ''
-        ${blockAIBotsSnippet}
-        ${robotsTxtSnippet}
-
-        root * /home/vincent/git/public
-        file_server browse {
-          hide .fancyindex README.md HEADER.md
-        }
-
-        ${gitSecurityHeaders}
-      '';
-    };
-  };
-
-  services.govanityurl = {
-    enable = true;
-    user = "caddy";
-    host = "go.sbr.pm";
-    config = ''
-      paths:
-        /x:
-          repo: https://github.com/vdemeester/x
-        /lord:
-          repo: https://github.com/vdemeester/lord
-        /ape:
-          repo: https://git.sr.ht/~vdemeester/ape
-        /nr:
-          repo: https://git.sr.ht/~vdemeester/nr
-        /ram:
-          repo: https://git.sr.ht/~vdemeester/ram
-        /sec:
-          repo: https://git.sr.ht/~vdemeester/sec
-    '';
-  };
-  security.acme = {
-    acceptTerms = true;
-    defaults.email = "vincent@sbr.pm";
-  };
-}
systems/kerkouane/hardware.nix
@@ -1,93 +0,0 @@
-{ lib, ... }:
-{
-  fileSystems."/" = {
-    device = "/dev/vda1";
-    fsType = "ext4";
-  };
-  swapDevices = [
-    {
-      device = "/swapfile";
-      size = 1024;
-    }
-  ];
-
-  # DigitalOcean volume for persistent data
-  fileSystems."/data" = {
-    device = "/dev/disk/by-uuid/c77f5139-e18a-4138-974f-f63300def495";
-    fsType = "ext4";
-    options = [
-      "defaults"
-      "noatime"
-      "discard"
-    ];
-  };
-
-  # Bind mounts for transparent access at original paths
-  fileSystems."/home/vincent/git" = {
-    device = "/data/home/vincent/git";
-    fsType = "none";
-    options = [ "bind" ];
-    depends = [ "/data" ];
-  };
-
-  fileSystems."/home/vincent/desktop" = {
-    device = "/data/home/vincent/desktop";
-    fsType = "none";
-    options = [ "bind" ];
-    depends = [ "/data" ];
-  };
-
-  fileSystems."/home/vincent/sync" = {
-    device = "/data/home/vincent/sync";
-    fsType = "none";
-    options = [ "bind" ];
-    depends = [ "/data" ];
-  };
-
-  fileSystems."/var/www" = {
-    device = "/data/www";
-    fsType = "none";
-    options = [ "bind" ];
-    depends = [ "/data" ];
-  };
-
-  # START OF DigitalOcean specifics
-  # This file was populated at runtime with the networking
-  # details gathered from the active system.
-  networking = {
-    nameservers = [
-      "67.207.67.2"
-      "67.207.67.3"
-    ];
-    defaultGateway = "188.166.64.1";
-    defaultGateway6 = null;
-    dhcpcd.enable = false;
-    usePredictableInterfaceNames = lib.mkForce true;
-    interfaces = {
-      eth0 = {
-        ipv4.addresses = [
-          {
-            address = "188.166.102.243";
-            prefixLength = 18;
-          }
-          {
-            address = "10.18.0.5";
-            prefixLength = 16;
-          }
-        ];
-        ipv6.addresses = [
-          {
-            address = "fe80::8035:3aff:fe72:1036";
-            prefixLength = 64;
-          }
-        ];
-      };
-
-    };
-  };
-  services.udev.extraRules = ''
-    ATTR{address}=="82:35:3a:72:10:36", NAME="eth0"
-
-  '';
-  # END OF DigitalOcean specifics
-}
systems/kerkouane/home.nix
@@ -1,2 +0,0 @@
-_: {
-}
flake.nix
@@ -129,12 +129,6 @@
           homeInput = inputs.home-manager-26_05;
           agenixInput = inputs.agenix-26_05;
         };
-        kerkouane = libx.mkHost {
-          hostname = "kerkouane";
-          pkgsInput = inputs.nixpkgs-26_05;
-          homeInput = inputs.home-manager-26_05;
-          agenixInput = inputs.agenix-26_05;
-        };
         carthage = libx.mkHost {
           hostname = "carthage";
           pkgsInput = inputs.nixpkgs-26_05;
globals.nix
@@ -316,34 +316,13 @@ _: {
         vincent = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxstR3xEf87leVVDS3GVPx8Ap9+eP+OfkSvM26V54XP vincent@shikoku";
       };
     };
-    kerkouane = {
-      net = {
-        vpn = {
-          pubkey = "+H3fxErP9HoFUrPgU19ra9+GDLQw+VwvLWx3lMct7QI=";
-          ips = [ "10.100.0.1" ];
-        };
-        names = [
-          "kerkouane.vpn"
-          "kerkouane.sbr.pm"
-        ];
-      };
-      ssh = {
-        hostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJguVoQYObRLyNxELFc3ai2yDJ25+naiM3tKrBGuxwwA";
-      };
-      syncthing = {
-        id = "QGD6ICB-EPSGCEN-IQWKN77-BCRWE67-56HX5IA-E4IDBCI-WE46DK3-EC63DQ7";
-        folders = {
-          sync = { };
-        };
-      };
-    };
-    # Hetzner Cloud VPS — replacement for kerkouane (DigitalOcean)
+    # Hetzner Cloud VPS
     # TODO: Update pubkey, hostKey, and syncthing id after provisioning
     carthage = {
       net = {
         vpn = {
           pubkey = "PQD1dtxhy9NMbmfy2OQPeLiQg0Alcfa1Mo4HVN5WqgA=";
-          ips = [ "10.100.0.1" ]; # Takes over kerkouane's VPN server role
+          ips = [ "10.100.0.1" ]; # VPN server
         };
         names = [
           "carthage.vpn"
Makefile
@@ -42,14 +42,6 @@ host/kobe/boot:
 host/aix/boot:
 	nixos-rebuild --target-host root@10.100.0.89 --flake .#aix boot
 
-.PHONY: host/kerkouane/boot
-host/kerkouane/boot:
-	nixos-rebuild --target-host root@kerkouane.vpn --flake .#kerkouane boot
-
-.PHONY: host/kerkouane/switch
-host/kerkouane/switch:
-	nixos-rebuild --target-host root@kerkouane.vpn --flake .#kerkouane switch
-
 .PHONY: host/carthage/boot
 host/carthage/boot:
 	nixos-rebuild --target-host root@carthage.vpn --flake .#carthage boot
secrets.nix
@@ -10,7 +10,6 @@ let
 
   athena = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM/4KRP1rzOwyA2zP1Nf1WlLRHqAGutLtOHYWfH732xh"; # ssh-keyscan -q -t ed25519 athena.sbr.pm
   demeter = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqQfEyHyjIGglayB9FtCqL7bnYfNSQlBXks2IuyCPmd"; # ssh-keyscan -q -t ed25519 demeter.sbr.pm
-  kerkouane = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJguVoQYObRLyNxELFc3ai2yDJ25+naiM3tKrBGuxwwA"; # ssh-keyscan -q -t ed25519 kerkouane.sbr.pm
   rhea = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKFH3Lk4bRgNyFRK/Hzg1PvVbL/dpyI1SmLJFkb6VQDw"; # ssh-keyscan -q -t ed25519 rhea.sbr.pm
   sakhalin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN/PMBThi4DhgZR8VywbRDzzMVh2Qp3T6NJAcPubfXz6"; # ssh-keyscan -q -t ed25519 sakhalin.sbr.pm
   shikoku = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH18c6kcorVbK2TwCgdewL6nQf29Cd5BVTeq8nRYUigm"; # ssh-keyscan -q -t ed25519 shikoku.sbr.pm
@@ -31,7 +30,6 @@ let
     athena
     carthage
     demeter
-    kerkouane
     nagoya
     rhea
     sakhalin
@@ -148,7 +146,6 @@ in
     aion
     okinawa
     rhea
-    kerkouane
     carthage
   ];
   "secrets/sakhalin/homeassistant-prometheus-token.age".publicKeys = users ++ [ sakhalin ];