{ lib, config, ... }: let cfg = config.crans.reverseProxy; mainTld = "org"; otherTld = [ "fr" "eu" ]; inherit (lib) literalExpression mkEnableOption mkIf mkOption types ; in { options.crans.reverseProxy = { enable = mkEnableOption "Configuration du reverseproxy."; virtualHosts = mkOption { type = types.attrsOf ( types.submodule { options = { serverAliases = mkOption { type = types.listOf types.str; default = [ ]; example = [ "everything" "voyager" ]; description = '' Déclaration des alias. ''; }; proxyPass = mkOption { type = types.nullOr types.str; default = null; description = '' Indique la destination à proxy. ''; example = "172.16.10.128:8000"; }; serveLocalFiles = mkOption { type = types.nullOr types.path; default = null; description = '' Chemin vers un dossier à exposer statiquement. ''; example = "/var/local/adopter-un-manchot"; }; globalRedirect = mkOption { type = types.nullOr types.str; default = null; description = '' Si définie, toutes les requêtes sont redirigées (via 301) sur cet hôte. ''; example = "perso.crans.org/club"; }; anubisConfig = mkOption { type = types.nullOr types.str; default = null; description = '' Chemin du fichier de configuration ''; example = "/var/www/anubis.conf"; }; anubisOpenGraph = mkOption { type = types.bool; default = true; description = '' Activer openGraph pour l'indexation et l'embedding ''; }; httpOnly = mkOption { type = types.bool; default = false; description = '' Interdit les connexions en https ''; example = "true"; }; proxyWebsockets = mkOption { type = types.bool; default = false; description = '' Activer les websockets ''; example = "true"; }; }; } ); default = { }; example = literalExpression '' { "framadate" = { host = "176.16.10.128:8000"; serverAliases = [ "everything" "voyager" ] }; }; ''; description = "Déclaration des machines."; }; }; config = { systemd.services = lib.mapAttrs (vhostName: vhostConfig: { wantedBy = [ "multi-user.target" ]; }) cfg.virtualHosts; # On fait un certificat commun pour tous les hosts security.acme.certs."all.crans.all" = mkIf cfg.enable { domain = "*.crans.${mainTld}"; dnsProvider = "rfc2136"; # Contient le serveur à contacter avec le protocole # et le mot de passe environmentFile = config.age.secrets.acme-env.path; extraDomainNames = lib.concatMap (tld: [ "*.crans.${tld}" "crans.${tld}" ]) otherTld ++ [ "crans.${mainTld}" ]; group = config.services.nginx.group; }; # Création des dossiers de logs system.activationScripts = { nginx-logs = { text = lib.concatMapStrings (f: "mkdir -p /var/log/nginx/${f}/\n") ( [ "anubis" ] ++ (lib.map (hn: "host/${hn}") (builtins.attrNames cfg.virtualHosts)) ) + "chown -R ${config.services.nginx.user}:${config.services.nginx.group} /var/log/nginx/\n"; }; }; services = mkIf cfg.enable { anubis = { defaultOptions.group = "nginx"; instances = lib.mapAttrs ( vhostName: vhostConfig: mkIf (vhostConfig.anubisConfig != null) { enable = true; settings = { BIND = "/run/anubis/anubis-${vhostName}/socket.sock"; BIND_NETWORK = "unix"; METRICS_BIND = "/run/anubis/anubis-${vhostName}/anubis-${vhostName}-metrics.sock"; TARGET = "unix:///run/nginx/nginx-${vhostName}.sock"; COOKIE_DOMAIN = "crans.${mainTld}"; REDIRECT_DOMAINS = "${vhostName}.crans.${mainTld}"; SOCKET_MODE = "0660"; # Secret partagé entre les instances ED25519_PRIVATE_KEY_HEX_FILE = "${../../../secrets/reverseproxy/anubis-secret.age}"; # OpenGraph config OG_PASSTHROUGH = vhostConfig.anubisOpenGraph; OG_EXPIRY_TIME = "24h"; OG_CACHE_CONSIDER_HOST = true; # Policy config POLICY_FNAME = vhostConfig.anubisConfig; }; } ) cfg.virtualHosts; }; logrotate.settings = let # https://github.com/NixOS/nixpkgs/blob/nixos-26.05/nixos/modules/services/web-servers/nginx/default.nix base = { su = "${config.services.nginx.user} ${config.services.nginx.group}"; postrotate = "[ ! -f /var/run/nginx/nginx.pid ] || kill -USR1 `cat /var/run/nginx/nginx.pid`"; frequency = "daily"; ifempty = true; compress = true; delaycompress = true; dateyesterday = true; }; in { nginx-anubis = base // { files = [ "/var/log/nginx/anubis/*.log" ]; rotate = 7; # une semaine de log pour débugguer }; nginx-hosts = base // { files = [ "/var/log/nginx/host/*/*.log" ]; rotate = 173; # 6 mois de logs dateext = true; dateformat = "%Y-%m-%d"; }; }; nginx = let configVhost = vhostName: vhostConfig: let sslConf = { forceSSL = !vhostConfig.httpOnly; rejectSSL = vhostConfig.httpOnly; useACMEHost = mkIf (!vhostConfig.httpOnly) "all.crans.all"; acmeRoot = null; }; # Conf pour l’hôte en entrée entryExtraConf = { extraConfig = '' set_real_ip_from 172.16.0.0/16; set_real_ip_from fd00::/56; real_ip_header X-Real-Ip; ''; } // sslConf; # Conf supplémentaire pour le proxy principal vhostExtraConf = if vhostConfig.anubisConfig != null then # Il reçoit les requêtes d’Anubis { listen = [ { addr = "unix:/run/nginx/nginx-${vhostName}.sock"; } ]; extraConfig = '' set_real_ip_from unix:; real_ip_header X-Real-IP; ''; } else # Il est en entrée entryExtraConf; logHostConf = '' access_log /var/log/nginx/host/${vhostName}/access.log; error_log /var/log/nginx/host/${vhostName}/error.log; ''; # Les alias : vhostName × otherTld U serverAliases × allTld mkHostName = als: tld: if als == "." then "crans.${tld}" else "${als}.crans.${tld}"; aliases = lib.foldr ( tld: acc: acc ++ (lib.foldr (alias: acc: acc ++ [ (mkHostName alias tld) ]) [ (mkHostName vhostName tld) ] vhostConfig.serverAliases) ) (lib.foldr (alias: acc: acc ++ [ (mkHostName alias mainTld) ]) [ ] vhostConfig.serverAliases) otherTld; in { # Configuration du service à proxy. "${vhostName}" = vhostExtraConf // { serverName = mkHostName vhostName mainTld; locations."/" = mkIf (vhostConfig.proxyPass != null) { proxyPass = "http://${vhostConfig.proxyPass}"; proxyWebsockets = vhostConfig.proxyWebsockets; }; root = vhostConfig.serveLocalFiles; globalRedirect = vhostConfig.globalRedirect; extraConfig = vhostExtraConf.extraConfig + logHostConf; }; # Entrée d’Anubis "${vhostName}-anubis" = mkIf (vhostConfig.anubisConfig != null) ( entryExtraConf // rec { serverName = mkHostName vhostName mainTld; locations."/" = { proxyPass = "http://unix:/run/anubis/anubis-${vhostName}/socket.sock"; proxyWebsockets = vhostConfig.proxyWebsockets; }; locations."/.within.website/x/cmd/anubis/api/honeypot/" = locations."/" // { extraConfig = '' add_header 'X-Robots-Tag' 'noindex,nofollow' always; ''; }; extraConfig = entryExtraConf.extraConfig + '' access_log /var/log/nginx/anubis/access.log; error_log /var/log/nginx/anubis/error.log; ''; } ); } # Les alias // lib.mergeAttrsList ( lib.map (alias: { "${vhostName}-alias-${alias}" = sslConf // { serverName = alias; globalRedirect = lib.defaultTo "${vhostName}.crans.${mainTld}" vhostConfig.globalRedirect; extraConfig = logHostConf; }; }) aliases ); in { enable = true; virtualHosts = lib.concatMapAttrs configVhost cfg.virtualHosts; }; }; }; }