Commit cdd1ec51081e
Changed files (15)
android
lib
systems
common
services
kerkouane
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 ];