{ config, pkgs, lib, ... }: let cfg = config.shb.nginx; contracts = pkgs.callPackage ../contracts {}; fqdn = c: "${c.subdomain}.${c.domain}"; vhostConfig = lib.types.submodule { options = { subdomain = lib.mkOption { type = lib.types.str; description = "Subdomain which must be protected."; example = "subdomain"; }; domain = lib.mkOption { type = lib.types.str; description = "Domain of the subdomain."; example = "mydomain.com"; }; ssl = lib.mkOption { description = "Path to SSL files"; type = lib.types.nullOr contracts.ssl.certs; default = null; }; upstream = lib.mkOption { type = lib.types.str; description = "Upstream url to be protected."; example = "http://127.0.0.1:1234"; }; authEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "Optional auth endpoint for SSO."; default = null; example = "https://authelia.example.com"; }; autheliaRules = lib.mkOption { type = lib.types.listOf (lib.types.attrsOf lib.types.anything); description = "Authelia rule configuration"; example = lib.literalExpression ''[{ policy = "two_factor"; subject = ["group:service_user"]; }]''; }; }; }; in { options.shb.nginx = { accessLog = lib.mkOption { type = lib.types.bool; description = "Log all requests"; default = false; example = true; }; debugLog = lib.mkOption { type = lib.types.bool; description = "Verbose debug of internal. This will print what servers were matched and why."; default = false; example = true; }; vhosts = lib.mkOption { description = "Endpoints to be protected by authelia."; type = lib.types.listOf vhostConfig; default = []; }; }; config = { networking.firewall.allowedTCPPorts = [ 80 443 ]; services.nginx.enable = true; services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn"; services.nginx.appendHttpConfig = lib.mkIf cfg.accessLog '' log_format apm '{' '"remote_addr":"$remote_addr",' '"remote_user":"$remote_user",' '"time_local":"$time_local",' '"request":"$request",' '"request_length":"$request_length",' '"server_name":"$server_name",' '"status":"$status",' '"bytes_sent":"$bytes_sent",' '"body_bytes_sent":"$body_bytes_sent",' '"referrer":"$http_referrer",' '"user_agent":"$http_user_agent",' '"gzip_ration":"$gzip_ratio",' '"post":"$request_body",' '"upstream_addr":"$upstream_addr",' '"upstream_status":"$upstream_status",' '"request_time":"$request_time",' '"upstream_response_time":"$upstream_response_time",' '"upstream_connect_time":"$upstream_connect_time",' '"upstream_header_time":"$upstream_header_time"' '}'; access_log syslog:server=unix:/dev/log apm; ''; services.nginx.virtualHosts = let vhostCfg = c: { ${fqdn c} = { forceSSL = !(isNull c.ssl); sslCertificate = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.cert; sslCertificateKey = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.key; # Taken from https://github.com/authelia/authelia/issues/178 locations."/".extraConfig = '' add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options "SAMEORIGIN"; add_header X-XSS-Protection "1; mode=block"; add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; add_header X-Download-Options noopen; add_header X-Permitted-Cross-Domain-Policies none; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_cache_bypass $http_upgrade; proxy_pass ${c.upstream}; '' + lib.optionalString (c.authEndpoint != null) '' auth_request /authelia; auth_request_set $user $upstream_http_remote_user; auth_request_set $groups $upstream_http_remote_groups; proxy_set_header X-Forwarded-User $user; proxy_set_header X-Forwarded-Groups $groups; # TODO: Are those needed? # auth_request_set $name $upstream_http_remote_name; # auth_request_set $email $upstream_http_remote_email; # proxy_set_header Remote-Name $name; # proxy_set_header Remote-Email $email; # TODO: Would be nice to have this working, I think. # set $new_cookie $http_cookie; # if ($http_cookie ~ "(.*)(?:^|;)\s*example\.com\.session\.id=[^;]+(.*)") { # set $new_cookie $1$2; # } # proxy_set_header Cookie $new_cookie; auth_request_set $redirect $scheme://$http_host$request_uri; error_page 401 =302 ${c.authEndpoint}?rd=$redirect; error_page 403 = ${c.authEndpoint}/error/403; ''; # Virtual endpoint created by nginx to forward auth requests. locations."/authelia".extraConfig = lib.mkIf (!(isNull c.authEndpoint)) '' internal; proxy_pass ${c.authEndpoint}/api/verify; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Original-URL $scheme://$host$request_uri; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Content-Length ""; proxy_pass_request_body off; # TODO: Would be nice to be able to enable this. # proxy_ssl_verify on; # proxy_ssl_trusted_certificate "/etc/ssl/certs/DST_Root_CA_X3.pem"; proxy_ssl_protocols TLSv1.2; proxy_ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; proxy_ssl_verify_depth 2; proxy_ssl_server_name on; ''; }; }; in lib.mkMerge (map vhostCfg cfg.vhosts); shb.authelia.rules = let authConfig = c: map (r: r // { domain = fqdn c; }) c.autheliaRules; in lib.flatten (map authConfig cfg.vhosts); security.acme.defaults.reloadServices = [ "nginx.service" ]; }; }