at 25.11-pre 6.3 kB view raw
1let 2 certs = import ./snakeoil-certs.nix; 3in 4{ pkgs, ... }: 5{ 6 name = "nginx-proxyprotocol"; 7 8 meta = { 9 maintainers = with pkgs.lib.maintainers; [ raitobezarius ]; 10 }; 11 12 nodes = { 13 webserver = 14 { pkgs, lib, ... }: 15 { 16 environment.systemPackages = [ pkgs.netcat ]; 17 security.pki.certificateFiles = [ 18 certs.ca.cert 19 ]; 20 21 networking.extraHosts = '' 22 127.0.0.5 proxy.test.nix 23 127.0.0.5 noproxy.test.nix 24 127.0.0.3 direct-nossl.test.nix 25 127.0.0.4 unsecure-nossl.test.nix 26 127.0.0.2 direct-noproxy.test.nix 27 127.0.0.1 direct-proxy.test.nix 28 ''; 29 services.nginx = { 30 enable = true; 31 defaultListen = [ 32 { 33 addr = "127.0.0.1"; 34 proxyProtocol = true; 35 ssl = true; 36 } 37 { addr = "127.0.0.2"; } 38 { 39 addr = "127.0.0.3"; 40 ssl = false; 41 } 42 { 43 addr = "127.0.0.4"; 44 ssl = false; 45 proxyProtocol = true; 46 } 47 ]; 48 commonHttpConfig = '' 49 log_format pcombined '(proxy_protocol=$proxy_protocol_addr) - (remote_addr=$remote_addr) - (realip=$realip_remote_addr) - (upstream=) - (remote_user=$remote_user) [$time_local] ' 50 '"$request" $status $body_bytes_sent ' 51 '"$http_referer" "$http_user_agent"'; 52 access_log /var/log/nginx/access.log pcombined; 53 error_log /var/log/nginx/error.log; 54 ''; 55 virtualHosts = 56 let 57 commonConfig = { 58 locations."/".return = "200 '$remote_addr'"; 59 extraConfig = '' 60 set_real_ip_from 127.0.0.5/32; 61 real_ip_header proxy_protocol; 62 ''; 63 }; 64 in 65 { 66 "*.test.nix" = commonConfig // { 67 sslCertificate = certs."*.test.nix".cert; 68 sslCertificateKey = certs."*.test.nix".key; 69 forceSSL = true; 70 }; 71 "direct-nossl.test.nix" = commonConfig; 72 "unsecure-nossl.test.nix" = commonConfig // { 73 extraConfig = '' 74 real_ip_header proxy_protocol; 75 ''; 76 }; 77 }; 78 }; 79 80 services.sniproxy = { 81 enable = true; 82 config = '' 83 error_log { 84 syslog daemon 85 } 86 access_log { 87 syslog daemon 88 } 89 listener 127.0.0.5:443 { 90 protocol tls 91 source 127.0.0.5 92 } 93 table { 94 ^proxy\.test\.nix$ 127.0.0.1 proxy_protocol 95 ^noproxy\.test\.nix$ 127.0.0.2 96 } 97 ''; 98 }; 99 }; 100 }; 101 102 testScript = '' 103 def check_origin_ip(src_ip: str, dst_url: str, failure: bool = False, proxy_protocol: bool = False, expected_ip: str | None = None): 104 check = webserver.fail if failure else webserver.succeed 105 if expected_ip is None: 106 expected_ip = src_ip 107 108 return check(f"curl {'--haproxy-protocol' if proxy_protocol else '''} --interface {src_ip} --fail -L {dst_url} | grep '{expected_ip}'") 109 110 webserver.wait_for_unit("nginx") 111 webserver.wait_for_unit("sniproxy") 112 # This should be closed by virtue of ssl = true; 113 webserver.wait_for_closed_port(80, "127.0.0.1") 114 # This should be open by virtue of no explicit ssl 115 webserver.wait_for_open_port(80, "127.0.0.2") 116 # This should be open by virtue of ssl = true; 117 webserver.wait_for_open_port(443, "127.0.0.1") 118 # This should be open by virtue of no explicit ssl 119 webserver.wait_for_open_port(443, "127.0.0.2") 120 # This should be open by sniproxy 121 webserver.wait_for_open_port(443, "127.0.0.5") 122 # This should be closed by sniproxy 123 webserver.wait_for_closed_port(80, "127.0.0.5") 124 125 # Sanity checks for the NGINX module 126 # direct-HTTP connection to NGINX without TLS, this checks that ssl = false; works well. 127 check_origin_ip("127.0.0.10", "http://direct-nossl.test.nix/") 128 # webserver.execute("openssl s_client -showcerts -connect direct-noproxy.test.nix:443") 129 # direct-HTTP connection to NGINX with TLS 130 check_origin_ip("127.0.0.10", "http://direct-noproxy.test.nix/") 131 check_origin_ip("127.0.0.10", "https://direct-noproxy.test.nix/") 132 # Well, sniproxy is not listening on 80 and cannot redirect 133 check_origin_ip("127.0.0.10", "http://proxy.test.nix/", failure=True) 134 check_origin_ip("127.0.0.10", "http://noproxy.test.nix/", failure=True) 135 136 # Actual PROXY protocol related tests 137 # Connecting through sniproxy should passthrough the originating IP address. 138 check_origin_ip("127.0.0.10", "https://proxy.test.nix/") 139 # Connecting through sniproxy to a non-PROXY protocol enabled listener should not pass the originating IP address. 140 check_origin_ip("127.0.0.10", "https://noproxy.test.nix/", expected_ip="127.0.0.5") 141 142 # Attack tests against spoofing 143 # Let's try to spoof our IP address by connecting direct-y to the PROXY protocol listener. 144 # FIXME(RaitoBezarius): rewrite it using Python + (Scapy|something else) as this is too much broken unfortunately. 145 # Or wait for upstream curl patch. 146 # def generate_attacker_request(original_ip: str, target_ip: str, dst_url: str): 147 # return f"""PROXY TCP4 {original_ip} {target_ip} 80 80 148 # GET / HTTP/1.1 149 # Host: {dst_url} 150 151 # """ 152 # def spoof(original_ip: str, target_ip: str, dst_url: str, tls: bool = False, expect_failure: bool = True): 153 # method = webserver.fail if expect_failure else webserver.succeed 154 # port = 443 if tls else 80 155 # print(webserver.execute(f"cat <<EOF | nc {target_ip} {port}\n{generate_attacker_request(original_ip, target_ip, dst_url)}\nEOF")) 156 # return method(f"cat <<EOF | nc {target_ip} {port} | grep {original_ip}\n{generate_attacker_request(original_ip, target_ip, dst_url)}\nEOF") 157 158 # check_origin_ip("127.0.0.10", "http://unsecure-nossl.test.nix", proxy_protocol=True) 159 # spoof("1.1.1.1", "127.0.0.4", "direct-nossl.test.nix") 160 # spoof("1.1.1.1", "127.0.0.4", "unsecure-nossl.test.nix", expect_failure=False) 161 ''; 162}