From 306be2896389d9c8d82a89d25dd8d88aea14ebbb Mon Sep 17 00:00:00 2001 From: Tito Kipkurgat Date: Tue, 21 Apr 2026 12:18:22 +0300 Subject: [PATCH 1/3] feat: openresty-support --- deploy/inventory/hosts.template | 2 +- .../files/openresty/default.conf | 22 +++ .../files/openresty/static.conf | 20 +++ .../roles/create-instance/handlers/main.yml | 11 ++ .../create-instance/tasks/proxy/openresty.yml | 136 ++++++++++++++++ .../templates/openresty/default.j2 | 81 ++++++++++ .../templates/openresty/instance.j2 | 54 +++++++ .../templates/openresty/site.j2 | 151 ++++++++++++++++++ deploy/roles/proxy/handlers/main.yml | 12 ++ .../roles/proxy/tasks/openresty-install.yml | 105 ++++++++++++ deploy/roles/proxy/tasks/securing-munin.yml | 12 +- .../proxy/templates/openresty/grafana.j2 | 16 ++ .../roles/proxy/templates/openresty/munin.j2 | 20 +++ .../proxy/templates/openresty/nginx.conf.j2 | 30 ++++ .../proxy/templates/openresty/prometheus.j2 | 16 ++ 15 files changed, 683 insertions(+), 5 deletions(-) create mode 100644 deploy/roles/create-instance/files/openresty/default.conf create mode 100644 deploy/roles/create-instance/files/openresty/static.conf create mode 100644 deploy/roles/create-instance/tasks/proxy/openresty.yml create mode 100644 deploy/roles/create-instance/templates/openresty/default.j2 create mode 100644 deploy/roles/create-instance/templates/openresty/instance.j2 create mode 100644 deploy/roles/create-instance/templates/openresty/site.j2 create mode 100644 deploy/roles/proxy/tasks/openresty-install.yml create mode 100644 deploy/roles/proxy/templates/openresty/grafana.j2 create mode 100644 deploy/roles/proxy/templates/openresty/munin.j2 create mode 100644 deploy/roles/proxy/templates/openresty/nginx.conf.j2 create mode 100644 deploy/roles/proxy/templates/openresty/prometheus.j2 diff --git a/deploy/inventory/hosts.template b/deploy/inventory/hosts.template index f4d69b4..7444950 100644 --- a/deploy/inventory/hosts.template +++ b/deploy/inventory/hosts.template @@ -70,7 +70,7 @@ guest_os_arch=amd64 # lxd_storage_driver=dir -# Options: nginx, apache2 defaults to nginx +# Options: nginx, apache2, openresty defaults to nginx proxy=nginx # Enable proxy_protocol if behind a reverse proxy that sends proxy protocol headers diff --git a/deploy/roles/create-instance/files/openresty/default.conf b/deploy/roles/create-instance/files/openresty/default.conf new file mode 100644 index 0000000..eefefea --- /dev/null +++ b/deploy/roles/create-instance/files/openresty/default.conf @@ -0,0 +1,22 @@ +# +ssl_certificate /etc/ssl/selfsigned/selfsigned.crt; +ssl_certificate_key /etc/ssl/selfsigned/selfsigned.key; +ssl_protocols TLSv1.2 TLSv1.3; +#ssl_session_cache shared:SSL:20m; +ssl_session_timeout 10h; +ssl_dhparam /usr/local/openresty/nginx/conf/dhparams.pem; +ssl_prefer_server_ciphers on; +# This is quite strict. If you have much older windoze browsers +ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; +underscores_in_headers on; +# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +add_header Referrer-Policy "no-referrer"; +add_header X-XSS-Protection "1; mode=block" always; +keepalive_timeout 10; +send_timeout 10; +error_log /usr/local/openresty/nginx/logs/error.log info; +ssl_session_tickets off; +client_body_timeout 10; +client_header_timeout 10; +client_max_body_size 100M; +proxy_set_header X-Frame-Options "SAMEORIGIN"; diff --git a/deploy/roles/create-instance/files/openresty/static.conf b/deploy/roles/create-instance/files/openresty/static.conf new file mode 100644 index 0000000..11f9cf3 --- /dev/null +++ b/deploy/roles/create-instance/files/openresty/static.conf @@ -0,0 +1,20 @@ +# +ssl_protocols TLSv1.2 TLSv1.3; +#ssl_session_cache shared:SSL:20m; +ssl_session_timeout 10h; +ssl_dhparam /usr/local/openresty/nginx/conf/dhparams.pem; +ssl_prefer_server_ciphers on; +ssl_session_tickets off; + # This is quite strict. If you have much older windoze browsers +ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; +underscores_in_headers on; +add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; +add_header Referrer-Policy "no-referrer"; +add_header X-XSS-Protection "1; mode=block" always; +keepalive_timeout 10; # helps mitigate denial of service attacks that establish too many persistent connections, exhausting server resources. +send_timeout 10; # mitigate slow HTTP denial of service attacks by ensuring write operations taking up large amounts of time are closed. +error_log /usr/local/openresty/nginx/logs/error.log info; +client_body_timeout 10; +client_header_timeout 10; +client_max_body_size 100M; +proxy_set_header X-Frame-Options "SAMEORIGIN"; diff --git a/deploy/roles/create-instance/handlers/main.yml b/deploy/roles/create-instance/handlers/main.yml index 4ad6aae..e66c457 100644 --- a/deploy/roles/create-instance/handlers/main.yml +++ b/deploy/roles/create-instance/handlers/main.yml @@ -45,6 +45,17 @@ state: reloaded enabled: true +- name: Reload Openresty + ansible.builtin.service: + name: openresty + state: reloaded + enabled: true + +- name: Restart Openresty + ansible.builtin.service: + name: openresty + state: restarted + - name: Restart Munin-Node ansible.builtin.service: name: munin-node diff --git a/deploy/roles/create-instance/tasks/proxy/openresty.yml b/deploy/roles/create-instance/tasks/proxy/openresty.yml new file mode 100644 index 0000000..61ba4c2 --- /dev/null +++ b/deploy/roles/create-instance/tasks/proxy/openresty.yml @@ -0,0 +1,136 @@ +--- +# Get OpenResty/nginx version for conditional features (e.g., ssl_reject_handshake requires nginx 1.19.4+) +- name: Get OpenResty version + ansible.builtin.command: openresty -v + register: openresty_version_output + changed_when: false + failed_when: false + +- name: Set nginx version fact from OpenResty + ansible.builtin.set_fact: + # openresty -v outputs "nginx version: openresty/1.x.x" — match either prefix. + # | default([]) guards against regex_search returning None (no match) before | first. + nginx_version: "{{ openresty_version_output.stderr | default('') | regex_search('(?:nginx|openresty)/([0-9.]+)', '\\1') | default([]) | first | default('0') }}" + when: openresty_version_output.rc == 0 + +- name: Set nginx version to 0 if OpenResty not installed + ansible.builtin.set_fact: + nginx_version: "0" + when: openresty_version_output.rc != 0 + +- name: Assert two instances using the same fqdn have unique Instance Names + ansible.builtin.assert: + that: + - hostvars[item]['dhis2_base_path'] != hostvars[ansible_loop.nextitem]['dhis2_base_path'] + fail_msg: "Instances Names for {{ item }}: hostvars[item]['dhis2_base_path'] and {{ ansible_loop.nextitem }}: hostvars[item]['dhis2_base_path'] are the same" + loop: "{{ groups['instances'] }}" + loop_control: + extended: true + label: "{{ item }}" + when: + - ansible_loop.nextitem is defined + - hostvars[item]['dhis2_base_path'] is defined and hostvars[ansible_loop.nextitem]['dhis2_base_path'] is defined + - hostvars[item]['fqdn'] is defined and hostvars[ansible_loop.nextitem]['fqdn'] is defined + - hostvars[item]['fqdn'] == hostvars[ansible_loop.nextitem]['fqdn'] + +- name: Include TLS configuration tasks + ansible.builtin.include_tasks: "tls/{{ TLS_TYPE | default('letsencrypt') }}.yml" + loop: "{{ groups['instances'] }}" + when: + - (hostvars[item]['fqdn'] | default('') | string | trim | length > 0) + - hostvars[item]['instance_state'] is undefined + +- name: Instance location configuration + ansible.builtin.template: + src: openresty/instance.j2 + dest: /usr/local/openresty/nginx/conf/conf.d/upstream/{{ item | to_fixed_string }}.conf + owner: root + group: root + mode: "0640" + loop: "{{ groups['instances'] }}" + notify: Reload Openresty + when: + - hostvars[item]['instance_state'] is undefined + +- name: Include selfsigned TLS Certificate tasks, fqdn is not defined + ansible.builtin.include_tasks: ./tls/selfsigned.yml + loop: "{{ groups['instances'] }}" + when: + - not (hostvars[item]['fqdn'] | default('') | string | trim | length > 0) + - hostvars[item]['instance_state'] is undefined + +- name: Find files to delete in /usr/local/openresty/nginx/conf/conf.d/ + ansible.builtin.find: + paths: /usr/local/openresty/nginx/conf/conf.d/ + patterns: '*' + file_type: file + excludes: "{{ groups['instances'] | map('extract', hostvars, 'fqdn') | map('default', 'default', true) | map('regex_replace', '^\\s+|\\s+$', '') | unique | map('regex_replace', '^$', 'default') | map('regex_replace', '(.+)', '\\1.conf') | list }}" + register: found_files + +- name: Clean /usr/local/openresty/nginx/conf/conf.d/ directory + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: "{{ found_files['files'] | map(attribute='path') }}" + +- name: Copy static OpenResty config for valid FQDNs + ansible.builtin.copy: + src: openresty/static.conf + dest: /usr/local/openresty/nginx/conf/static/{{ hostvars[item]['fqdn'] | trim }}.conf + group: root + owner: root + force: false + mode: "0640" + loop: "{{ groups['instances'] }}" + when: + - (hostvars[item]['fqdn'] | default('') | string | trim | length > 0) + - hostvars[item]['instance_state'] is undefined + notify: Restart Openresty + +- name: Generate OpenResty config for instances with valid FQDNs + ansible.builtin.template: + src: openresty/site.j2 + dest: /usr/local/openresty/nginx/conf/conf.d/{{ hostvars[item]['fqdn'] | trim }}.conf + owner: root + group: root + mode: "0640" + loop: "{{ groups['instances'] }}" + when: + - (hostvars[item]['fqdn'] | default('') | string | trim | length > 0) + - hostvars[item]['instance_state'] is undefined + notify: Restart Openresty + +- name: Copy static OpenResty config for instances without FQDNs + ansible.builtin.copy: + src: openresty/default.conf + dest: /usr/local/openresty/nginx/conf/static/default.conf + owner: root + group: root + mode: "0640" + force: false + loop: "{{ groups['instances'] }}" + when: + - not (hostvars[item]['fqdn'] | default('') | string | trim | length > 0) + - hostvars[item]['instance_state'] is undefined + notify: Restart Openresty + +- name: Generate dynamic OpenResty config for instances without FQDN + ansible.builtin.template: + src: openresty/default.j2 + dest: /usr/local/openresty/nginx/conf/conf.d/default.conf + owner: root + group: root + mode: "0640" + loop: "{{ groups['instances'] }}" + when: + - not (hostvars[item]['fqdn'] | default('') | string | trim | length > 0) + - hostvars[item]['instance_state'] is undefined + notify: Restart Openresty + +- name: Start OpenResty service + ansible.builtin.service: + name: openresty + state: started + +- name: Flush Handlers + ansible.builtin.meta: flush_handlers diff --git a/deploy/roles/create-instance/templates/openresty/default.j2 b/deploy/roles/create-instance/templates/openresty/default.j2 new file mode 100644 index 0000000..16acd4c --- /dev/null +++ b/deploy/roles/create-instance/templates/openresty/default.j2 @@ -0,0 +1,81 @@ +# WARNING: This file is automatically generated by an Ansible template. +# Any custom modifications made directly to this file will be overwritten +# during future Ansible runs. + +# Please place any custom configurations in the following file +# /usr/local/openresty/nginx/conf/custom/default.conf + +# configurations in /usr/local/openresty/nginx/conf/static/default.conf are included in this openresty configuration. + + +# Let local monitoring agent access stats +server { + listen 127.0.0.1; + server_name localhost; + location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + deny all; + } +} + +# http block +server { + listen {{ http_port | default('80') }}{{ ' proxy_protocol' if use_proxy_protocol | default(false) and ansible_connection == 'lxd' else '' }}; + server_name _; + return 302 https://$host:{{ https_port | default("$server_port") }}$request_uri; +} + +# https block +server { + listen {{ https_port | default('443') }} ssl{{ ' proxy_protocol' if use_proxy_protocol | default(false) and ansible_connection == 'lxd' else '' }}; + http2 on; + server_name _; + include static/default.conf; +{% if use_proxy_protocol | default(false) and ansible_connection == 'lxd' %} + # Extract real client IP from proxy protocol header + set_real_ip_from {{ proxy_protocol_trusted_cidr | default(lxd_network | default('172.19.0.0/16')) }}; + real_ip_header proxy_protocol; +{% endif %} + + {% for instance in groups['instances'] + if (hostvars[instance]['instance_state'] is not defined) + and (hostvars[instance]['proxy_rewrite'] | default(false) in [true, 'true']) + and not (hostvars[instance]['fqdn'] | default('', true) | trim) and ('ROOT' not in groups['instances'] | + map('extract', hostvars, 'dhis2_base_path') | + map('default', 'None')) %} + {% if loop.first %} + # in case you want '/' access re-written to /{{ hostvars[instance]['dhis2_base_path'] | default(instance) | to_fixed_string }} + rewrite ^/$ /{{ hostvars[instance]['dhis2_base_path'] | default(instance) | to_fixed_string }}; + {% endif %} +{% endfor %} + +# location configs for dhis2 instances +{% for instance in groups['instances'] if (hostvars[instance]['instance_state'] is not defined) +and not (hostvars[instance]['fqdn'] | default('', true) | trim) %} + include conf.d/upstream/{{ instance | to_fixed_string }}.conf; +{%endfor%} + +{% set monitoring = (server_monitoring | default('') | lower | trim) %} +{% if monitoring == 'munin' %} + # Location config for Munin server backend + include conf.d/upstream/munin.conf; +{% elif monitoring in ['grafana', 'prometheus', 'grafana/prometheus'] %} + # Location config for Grafana/Prometheus backend + include conf.d/upstream/grafana.conf; +{% elif monitoring %} + # server_monitoring is defined but not recognized: {{ server_monitoring }} +{% else %} + # server_monitoring is not defined or empty +{% endif %} + +{% for instance in groups['instances'] if 'ROOT' not in groups['instances'] | + map('extract', hostvars, 'dhis2_base_path') | map('default', 'None') %} +{% if loop.first %} +location / { + return 444; + } +{%endif%} +{%endfor%} +} diff --git a/deploy/roles/create-instance/templates/openresty/instance.j2 b/deploy/roles/create-instance/templates/openresty/instance.j2 new file mode 100644 index 0000000..01eb12f --- /dev/null +++ b/deploy/roles/create-instance/templates/openresty/instance.j2 @@ -0,0 +1,54 @@ +{% if hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string == "ROOT" %} +location / { + proxy_pass http://{{hostvars[item]['ansible_host']+':8080' }}; + include /usr/local/openresty/nginx/conf/proxy_params; + proxy_redirect off; + proxy_set_header X-Frame-Options "SAMEORIGIN"; # protect against clickjacking attacks + proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks + proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Powered-By; + proxy_hide_header Server; + } +{% if app_monitoring is defined and app_monitoring | trim == 'glowroot' %} +{# glowroot location block configs #} +location /glowroot/ { + proxy_pass http://{{ hostvars[item]['ansible_host']+':4000' }}/glowroot/; + include /usr/local/openresty/nginx/conf/proxy_params; + proxy_redirect off; + proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks + proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks + proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Powered-By; + proxy_hide_header Server; + } +{% endif %} +{% else %} +location /{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}/ { + {# instance location block configs #} + proxy_pass http://{{hostvars[item]['ansible_host']+':8080' }}/{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}/; + include /usr/local/openresty/nginx/conf/proxy_params; + proxy_redirect off; + proxy_set_header X-Frame-Options "SAMEORIGIN"; # protect against clickjacking attacks + proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks + proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Powered-By; + proxy_hide_header Server; + } +{% if app_monitoring is defined and app_monitoring | trim == 'glowroot' %} +{# glowroot location block configs #} +location /{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}-glowroot { + proxy_pass http://{{ hostvars[item]['ansible_host']+':4000' }}/{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}-glowroot; + include /usr/local/openresty/nginx/conf/proxy_params; + proxy_redirect off; + proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks + proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks + proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Powered-By; + proxy_hide_header Server; + } +{% endif %} +{% endif %} diff --git a/deploy/roles/create-instance/templates/openresty/site.j2 b/deploy/roles/create-instance/templates/openresty/site.j2 new file mode 100644 index 0000000..7de3dad --- /dev/null +++ b/deploy/roles/create-instance/templates/openresty/site.j2 @@ -0,0 +1,151 @@ +# WARNING: This file is automatically generated by an Ansible template. +# Any custom modifications made directly to this file will be overwritten +# during future Ansible runs. + +# Please place any custom configurations in the following file +# /usr/local/openresty/nginx/conf/custom/{{ hostvars[item]['fqdn'] }}.conf + +# configurations within that file will be included in this openresty configuration. + +{% if groups['instances'] | all_have_fqdn(hostvars) %} +# SNI protection - prevents certificate/domain leakage via direct IP access +# Only deployed when all instances have fqdn defined +server { + listen 80{{ ' proxy_protocol' if use_proxy_protocol | default(false) and ansible_connection == 'lxd' else '' }} default_server; + server_name _; + return 444; +} + +server { + listen 443 ssl{{ ' proxy_protocol' if use_proxy_protocol | default(false) and ansible_connection == 'lxd' else '' }} default_server; + http2 on; + server_name _; +{% if nginx_version is defined and nginx_version is version('1.19.4', '>=') %} + ssl_reject_handshake on; +{% else %} + # ssl_reject_handshake requires nginx >= 1.19.4 (current: {{ nginx_version | default('unknown') }}) + # Fallback: return 444 with self-signed cert + ssl_certificate /etc/ssl/selfsigned/selfsigned.crt; + ssl_certificate_key /etc/ssl/selfsigned/selfsigned.key; + return 444; +{% endif %} +} +{% endif %} + +# Let local monitoring agent access stats +server { + listen 127.0.0.1; + server_name localhost; + location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + deny all; + } +} + +# http block +server { + {%if TLS_TYPE is undefined or TLS_TYPE == 'letsencrypt' -%} + listen 80; + server_name {{ hostvars[item]['fqdn'] | trim }}; + return 302 https://$host:{{ https_port | default("$server_port") }}$request_uri; + {% else %} + listen {{ http_port | default('80') }}; + server_name {{ hostvars[item]['fqdn'] | trim }}; + return 302 https://$host:{{ https_port | default("$server_port") }}$request_uri; + {%endif-%} +} + +# https block +server { + {%if TLS_TYPE is undefined or TLS_TYPE == 'letsencrypt' -%} + listen 443 ssl; + http2 on; + server_name {{ hostvars[item]['fqdn'] | trim }}; + # ssl settings + ssl_certificate /etc/letsencrypt/live/{{ hostvars[item]['fqdn'] | trim }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ hostvars[item]['fqdn'] | trim }}/privkey.pem; + {%elif TLS_TYPE is defined and TLS_TYPE == 'customssl' -%} + listen {{ https_port | default('443') }} ssl; + http2 on; + server_name {{ hostvars[item]['fqdn'] | trim }}; + # ssl settings + ssl_certificate /etc/ssl/{{ hostvars[item]['fqdn'] | trim }}/customssl.crt; + ssl_certificate_key /etc/ssl/{{ hostvars[item]['fqdn'] | trim }}/customssl.key; + {%elif TLS_TYPE is defined and TLS_TYPE == 'selfsigned' -%} + listen {{ https_port | default('443') }} ssl; + http2 on; + server_name {{ hostvars[item]['fqdn'] | trim }}; + # ssl settings + ssl_certificate /etc/ssl/selfsigned/selfsigned.crt; + ssl_certificate_key /etc/ssl/selfsigned/selfsigned.key; + {%endif-%} +{% if TLS_TYPE is defined and TLS_TYPE == 'customssl' %} + # ssl_stapling only for customssl (Let's Encrypt phased out OCSP in 2024) + ssl_stapling on; + ssl_stapling_verify on; +{% endif %} +{% if use_proxy_protocol | default(false) and ansible_connection == 'lxd' %} + + # Extract real client IP from proxy protocol header + set_real_ip_from {{ proxy_protocol_trusted_cidr | default(lxd_network | default('172.19.0.0/16')) }}; + real_ip_header proxy_protocol; +{% endif %} + + # please write your custom changes to the static/{{ hostvars[item]['fqdn'] }}.conf; and + # will be kept across subsequent ansible runs +{% for instance in groups['instances'] + if (hostvars[instance]['fqdn'] | default('') | trim) == (hostvars[item]['fqdn'] | default('') | trim) + and (hostvars[instance]['fqdn'] | default('') | trim) and (hostvars[instance]['instance_state'] is undefined) %} +{% if loop.first %} + include static/{{ hostvars[instance]['fqdn'] | trim }}.conf; +{% endif %} +{% endfor %} + + # rewrite rule +{% for instance in groups['instances'] + if (hostvars[instance]['fqdn'] | default('') | trim == hostvars[item]['fqdn'] | trim) + and (hostvars[instance]['instance_state'] is not defined) + and (hostvars[instance]['dhis2_base_path'] | default('') != 'ROOT') + and (hostvars[instance]['proxy_rewrite'] | default(false) in [true, 'true']) %} + {% if loop.first %} + rewrite ^/$ /{{ hostvars[instance]['dhis2_base_path'] | default(instance) | to_fixed_string }}; + {% endif %} +{% endfor %} + + # Location configuration for the instances +{% for instance in groups['instances'] + if (hostvars[instance]['fqdn'] | default('') | trim == hostvars[item]['fqdn'] | trim) + and (hostvars[instance]['instance_state'] is undefined) %} + include conf.d/upstream/{{ instance | to_fixed_string }}.conf; +{% endfor %} + +{% set monitoring = (server_monitoring | default('') | lower | trim) %} + # Location configs for server monitoring +{% if monitoring == 'munin' %} + include conf.d/upstream/munin.conf; +{% elif monitoring in ['grafana', 'prometheus', 'grafana/prometheus'] %} + include conf.d/upstream/grafana.conf; +{% elif monitoring %} + # server_monitoring is set but not recognized: {{ server_monitoring }} +{% else %} + # server_monitoring is not defined or empty +{% endif %} + # Return 444 if nothing is served on '/' + {% for instance in groups['instances'] + if (hostvars[instance]['fqdn'] | default('') | trim == hostvars[item]['fqdn'] | trim) + and ('ROOT' not in groups['instances'] + | map('extract', hostvars, 'dhis2_base_path') + | map('default', 'None')) %} +{% if loop.first %} + location / { + return 444; + } +{% endif %} +{% endfor %} + +if ($host != "{{ hostvars[item]['fqdn'] | trim }}") { + return 444; + } +} diff --git a/deploy/roles/proxy/handlers/main.yml b/deploy/roles/proxy/handlers/main.yml index c0d6f93..ed1583d 100644 --- a/deploy/roles/proxy/handlers/main.yml +++ b/deploy/roles/proxy/handlers/main.yml @@ -17,3 +17,15 @@ name: apache2 state: reloaded enabled: true + +- name: Reload Openresty + ansible.builtin.service: + name: openresty + state: reloaded + enabled: true + +- name: Restart Openresty + ansible.builtin.service: + name: openresty + state: restarted + enabled: true diff --git a/deploy/roles/proxy/tasks/openresty-install.yml b/deploy/roles/proxy/tasks/openresty-install.yml new file mode 100644 index 0000000..515371a --- /dev/null +++ b/deploy/roles/proxy/tasks/openresty-install.yml @@ -0,0 +1,105 @@ +--- +- name: Open http port {{ http_port | default('80') }} + community.general.ufw: + rule: allow + port: "{{ http_port | default('80') }}" + proto: tcp + comment: http + state: enabled + +- name: Open https port {{ https_port | default('443') }} + community.general.ufw: + rule: allow + port: "{{ https_port | default('443') }}" + proto: tcp + comment: https + +- name: Stop and disable other proxy services + ansible.builtin.include_tasks: stop-other-proxies.yml + +- name: Updating and Upgrading + ansible.builtin.apt: + upgrade: "yes" + update_cache: true + cache_valid_time: 3600 + +- name: Install prerequisites for OpenResty repo + ansible.builtin.apt: + name: + - curl + - gnupg2 + - ca-certificates + - lsb-release + state: present + +- name: Add OpenResty signing key + ansible.builtin.shell: + cmd: curl -fsSL https://openresty.org/package/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/openresty-archive-keyring.gpg + creates: /usr/share/keyrings/openresty-archive-keyring.gpg + +- name: Add OpenResty official repository + ansible.builtin.apt_repository: + repo: "deb [signed-by=/usr/share/keyrings/openresty-archive-keyring.gpg] https://openresty.org/package/ubuntu {{ ansible_distribution_release }} main" + state: present + filename: openresty + +- name: Installing OpenResty + ansible.builtin.apt: + name: + - openresty + - libwww-perl # required for munin monitoring + state: latest + update_cache: true + +- name: Creating OpenResty config directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0750" + loop: + - '/usr/local/openresty/nginx/conf/conf.d/upstream' + - '/usr/local/openresty/nginx/conf/static' + +- name: Deploy main nginx.conf + ansible.builtin.template: + src: openresty/nginx.conf.j2 + dest: /usr/local/openresty/nginx/conf/nginx.conf + owner: root + group: root + mode: "0644" + notify: Reload Openresty + +- name: Create proxy_params file + ansible.builtin.copy: + dest: /usr/local/openresty/nginx/conf/proxy_params + content: | + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + mode: "0644" + force: false + +- name: Generating Diffie-Hellman key parameters, takes a while, you may grab some coffee + ansible.builtin.command: openssl dhparam -out /usr/local/openresty/nginx/conf/dhparams.pem 2048 + args: + creates: /usr/local/openresty/nginx/conf/dhparams.pem + notify: Reload Openresty + +- name: Location configs {{ server_monitoring }} + ansible.builtin.template: + src: openresty/{{ server_monitoring | trim }}.j2 + dest: /usr/local/openresty/nginx/conf/conf.d/upstream/{{ server_monitoring }}.conf + owner: root + group: root + mode: "0640" + loop: "{{ groups['monitoring'] }}" + notify: Reload Openresty + when: + - groups['monitoring'] | length > 0 + - server_monitoring is defined + +- name: Flushing handlers + ansible.builtin.meta: flush_handlers diff --git a/deploy/roles/proxy/tasks/securing-munin.yml b/deploy/roles/proxy/tasks/securing-munin.yml index 8b26a1d..f2847a6 100644 --- a/deploy/roles/proxy/tasks/securing-munin.yml +++ b/deploy/roles/proxy/tasks/securing-munin.yml @@ -6,10 +6,14 @@ - name: Detect proxy worker user from config ansible.builtin.shell: | - grep -E '^\s*user\s+' /etc/{{ proxy }}/{{ proxy }}.conf | awk '{print $2}' | tr -d ';' | head -1 + # OpenResty uses nginx.conf as its main config file + conf_file="/etc/{{ proxy }}/{{ proxy }}.conf" + if [ ! -f "$conf_file" ]; then + conf_file="/usr/local/openresty/nginx/conf/nginx.conf" + fi + grep -E '^\s*user\s+' "$conf_file" | awk '{print $2}' | tr -d ';' | head -1 register: _proxy_worker_user - changed_when: false - failed_when: false + - name: Set proxy_user fact ansible.builtin.set_fact: @@ -17,7 +21,7 @@ - name: "Munin authenticatin, defaults: Username: admin, password: district" community.general.htpasswd: - path: /etc/{{ proxy }}/.htpasswd + path: "{{ '/usr/local/openresty/nginx/conf/.htpasswd' if proxy == 'openresty' else '/etc/' + proxy + '/.htpasswd' }}" name: "{{ item.name }}" password: "{{ item.password }}" owner: root diff --git a/deploy/roles/proxy/templates/openresty/grafana.j2 b/deploy/roles/proxy/templates/openresty/grafana.j2 new file mode 100644 index 0000000..4b6bf3c --- /dev/null +++ b/deploy/roles/proxy/templates/openresty/grafana.j2 @@ -0,0 +1,16 @@ +{% if hostvars['proxy']['grafana_base_path'] is defined and hostvars['proxy']['grafana_base_path'] is not none and hostvars['proxy']['grafana_base_path'] | trim != '' %} +location /{{ grafana_base_path }} { + proxy_pass http://{{hostvars[item]['ansible_host']+':3000' }}/{{ grafana_base_path }}; +{% else %} +location /{{ server_monitoring }} { + proxy_pass http://{{hostvars[item]['ansible_host']+':3000' }}/{{ server_monitoring }}; +{% endif %} + include /usr/local/openresty/nginx/conf/proxy_params; + proxy_redirect off; + proxy_hide_header X-Powered-By; + proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks + proxy_set_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks + proxy_set_header X-Xss-Protection "1; mode=block"; #To leverage browser-based protections against cross-site scripting + proxy_hide_header Server; +} diff --git a/deploy/roles/proxy/templates/openresty/munin.j2 b/deploy/roles/proxy/templates/openresty/munin.j2 new file mode 100644 index 0000000..398b6e0 --- /dev/null +++ b/deploy/roles/proxy/templates/openresty/munin.j2 @@ -0,0 +1,20 @@ +{% for host in groups['monitoring'] %} +{% if hostvars['proxy']['munin_base_path'] is defined and hostvars['proxy']['munin_base_path'] is not none and hostvars['proxy']['munin_base_path'] | trim != '' %} +location /{{ munin_base_path }} { + proxy_pass http://{{hostvars[host]['ansible_host']+':80' }}/{{ munin_base_path }}; +{% else %} +location /{{ server_monitoring }} { + proxy_pass http://{{hostvars[host]['ansible_host']+':80' }}/{{ server_monitoring }}; +{% endif %} + include /usr/local/openresty/nginx/conf/proxy_params; + proxy_redirect off; + proxy_hide_header X-Powered-By; + proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks + proxy_set_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks + proxy_set_header X-Xss-Protection "1; mode=block"; #To leverage browser-based protections against cross-site scripting + proxy_hide_header Server; + auth_basic "Restricted Content"; + auth_basic_user_file /usr/local/openresty/nginx/conf/.htpasswd; +} +{% endfor %} diff --git a/deploy/roles/proxy/templates/openresty/nginx.conf.j2 b/deploy/roles/proxy/templates/openresty/nginx.conf.j2 new file mode 100644 index 0000000..1b9f2ec --- /dev/null +++ b/deploy/roles/proxy/templates/openresty/nginx.conf.j2 @@ -0,0 +1,30 @@ +user www-data; +worker_processes auto; + +error_log /usr/local/openresty/nginx/logs/error.log notice; +pid /usr/local/openresty/nginx/logs/nginx.pid; + +events { + worker_connections 1024; +} + +http { + server_tokens off; + include mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + log_format performance '$remote_addr\t$remote_user\t[$time_local]\t"$request"\t' + '$status\t$body_bytes_sent\t"$http_referer"\t"$http_user_agent"\t' + '$request_time'; + + access_log /usr/local/openresty/nginx/logs/perf.log performance; + + sendfile on; + keepalive_timeout 65; + + include /usr/local/openresty/nginx/conf/conf.d/*.conf; +} diff --git a/deploy/roles/proxy/templates/openresty/prometheus.j2 b/deploy/roles/proxy/templates/openresty/prometheus.j2 new file mode 100644 index 0000000..4b6bf3c --- /dev/null +++ b/deploy/roles/proxy/templates/openresty/prometheus.j2 @@ -0,0 +1,16 @@ +{% if hostvars['proxy']['grafana_base_path'] is defined and hostvars['proxy']['grafana_base_path'] is not none and hostvars['proxy']['grafana_base_path'] | trim != '' %} +location /{{ grafana_base_path }} { + proxy_pass http://{{hostvars[item]['ansible_host']+':3000' }}/{{ grafana_base_path }}; +{% else %} +location /{{ server_monitoring }} { + proxy_pass http://{{hostvars[item]['ansible_host']+':3000' }}/{{ server_monitoring }}; +{% endif %} + include /usr/local/openresty/nginx/conf/proxy_params; + proxy_redirect off; + proxy_hide_header X-Powered-By; + proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks + proxy_set_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks + proxy_set_header X-Xss-Protection "1; mode=block"; #To leverage browser-based protections against cross-site scripting + proxy_hide_header Server; +} From 431b92ab39f00b30a82afe88a887218ef28650df Mon Sep 17 00:00:00 2001 From: Tito Kipkurgat Date: Tue, 28 Apr 2026 11:43:57 +0300 Subject: [PATCH 2/3] chore: openresty and nginx optimizations chore: Openresty optimization fix: nginx and openresty optimization doc: fix documentation on adding an instance chore: refactored security headers to a separate file fix: deploy custom server.xml on every run fix: make proxy worker user detection idempotent --- README.md | 2 +- .../create-instance/files/nginx/default.conf | 12 ++-- .../create-instance/files/nginx/static.conf | 12 ++-- .../files/openresty/default.conf | 12 ++-- .../files/openresty/static.conf | 12 ++-- .../create-instance/tasks/tomcat-setup.yml | 1 - .../templates/nginx/default.j2 | 17 +++++- .../templates/nginx/instance.j2 | 18 +++--- .../create-instance/templates/nginx/site.j2 | 28 +++++++--- .../templates/openresty/default.j2 | 12 ++++ .../templates/openresty/instance.j2 | 16 ++---- .../templates/openresty/site.j2 | 16 +++++- .../roles/proxy/files/security_headers.conf | 12 ++++ deploy/roles/proxy/tasks/nginx-install.yml | 55 ++++++++++++++++++- .../roles/proxy/tasks/openresty-install.yml | 18 +++++- deploy/roles/proxy/tasks/securing-munin.yml | 2 + deploy/roles/proxy/templates/nginx/grafana.j2 | 5 +- deploy/roles/proxy/templates/nginx/munin.j2 | 5 +- .../proxy/templates/openresty/grafana.j2 | 5 +- .../roles/proxy/templates/openresty/munin.j2 | 5 +- .../proxy/templates/openresty/nginx.conf.j2 | 20 +++++++ .../proxy/templates/openresty/prometheus.j2 | 5 +- 22 files changed, 205 insertions(+), 85 deletions(-) create mode 100644 deploy/roles/proxy/files/security_headers.conf diff --git a/README.md b/README.md index 1b74490..c3f9859 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ NOTE: - You will need to have two files, named `customssl.crt` and `customssl.key`.
`customssl.crt` should contain the main certificate concatenated with intermediate and root certificates. -- Copy these two files into `dhis2-server-tools/deploy/roles/proxy/files/` directory, preserving their names. +- Copy these two files into `dhis2-server-tools/deploy/roles/create-instance/files/` directory, preserving their names. - Edit hosts file and set `TLS_TYPE=customssl` ``` vim dhis2-server-tools/deploy/inventory/hosts diff --git a/deploy/roles/create-instance/files/nginx/default.conf b/deploy/roles/create-instance/files/nginx/default.conf index 5faa3b4..c4baa3e 100644 --- a/deploy/roles/create-instance/files/nginx/default.conf +++ b/deploy/roles/create-instance/files/nginx/default.conf @@ -2,7 +2,6 @@ ssl_certificate /etc/ssl/selfsigned/selfsigned.crt; ssl_certificate_key /etc/ssl/selfsigned/selfsigned.key; ssl_protocols TLSv1.2 TLSv1.3; -#ssl_session_cache shared:SSL:20m; ssl_session_timeout 10h; ssl_dhparam /etc/nginx/dhparams.pem; ssl_prefer_server_ciphers on; @@ -11,12 +10,11 @@ ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; underscores_in_headers on; # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Referrer-Policy "no-referrer"; -add_header X-XSS-Protection "1; mode=block" always; -keepalive_timeout 10; -send_timeout 10; +keepalive_timeout 65; +send_timeout 60s; error_log /var/log/nginx/error.log info; ssl_session_tickets off; -client_body_timeout 10; -client_header_timeout 10; +client_body_timeout 60s; +client_header_timeout 30s; client_max_body_size 100M; -proxy_set_header X-Frame-Options "SAMEORIGIN"; +add_header X-Frame-Options "SAMEORIGIN" always; diff --git a/deploy/roles/create-instance/files/nginx/static.conf b/deploy/roles/create-instance/files/nginx/static.conf index f074019..df60c36 100644 --- a/deploy/roles/create-instance/files/nginx/static.conf +++ b/deploy/roles/create-instance/files/nginx/static.conf @@ -1,6 +1,5 @@ # ssl_protocols TLSv1.2 TLSv1.3; -#ssl_session_cache shared:SSL:20m; ssl_session_timeout 10h; ssl_dhparam /etc/nginx/dhparams.pem; ssl_prefer_server_ciphers on; @@ -10,11 +9,10 @@ ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; underscores_in_headers on; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Referrer-Policy "no-referrer"; -add_header X-XSS-Protection "1; mode=block" always; -keepalive_timeout 10; # helps mitigate denial of service attacks that establish too many persistent connections, exhausting server resources. -send_timeout 10; # mitigate slow HTTP denial of service attacks by ensuring write operations taking up large amounts of time are closed. +keepalive_timeout 65; +send_timeout 60s; error_log /var/log/nginx/error.log info; -client_body_timeout 10; -client_header_timeout 10; +client_body_timeout 60s; +client_header_timeout 30s; client_max_body_size 100M; -proxy_set_header X-Frame-Options "SAMEORIGIN"; +add_header X-Frame-Options "SAMEORIGIN" always; diff --git a/deploy/roles/create-instance/files/openresty/default.conf b/deploy/roles/create-instance/files/openresty/default.conf index eefefea..e8cc538 100644 --- a/deploy/roles/create-instance/files/openresty/default.conf +++ b/deploy/roles/create-instance/files/openresty/default.conf @@ -2,7 +2,6 @@ ssl_certificate /etc/ssl/selfsigned/selfsigned.crt; ssl_certificate_key /etc/ssl/selfsigned/selfsigned.key; ssl_protocols TLSv1.2 TLSv1.3; -#ssl_session_cache shared:SSL:20m; ssl_session_timeout 10h; ssl_dhparam /usr/local/openresty/nginx/conf/dhparams.pem; ssl_prefer_server_ciphers on; @@ -11,12 +10,11 @@ ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; underscores_in_headers on; # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Referrer-Policy "no-referrer"; -add_header X-XSS-Protection "1; mode=block" always; -keepalive_timeout 10; -send_timeout 10; +keepalive_timeout 65; +send_timeout 60s; error_log /usr/local/openresty/nginx/logs/error.log info; ssl_session_tickets off; -client_body_timeout 10; -client_header_timeout 10; +client_body_timeout 60s; +client_header_timeout 30s; client_max_body_size 100M; -proxy_set_header X-Frame-Options "SAMEORIGIN"; +add_header X-Frame-Options "SAMEORIGIN" always; diff --git a/deploy/roles/create-instance/files/openresty/static.conf b/deploy/roles/create-instance/files/openresty/static.conf index 11f9cf3..8753a37 100644 --- a/deploy/roles/create-instance/files/openresty/static.conf +++ b/deploy/roles/create-instance/files/openresty/static.conf @@ -1,6 +1,5 @@ # ssl_protocols TLSv1.2 TLSv1.3; -#ssl_session_cache shared:SSL:20m; ssl_session_timeout 10h; ssl_dhparam /usr/local/openresty/nginx/conf/dhparams.pem; ssl_prefer_server_ciphers on; @@ -10,11 +9,10 @@ ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; underscores_in_headers on; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Referrer-Policy "no-referrer"; -add_header X-XSS-Protection "1; mode=block" always; -keepalive_timeout 10; # helps mitigate denial of service attacks that establish too many persistent connections, exhausting server resources. -send_timeout 10; # mitigate slow HTTP denial of service attacks by ensuring write operations taking up large amounts of time are closed. +keepalive_timeout 65; +send_timeout 60s; error_log /usr/local/openresty/nginx/logs/error.log info; -client_body_timeout 10; -client_header_timeout 10; +client_body_timeout 60s; +client_header_timeout 30s; client_max_body_size 100M; -proxy_set_header X-Frame-Options "SAMEORIGIN"; +add_header X-Frame-Options "SAMEORIGIN" always; diff --git a/deploy/roles/create-instance/tasks/tomcat-setup.yml b/deploy/roles/create-instance/tasks/tomcat-setup.yml index dd970ae..4dfc240 100644 --- a/deploy/roles/create-instance/tasks/tomcat-setup.yml +++ b/deploy/roles/create-instance/tasks/tomcat-setup.yml @@ -57,7 +57,6 @@ mode: "0640" owner: root group: tomcat - force: false notify: Restart Tomcat - name: Create tomcat service directory diff --git a/deploy/roles/create-instance/templates/nginx/default.j2 b/deploy/roles/create-instance/templates/nginx/default.j2 index 1ea5aa6..c4b56da 100644 --- a/deploy/roles/create-instance/templates/nginx/default.j2 +++ b/deploy/roles/create-instance/templates/nginx/default.j2 @@ -3,10 +3,21 @@ # during future Ansible runs. # Please place any custom configurations in the following file -# /etc/nginx/custom/default.conf +# /etc/nginx/custom/default.conf # configurations in /etc/nginx/static/default.conf are be included in this nginx configuration. +# Tomcat upstreams with keepalive — for instances without an FQDN +{% for instance in groups['instances'] + if (hostvars[instance]['instance_state'] is not defined) + and not (hostvars[instance]['fqdn'] | default('', true) | trim) %} +upstream {{ instance | replace('-', '_') }}_tomcat { + server {{ hostvars[instance]['ansible_host'] }}:8080; + keepalive {{ tomcat_keepalive | default(32) }}; + keepalive_requests {{ tomcat_keepalive_requests | default(1000) }}; + keepalive_timeout {{ tomcat_keepalive_timeout | default('60s') }}; +} +{% endfor %} # Let local monitoring agent access stats server { @@ -29,8 +40,10 @@ server { # https block server { - listen {{ https_port | default('443') }} ssl http2{{ ' proxy_protocol' if use_proxy_protocol | default(false) and ansible_connection == 'lxd' else '' }}; + listen {{ https_port | default('443') }} ssl{{ ' proxy_protocol' if use_proxy_protocol | default(false) and ansible_connection == 'lxd' else '' }}; + http2 on; server_name _; + ssl_session_cache shared:SSL:20m; include static/default.conf; {% if use_proxy_protocol | default(false) and ansible_connection == 'lxd' %} # Extract real client IP from proxy protocol header diff --git a/deploy/roles/create-instance/templates/nginx/instance.j2 b/deploy/roles/create-instance/templates/nginx/instance.j2 index e06e979..38e95ea 100644 --- a/deploy/roles/create-instance/templates/nginx/instance.j2 +++ b/deploy/roles/create-instance/templates/nginx/instance.j2 @@ -1,14 +1,13 @@ {% if hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string == "ROOT" %} location / { - proxy_pass http://{{hostvars[item]['ansible_host']+':8080' }}; + proxy_pass http://{{ item | replace('-', '_') }}_tomcat; include /etc/nginx/proxy_params; proxy_redirect off; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # protect against clickjacking attacks - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; proxy_hide_header Strict-Transport-Security; proxy_hide_header X-Powered-By; proxy_hide_header Server; + include /etc/nginx/security_headers.conf; } {% if app_monitoring is defined and app_monitoring | trim == 'glowroot' %} {# glowroot location block configs #} @@ -16,26 +15,24 @@ location /glowroot/ { proxy_pass http://{{ hostvars[item]['ansible_host']+':4000' }}/glowroot/; include /etc/nginx/proxy_params; proxy_redirect off; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; proxy_hide_header Strict-Transport-Security; proxy_hide_header X-Powered-By; proxy_hide_header Server; + include /etc/nginx/security_headers.conf; } {% endif %} {% else %} location /{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}/ { - {# instance location block configs #} - proxy_pass http://{{hostvars[item]['ansible_host']+':8080' }}/{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}/; + {# instance location block configs #} + proxy_pass http://{{ item | replace('-', '_') }}_tomcat/{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}/; include /etc/nginx/proxy_params; proxy_redirect off; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # protect against clickjacking attacks - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; proxy_hide_header Strict-Transport-Security; proxy_hide_header X-Powered-By; proxy_hide_header Server; + include /etc/nginx/security_headers.conf; } {% if app_monitoring is defined and app_monitoring | trim == 'glowroot' %} {# glowroot location block configs #} @@ -43,12 +40,11 @@ location /{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string proxy_pass http://{{ hostvars[item]['ansible_host']+':4000' }}/{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}-glowroot; include /etc/nginx/proxy_params; proxy_redirect off; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; proxy_hide_header Strict-Transport-Security; proxy_hide_header X-Powered-By; proxy_hide_header Server; + include /etc/nginx/security_headers.conf; } {% endif %} {% endif %} diff --git a/deploy/roles/create-instance/templates/nginx/site.j2 b/deploy/roles/create-instance/templates/nginx/site.j2 index e85d74c..877f01a 100644 --- a/deploy/roles/create-instance/templates/nginx/site.j2 +++ b/deploy/roles/create-instance/templates/nginx/site.j2 @@ -7,6 +7,18 @@ # configurations within that file will be included in this nginx configuration. +# Tomcat upstreams with keepalive — one per instance under this FQDN +{% for instance in groups['instances'] + if (hostvars[instance]['fqdn'] | default('') | trim == hostvars[item]['fqdn'] | trim) + and (hostvars[instance]['instance_state'] is undefined) %} +upstream {{ instance | replace('-', '_') }}_tomcat { + server {{ hostvars[instance]['ansible_host'] }}:8080; + keepalive {{ tomcat_keepalive | default(32) }}; + keepalive_requests {{ tomcat_keepalive_requests | default(1000) }}; + keepalive_timeout {{ tomcat_keepalive_timeout | default('60s') }}; +} +{% endfor %} + {% if groups['instances'] | all_have_fqdn(hostvars) %} # SNI protection - prevents certificate/domain leakage via direct IP access # Only deployed when all instances have fqdn defined @@ -17,7 +29,8 @@ server { } server { - listen 443 ssl http2{{ ' proxy_protocol' if use_proxy_protocol | default(false) and ansible_connection == 'lxd' else '' }} default_server; + listen 443 ssl{{ ' proxy_protocol' if use_proxy_protocol | default(false) and ansible_connection == 'lxd' else '' }} default_server; + http2 on; server_name _; {% if nginx_version is defined and nginx_version is version('1.19.4', '>=') %} ssl_reject_handshake on; @@ -59,19 +72,22 @@ server { # https block server { {%if TLS_TYPE is undefined or TLS_TYPE == 'letsencrypt' -%} - listen 443 ssl http2; + listen 443 ssl; + http2 on; server_name {{ hostvars[item]['fqdn'] | trim }}; # ssl settings ssl_certificate /etc/letsencrypt/live/{{ hostvars[item]['fqdn'] | trim }}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/{{ hostvars[item]['fqdn'] | trim }}/privkey.pem; {%elif TLS_TYPE is defined and TLS_TYPE == 'customssl' -%} - listen {{ https_port | default('443') }} ssl http2; + listen {{ https_port | default('443') }} ssl; + http2 on; server_name {{ hostvars[item]['fqdn'] | trim }}; # ssl settings ssl_certificate /etc/ssl/{{ hostvars[item]['fqdn'] | trim }}/customssl.crt; ssl_certificate_key /etc/ssl/{{ hostvars[item]['fqdn'] | trim }}/customssl.key; {%elif TLS_TYPE is defined and TLS_TYPE == 'selfsigned' -%} - listen {{ https_port | default('443') }} ssl http2; + listen {{ https_port | default('443') }} ssl; + http2 on; server_name {{ hostvars[item]['fqdn'] | trim }}; # ssl settings ssl_certificate /etc/ssl/selfsigned/selfsigned.crt; @@ -82,6 +98,7 @@ server { ssl_stapling on; ssl_stapling_verify on; {% endif %} + ssl_session_cache shared:SSL:20m; {% if use_proxy_protocol | default(false) and ansible_connection == 'lxd' %} # Extract real client IP from proxy protocol header @@ -141,7 +158,4 @@ server { {% endif %} {% endfor %} -if ($host != "{{ hostvars[item]['fqdn'] | trim }}") { - return 444; - } } diff --git a/deploy/roles/create-instance/templates/openresty/default.j2 b/deploy/roles/create-instance/templates/openresty/default.j2 index 16acd4c..1c89bd0 100644 --- a/deploy/roles/create-instance/templates/openresty/default.j2 +++ b/deploy/roles/create-instance/templates/openresty/default.j2 @@ -7,6 +7,17 @@ # configurations in /usr/local/openresty/nginx/conf/static/default.conf are included in this openresty configuration. +# Tomcat upstreams with keepalive — for instances without an FQDN +{% for instance in groups['instances'] + if (hostvars[instance]['instance_state'] is not defined) + and not (hostvars[instance]['fqdn'] | default('', true) | trim) %} +upstream {{ instance | replace('-', '_') }}_tomcat { + server {{ hostvars[instance]['ansible_host'] }}:8080; + keepalive {{ tomcat_keepalive | default(32) }}; + keepalive_requests {{ tomcat_keepalive_requests | default(1000) }}; + keepalive_timeout {{ tomcat_keepalive_timeout | default('60s') }}; +} +{% endfor %} # Let local monitoring agent access stats server { @@ -32,6 +43,7 @@ server { listen {{ https_port | default('443') }} ssl{{ ' proxy_protocol' if use_proxy_protocol | default(false) and ansible_connection == 'lxd' else '' }}; http2 on; server_name _; + ssl_session_cache shared:SSL:20m; include static/default.conf; {% if use_proxy_protocol | default(false) and ansible_connection == 'lxd' %} # Extract real client IP from proxy protocol header diff --git a/deploy/roles/create-instance/templates/openresty/instance.j2 b/deploy/roles/create-instance/templates/openresty/instance.j2 index 01eb12f..542e047 100644 --- a/deploy/roles/create-instance/templates/openresty/instance.j2 +++ b/deploy/roles/create-instance/templates/openresty/instance.j2 @@ -1,14 +1,13 @@ {% if hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string == "ROOT" %} location / { - proxy_pass http://{{hostvars[item]['ansible_host']+':8080' }}; + proxy_pass http://{{ item | replace('-', '_') }}_tomcat; include /usr/local/openresty/nginx/conf/proxy_params; proxy_redirect off; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # protect against clickjacking attacks - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; proxy_hide_header Strict-Transport-Security; proxy_hide_header X-Powered-By; proxy_hide_header Server; + include /usr/local/openresty/nginx/conf/security_headers.conf; } {% if app_monitoring is defined and app_monitoring | trim == 'glowroot' %} {# glowroot location block configs #} @@ -16,26 +15,24 @@ location /glowroot/ { proxy_pass http://{{ hostvars[item]['ansible_host']+':4000' }}/glowroot/; include /usr/local/openresty/nginx/conf/proxy_params; proxy_redirect off; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; proxy_hide_header Strict-Transport-Security; proxy_hide_header X-Powered-By; proxy_hide_header Server; + include /usr/local/openresty/nginx/conf/security_headers.conf; } {% endif %} {% else %} location /{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}/ { {# instance location block configs #} - proxy_pass http://{{hostvars[item]['ansible_host']+':8080' }}/{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}/; + proxy_pass http://{{ item | replace('-', '_') }}_tomcat/{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}/; include /usr/local/openresty/nginx/conf/proxy_params; proxy_redirect off; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # protect against clickjacking attacks - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; proxy_hide_header Strict-Transport-Security; proxy_hide_header X-Powered-By; proxy_hide_header Server; + include /usr/local/openresty/nginx/conf/security_headers.conf; } {% if app_monitoring is defined and app_monitoring | trim == 'glowroot' %} {# glowroot location block configs #} @@ -43,12 +40,11 @@ location /{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string proxy_pass http://{{ hostvars[item]['ansible_host']+':4000' }}/{{ hostvars[item]['dhis2_base_path'] | default(item) | to_fixed_string }}-glowroot; include /usr/local/openresty/nginx/conf/proxy_params; proxy_redirect off; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks proxy_set_header X-Forwarded-Port {{ https_port | default('443') }}; proxy_hide_header Strict-Transport-Security; proxy_hide_header X-Powered-By; proxy_hide_header Server; + include /usr/local/openresty/nginx/conf/security_headers.conf; } {% endif %} {% endif %} diff --git a/deploy/roles/create-instance/templates/openresty/site.j2 b/deploy/roles/create-instance/templates/openresty/site.j2 index 7de3dad..3c16c4e 100644 --- a/deploy/roles/create-instance/templates/openresty/site.j2 +++ b/deploy/roles/create-instance/templates/openresty/site.j2 @@ -7,6 +7,18 @@ # configurations within that file will be included in this openresty configuration. +# Tomcat upstreams with keepalive — one per instance under this FQDN +{% for instance in groups['instances'] + if (hostvars[instance]['fqdn'] | default('') | trim == hostvars[item]['fqdn'] | trim) + and (hostvars[instance]['instance_state'] is undefined) %} +upstream {{ instance | replace('-', '_') }}_tomcat { + server {{ hostvars[instance]['ansible_host'] }}:8080; + keepalive {{ tomcat_keepalive | default(32) }}; + keepalive_requests {{ tomcat_keepalive_requests | default(1000) }}; + keepalive_timeout {{ tomcat_keepalive_timeout | default('60s') }}; +} +{% endfor %} + {% if groups['instances'] | all_have_fqdn(hostvars) %} # SNI protection - prevents certificate/domain leakage via direct IP access # Only deployed when all instances have fqdn defined @@ -86,6 +98,7 @@ server { ssl_stapling on; ssl_stapling_verify on; {% endif %} + ssl_session_cache shared:SSL:20m; {% if use_proxy_protocol | default(false) and ansible_connection == 'lxd' %} # Extract real client IP from proxy protocol header @@ -145,7 +158,4 @@ server { {% endif %} {% endfor %} -if ($host != "{{ hostvars[item]['fqdn'] | trim }}") { - return 444; - } } diff --git a/deploy/roles/proxy/files/security_headers.conf b/deploy/roles/proxy/files/security_headers.conf new file mode 100644 index 0000000..a56b7a4 --- /dev/null +++ b/deploy/roles/proxy/files/security_headers.conf @@ -0,0 +1,12 @@ +# Managed by Ansible — do not edit, changes will be overwritten. +# Include from any server{} or location{} block to emit security headers. +# Re-include in each location that has its own add_header (nginx drops +# server-level add_header inheritance the moment a location adds any). +# +# HSTS is intentionally NOT included here — it must stay scoped to vhosts +# with a valid CA-signed cert (managed via static/.conf), never on +# self-signed/IP-based access. + +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Content-Type-Options "nosniff" always; +add_header Referrer-Policy "no-referrer" always; diff --git a/deploy/roles/proxy/tasks/nginx-install.yml b/deploy/roles/proxy/tasks/nginx-install.yml index 47c05a0..12e392d 100644 --- a/deploy/roles/proxy/tasks/nginx-install.yml +++ b/deploy/roles/proxy/tasks/nginx-install.yml @@ -71,8 +71,24 @@ proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + # Keepalive to upstream — requires HTTP/1.1 and an empty Connection header. + # Activates only when proxy_pass targets an upstream{} block with keepalive set. + proxy_http_version 1.1; + proxy_set_header Connection ""; + # Timeouts for long-running DHIS2 operations (analytics, imports) + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; mode: "0644" - force: false + +- name: Deploy security_headers.conf + ansible.builtin.copy: + src: security_headers.conf + dest: /etc/nginx/security_headers.conf + owner: root + group: root + mode: "0644" + notify: Reload Nginx - name: Disabling nginx version show ansible.builtin.lineinfile: @@ -97,6 +113,43 @@ tags: [nginx-perf-log] notify: Reload Nginx +- name: Nginx TCP optimizations + ansible.builtin.blockinfile: + path: /etc/nginx/nginx.conf + block: | + tcp_nopush on; + tcp_nodelay on; + marker: "# {mark} TCP OPTIMIZATIONS" + insertafter: "http {" + tags: [nginx-tcp] + notify: Reload Nginx + +- name: Nginx gzip compression + ansible.builtin.blockinfile: + path: /etc/nginx/nginx.conf + block: | + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 5; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml + application/rss+xml + application/atom+xml + application/wasm + image/svg+xml; + marker: "# {mark} GZIP COMPRESSION" + insertafter: "http {" + tags: [nginx-gzip] + notify: Reload Nginx + - name: Remove default nginx site ansible.builtin.file: name: /etc/nginx/sites-enabled/default diff --git a/deploy/roles/proxy/tasks/openresty-install.yml b/deploy/roles/proxy/tasks/openresty-install.yml index 515371a..b5c9690 100644 --- a/deploy/roles/proxy/tasks/openresty-install.yml +++ b/deploy/roles/proxy/tasks/openresty-install.yml @@ -79,8 +79,24 @@ proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + # Keepalive to upstream — requires HTTP/1.1 and an empty Connection header. + # Activates only when proxy_pass targets an upstream{} block with keepalive set. + proxy_http_version 1.1; + proxy_set_header Connection ""; + # Timeouts for long-running DHIS2 operations (analytics, imports) + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; mode: "0644" - force: false + +- name: Deploy security_headers.conf + ansible.builtin.copy: + src: security_headers.conf + dest: /usr/local/openresty/nginx/conf/security_headers.conf + owner: root + group: root + mode: "0644" + notify: Reload Openresty - name: Generating Diffie-Hellman key parameters, takes a while, you may grab some coffee ansible.builtin.command: openssl dhparam -out /usr/local/openresty/nginx/conf/dhparams.pem 2048 diff --git a/deploy/roles/proxy/tasks/securing-munin.yml b/deploy/roles/proxy/tasks/securing-munin.yml index f2847a6..2971eb4 100644 --- a/deploy/roles/proxy/tasks/securing-munin.yml +++ b/deploy/roles/proxy/tasks/securing-munin.yml @@ -13,6 +13,8 @@ fi grep -E '^\s*user\s+' "$conf_file" | awk '{print $2}' | tr -d ';' | head -1 register: _proxy_worker_user + changed_when: false + check_mode: false - name: Set proxy_user fact diff --git a/deploy/roles/proxy/templates/nginx/grafana.j2 b/deploy/roles/proxy/templates/nginx/grafana.j2 index e6979bc..5a82fa5 100644 --- a/deploy/roles/proxy/templates/nginx/grafana.j2 +++ b/deploy/roles/proxy/templates/nginx/grafana.j2 @@ -8,9 +8,6 @@ location /{{ server_monitoring }} { include /etc/nginx/proxy_params; proxy_redirect off; proxy_hide_header X-Powered-By; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks - proxy_set_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks - proxy_set_header X-Xss-Protection "1; mode=block"; #To leverage browser-based protections against cross-site scripting proxy_hide_header Server; + include /etc/nginx/security_headers.conf; } diff --git a/deploy/roles/proxy/templates/nginx/munin.j2 b/deploy/roles/proxy/templates/nginx/munin.j2 index e4164ab..e02b77b 100644 --- a/deploy/roles/proxy/templates/nginx/munin.j2 +++ b/deploy/roles/proxy/templates/nginx/munin.j2 @@ -9,11 +9,8 @@ location /{{ server_monitoring }} { include /etc/nginx/proxy_params; proxy_redirect off; proxy_hide_header X-Powered-By; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks - proxy_set_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks - proxy_set_header X-Xss-Protection "1; mode=block"; #To leverage browser-based protections against cross-site scripting proxy_hide_header Server; + include /etc/nginx/security_headers.conf; auth_basic "Restricted Content"; auth_basic_user_file /etc/nginx/.htpasswd; } diff --git a/deploy/roles/proxy/templates/openresty/grafana.j2 b/deploy/roles/proxy/templates/openresty/grafana.j2 index 4b6bf3c..890835b 100644 --- a/deploy/roles/proxy/templates/openresty/grafana.j2 +++ b/deploy/roles/proxy/templates/openresty/grafana.j2 @@ -8,9 +8,6 @@ location /{{ server_monitoring }} { include /usr/local/openresty/nginx/conf/proxy_params; proxy_redirect off; proxy_hide_header X-Powered-By; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks - proxy_set_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks - proxy_set_header X-Xss-Protection "1; mode=block"; #To leverage browser-based protections against cross-site scripting proxy_hide_header Server; + include /usr/local/openresty/nginx/conf/security_headers.conf; } diff --git a/deploy/roles/proxy/templates/openresty/munin.j2 b/deploy/roles/proxy/templates/openresty/munin.j2 index 398b6e0..a87b3b9 100644 --- a/deploy/roles/proxy/templates/openresty/munin.j2 +++ b/deploy/roles/proxy/templates/openresty/munin.j2 @@ -9,11 +9,8 @@ location /{{ server_monitoring }} { include /usr/local/openresty/nginx/conf/proxy_params; proxy_redirect off; proxy_hide_header X-Powered-By; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks - proxy_set_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks - proxy_set_header X-Xss-Protection "1; mode=block"; #To leverage browser-based protections against cross-site scripting proxy_hide_header Server; + include /usr/local/openresty/nginx/conf/security_headers.conf; auth_basic "Restricted Content"; auth_basic_user_file /usr/local/openresty/nginx/conf/.htpasswd; } diff --git a/deploy/roles/proxy/templates/openresty/nginx.conf.j2 b/deploy/roles/proxy/templates/openresty/nginx.conf.j2 index 1b9f2ec..1ab5858 100644 --- a/deploy/roles/proxy/templates/openresty/nginx.conf.j2 +++ b/deploy/roles/proxy/templates/openresty/nginx.conf.j2 @@ -24,7 +24,27 @@ http { access_log /usr/local/openresty/nginx/logs/perf.log performance; sendfile on; + tcp_nopush on; + tcp_nodelay on; keepalive_timeout 65; + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 5; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml + application/rss+xml + application/atom+xml + application/wasm + image/svg+xml; + include /usr/local/openresty/nginx/conf/conf.d/*.conf; } diff --git a/deploy/roles/proxy/templates/openresty/prometheus.j2 b/deploy/roles/proxy/templates/openresty/prometheus.j2 index 4b6bf3c..890835b 100644 --- a/deploy/roles/proxy/templates/openresty/prometheus.j2 +++ b/deploy/roles/proxy/templates/openresty/prometheus.j2 @@ -8,9 +8,6 @@ location /{{ server_monitoring }} { include /usr/local/openresty/nginx/conf/proxy_params; proxy_redirect off; proxy_hide_header X-Powered-By; - proxy_set_header X-Frame-Options "SAMEORIGIN"; # To mitigate the risk of clickjacking attacks - proxy_set_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; - proxy_set_header X-Content-Type-Options "nosniff"; # drive-by download attacks - proxy_set_header X-Xss-Protection "1; mode=block"; #To leverage browser-based protections against cross-site scripting proxy_hide_header Server; + include /usr/local/openresty/nginx/conf/security_headers.conf; } From 699419521590d61c96063bb531b085de85c4ffa9 Mon Sep 17 00:00:00 2001 From: Tito Kipkurgat Date: Tue, 28 Apr 2026 12:55:34 +0300 Subject: [PATCH 3/3] fix: tighten proxy config file permissions to 0640 Signed-off-by: Tito Kipkurgat --- deploy/roles/proxy/tasks/nginx-install.yml | 10 +++++++--- deploy/roles/proxy/tasks/openresty-install.yml | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/deploy/roles/proxy/tasks/nginx-install.yml b/deploy/roles/proxy/tasks/nginx-install.yml index 12e392d..3b1bee5 100644 --- a/deploy/roles/proxy/tasks/nginx-install.yml +++ b/deploy/roles/proxy/tasks/nginx-install.yml @@ -52,7 +52,9 @@ Pin: origin nginx.org Pin: release o=nginx Pin-Priority: 900 - mode: "0644" + owner: root + group: root + mode: "0640" - name: Installing Nginx ansible.builtin.apt: @@ -79,7 +81,9 @@ proxy_connect_timeout 60s; proxy_send_timeout 300s; proxy_read_timeout 300s; - mode: "0644" + owner: root + group: root + mode: "0640" - name: Deploy security_headers.conf ansible.builtin.copy: @@ -87,7 +91,7 @@ dest: /etc/nginx/security_headers.conf owner: root group: root - mode: "0644" + mode: "0640" notify: Reload Nginx - name: Disabling nginx version show diff --git a/deploy/roles/proxy/tasks/openresty-install.yml b/deploy/roles/proxy/tasks/openresty-install.yml index b5c9690..a1df5c5 100644 --- a/deploy/roles/proxy/tasks/openresty-install.yml +++ b/deploy/roles/proxy/tasks/openresty-install.yml @@ -68,7 +68,7 @@ dest: /usr/local/openresty/nginx/conf/nginx.conf owner: root group: root - mode: "0644" + mode: "0640" notify: Reload Openresty - name: Create proxy_params file @@ -87,7 +87,9 @@ proxy_connect_timeout 60s; proxy_send_timeout 300s; proxy_read_timeout 300s; - mode: "0644" + owner: root + group: root + mode: "0640" - name: Deploy security_headers.conf ansible.builtin.copy: @@ -95,7 +97,7 @@ dest: /usr/local/openresty/nginx/conf/security_headers.conf owner: root group: root - mode: "0644" + mode: "0640" notify: Reload Openresty - name: Generating Diffie-Hellman key parameters, takes a while, you may grab some coffee