diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md index 972a3777bb79..69b7aeb64de1 100644 --- a/nixos/doc/manual/release-notes/rl-2311.section.md +++ b/nixos/doc/manual/release-notes/rl-2311.section.md @@ -17,3 +17,5 @@ ## Other Notable Changes {#sec-release-23.11-notable-changes} - A new option was added to the virtualisation module that enables specifying explicitly named network interfaces in QEMU VMs. The existing `virtualisation.vlans` is still supported for cases where the name of the network interface is irrelevant. + +- `services.nginx` gained a `defaultListen` option at server-level with support for PROXY protocol listeners, also `proxyProtocol` is now exposed in `services.nginx.virtualHosts..listen` option. It is now possible to run PROXY listeners and non-PROXY listeners at a server-level, see [#213510](https://github.com/NixOS/nixpkgs/pull/213510/) for more details. diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index f5fa2ef5f866..12ccba164738 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -309,36 +309,54 @@ let onlySSL = vhost.onlySSL || vhost.enableSSL; hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL; + # First evaluation of defaultListen based on a set of listen lines. + mkDefaultListenVhost = listenLines: + # If this vhost has SSL or is a SSL rejection host. + # We enable a TLS variant for lines without explicit ssl or ssl = true. + optionals (hasSSL || vhost.rejectSSL) + (map (listen: { port = cfg.defaultSSLListenPort; ssl = true; } // listen) + (filter (listen: !(listen ? ssl) || listen.ssl) listenLines)) + # If this vhost is supposed to serve HTTP + # We provide listen lines for those without explicit ssl or ssl = false. + ++ optionals (!onlySSL) + (map (listen: { port = cfg.defaultHTTPListenPort; ssl = false; } // listen) + (filter (listen: !(listen ? ssl) || !listen.ssl) listenLines)); + defaultListen = if vhost.listen != [] then vhost.listen + else + if cfg.defaultListen != [] then mkDefaultListenVhost + # Cleanup nulls which will mess up with //. + # TODO: is there a better way to achieve this? i.e. mergeButIgnoreNullPlease? + (map (listenLine: filterAttrs (_: v: (v != null)) listenLine) cfg.defaultListen) else let addrs = if vhost.listenAddresses != [] then vhost.listenAddresses else cfg.defaultListenAddresses; - in optionals (hasSSL || vhost.rejectSSL) (map (addr: { inherit addr; port = cfg.defaultSSLListenPort; ssl = true; }) addrs) - ++ optionals (!onlySSL) (map (addr: { inherit addr; port = cfg.defaultHTTPListenPort; ssl = false; }) addrs); + in mkDefaultListenVhost (map (addr: { inherit addr; }) addrs); + hostListen = if vhost.forceSSL then filter (x: x.ssl) defaultListen else defaultListen; - listenString = { addr, port, ssl, extraParameters ? [], ... }: + listenString = { addr, port, ssl, proxyProtocol ? false, extraParameters ? [], ... }: # UDP listener for QUIC transport protocol. (optionalString (ssl && vhost.quic) (" listen ${addr}:${toString port} quic " + optionalString vhost.default "default_server " + optionalString vhost.reuseport "reuseport " - + optionalString (extraParameters != []) (concatStringsSep " " ( - let inCompatibleParameters = [ "ssl" "proxy_protocol" "http2" ]; + + optionalString (extraParameters != []) (concatStringsSep " " + (let inCompatibleParameters = [ "ssl" "proxy_protocol" "http2" ]; isCompatibleParameter = param: !(any (p: p == param) inCompatibleParameters); in filter isCompatibleParameter extraParameters)) + ";")) + " - listen ${addr}:${toString port} " + optionalString (ssl && vhost.http2) "http2 " + optionalString ssl "ssl " + optionalString vhost.default "default_server " + optionalString vhost.reuseport "reuseport " + + optionalString proxyProtocol "proxy_protocol " + optionalString (extraParameters != []) (concatStringsSep " " extraParameters) + ";"; @@ -539,6 +557,49 @@ in ''; }; + defaultListen = mkOption { + type = with types; listOf (submodule { + options = { + addr = mkOption { + type = str; + description = lib.mdDoc "IP address."; + }; + port = mkOption { + type = nullOr port; + description = lib.mdDoc "Port number."; + default = null; + }; + ssl = mkOption { + type = nullOr bool; + default = null; + description = lib.mdDoc "Enable SSL."; + }; + proxyProtocol = mkOption { + type = bool; + description = lib.mdDoc "Enable PROXY protocol."; + default = false; + }; + extraParameters = mkOption { + type = listOf str; + description = lib.mdDoc "Extra parameters of this listen directive."; + default = [ ]; + example = [ "backlog=1024" "deferred" ]; + }; + }; + }); + default = []; + example = literalExpression ''[ + { addr = "10.0.0.12"; proxyProtocol = true; ssl = true; } + { addr = "0.0.0.0"; } + { addr = "[::0]"; } + ]''; + description = lib.mdDoc '' + If vhosts do not specify listen, use these addresses by default. + This option takes precedence over {option}`defaultListenAddresses` and + other listen-related defaults options. + ''; + }; + defaultListenAddresses = mkOption { type = types.listOf types.str; default = [ "0.0.0.0" ] ++ optional enableIPv6 "[::0]"; @@ -546,6 +607,7 @@ in example = literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]''; description = lib.mdDoc '' If vhosts do not specify listenAddresses, use these addresses by default. + This is akin to writing `defaultListen = [ { addr = "0.0.0.0" } ]`. ''; }; @@ -1078,6 +1140,32 @@ in which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`. ''; } + + { + # The idea is to understand whether there is a virtual host with a listen configuration + # that requires ACME configuration but has no HTTP listener which will make deterministically fail + # this operation. + # Options' priorities are the following at the moment: + # listen (vhost) > defaultListen (server) > listenAddresses (vhost) > defaultListenAddresses (server) + assertion = + let + hasAtLeastHttpListener = listenOptions: any (listenLine: if listenLine ? proxyProtocol then !listenLine.proxyProtocol else true) listenOptions; + hasAtLeastDefaultHttpListener = if cfg.defaultListen != [] then hasAtLeastHttpListener cfg.defaultListen else (cfg.defaultListenAddresses != []); + in + all (host: + let + hasAtLeastVhostHttpListener = if host.listen != [] then hasAtLeastHttpListener host.listen else (host.listenAddresses != []); + vhostAuthority = host.listen != [] || (cfg.defaultListen == [] && host.listenAddresses != []); + in + # Either vhost has precedence and we need a vhost specific http listener + # Either vhost set nothing and inherit from server settings + host.enableACME -> ((vhostAuthority && hasAtLeastVhostHttpListener) || (!vhostAuthority && hasAtLeastDefaultHttpListener)) + ) (attrValues virtualHosts); + message = '' + services.nginx.virtualHosts..enableACME requires a HTTP listener + to answer to ACME requests. + ''; + } ] ++ map (name: mkCertOwnershipAssertion { inherit (cfg) group user; cert = config.security.acme.certs.${name}; diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix index 5b33694cf2da..7636c1b26115 100644 --- a/nixos/modules/services/web-servers/nginx/vhost-options.nix +++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix @@ -27,12 +27,35 @@ with lib; }; listen = mkOption { - type = with types; listOf (submodule { options = { - addr = mkOption { type = str; description = lib.mdDoc "IP address."; }; - port = mkOption { type = port; description = lib.mdDoc "Port number."; default = 80; }; - ssl = mkOption { type = bool; description = lib.mdDoc "Enable SSL."; default = false; }; - extraParameters = mkOption { type = listOf str; description = lib.mdDoc "Extra parameters of this listen directive."; default = []; example = [ "backlog=1024" "deferred" ]; }; - }; }); + type = with types; listOf (submodule { + options = { + addr = mkOption { + type = str; + description = lib.mdDoc "IP address."; + }; + port = mkOption { + type = port; + description = lib.mdDoc "Port number."; + default = 80; + }; + ssl = mkOption { + type = bool; + description = lib.mdDoc "Enable SSL."; + default = false; + }; + proxyProtocol = mkOption { + type = bool; + description = lib.mdDoc "Enable PROXY protocol."; + default = false; + }; + extraParameters = mkOption { + type = listOf str; + description = lib.mdDoc "Extra parameters of this listen directive."; + default = [ ]; + example = [ "backlog=1024" "deferred" ]; + }; + }; + }); default = []; example = [ { addr = "195.154.1.1"; port = 443; ssl = true; } @@ -45,7 +68,7 @@ with lib; and `onlySSL`. If you only want to set the addresses manually and not - the ports, take a look at `listenAddresses` + the ports, take a look at `listenAddresses`. ''; }; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a4e4679a2ccd..49a7db012a17 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -521,6 +521,7 @@ in { nginx-sandbox = handleTestOn ["x86_64-linux"] ./nginx-sandbox.nix {}; nginx-sso = handleTest ./nginx-sso.nix {}; nginx-variants = handleTest ./nginx-variants.nix {}; + nginx-proxyprotocol = handleTest ./nginx-proxyprotocol {}; nifi = handleTestOn ["x86_64-linux"] ./web-apps/nifi.nix {}; nitter = handleTest ./nitter.nix {}; nix-ld = handleTest ./nix-ld.nix {}; diff --git a/nixos/tests/nginx-proxyprotocol/_.test.nix.cert.pem b/nixos/tests/nginx-proxyprotocol/_.test.nix.cert.pem new file mode 100644 index 000000000000..e5cea72610b9 --- /dev/null +++ b/nixos/tests/nginx-proxyprotocol/_.test.nix.cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLjCCAhagAwIBAgIIP2+4GFxOYMgwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgNGU3NTJiMB4XDTIzMDEzMDAzNDExOFoXDTQzMDEz +MDAzNDExOFowFTETMBEGA1UEAwwKKi50ZXN0Lm5peDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMarJSCzelnzTMT5GMoIKA/MXBNk5j277uI2Gq2MCky/ +DlBpx+tjSsKsz6QLBduKMF8OH5AgjrVAKQAtsVPDseY0Qcyx/5dgJjkdO4on+DFb +V0SJ3ZhYPKACrqQ1SaoG+Xup37puw7sVR13J7oNvP6fAYRcjYqCiFC7VMjJNG4dR +251jvWWidSc7v5CYw2AxrngtBgHeQuyG9QCJ1DRH8h6ioV7IeonwReN7noYtTWh8 +NDjGnw9HH2nYMcL91E+DWCxWVmbC9/orvYOT7u0Orho0t1w9BB0/zzcdojwQpMCv +HahEmFQmdGbWTuI4caBeaDBJVsSwKlTcxLSS4MAZ0c8CAwEAAaN3MHUwDgYDVR0P +AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMB +Af8EAjAAMB8GA1UdIwQYMBaAFGyXySYI3gL88d7GHnGMU6wpiBf2MBUGA1UdEQQO +MAyCCioudGVzdC5uaXgwDQYJKoZIhvcNAQELBQADggEBAJ/DpwiLVBgWyozsn++f +kR4m0dUjnuCgpHo2EMoMZh+9og+OC0vq6WITXHaJytB3aBMxFOUTim3vwxPyWPXX +/vy+q6jJ6QMLx1J3VIWZdmXsT+qLGbVzL/4gNoaRsLPGO06p3yVjhas+OBFx1Fee +6kTHb82S/dzBojOJLRRo18CU9yw0FUXOPqN7HF7k2y+Twe6+iwCuCKGSFcvmRjxe +bWy11C921bTienW0Rmq6ppFWDaUNYP8kKpMN2ViAvc0tyF6wwk5lyOiqCR+pQHJR +H/J4qSeKDchYLKECuzd6SySz8FW/xPKogQ28zba+DBD86hpqiEJOBzxbrcN3cjUn +7N4= +-----END CERTIFICATE----- diff --git a/nixos/tests/nginx-proxyprotocol/_.test.nix.key.pem b/nixos/tests/nginx-proxyprotocol/_.test.nix.key.pem new file mode 100644 index 000000000000..ed2b17af0bf6 --- /dev/null +++ b/nixos/tests/nginx-proxyprotocol/_.test.nix.key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAxqslILN6WfNMxPkYyggoD8xcE2TmPbvu4jYarYwKTL8OUGnH +62NKwqzPpAsF24owXw4fkCCOtUApAC2xU8Ox5jRBzLH/l2AmOR07iif4MVtXRInd +mFg8oAKupDVJqgb5e6nfum7DuxVHXcnug28/p8BhFyNioKIULtUyMk0bh1HbnWO9 +ZaJ1Jzu/kJjDYDGueC0GAd5C7Ib1AInUNEfyHqKhXsh6ifBF43uehi1NaHw0OMaf +D0cfadgxwv3UT4NYLFZWZsL3+iu9g5Pu7Q6uGjS3XD0EHT/PNx2iPBCkwK8dqESY +VCZ0ZtZO4jhxoF5oMElWxLAqVNzEtJLgwBnRzwIDAQABAoIBAFuNGOH184cqKJGI +3RSVJ6kIGtJRKA0A4vfZyPd61nBBhx4lcRyXOCd4LYPCFKP0DZBwWLk5V6pM89gC +NnqMbxnPsRbcXBVtGJAvWXW0L5rHJfMOuVBwMRfnxIUljVnONv/264PlcUtwZd/h +o4lsJeBvNg7MnrG5nyVp1+T4RZxYm1P86HLp5zyT+fdj4Cr82b9j6QpxGXEfm1jV +QA1xr1ZkrV8fgETyaE0TBIKcdt6xNfv1mpI1RE5gaP/YzcCs/mL+G0kMar4l7pO/ +6OHXTvHz+W3G6Xlha7Wq1ADoqYz2K7VoL/OgSQhIxRNujyWR6lir7eladVrKkCzu +uzFi/HECgYEA0vSNCIK3useSypMPHhYUVNbZ4hbK0WgqSAxfJQtL3nC7KviVMAXj +IKVR90xuzJB+ih88KCJpH84JH90paMpW0Gq1yEae90bnWa8Nj7ULLS/Zuj0WrelU ++DEGbx47IUPOtiLBxooxFKyIVhX3hWRwZ0pokSQzbgb5zYnlM6tqZ3cCgYEA8Rb2 +wtt0XmqEQedFacs4fobJoVWMcETjpuxYp0m5Kje/4QkptZIbspXGBgNtPBBRGg51 +AYSu8wYkGEueI77KiFDgY8AAkpOk2MrMVPszjOhUiO1oEfbT6ynOY5RDOuXcY6jo +8RpSk46VkfVxt6LVmappqcVFtVWcAjdGfXeSLmkCgYAWP7SgMSkvidzxgJEXmzyJ +th9EuSKq81GCR8vBHG/kBf+3iIAzkGtkBgufCXCmIpc1+hVeJkLwF8rekXTMmIqP +cLG7bbdWXSQJUW0cuvtyyJkuC0NZFELh6knDbmzOFVi33PKS/gAvLgMzER4J843n +VvGwXSEPeazfAKwrxuhyAQKBgQCOm5TPYlyNVNhy20h18d2zCivOoPn3luhKXtd5 +7OP4kw2PIYpoesqjcnC2MeS1eLlgfli70y5hVqqXLHOYlUzcIWr51iMAkREbo6oG +QqkVmoAWlsfOiICGRC5vPM4f0sPwt4NCyt05p0fWFKd1hn5u7Ryfba90OfWUYfny +UX5IsQKBgQCswer4Qc3UepkiYxGwSTxgIh4kYlmamU2I00Kar4uFAr9JsCbk98f0 +kaCUNZjrrvTwgRmdhwcpMDiMW/F4QkNk0I2unHcoAvzNop6c22VhHJU2XJhrQ57h +n1iPiw0NLXiA4RQwMUMjtt3nqlpLOTXGtsF8TmpWPcAN2QcTxOutzw== +-----END RSA PRIVATE KEY----- diff --git a/nixos/tests/nginx-proxyprotocol/ca.cert.pem b/nixos/tests/nginx-proxyprotocol/ca.cert.pem new file mode 100644 index 000000000000..c0b2cc8f3df2 --- /dev/null +++ b/nixos/tests/nginx-proxyprotocol/ca.cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSzCCAjOgAwIBAgIITnUr3xFw4oEwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgNGU3NTJiMCAXDTIzMDEzMDAzNDExOFoYDzIxMjMw +MTMwMDM0MTE4WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA0ZTc1MmIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1SrJT9k3zXIXApEyL5UDlw7F6 +MMOqE5d+8ZwMccHbEKLu0ssNRY+j31tnNYQ/r5iCNeNgUZccKBgzdU0ysyw5n4tw +0y+MTD9fCfUXYcc8pJRPRolo6zxYO9W7WJr0nfJZ+p7zFRAjRCmzXdnZjKz0EGcg +x9mHwn//3SuLt1ItK1n3aZ6im9NlcVtunDe3lCSL0tRgy7wDGNvWDZMO49jk4AFU +BlMqScuiNpUzYgCxNaaGMuH3M0f0YyRAxSs6FWewLtqTIaVql7HL+3PcGAhvlKEZ +fvfaf80F9aWI88sbEddTA0s5837zEoDwGpZl3K5sPU/O3MVEHIhAY5ICG0IBAgMB +AAGjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr +BgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRsl8kmCN4C/PHe +xh5xjFOsKYgX9jAfBgNVHSMEGDAWgBRsl8kmCN4C/PHexh5xjFOsKYgX9jANBgkq +hkiG9w0BAQsFAAOCAQEAmvgpU+q+TBbz+9Y2rdiIeTfeDXtMNPf+nKI3zxYztRGC +MoKP6jCQaFSQra4BVumFLV38DoqR1pOV1ojkiyO5c/9Iym/1Wmm8LeqgsHNqSgyS +C7wvBcb/N9PzIBQFq/RiboDoC7bqK/0zQguCmBtGceH+AVpQyfXM+P78B1EkHozu +67igP8GfouPp2s4Vd5P2XGkA6vMgYCtFEnCbtmmo7C8B+ymhD/D9axpMKQ1OaBg9 +jfqLOlk+Rc2nYZuaDjnUmlTkYjC6EwCNe9weYkSJgQ9QzoGJLIRARsdQdsp3C2fZ +l2UZKkDJ2GPrrc+TdaGXZTYi0uMmvQsEKZXtqAzorQ== +-----END CERTIFICATE----- diff --git a/nixos/tests/nginx-proxyprotocol/ca.key.pem b/nixos/tests/nginx-proxyprotocol/ca.key.pem new file mode 100644 index 000000000000..717948f5b879 --- /dev/null +++ b/nixos/tests/nginx-proxyprotocol/ca.key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAtUqyU/ZN81yFwKRMi+VA5cOxejDDqhOXfvGcDHHB2xCi7tLL +DUWPo99bZzWEP6+YgjXjYFGXHCgYM3VNMrMsOZ+LcNMvjEw/Xwn1F2HHPKSUT0aJ +aOs8WDvVu1ia9J3yWfqe8xUQI0Qps13Z2Yys9BBnIMfZh8J//90ri7dSLStZ92me +opvTZXFbbpw3t5Qki9LUYMu8Axjb1g2TDuPY5OABVAZTKknLojaVM2IAsTWmhjLh +9zNH9GMkQMUrOhVnsC7akyGlapexy/tz3BgIb5ShGX732n/NBfWliPPLGxHXUwNL +OfN+8xKA8BqWZdyubD1PztzFRByIQGOSAhtCAQIDAQABAoIBAQCLeAWs1kWtvTYg +t8UzspC0slItAKrmgt//hvxYDoPmdewC8yPG+AbDOSfmRKOTIxGeyro79UjdHnNP +0yQqpvCU/AqYJ7/inR37jXuCG3TdUHfQbSF1F9N6xb1tvYKoQYKaelYiB8g8eUnj +dYYM+U5tDNlpvJW6/YTfYFUJzWRo3i8jj5lhbkjcJDvdOhVxMXNXJgJAymu1KysE +N1da2l4fzmuoN82wFE9KMyYSn+LOLWBReQQmXHZPP+2LjRIVrWoFoV49k2Ylp9tH +yeaFx1Ya/wVx3PRnSW+zebWDcc0bAua9XU3Fi42yRq5iXOyoXHyefDfJoId7+GAO +IF2qRw9hAoGBAM1O1l4ceOEDsEBh7HWTvmfwVfkXgT6VHeI6LGEjb88FApXgT+wT +1s1IWVVOigLl9OKQbrjqlg9xgzrPDHYRwu5/Oz3X2WaH6wlF+d+okoqls6sCEAeo +GfzF3sKOHQyIYjttCXE5G38uhIgVFFFfK97AbUiY8egYBr0zjVXK7xINAoGBAOIN +1pDBFBQIoKj64opm/G9lJBLUpWLBFdWXhXS6q2jNsdY1mLMRmu/RBaKSfGz7W1a/ +a2WBedjcnTWJ/84tBsn4Qj5tLl8xkcXiN/pslWzg724ZnVsbyxM9KvAdXAma3F0g +2EsYq8mhvbAEkpE+aoM6jwOJBnMhTRZrNMKN2lbFAoGAHmZWB4lfvLG3H1FgmehO +gUVs9X0tff7GdgD3IUsF+zlasKaOLv6hB7R2xdLjTJqQMBwCyQ6zOYYtUD/oMHNg +0b+1HesgHbZybuUVorBrQmxWtjOP/BJABtWlrlkso/Zt1S7H/yPdlm9k4GF+qK3W +6RzFEcLTzvH/zXQcsV9jFuECgYEAhaX+1KiC0XFkY2OpaoCHAOlAUa3NdjyIRzcF +XUU8MINkgCxB8qUXAHCJL1wCGoDluL0FpwbM3m1YuR200tYGLIUNzVDJ2Ng6wk8E +H5fxJGU8ydB1Gzescdx5NWt2Tet0G89ecc/NSTHKL3YUnbDUUm/dvA5YdNscc4PA +tsIdc60CgYEArvU1MwqGQUTDKUmaM2t3qm70fbwmOViHfyTWpn4aAQR3sK16iJMm +V+dka62L/VYs5CIbzXvCioyugUMZGJi/zIwrViRzqJQbNnPADAW4lG88UxXqHHAH +q33ivjgd9omGFb37saKOmR44KmjUIDvSIZF4W3EPwAMEyl5mM31Ryns= +-----END RSA PRIVATE KEY----- diff --git a/nixos/tests/nginx-proxyprotocol/default.nix b/nixos/tests/nginx-proxyprotocol/default.nix new file mode 100644 index 000000000000..9ef5d01cd65b --- /dev/null +++ b/nixos/tests/nginx-proxyprotocol/default.nix @@ -0,0 +1,144 @@ +let + certs = import ./snakeoil-certs.nix; +in +import ../make-test-python.nix ({ pkgs, ... }: { + name = "nginx-proxyprotocol"; + + nodes = { + webserver = { pkgs, lib, ... }: { + environment.systemPackages = [ pkgs.netcat ]; + security.pki.certificateFiles = [ + certs.ca.cert + ]; + + networking.extraHosts = '' + 127.0.0.5 proxy.test.nix + 127.0.0.5 noproxy.test.nix + 127.0.0.3 direct-nossl.test.nix + 127.0.0.4 unsecure-nossl.test.nix + 127.0.0.2 direct-noproxy.test.nix + 127.0.0.1 direct-proxy.test.nix + ''; + services.nginx = { + enable = true; + defaultListen = [ + { addr = "127.0.0.1"; proxyProtocol = true; ssl = true; } + { addr = "127.0.0.2"; } + { addr = "127.0.0.3"; ssl = false; } + { addr = "127.0.0.4"; ssl = false; proxyProtocol = true; } + ]; + commonHttpConfig = '' + log_format pcombined '(proxy_protocol=$proxy_protocol_addr) - (remote_addr=$remote_addr) - (realip=$realip_remote_addr) - (upstream=) - (remote_user=$remote_user) [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent"'; + access_log /var/log/nginx/access.log pcombined; + error_log /var/log/nginx/error.log; + ''; + virtualHosts = + let + commonConfig = { + locations."/".return = "200 '$remote_addr'"; + extraConfig = '' + set_real_ip_from 127.0.0.5/32; + real_ip_header proxy_protocol; + ''; + }; + in + { + "*.test.nix" = commonConfig // { + sslCertificate = certs."*.test.nix".cert; + sslCertificateKey = certs."*.test.nix".key; + forceSSL = true; + }; + "direct-nossl.test.nix" = commonConfig; + "unsecure-nossl.test.nix" = commonConfig // { + extraConfig = '' + real_ip_header proxy_protocol; + ''; + }; + }; + }; + + services.sniproxy = { + enable = true; + config = '' + error_log { + syslog daemon + } + access_log { + syslog daemon + } + listener 127.0.0.5:443 { + protocol tls + source 127.0.0.5 + } + table { + ^proxy\.test\.nix$ 127.0.0.1 proxy_protocol + ^noproxy\.test\.nix$ 127.0.0.2 + } + ''; + }; + }; + }; + + testScript = '' + def check_origin_ip(src_ip: str, dst_url: str, failure: bool = False, proxy_protocol: bool = False, expected_ip: str | None = None): + check = webserver.fail if failure else webserver.succeed + if expected_ip is None: + expected_ip = src_ip + + return check(f"curl {'--haproxy-protocol' if proxy_protocol else '''} --interface {src_ip} --fail -L {dst_url} | grep '{expected_ip}'") + + webserver.wait_for_unit("nginx") + webserver.wait_for_unit("sniproxy") + # This should be closed by virtue of ssl = true; + webserver.wait_for_closed_port(80, "127.0.0.1") + # This should be open by virtue of no explicit ssl + webserver.wait_for_open_port(80, "127.0.0.2") + # This should be open by virtue of ssl = true; + webserver.wait_for_open_port(443, "127.0.0.1") + # This should be open by virtue of no explicit ssl + webserver.wait_for_open_port(443, "127.0.0.2") + # This should be open by sniproxy + webserver.wait_for_open_port(443, "127.0.0.5") + # This should be closed by sniproxy + webserver.wait_for_closed_port(80, "127.0.0.5") + + # Sanity checks for the NGINX module + # direct-HTTP connection to NGINX without TLS, this checks that ssl = false; works well. + check_origin_ip("127.0.0.10", "http://direct-nossl.test.nix/") + # webserver.execute("openssl s_client -showcerts -connect direct-noproxy.test.nix:443") + # direct-HTTP connection to NGINX with TLS + check_origin_ip("127.0.0.10", "http://direct-noproxy.test.nix/") + check_origin_ip("127.0.0.10", "https://direct-noproxy.test.nix/") + # Well, sniproxy is not listening on 80 and cannot redirect + check_origin_ip("127.0.0.10", "http://proxy.test.nix/", failure=True) + check_origin_ip("127.0.0.10", "http://noproxy.test.nix/", failure=True) + + # Actual PROXY protocol related tests + # Connecting through sniproxy should passthrough the originating IP address. + check_origin_ip("127.0.0.10", "https://proxy.test.nix/") + # Connecting through sniproxy to a non-PROXY protocol enabled listener should not pass the originating IP address. + check_origin_ip("127.0.0.10", "https://noproxy.test.nix/", expected_ip="127.0.0.5") + + # Attack tests against spoofing + # Let's try to spoof our IP address by connecting direct-y to the PROXY protocol listener. + # FIXME(RaitoBezarius): rewrite it using Python + (Scapy|something else) as this is too much broken unfortunately. + # Or wait for upstream curl patch. + # def generate_attacker_request(original_ip: str, target_ip: str, dst_url: str): + # return f"""PROXY TCP4 {original_ip} {target_ip} 80 80 + # GET / HTTP/1.1 + # Host: {dst_url} + + # """ + # def spoof(original_ip: str, target_ip: str, dst_url: str, tls: bool = False, expect_failure: bool = True): + # method = webserver.fail if expect_failure else webserver.succeed + # port = 443 if tls else 80 + # print(webserver.execute(f"cat < {}, + minica ? pkgs.minica, + runCommandCC ? pkgs.runCommandCC, +}: +let + conf = import ./snakeoil-certs.nix; + domain = conf.domain; + domainSanitized = pkgs.lib.replaceStrings ["*"] ["_"] domain; +in + runCommandCC "generate-tests-certs" { + buildInputs = [ (minica.overrideAttrs (old: { + postPatch = '' + sed -i 's_NotAfter: time.Now().AddDate(2, 0, 30),_NotAfter: time.Now().AddDate(20, 0, 0),_' main.go + ''; + })) ]; + + } '' + minica \ + --ca-key ca.key.pem \ + --ca-cert ca.cert.pem \ + --domains "${domain}" + + mkdir -p $out + mv ca.*.pem $out/ + mv ${domainSanitized}/key.pem $out/${domainSanitized}.key.pem + mv ${domainSanitized}/cert.pem $out/${domainSanitized}.cert.pem + '' diff --git a/nixos/tests/nginx-proxyprotocol/snakeoil-certs.nix b/nixos/tests/nginx-proxyprotocol/snakeoil-certs.nix new file mode 100644 index 000000000000..61af6351ca65 --- /dev/null +++ b/nixos/tests/nginx-proxyprotocol/snakeoil-certs.nix @@ -0,0 +1,14 @@ +let + domain = "*.test.nix"; + domainSanitized = "_.test.nix"; +in { + inherit domain; + ca = { + cert = ./ca.cert.pem; + key = ./ca.key.pem; + }; + "${domain}" = { + cert = ./. + "/${domainSanitized}.cert.pem"; + key = ./. + "/${domainSanitized}.key.pem"; + }; +} diff --git a/pkgs/servers/http/nginx/generic.nix b/pkgs/servers/http/nginx/generic.nix index b740747975cd..63ce7f759066 100644 --- a/pkgs/servers/http/nginx/generic.nix +++ b/pkgs/servers/http/nginx/generic.nix @@ -178,7 +178,7 @@ stdenv.mkDerivation { passthru = { inherit modules; tests = { - inherit (nixosTests) nginx nginx-auth nginx-etag nginx-globalredirect nginx-http3 nginx-pubhtml nginx-sandbox nginx-sso; + inherit (nixosTests) nginx nginx-auth nginx-etag nginx-globalredirect nginx-http3 nginx-pubhtml nginx-sandbox nginx-sso nginx-proxyprotocol; variants = lib.recurseIntoAttrs nixosTests.nginx-variants; acme-integration = nixosTests.acme; } // passthru.tests;