diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index 2fcb6c7..658ab4c 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -1,5 +1,7 @@ [defaults] inventory = ./inventory/ +gathering = smart +vault_password_file = ./.vault_pass [ssh_connection] pipelining = True diff --git a/ansible/ansible_local.env_example b/ansible/ansible_local.env_example new file mode 100644 index 0000000..7442575 --- /dev/null +++ b/ansible/ansible_local.env_example @@ -0,0 +1,5 @@ +ANSIBLE_STRATEGY_PLUGINS=/home/seb/.local/share/pipx/venvs/ansible/lib/python3.13/site-packages/ansible_mitogen/plugins/strategy +export ANSIBLE_STRATEGY_PLUGINS + +ANSIBLE_STRATEGY=mitogen_linear +export ANSIBLE_STRATEGY diff --git a/ansible/play-config.yml b/ansible/play-config.yml new file mode 100644 index 0000000..d597aea --- /dev/null +++ b/ansible/play-config.yml @@ -0,0 +1,24 @@ +- name: Web Application Firewalls (config) + hosts: waf + vars_files: + - "vars/common.yml" + - "vars/secrets.yml" + vars: + certs_ca_passphrase: "{{ secrets.ca.passphrase }}" + certs_default_country_name: "{{ common.certificates.country_name }}" + certs_default_locality_name: "{{ common.certificates.locality_name }}" + certs_default_organization_name: "{{ common.certificates.organization_name }}" + certs_default_email_address: "{{ common.certificates.email_address }}" + certs_list: + - cn: "test.lab" + domains: + - "DNS:test.lab" + - "DNS:{{ ansible_fqdn }}" + - "DNS:{{ ansible_hostname }}" + - "IP:{{ ansible_default_ipv4.address }}" + waf_config_sites: "{{ common.sites }}" + roles: + - "certs" + - "waf_config" + tags: + - "waf" diff --git a/ansible/play-main.yml b/ansible/play-main.yml index 091209f..be0e326 100644 --- a/ansible/play-main.yml +++ b/ansible/play-main.yml @@ -10,14 +10,15 @@ hosts: all gather_facts: false vars_files: - - "vars.yml" + - "vars/common.yml" + - "vars/secrets.yml" vars: local_ca_common_name: "Lab CA" - local_ca_passphrase: "{{ vault.ca.passphrase }}" - local_ca_country_name: "FR" - local_ca_locality_name: "Vannes" - local_ca_organization_name: "iTik" - local_ca_email_address: "ca@itik.fr" + local_ca_passphrase: "{{ secrets.ca.passphrase }}" + local_ca_country_name: "{{ common.certificates.country_name }}" + local_ca_locality_name: "{{ common.certificates.locality_name }}" + local_ca_organization_name: "{{ common.certificates.organization_name }}" + local_ca_email_address: "{{ common.certificates.email_address }}" roles: - "local_ca" tags: @@ -26,6 +27,9 @@ - name: Proxy server hosts: proxy gather_facts: false + vars_files: + - "vars/common.yml" + - "vars/secrets.yml" vars: squid_authorized_networks: - "{{ ansible_default_ipv4.network }}/24" @@ -38,7 +42,8 @@ hosts: all gather_facts: false vars_files: - - "vars.yml" + - "vars/common.yml" + - "vars/secrets.yml" vars: common_proxy_server: "{{ hostvars['proxy1'].ansible_default_ipv4.address }}:3128" roles: @@ -49,39 +54,66 @@ - name: HTTP Servers hosts: www gather_facts: false + vars_files: + - "vars/common.yml" + - "vars/secrets.yml" roles: - "www" tags: - "www" -- name: Web Application Firewalls +- name: Web Application Firewalls (install) hosts: waf gather_facts: false vars_files: - - "vars.yml" + - "vars/common.yml" + - "vars/secrets.yml" vars: - cert_domains: - - "DNS:test.lab" - - "DNS:{{ ansible_fqdn }}" - - "DNS:{{ ansible_hostname }}" - - "IP:{{ ansible_default_ipv4.address }}" - - "DNS:server.{{ consul_datacenter }}.{{ consul_domain }}" - cert_country_name: "FR" - cert_locality_name: "Vannes" - cert_organization_name: "iTik" - cert_email_address: "ca@itik.fr" - consul_domain: "itik.fr" - consul_datacenter: "lab" - consul_secret_key: "{{ vault.consul.secret_key }}" + certs_ca_passphrase: "{{ secrets.ca.passphrase }}" + certs_default_country_name: "{{ common.certificates.country_name }}" + certs_default_locality_name: "{{ common.certificates.locality_name }}" + certs_default_organization_name: "{{ common.certificates.organization_name }}" + certs_default_email_address: "{{ common.certificates.email_address }}" + certs_list: + # Consul + - cn: "server.{{ common.consul.datacenter }}.{{ common.consul.domain }}" + domains: + - "DNS:server.{{ common.consul.datacenter }}.{{ common.consul.domain }}" + # Sites + - cn: "test.lab" + domains: + - "DNS:test.lab" + - "DNS:{{ ansible_fqdn }}" + - "DNS:{{ ansible_hostname }}" + - "IP:{{ ansible_default_ipv4.address }}" + consul_domain: "{{ common.consul.domain }}" + consul_datacenter: "{{ common.consul.datacenter }}" + consul_secret_key: "{{ secrets.consul.secret_key }}" consul_nodes: - "waf1" - "waf2" - "waf3" - bgwaf_cluster: false - bgwaf_sites: "{{ waf.sites }}" + # FIXME: -> Cady modules + waf_install_cluster: true + vip_script_user: "caddy" + vip_monitored_process: "caddy" + vip_vrrp_password: "{{ secrets.keepalived.secret }}" + vip_vrrp_ips: + - master: "waf1" + virtual_router_id: 11 + addr: "{{ {{ ansible_default_ipv4.address }} | split('.')[0-3] | join('.') }}.250" + - master: "waf2" + virtual_router_id: 12 + addr: "{{ {{ ansible_default_ipv4.address }} | split('.')[0-3] | join('.') }}.251" + - master: "waf3" + virtual_router_id: 13 + addr: "{{ {{ ansible_default_ipv4.address }} | split('.')[0-3] | join('.') }}.252" + waf_config_sites: "{{ common.sites }}" roles: - - "cert" + - "certs" - "consul" - - "bgwaf" + - "waf_install" + - "vip" + - "waf_config" tags: - - "bgwaf" + - "waf" diff --git a/ansible/roles/bgwaf/tasks/main.yml b/ansible/roles/bgwaf/tasks/main.yml deleted file mode 100644 index ddac5e5..0000000 --- a/ansible/roles/bgwaf/tasks/main.yml +++ /dev/null @@ -1,201 +0,0 @@ -- name: Remove packaged Go - ansible.builtin.apt: - name: - - "golang" - state: "absent" - autoremove: true - purge: true - -- name: Install requirements - ansible.builtin.apt: - name: - - "git" - - "libnss3-tools" - -- name: Create Go directory - ansible.builtin.file: - path: "/usr/local/go-{{ bgwaf_go_version }}" - state: "directory" - mode: "0755" - -- name: Install Go - ansible.builtin.unarchive: - src: "https://go.dev/dl/go{{ bgwaf_go_version }}.linux-amd64.tar.gz" - dest: "/usr/local/go-{{ bgwaf_go_version }}" - remote_src: true - -- name: Link Go - ansible.builtin.file: - src: "/usr/local/go-{{ bgwaf_go_version }}/go/bin/go" - dest: "/usr/local/bin/go" - state: "link" - -- name: Install xcaddy - when: | - 'xcaddy' not in ansible_facts.packages - or ansible_facts.packages['xcaddy'][0]['version'] != bgwaf_xcaddy_version - ansible.builtin.apt: - deb: "{{ bgwaf_xcaddy_download }}/v{{ bgwaf_xcaddy_version }}/xcaddy_{{ bgwaf_xcaddy_version }}_linux_amd64.deb" - force: true - -- name: Create Caddy build command - ansible.builtin.set_fact: - bgwaf_build_command: > - xcaddy build v{{ bgwaf_caddy_version }} - {% if bgwaf_cluster %} --with github.com/pteich/caddy-tlsconsul{% endif %} - --with github.com/corazawaf/coraza-caddy/v2@v{{ bgwaf_coraza_caddy_version }} - --with github.com/mholt/caddy-ratelimit - --with github.com/steffenbusch/caddy-bot-barrier - --output /usr/local/bin/caddy - -- name: Test Caddy version - ansible.builtin.copy: - dest: "/usr/local/bin/.caddy.build" - content: "{{ bgwaf_build_command }}" - mode: "0644" - register: bgwaf_test_caddy_version - -- name: Build Caddy - # noqa: no-handler - when: "bgwaf_test_caddy_version.changed" - ansible.builtin.command: - cmd: "{{ bgwaf_build_command }}" - creates: "/usr/local/bin/caddy" - notify: - - "Restart Caddy" - -- name: Create caddy system group - ansible.builtin.group: - name: "caddy" - system: true - -- name: Create caddy system user - ansible.builtin.user: - name: "caddy" - group: "caddy" - groups: ["ssl-cert"] - password: "!" - system: true - home: "/opt/caddy" - -- name: Create caddy config directories - ansible.builtin.file: - path: "{{ item }}" - state: "directory" - group: "caddy" - mode: "0750" - loop: - - "/etc/caddy" - - "/etc/caddy/plugins" - - "/etc/caddy/sites" - -- name: Create caddy log dir - ansible.builtin.file: - path: "/var/log/caddy" - state: "directory" - group: "caddy" - mode: "0770" - -- name: Install OWASP CRS - ansible.builtin.unarchive: - src: "https://github.com/coreruleset/coreruleset/archive/refs/tags/v{{ bgwaf_owasp_crs_version }}.tar.gz" - dest: "/etc/caddy" - group: "caddy" - remote_src: true - -- name: Install plugins - loop: "{{ bgwaf_crs_plugins }}" - loop_control: - loop_var: "plugin" - ansible.builtin.include_tasks: "install_plugin.yml" - -- name: Create sites config directories - loop: "{{ bgwaf_sites }}" - loop_control: - loop_var: "site" - ansible.builtin.file: - path: "/etc/caddy/sites/{{ site.name }}" - state: "directory" - group: "caddy" - mode: "0750" - -- name: Generate sites coraza.conf config file - loop: "{{ bgwaf_sites }}" - loop_control: - loop_var: "site" - ansible.builtin.template: - src: "templates/coraza.conf" - dest: "/etc/caddy/sites/{{ site.name }}/coraza.conf" - group: "caddy" - mode: "0640" - notify: - - "Reload Caddy" - -- name: Generate sites crs-setup config file - loop: "{{ bgwaf_sites }}" - loop_control: - loop_var: "site" - ansible.builtin.template: - src: "templates/crs-setup.conf" - dest: "/etc/caddy/sites/{{ site.name }}/crs-setup.conf" - group: "caddy" - mode: "0640" - notify: - - "Reload Caddy" - -- name: Generate sites exclusions before request file - loop: "{{ bgwaf_sites }}" - loop_control: - loop_var: "site" - ansible.builtin.template: - src: "templates/exclusions-request-before.conf" - dest: "/etc/caddy/sites/{{ site.name }}/exclusions-request-before.conf" - group: "caddy" - mode: "0640" - notify: - - "Reload Caddy" - -- name: Generate sites exclusions after response file - loop: "{{ bgwaf_sites }}" - loop_control: - loop_var: "site" - ansible.builtin.template: - src: "templates/exclusions-response-after.conf" - dest: "/etc/caddy/sites/{{ site.name }}/exclusions-response-after.conf" - group: "caddy" - mode: "0640" - notify: - - "Reload Caddy" - -- name: Generate Caddyfile - ansible.builtin.template: - src: "templates/Caddyfile" - dest: "/etc/caddy/Caddyfile" - group: "caddy" - mode: "0640" - notify: - - "Reload Caddy" - -- name: Copy bot barrier template - ansible.builtin.copy: - src: "files/bot_barrier_template.html" - dest: "/etc/caddy/bot_barrier_template.html" - group: "caddy" - mode: "0640" - notify: - - "Reload Caddy" - -- name: Deploy caddy service file - ansible.builtin.template: - src: "templates/caddy.service" - dest: "/etc/systemd/system/caddy.service" - mode: "0644" - notify: - - "Restart Caddy" - -- name: Enable and start Caddy - ansible.builtin.service: - name: "caddy" - state: "started" - enabled: true - daemon_reload: true diff --git a/ansible/roles/bgwaf/vars/main.yml b/ansible/roles/bgwaf/vars/main.yml deleted file mode 100644 index 1e153a9..0000000 --- a/ansible/roles/bgwaf/vars/main.yml +++ /dev/null @@ -1 +0,0 @@ -bgwaf_xcaddy_download: "https://github.com/caddyserver/xcaddy/releases/download" diff --git a/ansible/roles/cert/defaults/main.yml b/ansible/roles/cert/defaults/main.yml deleted file mode 100644 index 31b21b1..0000000 --- a/ansible/roles/cert/defaults/main.yml +++ /dev/null @@ -1,10 +0,0 @@ -# Defaults -cert_not_after: "+365d" # valid for one year -cert_not_before: "-1d" # valid since yesterday - -# Required -cert_domains: [] -cert_country_name: None -cert_locality_name: None -cert_organization_name: None -cert_email_address: None diff --git a/ansible/roles/cert/tasks/main.yml b/ansible/roles/cert/tasks/main.yml deleted file mode 100644 index 312b109..0000000 --- a/ansible/roles/cert/tasks/main.yml +++ /dev/null @@ -1,53 +0,0 @@ -- name: Install requirements - ansible.builtin.apt: - name: - - "ssl-cert" - -- name: Copy CA certificate file on server - ansible.builtin.copy: - src: "ca/ca-certificate.crt" - dest: "/etc/ssl/certs/local-ca.crt" - mode: "0644" - -- name: Create private key for new certificate - community.crypto.openssl_privatekey: - path: "/etc/ssl/private/local.key" - mode: "0640" - group: "ssl-cert" - -- name: Create CSR for new certificate - community.crypto.openssl_csr: - path: "/etc/ssl/certs/local.csr" - privatekey_path: "/etc/ssl/private/local.key" - common_name: "{{ cert_domains[0] }}" - subject_alt_name: "{{ cert_domains }}" - country_name: "{{ cert_country_name }}" - locality_name: "{{ cert_locality_name }}" - organization_name: "{{ cert_organization_name }}" - email_address: "{{ cert_email_address }}" - return_content: true - register: cert_csr - -- name: Sign certificate with our CA - delegate_to: "localhost" - # noqa: no-handler - when: "cert_csr.changed" - community.crypto.x509_certificate_pipe: - csr_content: "{{ cert_csr.csr }}" - provider: "ownca" - ownca_path: "ca/ca-certificate.crt" - ownca_privatekey_path: "ca/ca-certificate.key" - ownca_privatekey_passphrase: "{{ vault.ca.passphrase }}" - ownca_not_after: "{{ cert_not_after }}" - ownca_not_before: "{{ cert_not_before }}" - register: cert_certificate - -- name: Write certificate file on server - # noqa: no-handler - when: "cert_csr.changed" - ansible.builtin.copy: - dest: "/etc/ssl/certs/local.crt" - content: "{{ cert_certificate.certificate }}" - mode: "0644" - notify: - - "Reboot host" diff --git a/ansible/roles/certs/defaults/main.yml b/ansible/roles/certs/defaults/main.yml new file mode 100644 index 0000000..9501a10 --- /dev/null +++ b/ansible/roles/certs/defaults/main.yml @@ -0,0 +1,19 @@ +# Defaults +certs_default_country_name: null +certs_default_locality_name: null +certs_default_organization_name: null +certs_default_email_address: null +certs_default_not_after: "+365d" # valid for one year +certs_default_not_before: "-1d" # valid since yesterday + +# Required +certs_ca_passphrase: null +certs_list: {} +# - cn: +# domains: +# country_name: +# locality_name: +# organization_name: +# email_address: +# not_after: +# not_before: diff --git a/ansible/roles/cert/handlers/main.yml b/ansible/roles/certs/handlers/main.yml similarity index 100% rename from ansible/roles/cert/handlers/main.yml rename to ansible/roles/certs/handlers/main.yml diff --git a/ansible/roles/certs/tasks/create_cert.yml b/ansible/roles/certs/tasks/create_cert.yml new file mode 100644 index 0000000..6276660 --- /dev/null +++ b/ansible/roles/certs/tasks/create_cert.yml @@ -0,0 +1,42 @@ +- name: Create private key for new certificate + community.crypto.openssl_privatekey: + path: "/etc/ssl/private/{{ cert.cn }}.key" + mode: "0640" + group: "ssl-cert" + +- name: Create CSR for new certificate + community.crypto.openssl_csr: + path: "/etc/ssl/certs/{{ cert.cn }}.csr" + privatekey_path: "/etc/ssl/private/{{ cert.cn }}.key" + common_name: "{{ cert.cn }}" + subject_alt_name: "{{ cert.domains }}" + country_name: "{{ cert.country_name | default(certs_default_country_name) | mandatory }}" + locality_name: "{{ cert.locality_name | default(certs_default_locality_name) | mandatory }}" + organization_name: "{{ cert.organization_name | default(certs_default_organization_name) | mandatory }}" + email_address: "{{ cert.email_address | default(certs_default_email_address) | mandatory }}" + return_content: true + register: certs_csr + +- name: Sign certificate with our CA + delegate_to: "localhost" + # noqa: no-handler + when: "certs_csr.changed" + community.crypto.x509_certificate_pipe: + csr_content: "{{ certs_csr.csr }}" + provider: "ownca" + ownca_path: "ca/ca-certificate.crt" + ownca_privatekey_path: "ca/ca-certificate.key" + ownca_privatekey_passphrase: "{{ certs_ca_passphrase }}" + ownca_not_after: "{{ cert.not_after | default(certs_default_not_after) }}" + ownca_not_before: "{{ cert.not_before | default(certs_default_not_before) }}" + register: certs_certificate + +- name: Write certificate file on server + # noqa: no-handler + when: "certs_csr.changed" + ansible.builtin.copy: + dest: "/etc/ssl/certs/{{ cert.cn }}.crt" + content: "{{ certs_certificate.certificate }}" + mode: "0644" + notify: + - "Reboot host" diff --git a/ansible/roles/certs/tasks/main.yml b/ansible/roles/certs/tasks/main.yml new file mode 100644 index 0000000..d860639 --- /dev/null +++ b/ansible/roles/certs/tasks/main.yml @@ -0,0 +1,16 @@ +- name: Install requirements + ansible.builtin.apt: + name: + - "ssl-cert" + +- name: Copy CA certificate file on server + ansible.builtin.copy: + src: "ca/ca-certificate.crt" + dest: "/etc/ssl/certs/local-ca.crt" + mode: "0644" + +- name: Create certs + loop: "{{ certs_list }}" + loop_control: + loop_var: "cert" + ansible.builtin.include_tasks: "create_cert.yml" diff --git a/ansible/roles/consul/defaults/main.yml b/ansible/roles/consul/defaults/main.yml index 373de4d..de3c30f 100644 --- a/ansible/roles/consul/defaults/main.yml +++ b/ansible/roles/consul/defaults/main.yml @@ -1,13 +1,13 @@ # Required consul_datacenter: null +consul_domain: null consul_secret_key: null consul_nodes: [] -consul_domain: None # Defaults consul_version: "1.22.2" -consul_key_file: "/etc/ssl/private/local.key" -consul_cert_file: "/etc/ssl/certs/local.crt" +consul_key_file: "/etc/ssl/private/server.{{ consul_datacenter }}.{{ consul_domain }}.key" +consul_cert_file: "/etc/ssl/certs/server.{{ consul_datacenter }}.{{ consul_domain }}.crt" consul_ca_file: "/etc/ssl/certs/local-ca.crt" consul_client_addr: "0.0.0.0" consul_bind_addr: "{{ ansible_default_ipv4.address }}" diff --git a/ansible/roles/local_ca/defaults/main.yml b/ansible/roles/local_ca/defaults/main.yml index 08b6ad4..9b54576 100644 --- a/ansible/roles/local_ca/defaults/main.yml +++ b/ansible/roles/local_ca/defaults/main.yml @@ -3,9 +3,9 @@ local_ca_key_size: 4096 local_ca_key_type: "RSA" # Required -local_ca_common_name: None -local_ca_passphrase: None -local_ca_country_name: None -local_ca_locality_name: None -local_ca_organization_name: None -local_ca_email_address: None +local_ca_common_name: null +local_ca_passphrase: null +local_ca_country_name: null +local_ca_locality_name: null +local_ca_organization_name: null +local_ca_email_address: null diff --git a/ansible/roles/local_ca/tasks/main.yml b/ansible/roles/local_ca/tasks/main.yml index 036797c..fc92a44 100644 --- a/ansible/roles/local_ca/tasks/main.yml +++ b/ansible/roles/local_ca/tasks/main.yml @@ -22,7 +22,7 @@ community.crypto.openssl_csr: path: "ca/ca-certificate.csr" privatekey_path: "ca/ca-certificate.key" - privatekey_passphrase: "{{ vault.ca.passphrase }}" + privatekey_passphrase: "{{ local_ca_passphrase }}" common_name: "{{ local_ca_common_name }}" use_common_name_for_san: false # since we do not specify SANs, don't use CN as a SAN country_name: "{{ local_ca_country_name }}" @@ -47,5 +47,5 @@ path: "ca/ca-certificate.crt" csr_content: "{{ local_ca_csr.csr }}" privatekey_path: "ca/ca-certificate.key" - privatekey_passphrase: "{{ vault.ca.passphrase }}" + privatekey_passphrase: "{{ local_ca_passphrase }}" provider: "selfsigned" diff --git a/ansible/roles/vip/defaults/main.yml b/ansible/roles/vip/defaults/main.yml new file mode 100644 index 0000000..dbafa3e --- /dev/null +++ b/ansible/roles/vip/defaults/main.yml @@ -0,0 +1,16 @@ +# Required +vip_script_user: null +vip_monitored_process: null +vip_vrrp_password: null # Max 8 characters +vip_vrrp_ips: null +# - master: +# virtual_router_id: +# addr: + +# Optionnal +vip_alerts_to: null +# - + +# Required if length vip_alerts_to > 0 +vip_smtp_from: null +vip_smtp_relay: null diff --git a/ansible/roles/vip/handlers/main.yml b/ansible/roles/vip/handlers/main.yml new file mode 100644 index 0000000..b70fe82 --- /dev/null +++ b/ansible/roles/vip/handlers/main.yml @@ -0,0 +1,4 @@ +- name: Restart keepalived + ansible.builtin.service: + name: "keepalived" + state: "restarted" diff --git a/ansible/roles/vip/tasks/main.yml b/ansible/roles/vip/tasks/main.yml new file mode 100644 index 0000000..5cbd2f3 --- /dev/null +++ b/ansible/roles/vip/tasks/main.yml @@ -0,0 +1,28 @@ +- name: Installation des paquetages + ansible.builtin.apt: + name: + - "keepalived" + notify: + - "Restart keepalived" + +- name: Tell kernel to allow binding non-local IP + ansible.posix.sysctl: + name: "net.ipv4.ip_nonlocal_bind" + value: "1" + sysctl_set: true + notify: + - "Restart keepalived" + +- name: Generate keepalived.conf file + ansible.builtin.template: + src: "templates/keepalived.conf" + dest: "/etc/keepalived/keepalived.conf" + mode: "0640" + notify: + - "Restart keepalived" + +- name: Activate and start keepalived + ansible.builtin.service: + name: "keepalived" + state: "started" + enabled: true diff --git a/ansible/roles/vip/templates/keepalived.conf b/ansible/roles/vip/templates/keepalived.conf new file mode 100644 index 0000000..85c4586 --- /dev/null +++ b/ansible/roles/vip/templates/keepalived.conf @@ -0,0 +1,45 @@ +global_defs { + enable_script_security + script_user "{{ vip_script_user }}" +{% if vip_alerts_to | default([]) | length > 0 %} + + notification_email { +{% for dest in vip_alerts_to %} + {{ dest }} +{% endfor %} + } + notification_email_from {{ vip_smtp_from }} + smtp_server {{ vip_smtp_relay }} + smtp_connect_timeout 30 + router_id {{ ansible_hostname }} +{% endif %} +} + +vrrp_script chk_{{ vip_monitored_process }} { + script "/usr/bin/killall -0 {{ vip_monitored_process }}" # Check if process is running + interval 2 # Check every 2 seconds + weight 2 # Weight to influence master election +} +{% for ip in vip_vrrp_ips %} + +# master: {{ ip.master }} +vrrp_instance VI_{{ ip.virtual_router_id }} { + state {{ (ansible_hostname==ip.master) | ternary('MASTER', 'BACKUP') }} + interface eth0 + virtual_router_id {{ ip.virtual_router_id }} + priority {{ (ansible_hostname==ip.master) | ternary('101', '100') }} + advert_int 1 + promote_secondaries + smtp_alert true + authentication { + auth_type PASS + auth_pass {{ vip_vrrp_password }} + } + virtual_ipaddress { + {{ ip.addr }} + } + track_script { + chk_{{ vip_monitored_process }} + } +} +{% endfor %} \ No newline at end of file diff --git a/ansible/roles/bgwaf/defaults/main.yml b/ansible/roles/waf_config/defaults/main.yml similarity index 77% rename from ansible/roles/bgwaf/defaults/main.yml rename to ansible/roles/waf_config/defaults/main.yml index 05ff2a9..43b57c7 100644 --- a/ansible/roles/bgwaf/defaults/main.yml +++ b/ansible/roles/waf_config/defaults/main.yml @@ -1,42 +1,17 @@ -bgwaf_go_version: "1.25.5" -bgwaf_xcaddy_version: "0.4.5" -bgwaf_caddy_version: "2.10.2" -bgwaf_coraza_caddy_version: "2.1.0" # Coraza v3.3.3 -bgwaf_owasp_crs_version: "4.21.0" - -# Cluster mode (e.g. with consul) -bgwaf_cluster: false +# Defaults +waf_config_owasp_crs_version: "4.21.0" # Required unless not site.custom_cert -bgwaf_email: null +waf_config_email: null # Required if site.bot_barrier -bgwaf_bot_barrier_secret: null +waf_config_bot_barrier_secret: null # Optionnal -bgwaf_local_ca_name: null -bgwaf_ca_root: null +waf_config_local_ca_name: null +waf_config_ca_root: null -# Cf.: https://github.com/coreruleset/plugin-registry -bgwaf_crs_plugins: - - name: "drupal-rule-exclusions" - version: "1.0.0" - - name: "wordpress-rule-exclusions" - version: "1.2.0" - - name: "nextcloud-rule-exclusions" - version: "1.5.0" - - name: "dokuwiki-rule-exclusions" - version: "1.0.0" - - name: "phpmyadmin-rule-exclusions" - version: "1.0.0" - - name: "roundcube-rule-exclusions" - version: "1.0.4" - provider: "EsadCetiner" - - name: "sogo-rule-exclusions" - version: "1.0.4" - provider: "EsadCetiner" - -bgwaf_sites: [] +waf_config_sites: [] # - name: "" # site: "" # paths: @@ -55,15 +30,19 @@ bgwaf_sites: [] # after_response: # - "" -bgwaf_defaults: +waf_config_defaults: redirect_from_aliases: true custom_cert: true custom_cert_file: "" custom_key_file: "" + rate_events: 1000 rate_window: "1m" + bot_barrier: false - log_only: true + + # CRS defaults + log_only: false paranoia_level: 1 detection_paranoia_level: null enforce_bodyproc_urlencoded: 1 @@ -100,8 +79,12 @@ bgwaf_defaults: - ".asa/" - ".asax/" - ".ascx/" + - ".back/" - ".backup/" - ".bak/" + - ".bck/" + - ".bk/" + - ".bkp/" - ".bat/" - ".cdx/" - ".cer/" @@ -158,9 +141,11 @@ bgwaf_defaults: - ".reg/" - ".resources/" - ".resx/" + - ".sav/" - ".save/" - ".scr/" - ".sct/" + - ".sh/" - ".shs/" - ".sql/" - ".sqlite/" diff --git a/ansible/roles/bgwaf/files/bot_barrier_template.html b/ansible/roles/waf_config/files/bot_barrier_template.html similarity index 100% rename from ansible/roles/bgwaf/files/bot_barrier_template.html rename to ansible/roles/waf_config/files/bot_barrier_template.html diff --git a/ansible/roles/waf_config/handlers/main.yml b/ansible/roles/waf_config/handlers/main.yml new file mode 100644 index 0000000..4932372 --- /dev/null +++ b/ansible/roles/waf_config/handlers/main.yml @@ -0,0 +1,4 @@ +- name: Reload Caddy + ansible.builtin.service: + name: "caddy" + state: "reloaded" diff --git a/ansible/roles/waf_config/tasks/main.yml b/ansible/roles/waf_config/tasks/main.yml new file mode 100644 index 0000000..81992e8 --- /dev/null +++ b/ansible/roles/waf_config/tasks/main.yml @@ -0,0 +1,75 @@ +- name: Create sites config directories + loop: "{{ waf_config_sites }}" + loop_control: + loop_var: "site" + ansible.builtin.file: + path: "/etc/caddy/sites/{{ site.name }}" + state: "directory" + group: "caddy" + mode: "0750" + +- name: Generate sites coraza.conf config file + loop: "{{ waf_config_sites }}" + loop_control: + loop_var: "site" + ansible.builtin.template: + src: "templates/coraza.conf" + dest: "/etc/caddy/sites/{{ site.name }}/coraza.conf" + group: "caddy" + mode: "0640" + notify: + - "Reload Caddy" + +- name: Generate sites crs-setup config file + loop: "{{ waf_config_sites }}" + loop_control: + loop_var: "site" + ansible.builtin.template: + src: "templates/crs-setup.conf" + dest: "/etc/caddy/sites/{{ site.name }}/crs-setup.conf" + group: "caddy" + mode: "0640" + notify: + - "Reload Caddy" + +- name: Generate sites exclusions before request file + loop: "{{ waf_config_sites }}" + loop_control: + loop_var: "site" + ansible.builtin.template: + src: "templates/exclusions-request-before.conf" + dest: "/etc/caddy/sites/{{ site.name }}/exclusions-request-before.conf" + group: "caddy" + mode: "0640" + notify: + - "Reload Caddy" + +- name: Generate sites exclusions after response file + loop: "{{ waf_config_sites }}" + loop_control: + loop_var: "site" + ansible.builtin.template: + src: "templates/exclusions-response-after.conf" + dest: "/etc/caddy/sites/{{ site.name }}/exclusions-response-after.conf" + group: "caddy" + mode: "0640" + notify: + - "Reload Caddy" + +- name: Generate Caddyfile + ansible.builtin.template: + src: "templates/Caddyfile" + dest: "/etc/caddy/Caddyfile" + group: "caddy" + mode: "0640" + notify: + - "Reload Caddy" + +- name: Copy bot barrier template + ansible.builtin.copy: + src: "files/bot_barrier_template.html" + dest: "/etc/caddy/bot_barrier_template.html" + group: "caddy" + mode: "0640" + notify: + - "Reload Caddy" diff --git a/ansible/roles/bgwaf/templates/Caddyfile b/ansible/roles/waf_config/templates/Caddyfile similarity index 85% rename from ansible/roles/bgwaf/templates/Caddyfile rename to ansible/roles/waf_config/templates/Caddyfile index 40ea2a8..954bcf4 100644 --- a/ansible/roles/bgwaf/templates/Caddyfile +++ b/ansible/roles/waf_config/templates/Caddyfile @@ -2,13 +2,13 @@ # storage redis grace_period 10s order coraza_waf first -{% if bgwaf_email | default(None) != None %} - email "{{ bgwaf_email }}" +{% if waf_config_email | default(None) != None %} + email "{{ waf_config_email }}" {% endif %} -{% if bgwaf_ca_root | default(None) != None %} +{% if waf_config_ca_root | default(None) != None %} tls { - ca_root "{{ bgwaf_ca_root }}" + ca_root "{{ waf_config_ca_root }}" } {% endif %} @@ -17,17 +17,17 @@ include "http.handlers.waf" output file /var/log/caddy/waf.log } -{% if bgwaf_local_ca_name | default(None) != None %} +{% if waf_config_local_ca_name | default(None) != None %} pki { ca local { - name "{{ bgwaf_local_ca_name }}" + name "{{ waf_config_local_ca_name }}" } } {% endif %} } -{% for site in bgwaf_sites %} +{% for site in waf_config_sites %} {% if (site.redirect_from_aliases | default(true)) and (site.aliases | default([]) | length) > 0 %} {{ site.aliases | join(', ') }} { redir https://{{ site.site }}{uri} @@ -79,7 +79,7 @@ {% endif %} {% if site.bot_barrier | default (false) %} bot_barrier { - secret {{ bgwaf_bot_barrier_secret }} + secret {{ waf_config_bot_barrier_secret }} complexity 18 valid_for 30m seed_cookie_name __chall_{{ site.name }}_seed @@ -106,7 +106,7 @@ Include "/etc/caddy/plugins/*-config.conf" Include "/etc/caddy/plugins/*-before.conf" Include "/etc/caddy/sites/{{ site.name }}/exclusions-request-before.conf" - Include "/etc/caddy/coreruleset-{{ bgwaf_owasp_crs_version }}/rules/*.conf" + Include "/etc/caddy/coreruleset-{{ waf_config_owasp_crs_version }}/rules/*.conf" Include "/etc/caddy/sites/{{ site.name }}/exclusions-response-after.conf" Include "/etc/caddy/plugins/*-after.conf" SecRuleEngine On diff --git a/ansible/roles/bgwaf/templates/coraza.conf b/ansible/roles/waf_config/templates/coraza.conf similarity index 100% rename from ansible/roles/bgwaf/templates/coraza.conf rename to ansible/roles/waf_config/templates/coraza.conf diff --git a/ansible/roles/bgwaf/templates/crs-setup.conf b/ansible/roles/waf_config/templates/crs-setup.conf similarity index 64% rename from ansible/roles/bgwaf/templates/crs-setup.conf rename to ansible/roles/waf_config/templates/crs-setup.conf index de776bf..66bb2f4 100644 --- a/ansible/roles/bgwaf/templates/crs-setup.conf +++ b/ansible/roles/waf_config/templates/crs-setup.conf @@ -10,9 +10,9 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.blocking_paranoia_level={{ site.paranoia_level | default(bgwaf_defaults.paranoia_level) }}" -{% if site.detection_paranoia_level | default(bgwaf_defaults.detection_paranoia_level) != None %} + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.blocking_paranoia_level={{ site.paranoia_level | default(waf_config_defaults.paranoia_level) }}" +{% if site.detection_paranoia_level | default(waf_config_defaults.detection_paranoia_level) != None %} ## Detection paranoia level SecAction \ @@ -22,8 +22,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.detection_paranoia_level={{ site.detection_paranoia_level | default(bgwaf_defaults.detection_paranoia_level) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.detection_paranoia_level={{ site.detection_paranoia_level | default(waf_config_defaults.detection_paranoia_level) }}" {% endif %} ## Enforce Body Processor URLENCODED @@ -34,8 +34,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.enforce_bodyproc_urlencoded={{ site.enforce_bodyproc_urlencoded | default(bgwaf_defaults.enforce_bodyproc_urlencoded) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.enforce_bodyproc_urlencoded={{ site.enforce_bodyproc_urlencoded | default(waf_config_defaults.enforce_bodyproc_urlencoded) }}" ## Anomaly Scoring Mode Severity Levels SecAction \ @@ -45,11 +45,11 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.critical_anomaly_score={{ site.critical_anomaly_score | default(bgwaf_defaults.critical_anomaly_score) }},\ - setvar:tx.error_anomaly_score={{ site.error_anomaly_score | default(bgwaf_defaults.error_anomaly_score) }},\ - setvar:tx.warning_anomaly_score={{ site.warning_anomaly_score | default(bgwaf_defaults.warning_anomaly_score) }},\ - setvar:tx.notice_anomaly_score={{ site.notice_anomaly_score | default(bgwaf_defaults.notice_anomaly_score) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.critical_anomaly_score={{ site.critical_anomaly_score | default(waf_config_defaults.critical_anomaly_score) }},\ + setvar:tx.error_anomaly_score={{ site.error_anomaly_score | default(waf_config_defaults.error_anomaly_score) }},\ + setvar:tx.warning_anomaly_score={{ site.warning_anomaly_score | default(waf_config_defaults.warning_anomaly_score) }},\ + setvar:tx.notice_anomaly_score={{ site.notice_anomaly_score | default(waf_config_defaults.notice_anomaly_score) }}" ## Anomaly Scoring Mode Blocking Threshold Levels SecAction \ @@ -59,9 +59,9 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.inbound_anomaly_score_threshold={% if site.log_only | default(bgwaf_defaults.log_only) %}1000{% else %}{{ site.inbound_anomaly_score_threshold | default(bgwaf_defaults.inbound_anomaly_score_threshold) }}{% endif %},\ - setvar:tx.outbound_anomaly_score_threshold={% if site.log_only | default(bgwaf_defaults.log_only) %}1000{% else %}{{ site.outbound_anomaly_score_threshold | default(bgwaf_defaults.outbound_anomaly_score_threshold) }}{% endif %}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.inbound_anomaly_score_threshold={% if site.log_only | default(waf_config_defaults.log_only) %}1000{% else %}{{ site.inbound_anomaly_score_threshold | default(waf_config_defaults.inbound_anomaly_score_threshold) }}{% endif %},\ + setvar:tx.outbound_anomaly_score_threshold={% if site.log_only | default(waf_config_defaults.log_only) %}1000{% else %}{{ site.outbound_anomaly_score_threshold | default(waf_config_defaults.outbound_anomaly_score_threshold) }}{% endif %}" ## Application Specific Rule Exclusions # FIXME @@ -87,8 +87,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.reporting_level={{ site.reporting_level | default(bgwaf_defaults.reporting_level) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.reporting_level={{ site.reporting_level | default(waf_config_defaults.reporting_level) }}" ## Early Anomaly Scoring Mode Blocking SecAction \ @@ -98,7 +98,7 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ setvar:tx.early_blocking=0" ## Initialize Default Collections @@ -109,7 +109,7 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ setvar:tx.enable_default_collections=1" ## HTTP Policy Settings @@ -120,8 +120,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:'tx.allowed_methods={{ site.allowed_methods | default(bgwaf_defaults.allowed_methods) | join(" ") }}'" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:'tx.allowed_methods={{ site.allowed_methods | default(waf_config_defaults.allowed_methods) | join(" ") }}'" # Content-Types that a client is allowed to send in a request SecAction \ @@ -131,8 +131,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:'tx.allowed_request_content_type={{ site.allowed_request_content_type | default(bgwaf_defaults.allowed_request_content_type) | join(" ") }}'" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:'tx.allowed_request_content_type={{ site.allowed_request_content_type | default(waf_config_defaults.allowed_request_content_type) | join(" ") }}'" ## Allowed HTTP versions SecAction \ @@ -142,8 +142,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:'tx.allowed_http_versions={{ site.allowed_http_versions | default(bgwaf_defaults.allowed_http_versions) | join(" ") }}'" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:'tx.allowed_http_versions={{ site.allowed_http_versions | default(waf_config_defaults.allowed_http_versions) | join(" ") }}'" ## Forbidden file extensions SecAction \ @@ -153,8 +153,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:'tx.restricted_extensions={{ site.restricted_extensions | default(bgwaf_defaults.restricted_extensions) | join(" ") }}'" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:'tx.restricted_extensions={{ site.restricted_extensions | default(waf_config_defaults.restricted_extensions) | join(" ") }}'" ## Restricted request headers SecAction \ @@ -164,8 +164,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:'tx.restricted_headers_basic={{ site.restricted_headers_basic | default(bgwaf_defaults.restricted_headers_basic) | join(" ") }}'" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:'tx.restricted_headers_basic={{ site.restricted_headers_basic | default(waf_config_defaults.restricted_headers_basic) | join(" ") }}'" ## Extended restricted request headers (forbidden at a higher paranoia level) SecAction \ @@ -175,8 +175,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:'tx.restricted_headers_extended={{ site.restricted_headers_extended | default(bgwaf.restricted_headers_extended) | join(" ") }}'" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:'tx.restricted_headers_extended={{ site.restricted_headers_extended | default(waf_config_defaults.restricted_headers_extended) | join(" ") }}'" # Content-Types charsets that a client is allowed to send in a request SecAction \ @@ -186,8 +186,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:'tx.allowed_request_content_type_charset={{ bgwaf_defaults.allowed_request_content_type_charset | default(bgwaf_defaults.allowed_request_content_type_charset) | join(" ") }}'" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:'tx.allowed_request_content_type_charset={{ waf_config_defaults.allowed_request_content_type_charset | default(waf_config_defaults.allowed_request_content_type_charset) | join(" ") }}'" # Block request if number of arguments is too high SecAction \ @@ -197,8 +197,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.max_num_args={{ site.max_num_args | default(bgwaf_defaults.max_num_args) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.max_num_args={{ site.max_num_args | default(waf_config_defaults.max_num_args) }}" # Block request if the length of any argument name is too high SecAction \ @@ -208,8 +208,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.arg_name_length={{ site.arg_name_length | default(bgwaf_defaults.arg_name_length) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.arg_name_length={{ site.arg_name_length | default(waf_config_defaults.arg_name_length) }}" # Block request if the length of any argument value is too high SecAction \ @@ -219,8 +219,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.arg_length={{ site.arg_length | default(bgwaf_defaults.arg_length) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.arg_length={{ site.arg_length | default(waf_config_defaults.arg_length) }}" # Block request if the total length of all combined arguments is too high SecAction \ @@ -230,8 +230,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.total_arg_length={{ site.total_arg_length | default(bgwaf_defaults.total_arg_length) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.total_arg_length={{ site.total_arg_length | default(waf_config_defaults.total_arg_length) }}" # Block request if the file size of any individual uploaded file is too high SecAction \ @@ -241,8 +241,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.max_file_size={{ site.max_file_size | default(bgwaf_defaults.max_file_size) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.max_file_size={{ site.max_file_size | default(waf_config_defaults.max_file_size) }}" # Block request if the total size of all combined uploaded files is too high SecAction \ @@ -252,8 +252,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.combined_file_sizes={{ site.combined_file_sizes | default(bgwaf_defaults.combined_file_sizes) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.combined_file_sizes={{ site.combined_file_sizes | default(waf_config_defaults.combined_file_sizes) }}" ## Easing In / Sampling Percentage SecAction \ @@ -262,8 +262,8 @@ SecAction \ pass,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.sampling_percentage={{ site.sampling_percentage | default(bgwaf_defaults.sampling_percentage) }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.sampling_percentage={{ site.sampling_percentage | default(waf_config_defaults.sampling_percentage) }}" ## Check UTF-8 encoding SecAction \ @@ -273,7 +273,7 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ setvar:tx.crs_validate_utf8_encoding=1" ## Skip Checking Responses @@ -290,7 +290,7 @@ SecAction \ # t:none,\ # nolog,\ # tag:'OWASP_CRS',\ -# ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ +# ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ # setvar:tx.crs_skip_response_analysis=1" ## End of setup @@ -301,5 +301,5 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ bgwaf_owasp_crs_version }}',\ - setvar:tx.crs_setup_version={{ bgwaf_owasp_crs_version | regex_replace('\.', '') }}" + ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + setvar:tx.crs_setup_version={{ waf_config_owasp_crs_version | regex_replace('\.', '') }}" diff --git a/ansible/roles/bgwaf/templates/exclusions-request-before.conf b/ansible/roles/waf_config/templates/exclusions-request-before.conf similarity index 100% rename from ansible/roles/bgwaf/templates/exclusions-request-before.conf rename to ansible/roles/waf_config/templates/exclusions-request-before.conf diff --git a/ansible/roles/bgwaf/templates/exclusions-response-after.conf b/ansible/roles/waf_config/templates/exclusions-response-after.conf similarity index 100% rename from ansible/roles/bgwaf/templates/exclusions-response-after.conf rename to ansible/roles/waf_config/templates/exclusions-response-after.conf diff --git a/ansible/roles/bgwaf/.gitignore b/ansible/roles/waf_install/.gitignore similarity index 100% rename from ansible/roles/bgwaf/.gitignore rename to ansible/roles/waf_install/.gitignore diff --git a/ansible/roles/bgwaf/README.md b/ansible/roles/waf_install/README.md similarity index 100% rename from ansible/roles/bgwaf/README.md rename to ansible/roles/waf_install/README.md diff --git a/ansible/roles/waf_install/defaults/main.yml b/ansible/roles/waf_install/defaults/main.yml new file mode 100644 index 0000000..d3278a2 --- /dev/null +++ b/ansible/roles/waf_install/defaults/main.yml @@ -0,0 +1,27 @@ +waf_install_go_version: "1.25.5" +waf_install_xcaddy_version: "0.4.5" +waf_install_caddy_version: "2.10.2" +waf_install_coraza_caddy_version: "2.1.0" # Coraza v3.3.3 +waf_install_owasp_crs_version: "4.21.0" + +# Cluster mode (e.g. with consul) +waf_install_cluster: false + +# Cf.: https://github.com/coreruleset/plugin-registry +waf_install_crs_plugins: + - name: "drupal-rule-exclusions" + version: "1.0.0" + - name: "wordpress-rule-exclusions" + version: "1.2.0" + - name: "nextcloud-rule-exclusions" + version: "1.5.0" + - name: "dokuwiki-rule-exclusions" + version: "1.0.0" + - name: "phpmyadmin-rule-exclusions" + version: "1.0.0" + - name: "roundcube-rule-exclusions" + version: "1.0.4" + provider: "EsadCetiner" + - name: "sogo-rule-exclusions" + version: "1.0.4" + provider: "EsadCetiner" diff --git a/ansible/roles/bgwaf/handlers/main.yml b/ansible/roles/waf_install/handlers/main.yml similarity index 56% rename from ansible/roles/bgwaf/handlers/main.yml rename to ansible/roles/waf_install/handlers/main.yml index 252119e..500c16c 100644 --- a/ansible/roles/bgwaf/handlers/main.yml +++ b/ansible/roles/waf_install/handlers/main.yml @@ -3,8 +3,3 @@ name: "caddy" state: "restarted" daemon_reload: true - -- name: Reload Caddy - ansible.builtin.service: - name: "caddy" - state: "reloaded" diff --git a/ansible/roles/bgwaf/tasks/install_plugin.yml b/ansible/roles/waf_install/tasks/install_plugin.yml similarity index 74% rename from ansible/roles/bgwaf/tasks/install_plugin.yml rename to ansible/roles/waf_install/tasks/install_plugin.yml index 85a12d6..9299ebd 100644 --- a/ansible/roles/bgwaf/tasks/install_plugin.yml +++ b/ansible/roles/waf_install/tasks/install_plugin.yml @@ -3,27 +3,27 @@ url: "https://github.com/{{ plugin.provider | default('coreruleset') }}/{{ plugin.name }}-plugin/archive/refs/tags/v{{ plugin.version }}.tar.gz" dest: "/usr/local/src/" mode: "0644" - register: "bgwaf_plugin_downloaded" + register: "waf_install_plugin_downloaded" - name: Deploy plugin {{ plugin.name }} # noqa: no-handler - when: "bgwaf_plugin_downloaded.changed" + when: "waf_install_plugin_downloaded.changed" block: - name: Get temp dir {{ plugin.name }} ansible.builtin.tempfile: state: "directory" - register: "bgwaf_temp_plugin_dir" + register: "waf_install_temp_plugin_dir" - name: Unarchive plugin {{ plugin.name }} ansible.builtin.unarchive: src: "/usr/local/src/{{ plugin.name }}-plugin-{{ plugin.version }}.tar.gz" - dest: "{{ bgwaf_temp_plugin_dir.path }}" + dest: "{{ waf_install_temp_plugin_dir.path }}" remote_src: true - name: Copy plugin files {{ plugin.name }} ansible.builtin.copy: - src: "{{ bgwaf_temp_plugin_dir.path }}/{{ plugin.name }}-plugin-{{ plugin.version }}/plugins/" + src: "{{ waf_install_temp_plugin_dir.path }}/{{ plugin.name }}-plugin-{{ plugin.version }}/plugins/" dest: "/etc/caddy/plugins/" mode: "0644" # directory_mode: "0750" @@ -41,7 +41,7 @@ mode: "0750" - name: Delete temp dir {{ plugin.name }} - when: "bgwaf_temp_plugin_dir.path is defined" + when: "waf_install_temp_plugin_dir.path is defined" ansible.builtin.file: - path: "{{ bgwaf_temp_plugin_dir.path }}" + path: "{{ waf_install_temp_plugin_dir.path }}" state: "absent" diff --git a/ansible/roles/waf_install/tasks/main.yml b/ansible/roles/waf_install/tasks/main.yml new file mode 100644 index 0000000..0e15b0b --- /dev/null +++ b/ansible/roles/waf_install/tasks/main.yml @@ -0,0 +1,125 @@ +- name: Remove packaged Go + ansible.builtin.apt: + name: + - "golang" + state: "absent" + autoremove: true + purge: true + +- name: Install requirements + ansible.builtin.apt: + name: + - "git" + - "libnss3-tools" + +- name: Create Go directory + ansible.builtin.file: + path: "/usr/local/go-{{ waf_install_go_version }}" + state: "directory" + mode: "0755" + +- name: Install Go + ansible.builtin.unarchive: + src: "https://go.dev/dl/go{{ waf_install_go_version }}.linux-amd64.tar.gz" + dest: "/usr/local/go-{{ waf_install_go_version }}" + remote_src: true + +- name: Link Go + ansible.builtin.file: + src: "/usr/local/go-{{ waf_install_go_version }}/go/bin/go" + dest: "/usr/local/bin/go" + state: "link" + +# FIXME: no .deb +- name: Install xcaddy + when: | + 'xcaddy' not in ansible_facts.packages + or ansible_facts.packages['xcaddy'][0]['version'] != waf_install_xcaddy_version + ansible.builtin.apt: + deb: "{{ waf_install_xcaddy_download }}/v{{ waf_install_xcaddy_version }}/xcaddy_{{ waf_install_xcaddy_version }}_linux_amd64.deb" + force: true + +- name: Create Caddy build command + ansible.builtin.set_fact: + waf_install_build_command: > + xcaddy build v{{ waf_install_caddy_version }} + {% if waf_install_cluster %} --with github.com/pteich/caddy-tlsconsul{% endif %} + --with github.com/corazawaf/coraza-caddy/v2@v{{ waf_install_coraza_caddy_version }} + --with github.com/mholt/caddy-ratelimit + --with github.com/steffenbusch/caddy-bot-barrier + --output /usr/local/bin/caddy + +- name: Test Caddy version + ansible.builtin.copy: + dest: "/usr/local/bin/.caddy.build" + content: "{{ waf_install_build_command }}" + mode: "0644" + register: waf_install_test_caddy_version + +- name: Build Caddy + # noqa: no-handler + when: "waf_install_test_caddy_version.changed" + ansible.builtin.command: + cmd: "{{ waf_install_build_command }}" + creates: "/usr/local/bin/caddy" + notify: + - "Restart Caddy" + +- name: Create caddy system group + ansible.builtin.group: + name: "caddy" + system: true + +- name: Create caddy system user + ansible.builtin.user: + name: "caddy" + group: "caddy" + groups: ["ssl-cert"] + password: "!" + system: true + home: "/opt/caddy" + +- name: Create caddy config directories + ansible.builtin.file: + path: "{{ item }}" + state: "directory" + group: "caddy" + mode: "0750" + loop: + - "/etc/caddy" + - "/etc/caddy/plugins" + - "/etc/caddy/sites" + +- name: Create caddy log dir + ansible.builtin.file: + path: "/var/log/caddy" + state: "directory" + group: "caddy" + mode: "0770" + +- name: Install OWASP CRS + ansible.builtin.unarchive: + src: "https://github.com/coreruleset/coreruleset/archive/refs/tags/v{{ waf_install_owasp_crs_version }}.tar.gz" + dest: "/etc/caddy" + group: "caddy" + remote_src: true + +- name: Install plugins + loop: "{{ waf_install_crs_plugins }}" + loop_control: + loop_var: "plugin" + ansible.builtin.include_tasks: "install_plugin.yml" + +- name: Deploy caddy service file + ansible.builtin.template: + src: "templates/caddy.service" + dest: "/etc/systemd/system/caddy.service" + mode: "0644" + notify: + - "Restart Caddy" + +- name: Enable Caddy + ansible.builtin.service: + name: "caddy" + enabled: true + daemon_reload: true diff --git a/ansible/roles/bgwaf/templates/caddy.service b/ansible/roles/waf_install/templates/caddy.service similarity index 100% rename from ansible/roles/bgwaf/templates/caddy.service rename to ansible/roles/waf_install/templates/caddy.service diff --git a/ansible/roles/waf_install/vars/main.yml b/ansible/roles/waf_install/vars/main.yml new file mode 100644 index 0000000..5db1438 --- /dev/null +++ b/ansible/roles/waf_install/vars/main.yml @@ -0,0 +1 @@ +waf_install_xcaddy_download: "https://github.com/caddyserver/xcaddy/releases/download" diff --git a/ansible/vars.yml b/ansible/vars/common.yml similarity index 59% rename from ansible/vars.yml rename to ansible/vars/common.yml index fa4b5c5..b3dfcd6 100644 --- a/ansible/vars.yml +++ b/ansible/vars/common.yml @@ -1,10 +1,15 @@ -vault: - ca: - passphrase: "plopplop" - consul: - secret_key: "MZu7pu72IxVkGzSjkB0ZGd+RTp5n34ByxOoxB+BKaSc=" +common: + + certificates: + country_name: "FR" + locality_name: "Vannes" + organization_name: "iTik" + email_address: "ca@itik.fr" + + consul: + domain: "itik.fr" + datacenter: "lab" -waf: sites: - name: "test" site: "test.lab" @@ -13,12 +18,9 @@ waf: - "http://www1" - "http://www2" - "http://www3" - log_only: true -# paranoia_level: 1 -# detection_paranoia_level: null custom_cert: true - custom_cert_file: "/etc/ssl/certs/local.crt" - custom_key_file: "/etc/ssl/private/local.key" + custom_cert_file: "/etc/ssl/certs/test.lab.crt" + custom_key_file: "/etc/ssl/private/test.lab.key" # exceptions: # before_request: # - 'SecRule REQUEST_URI "@beginsWith /?bin/sh" "id:1,nolog,ctl:ruleRemoveById=932160"' diff --git a/ansible/vars/secrets.yml b/ansible/vars/secrets.yml new file mode 100644 index 0000000..1a204b2 --- /dev/null +++ b/ansible/vars/secrets.yml @@ -0,0 +1,13 @@ +$ANSIBLE_VAULT;1.1;AES256 +61643639653530613066383464333939383034643565386434616361653331656237303835653837 +3431646133336265393237383366653364373039376365340a396438643530383965343865383837 +62636338366437356661356561356138383032353133396461643362323536303965353564666232 +3735363038323139620a386364633933303139623436626337346535663737386264303036323866 +31303762336638313961383034386137376566383662323961336536303230303063396661333135 +66646133333565656233303461356166303862303037663133383534613263383130316261643738 +65656165346432636363393436646532643531393066346332326561656230316232383866643665 +36626333656335626534333737633231396561633831653464383036323431616431306433623139 +61346630663636313463376237353036343264613936366539393165623265656233303864386563 +63386661343061373930383139376666353035373531386230306432663132653130633039616665 +38383735326339353738383166623932316564636634343830326432613563306436666532313562 +65326431346639636338 diff --git a/tofu/.gitignore b/tofu/.gitignore index 937e770..56cf6fa 100644 --- a/tofu/.gitignore +++ b/tofu/.gitignore @@ -1,2 +1,4 @@ /.terraform/ /.terraform.lock.hcl +/terraform.tfstate +/terraform.tfstate.backup diff --git a/tofu/terraform.tf b/tofu/_t/terraform.tf similarity index 100% rename from tofu/terraform.tf rename to tofu/_t/terraform.tf diff --git a/tofu/terraform.tfstate b/tofu/_t/terraform.tfstate similarity index 95% rename from tofu/terraform.tfstate rename to tofu/_t/terraform.tfstate index 863a1e5..d7b9b87 100644 --- a/tofu/terraform.tfstate +++ b/tofu/_t/terraform.tfstate @@ -1 +1 @@ -{"version":4,"terraform_version":"1.11.2","serial":31,"lineage":"5ba1e29d-946a-a982-e845-5fae39bcee6e","outputs":{},"resources":[{"mode":"managed","type":"multipass_instance","name":"this","provider":"provider[\"registry.opentofu.org/larstobi/multipass\"]","instances":[{"index_key":"proxy1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"20g","image":"24.04","memory":"8g","name":"proxy1"},"sensitive_attributes":[]},{"index_key":"waf1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"waf1"},"sensitive_attributes":[]},{"index_key":"waf2","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"waf2"},"sensitive_attributes":[]},{"index_key":"waf3","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"waf3"},"sensitive_attributes":[]},{"index_key":"www1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www1"},"sensitive_attributes":[]},{"index_key":"www2","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www2"},"sensitive_attributes":[]},{"index_key":"www3","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www3"},"sensitive_attributes":[]}]}],"check_results":null} +{"version":4,"terraform_version":"1.11.2","serial":37,"lineage":"5ba1e29d-946a-a982-e845-5fae39bcee6e","outputs":{},"resources":[{"mode":"managed","type":"multipass_instance","name":"this","provider":"provider[\"registry.opentofu.org/larstobi/multipass\"]","instances":[{"index_key":"proxy1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"20g","image":"24.04","memory":"8g","name":"proxy1"},"sensitive_attributes":[]},{"index_key":"waf1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"waf1"},"sensitive_attributes":[]},{"index_key":"waf2","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"waf2"},"sensitive_attributes":[]},{"index_key":"waf3","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"waf3"},"sensitive_attributes":[]},{"index_key":"www1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www1"},"sensitive_attributes":[]},{"index_key":"www2","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www2"},"sensitive_attributes":[]},{"index_key":"www3","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www3"},"sensitive_attributes":[]}]}],"check_results":null} diff --git a/tofu/_t/terraform.tfstate.backup b/tofu/_t/terraform.tfstate.backup new file mode 100644 index 0000000..d750a1a --- /dev/null +++ b/tofu/_t/terraform.tfstate.backup @@ -0,0 +1 @@ +{"version":4,"terraform_version":"1.11.2","serial":36,"lineage":"5ba1e29d-946a-a982-e845-5fae39bcee6e","outputs":{},"resources":[{"mode":"managed","type":"multipass_instance","name":"this","provider":"provider[\"registry.opentofu.org/larstobi/multipass\"]","instances":[{"index_key":"proxy1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"20g","image":"24.04","memory":"8g","name":"proxy1"},"sensitive_attributes":[]},{"index_key":"waf3","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"waf3"},"sensitive_attributes":[]},{"index_key":"www1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www1"},"sensitive_attributes":[]},{"index_key":"www2","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www2"},"sensitive_attributes":[]},{"index_key":"www3","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www3"},"sensitive_attributes":[]}]}],"check_results":null} diff --git a/tofu/terraform.tfstate.backup b/tofu/terraform.tfstate.backup deleted file mode 100644 index dd3771f..0000000 --- a/tofu/terraform.tfstate.backup +++ /dev/null @@ -1 +0,0 @@ -{"version":4,"terraform_version":"1.11.2","serial":30,"lineage":"5ba1e29d-946a-a982-e845-5fae39bcee6e","outputs":{},"resources":[{"mode":"managed","type":"multipass_instance","name":"this","provider":"provider[\"registry.opentofu.org/larstobi/multipass\"]","instances":[{"index_key":"proxy1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"20g","image":"24.04","memory":"8g","name":"proxy1"},"sensitive_attributes":[]},{"index_key":"waf2","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"waf2"},"sensitive_attributes":[]},{"index_key":"waf3","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"waf3"},"sensitive_attributes":[]},{"index_key":"www1","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www1"},"sensitive_attributes":[]},{"index_key":"www2","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www2"},"sensitive_attributes":[]},{"index_key":"www3","schema_version":0,"attributes":{"cloudinit_file":"cloud-init.yml","cpus":2,"disk":"12g","image":"24.04","memory":"2g","name":"www3"},"sensitive_attributes":[]}]}],"check_results":null}