diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c8762ee..695a92a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -125,6 +125,9 @@ jobs: DB_UPDATE_URL: ${{ secrets.DB_UPDATE_URL }} ORIGIN_CERT: ${{ secrets.ORIGIN_CERT }} ORIGIN_KEY: ${{ secrets.ORIGIN_KEY }} + IPV4_DOMAIN: ${{ secrets.IPV4_DOMAIN }} + IPV4_ORIGIN_CERT: ${{ secrets.IPV4_ORIGIN_CERT }} + IPV4_ORIGIN_KEY: ${{ secrets.IPV4_ORIGIN_KEY }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} FIRST_DEPLOY: ${{ steps.detect.outputs.first_deploy }} diff --git a/deploy/ansible/playbook.yml b/deploy/ansible/playbook.yml index 63e16e0..afce703 100644 --- a/deploy/ansible/playbook.yml +++ b/deploy/ansible/playbook.yml @@ -10,6 +10,9 @@ db_update_url: "{{ lookup('env', 'DB_UPDATE_URL') }}" origin_cert: "{{ lookup('env', 'ORIGIN_CERT') }}" origin_key: "{{ lookup('env', 'ORIGIN_KEY') }}" + ipv4_domain: "{{ lookup('env', 'IPV4_DOMAIN') | default('', true) }}" + ipv4_origin_cert: "{{ lookup('env', 'IPV4_ORIGIN_CERT') | default('', true) }}" + ipv4_origin_key: "{{ lookup('env', 'IPV4_ORIGIN_KEY') | default('', true) }}" dockerhub_token: "{{ lookup('env', 'DOCKERHUB_TOKEN') }}" dockerhub_username: "{{ lookup('env', 'DOCKERHUB_USERNAME') }}" first_deploy: "{{ lookup('env', 'FIRST_DEPLOY') | default('false', true) }}" diff --git a/deploy/ansible/roles/app/tasks/main.yml b/deploy/ansible/roles/app/tasks/main.yml index 9f51353..ccbd99e 100644 --- a/deploy/ansible/roles/app/tasks/main.yml +++ b/deploy/ansible/roles/app/tasks/main.yml @@ -32,6 +32,44 @@ mode: "0644" notify: Reload haproxy +- name: Create IPv4 domain TLS certificate directory + ansible.builtin.file: + path: /opt/rustyip/haproxy/certs-ipv4 + state: directory + owner: root + group: root + mode: "0755" + when: ipv4_domain | length > 0 + +- name: Deploy IPv4 domain TLS origin certificate + ansible.builtin.copy: + content: "{{ ipv4_origin_cert }}\n{{ ipv4_origin_key }}" + dest: /opt/rustyip/haproxy/certs-ipv4/origin.pem + owner: root + group: root + mode: "0644" + notify: Reload haproxy + when: ipv4_domain | length > 0 + +- name: Deploy IPv4 domain list for HAProxy + ansible.builtin.copy: + content: "{{ ipv4_domain }}\n" + dest: /opt/rustyip/haproxy/ipv4_domain.lst + owner: root + group: root + mode: "0644" + when: ipv4_domain | length > 0 + notify: Reload haproxy + +- name: Deploy empty IPv4 domain list when not configured + ansible.builtin.copy: + content: "# IPv4-only domain not configured\n" + dest: /opt/rustyip/haproxy/ipv4_domain.lst + owner: root + group: root + mode: "0644" + when: ipv4_domain | length == 0 + - name: Deploy environment file ansible.builtin.template: src: env.j2 diff --git a/deploy/ansible/roles/app/templates/docker-compose.yml.j2 b/deploy/ansible/roles/app/templates/docker-compose.yml.j2 index 23f50eb..f69cedf 100644 --- a/deploy/ansible/roles/app/templates/docker-compose.yml.j2 +++ b/deploy/ansible/roles/app/templates/docker-compose.yml.j2 @@ -7,7 +7,11 @@ services: - "443:443" volumes: - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro + - ./haproxy/ipv4_domain.lst:/usr/local/etc/haproxy/ipv4_domain.lst:ro - ./haproxy/certs:/etc/haproxy/certs:ro +{% if ipv4_domain %} + - ./haproxy/certs-ipv4:/etc/haproxy/certs-ipv4:ro +{% endif %} restart: unless-stopped ulimits: nofile: diff --git a/deploy/ansible/roles/app/templates/env.j2 b/deploy/ansible/roles/app/templates/env.j2 index 8cbcf83..4d154f9 100644 --- a/deploy/ansible/roles/app/templates/env.j2 +++ b/deploy/ansible/roles/app/templates/env.j2 @@ -3,3 +3,6 @@ DB_PATH=/data/Merged-IP.mmdb DB_UPDATE_URL={{ db_update_url }} DB_UPDATE_INTERVAL_HOURS=24 SITE_DOMAIN={{ site_domain }} +{% if ipv4_domain %} +IPV4_DOMAIN={{ ipv4_domain }} +{% endif %} diff --git a/deploy/ansible/roles/app/templates/haproxy.cfg.j2 b/deploy/ansible/roles/app/templates/haproxy.cfg.j2 index b145774..e71f163 100644 --- a/deploy/ansible/roles/app/templates/haproxy.cfg.j2 +++ b/deploy/ansible/roles/app/templates/haproxy.cfg.j2 @@ -36,11 +36,25 @@ frontend http_in # Access to port 80 is restricted to Cloudflare IPs at the firewall level # All requests reaching HAProxy come from Cloudflare; no additional ACL validation is required. - # HAProxy's own forwardfor is not enabled; real IP is set explicitly below via CF-Connecting-IP +{% if ipv4_domain %} + # Identify IPv4-only domain traffic by Host header + acl is_ipv4_domain hdr(host) -f /usr/local/etc/haproxy/ipv4_domain.lst +{% endif %} + # HAProxy's own forwardfor is not enabled; real IP is set explicitly below via CF-Connecting-IP +{% if ipv4_domain %} + # Main domain: standard headers + http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if !is_ipv4_domain + http-request set-header X-Real-IP %[req.hdr(CF-Connecting-IP)] if !is_ipv4_domain + # IPv4-only domain: separate header to prevent confusion with the main domain + http-request set-header X-IPv4-Client-IP %[req.hdr(CF-Connecting-IP)] if is_ipv4_domain + http-request del-header X-Forwarded-For if is_ipv4_domain + http-request del-header X-Real-IP if is_ipv4_domain +{% else %} # Overwrite standard headers directly with the real IP forcibly added by Cloudflare http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] http-request set-header X-Real-IP %[req.hdr(CF-Connecting-IP)] +{% endif %} # Rate limiting: tiered based on global request rate # Track per-IP request rate (per minute) @@ -63,7 +77,11 @@ frontend http_in default_backend app frontend https_in +{% if ipv4_domain %} + bind *:443 ssl crt /etc/haproxy/certs/origin.pem crt /etc/haproxy/certs-ipv4/origin.pem alpn h2,http/1.1 strict-sni +{% else %} bind *:443 ssl crt /etc/haproxy/certs/origin.pem alpn h2,http/1.1 strict-sni +{% endif %} # Gzip compression compression algo gzip @@ -72,11 +90,25 @@ frontend https_in # Access to port 443 is restricted to Cloudflare IPs at the firewall level # All requests reaching HAProxy come from Cloudflare; no additional ACL validation is required. - # HAProxy's own forwardfor is not enabled; real IP is set explicitly below via CF-Connecting-IP +{% if ipv4_domain %} + # Identify IPv4-only domain traffic by Host header + acl is_ipv4_domain hdr(host) -f /usr/local/etc/haproxy/ipv4_domain.lst +{% endif %} + # HAProxy's own forwardfor is not enabled; real IP is set explicitly below via CF-Connecting-IP +{% if ipv4_domain %} + # Main domain: standard headers + http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if !is_ipv4_domain + http-request set-header X-Real-IP %[req.hdr(CF-Connecting-IP)] if !is_ipv4_domain + # IPv4-only domain: separate header to prevent confusion with the main domain + http-request set-header X-IPv4-Client-IP %[req.hdr(CF-Connecting-IP)] if is_ipv4_domain + http-request del-header X-Forwarded-For if is_ipv4_domain + http-request del-header X-Real-IP if is_ipv4_domain +{% else %} # Overwrite standard headers directly with the real IP forcibly added by Cloudflare http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] http-request set-header X-Real-IP %[req.hdr(CF-Connecting-IP)] +{% endif %} # Rate limiting uses the shared stick-table from http_in frontend http-request track-sc0 req.hdr(CF-Connecting-IP) table http_in