diff --git a/haproxy-sni/.gitignore b/haproxy-sni/.gitignore new file mode 100644 index 0000000..2232829 --- /dev/null +++ b/haproxy-sni/.gitignore @@ -0,0 +1 @@ +*.dump diff --git a/haproxy-sni/README.md b/haproxy-sni/README.md new file mode 100644 index 0000000..9d7de21 --- /dev/null +++ b/haproxy-sni/README.md @@ -0,0 +1,20 @@ +# HAProxy with SNI & Host-based rules + +This is a proof of concept, enabling HAProxy to use *either* SNI to redirect to backends with their own HTTPS certificates (which are then fully exposed to the client; HAProxy only proxies on a TCP level in that case), *as well as* to terminate HTTPS and use the Host header to redirect to backends that use HTTP (or a new HTTPS connection). + +## How it works +1. The `http_redirect_frontend` is only there to listen on port 80 and redirect every request to HTTPS. +2. The `https_sni_frontend` listens on port 443 and chooses a backend based on the SNI hostname of the TLS connection. +3. The `https_termination_backend` passes all requests to a unix socket (using the plain TCP data). +4. The `https_termination_frontend` listens on said unix socket, terminates the HTTPS connections and then chooses a backend based on the Host header. + +In the example (see [haproxy.cfg](haproxy.cfg)), the `pages_backend` is listening via HTTPS and is providing its own HTTPS certificates, while the `gitea_backend` only provides HTTP. + +## How to test +```bash +docker-compose up -d +./test.sh + +# For manual testing: all HTTPS URLs connect to localhost:443 & certificates are not verified. +./test.sh [curl-options...] +``` diff --git a/haproxy-sni/dhparam.pem b/haproxy-sni/dhparam.pem new file mode 100644 index 0000000..088f967 --- /dev/null +++ b/haproxy-sni/dhparam.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== +-----END DH PARAMETERS----- \ No newline at end of file diff --git a/haproxy-sni/docker-compose.yml b/haproxy-sni/docker-compose.yml new file mode 100644 index 0000000..4dd8677 --- /dev/null +++ b/haproxy-sni/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3" +services: + haproxy: + image: haproxy + ports: ["443:443"] + volumes: + - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro + - ./dhparam.pem:/etc/ssl/dhparam.pem:ro + - ./haproxy-certificates:/etc/ssl/private/haproxy:ro + cap_add: + - NET_ADMIN + gitea: + image: caddy + volumes: + - ./gitea-www:/srv:ro + - ./gitea.Caddyfile:/etc/caddy/Caddyfile:ro + pages: + image: caddy + volumes: + - ./pages-www:/srv:ro + - ./pages.Caddyfile:/etc/caddy/Caddyfile:ro + diff --git a/haproxy-sni/gitea-www/index.html b/haproxy-sni/gitea-www/index.html new file mode 100644 index 0000000..d092750 --- /dev/null +++ b/haproxy-sni/gitea-www/index.html @@ -0,0 +1 @@ +Hello to Gitea! diff --git a/haproxy-sni/gitea.Caddyfile b/haproxy-sni/gitea.Caddyfile new file mode 100644 index 0000000..e92a157 --- /dev/null +++ b/haproxy-sni/gitea.Caddyfile @@ -0,0 +1,3 @@ +http://codeberg.org + +file_server diff --git a/haproxy-sni/haproxy-certificates/codeberg.org.pem b/haproxy-sni/haproxy-certificates/codeberg.org.pem new file mode 100644 index 0000000..e85b673 --- /dev/null +++ b/haproxy-sni/haproxy-certificates/codeberg.org.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEUDCCArigAwIBAgIRAMq3iwF963VGkzXFpbrpAtkwDQYJKoZIhvcNAQELBQAw +gYkxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEvMC0GA1UECwwmbW9t +YXJAbW9yaXR6LWxhcHRvcCAoTW9yaXR6IE1hcnF1YXJkdCkxNjA0BgNVBAMMLW1r +Y2VydCBtb21hckBtb3JpdHotbGFwdG9wIChNb3JpdHogTWFycXVhcmR0KTAeFw0y +MTA2MDYwOTQ4NDFaFw0yMzA5MDYwOTQ4NDFaMFoxJzAlBgNVBAoTHm1rY2VydCBk +ZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEvMC0GA1UECwwmbW9tYXJAbW9yaXR6LWxh +cHRvcCAoTW9yaXR6IE1hcnF1YXJkdCkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCrSPSPM6grNZMG4ZKFCVxuXu+qkHdzSR96QUxi00VkIrkGPmyMN7q7 +rUQJto9C9guJio3n7y3Bvr5kBjICjyWQd7GfkVuYgiYiG/O2hy1u1dIMCAB/Zhx1 +F1mvRfn/Q4eZk2GSOUM+kC0xaNsn2827VGLOGFywUhRmu7J9QSQ3x1Pi5BME7eNC +AKup0CbrMrZSzKAEuYujLY0UYRxUrguMnV60wxJDCYE14YDxn9t0g7wQmzyndupk +AMLNJZX5L83RA6vUEuTVYBFcyB0Fu3oBLQ31y5QOZ7WF/QiO5cPicQJI/oyXlHq4 +97BWS/H28kj1H5ZM8+5yhCYDtgj7dERpAgMBAAGjYTBfMA4GA1UdDwEB/wQEAwIF +oDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSOSXQZqt2gjbTOkE9Q +ddI8SYPqrDAXBgNVHREEEDAOggxjb2RlYmVyZy5vcmcwDQYJKoZIhvcNAQELBQAD +ggGBAJ/57DGqfuOa3aS/nLeAzl8komvyHuoOZi9yDK2Jqr+COxP58zSu8xwhiZfc +TJvIyB9QR7imGiQ7fEKby40q8uxGGx13oY7gQy7PG8hHk2dkfDZuSQacnpPRC3W0 +0dL2CQIog6rw6jJHjxneitkX9FUmOnHIKy7LHya0Sthg36Z0Qw5JA3SCy6OQNepR +R2XzwTZ0KFk6gAuKCto8ENUlU5lV9PM4X3U0cBOIc5LJAPM+cxEDUocFtFqKJPbe +YYlSeB200YhYOdi+x34n9xnQjFu/jVlWF+Y0tMBB1WWq6rZbnuylwWLYQZAo10Co +D3oWsYRlD/ZL7X20ztIy8vRXz33ugnxxf88Q7csWDYb4S325svLfI2EjciIxYmBo +dSJxXRQkadjIoI7gNvzeWBkYSJpQUbaD4nT2xRS8vfuv42/DrIehb8SbTivHmcB3 +OibpWIvDtS1B8thIlzl0edb+8pb6mof7pOBxoZdcBsSAk2/48s+jfRHfD9XcuKnv +hGCdSQ== +-----END CERTIFICATE----- diff --git a/haproxy-sni/haproxy-certificates/codeberg.org.pem.key b/haproxy-sni/haproxy-certificates/codeberg.org.pem.key new file mode 100644 index 0000000..b9c4d61 --- /dev/null +++ b/haproxy-sni/haproxy-certificates/codeberg.org.pem.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrSPSPM6grNZMG +4ZKFCVxuXu+qkHdzSR96QUxi00VkIrkGPmyMN7q7rUQJto9C9guJio3n7y3Bvr5k +BjICjyWQd7GfkVuYgiYiG/O2hy1u1dIMCAB/Zhx1F1mvRfn/Q4eZk2GSOUM+kC0x +aNsn2827VGLOGFywUhRmu7J9QSQ3x1Pi5BME7eNCAKup0CbrMrZSzKAEuYujLY0U +YRxUrguMnV60wxJDCYE14YDxn9t0g7wQmzyndupkAMLNJZX5L83RA6vUEuTVYBFc +yB0Fu3oBLQ31y5QOZ7WF/QiO5cPicQJI/oyXlHq497BWS/H28kj1H5ZM8+5yhCYD +tgj7dERpAgMBAAECggEAAeW+/88cr83aIRtimiKuaXKXyRXsnNRUivAqPnYEsMVJ +s24BmdQMN4QF2u2wzJcZLZ7hT45wvVK1nToMV8bqLZ2F1DSyBRB8B6iznHQG5tFr +kEKObtrcuddWYQCvckp3OBZP4GTN/+Vs+r0koF5o+whGR+4xKKrgGvs9UPHlytBf +0DMzAzWzGPp6qBPw2sUx/fa9r5TqFW+p4SEOZJUqL2/zEZ6KBWbKw5T1e1y2kMEc +cquUQ4avqK/N1nwRNKUnTvW827v0k7HQ2cFdrjIATNlICslOWJQicG5GUOuSBkTC +0FFkSTtHP4qm0BqShjv6NDmzX+3WCVkGOKFOI+zuWQKBgQDBq8yEcvfMJY98KNlR +eKKdJAMJvKdoD65Yv6EG7ZzpeEWHaTGhu71RPgHYkHn8h1T/9WniroSk19+zb4lP +mMsBwxpg5HejWPzIiiJRkRCRA7aZZfvaXfIWryB4kI1tlGHBNN/+SYpG1zdNumtp +Xyb/sQWMMWRZdRgclF8V+NvduwKBgQDiaM59gBROleREduFZE1a0oXtt+CrwrPlz +hclrkYl1FbTA4TdL4JNbj5jCXCR8YakFhxWEmhwq+Dgl1NQY/YjHyG3w2imaeASX +QUsEvAIvNrv1mIELiYCLmUElyX4WL3UhqveOFcZUvR1Z4TTwruPQmXf6BJEBLbWI +f7odmG6yKwKBgQCzpuLjZiZY9+qe2OGmQopNzE8JJDgCPrGS38fGvnnU1N1iXAFP +LvDRwPxDYNnXl84QVR2wygR/SUTYlTlBXdHKw6nfgW89Vlm+yOxGz5MXgeNLbp/u +k0DzK+aqECUxJfh8GclCgANF7XP+pVPn/f0WKKalwld86DLCqBuALUX+6wKBgCUh +gxvZ8Xqh4nnH9VUicsnU4eU7Ge+2roJfopTdnWlyUd6AEQ2EmyYc+rSFYAZ2Db42 +VTUWASCa7LpnmREwI0qAeGdToBcRL8+OibsRClqr409331IBDu/WBnUoAmGpDtCi +tU68C3bCPRoMcR430GzZfm+maBGFaYwlRmSsJxtZAoGADSA3uAZBuWNDPNKUas2k +Z2dXFEPNpViMjQzJ+Ko7lbOBpUUUQfZF2VMSK4lcnhhbmhcMrYzWWmh6uaw78aHY +e3M//BfcVMdxHw7EemGOViNNq3uDIwzvYteoe6fAOA7MaV+WjJaf+smceR4o38fk +U9RTkKpRJIcvEW5bvTI9h4o= +-----END PRIVATE KEY----- diff --git a/haproxy-sni/haproxy.cfg b/haproxy-sni/haproxy.cfg new file mode 100644 index 0000000..869bae3 --- /dev/null +++ b/haproxy-sni/haproxy.cfg @@ -0,0 +1,98 @@ +##################################### +## Global Configuration & Defaults ## +##################################### + +global + log stderr format iso local7 + + # generated 2021-06-05, Mozilla Guideline v5.6, HAProxy 2.1, OpenSSL 1.1.1d, intermediate configuration + # https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.1d&guideline=5.6 + ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam + ssl-dh-param-file /etc/ssl/dhparam.pem + +defaults + log global + timeout connect 30000 + timeout check 300000 + timeout client 300000 + timeout server 300000 + +############################################################################ +## Frontends: HTTP; HTTPS → HTTPS SNI-based; HTTPS → HTTP(S) header-based ## +############################################################################ + +frontend http_redirect_frontend + # HTTP backend to redirect everything to HTTPS + bind :::80 v4v6 + mode http + http-request redirect scheme https + +frontend https_sni_frontend + # TCP backend to forward to HTTPS backends based on SNI + bind :::443 v4v6 + mode tcp + + # Wait up to 5s for a SNI header & only accept TLS connections + tcp-request inspect-delay 5s + tcp-request content capture req.ssl_sni len 255 + log-format "%ci:%cp -> %[capture.req.hdr(0)] @ %f (%fi:%fp) -> %b (%bi:%bp)" + tcp-request content accept if { req.ssl_hello_type 1 } + + ################################################### + ## Rules: forward to HTTPS(S) header-based rules ## + ################################################### + acl use_http_backend req.ssl_sni -i "codeberg.org" + acl use_http_backend req.ssl_sni -i "join.codeberg.org" + use_backend https_termination_backend if use_http_backend + + ############################ + ## Rules: HTTPS SNI-based ## + ############################ + # use_backend xyz_backend if { req.ssl_sni -i "xyz" } + default_backend pages_backend + +frontend https_termination_frontend + # Terminate TLS for HTTP backends + bind /tmp/haproxy-tls-termination.sock accept-proxy ssl strict-sni alpn h2,http/1.1 crt /etc/ssl/private/haproxy/ + mode http + + # HSTS (63072000 seconds) + http-response set-header Strict-Transport-Security max-age=63072000 + + http-request capture req.hdr(Host) len 255 + log-format "%ci:%cp -> %[capture.req.hdr(0)] @ %f (%fi:%fp) -> %b (%bi:%bp)" + + ################################## + ## Rules: HTTPS(S) header-based ## + ################################## + use_backend gitea_backend if { hdr(host) -i codeberg.org } + +backend https_termination_backend + # Redirect to the terminating HTTPS frontend for all HTTP backends + server https_termination_server /tmp/haproxy-tls-termination.sock send-proxy-v2-ssl-cn + mode tcp + +############################### +## Backends: HTTPS SNI-based ## +############################### + +backend pages_backend + # Pages server is a HTTP backend that uses its own certificates for custom domains + server pages_server pages:443 + mode tcp + +#################################### +## Backends: HTTP(S) header-based ## +#################################### + +backend gitea_backend + server gitea_server gitea:80 + mode http diff --git a/haproxy-sni/pages-www/index.html b/haproxy-sni/pages-www/index.html new file mode 100644 index 0000000..dc24785 --- /dev/null +++ b/haproxy-sni/pages-www/index.html @@ -0,0 +1 @@ +Hello to Pages! diff --git a/haproxy-sni/pages.Caddyfile b/haproxy-sni/pages.Caddyfile new file mode 100644 index 0000000..17adc19 --- /dev/null +++ b/haproxy-sni/pages.Caddyfile @@ -0,0 +1,4 @@ +https://example-page.org + +tls internal +file_server diff --git a/haproxy-sni/test.sh b/haproxy-sni/test.sh new file mode 100755 index 0000000..89e2dfd --- /dev/null +++ b/haproxy-sni/test.sh @@ -0,0 +1,22 @@ +#!/bin/sh +if [ $# -gt 0 ]; then + exec curl -k --resolve '*:443:127.0.0.1' "$@" +fi + +fail() { + echo "[FAIL] $@" + exit 1 +} + +echo "Connecting to Gitea..." +res=$(curl https://codeberg.org -sk --resolve '*:443:127.0.0.1' --trace-ascii gitea.dump | tee /dev/stderr) +echo "$res" | grep -Fx 'Hello to Gitea!' >/dev/null || fail "Gitea didn't answer" +grep '^== Info: issuer: O=mkcert development CA;' gitea.dump || { grep grep '^== Info: issuer:' gitea.dump; fail "Gitea didn't use the correct certificate!"; } + +echo "Connecting to Pages..." +res=$(curl https://example-page.org -sk --resolve '*:443:127.0.0.1' --trace-ascii pages.dump | tee /dev/stderr) +echo "$res" | grep -Fx 'Hello to Pages!' >/dev/null || fail "Pages didn't answer" +grep '^== Info: issuer: CN=Caddy Local Authority\b' pages.dump || { grep '^== Info: issuer:' pages.dump; fail "Pages didn't use the correct certificate!"; } + +echo "All tests succeeded" +rm *.dump