diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index 1bd8906cf64f36c..9dc3a5d64a1ec4d 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -174,6 +174,8 @@ In addition to numerous new and upgraded packages, this release has the followin - `services.mastodon` gained a tootctl wrapped named `mastodon-tootctl` similar to `nextcloud-occ` which can be executed from any user and switches to the configured mastodon user with sudo and sources the environment variables. +- `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. + - DocBook option documentation, which has been deprecated since 22.11, will now cause a warning when documentation is built. Out-of-tree modules should migrate to using CommonMark documentation as outlined in [](#sec-option-declarations) to silence this warning. DocBook option documentation support will be removed in the next release and CommonMark will become the default. DocBook option documentation that has not been migrated until then will no longer render properly or cause errors. diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index 905dd5bef1f71c1..e3596fde3b91065 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -290,24 +290,43 @@ let onlySSL = vhost.onlySSL || vhost.enableSSL; hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL; + # First evaluation of defaultListen based on a set of listen lines. + defaultListen1 = 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 defaultListen1 + # 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 defaultListen1 (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 ? [], ... }: (if ssl && vhost.http3 then " # UDP listener for **QUIC+HTTP/3 listen ${addr}:${toString port} http3 " + optionalString vhost.default "default_server " + optionalString vhost.reuseport "reuseport " + + optionalString proxyProtocol "proxy_protocol " + optionalString (extraParameters != []) (concatStringsSep " " extraParameters) + ";" else "") + " @@ -317,6 +336,7 @@ let + optionalString ssl "ssl " + optionalString vhost.default "default_server " + optionalString vhost.reuseport "reuseport " + + optionalString proxyProtocol "proxy_protocol " + optionalString (extraParameters != []) (concatStringsSep " " extraParameters) + ";"; @@ -499,6 +519,45 @@ 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 = nullOr (listOf str); + description = lib.mdDoc "Extra parameters of this listen directive."; + default = null; + 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]"; @@ -506,6 +565,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" } ]`. ''; }; @@ -1009,6 +1069,31 @@ in services.nginx.virtualHosts..useACMEHost are mutually exclusive. ''; } + { + # 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: !listenLine.proxyProtocol) 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 089decb5f433587..f87c7ab6d23e377 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 0c3310cabe420ac..113c9f3b0aefa83 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -471,6 +471,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 000000000000000..e5cea72610b9179 --- /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 000000000000000..ed2b17af0bf6be0 --- /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 000000000000000..c0b2cc8f3df213e --- /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 000000000000000..717948f5b879a76 --- /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 000000000000000..9ef5d01cd65b096 --- /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 000000000000000..61af6351ca65587 --- /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 7d0ab6ac42072eb..0e582d46c344a88 100644 --- a/pkgs/servers/http/nginx/generic.nix +++ b/pkgs/servers/http/nginx/generic.nix @@ -178,7 +178,7 @@ stdenv.mkDerivation { passthru = { modules = 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;