diff --git a/.gitignore b/.gitignore index aa77138..c608261 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -.DS_Store .ansible/ +__pycache__/ +.DS_Store diff --git a/README.md b/README.md index 33768d1..f516595 100644 --- a/README.md +++ b/README.md @@ -67,58 +67,26 @@ Voir: https://github.com/coreruleset/plugin-registry Fonctionnalités déjà implémentées: +- Déploiement et configuration avec Ansible +- Serveurs transient +- VIP (avec Keepalived) +- DNS dynamique (via Nomad) +- Cluster via Nomad - OWASP CRS -- Multisite (cavec configuration des CRS par site) -- Redirection vers nom canonique -- Rediction HTTP vers HTTPS -- Certificats SSL (automatiques via Let's Encrypt et customs) -- Redirect (SSL et www et autres) +- Multisite (avec configuration complète des CRS par site) +- Alias avec redirection optionnelle vers nom canonique +- Redirection HTTP vers HTTPS +- Certificats SSL (personnalisés automatiques via Let's Encrypt) - Filtrage par: - adresses IP - réseaux IP - Antibot par: - défi JavaScript - Passive backend checks -- Compression Gzip et zstd -- Mieux tester le configuration des CRS +- Compression Gzip et zstd - Mode détection sans blocage - Filtrage par: - méthode HTTP - -Urgemment: - -- Connexion à consul -- ACL consul -- Changement de format de configuration: Caddyfile -> JSON -- Lire doc. config JSON de Caddy -- Renommer tous les répertoires/utilisateurs caddy -> byow -- Collection Ansible - -Fomctionnalités à ajouter: - -- Active backend checks -- Antibot par: - - Cookie - - Captcha - - reCAPTCHA - - hCaptcha - - Turnstile - - mCaptcha -- Mise en œuvre d'un cluster Redis (ou Consul) pour le partage de informations entre les WAF d'un cluster -- Filtrage par: - - rDNS - - DNSBL - - ASN - - User-Agent (robots, crawlers, IA bots) - - taille - - URI patterns - - pays - - (ip:danmeuk-tor-exit, ua:mitchellkrogza-bad-user-agents, /etc/hapee-1.9/blacklist.acl) -- Gestion CORS -- Client cache -- Proxy cache -- Filtrage par pays -- Compression Brotli - En-têtes de sécurité: - HSTS - CSP @@ -129,17 +97,54 @@ Fomctionnalités à ajouter: - Keep Headers - Cookie Flags - Custom headers + - suppression d'en-têtes +- Collecte des logs via OVH LDP + +Urgemment: + +- Connexion à consul, ACL consul +- Collection Ansible +- Gestion CORS +- Active backend checks +- Correction de la collecte des logs via OVH LDP + +Fomctionnalités à ajouter: + +- Rate limiting distribué +- Antibot par: + - Cookie + - Captcha + - reCAPTCHA + - hCaptcha + - Turnstile + - mCaptcha +- Filtrage par: + - rDNS + - DNSBL + - ASN + - User-Agent (robots, crawlers, IA bots) + - taille + - URI patterns + - pays + - (ip:danmeuk-tor-exit, ua:mitchellkrogza-bad-user-agents, /etc/hapee-1.9/blacklist.acl) +- Client cache +- Proxy cache +- Filtrage par pays +- Compression Brotli - Reverse scan - Robots.txt (DarkVisitors API/Community Lists/Custom URLs/Manual Rules) -- Sessions +- Gestion des sessions - Whitelist - Bad behavior -- Auth +- Authentification - Anti DDoS - Greylist - HTML injection - Metrics (Elasticsearch + Metabase) -- Static files et FastCGI pour PHP +- Static files et FastCGI pour PHP (est-ce une bonne idée ?!) +- Lire doc. config JSON de Caddy +- Crowdsec +- https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master/_generator_lists/bad-user-agents.list ## Ressources diff --git a/ansible/filter_plugins/filters.py b/ansible/filter_plugins/filters.py new file mode 100644 index 0000000..8e7cfbc --- /dev/null +++ b/ansible/filter_plugins/filters.py @@ -0,0 +1,21 @@ +#!/usr/bin/python + +class FilterModule(object): + def filters(self): + return { + 'dict2str': self.dict2str + } + + def dict2str(self, d, sep1='=', sep2=';', sep3=''): + return sep2.join( + [ str(k) + (sep1+str(v) if v!=None and v!='' else '') for k, v in d.items() ] + ) + sep3 + + +if __name__ == "__main__": + # Tests + filters = FilterModule() + print(filters.filters()) + print(filters.dict2str( + {"un": 1, "deux": 2, "str": "a string", "rien": None, "bool": True} + )) diff --git a/ansible/play-config.yml b/ansible/play-config.yml index 90f1220..9a70a67 100644 --- a/ansible/play-config.yml +++ b/ansible/play-config.yml @@ -16,9 +16,10 @@ - "DNS:{{ ansible_fqdn }}" - "DNS:{{ ansible_hostname }}" - "IP:{{ ansible_default_ipv4.address }}" - waf_config_sites: "{{ common.sites }}" + caddy_config_only: true + caddy_sites: "{{ common.sites }}" roles: - "local_ca_certs" - - "waf_config" + - "caddy" tags: - "waf" diff --git a/ansible/play-main.yml b/ansible/play-main.yml index f3f181f..6102503 100644 --- a/ansible/play-main.yml +++ b/ansible/play-main.yml @@ -98,15 +98,14 @@ unbound_stub_zones: - name: "{{ common.consul.datacenter }}.{{ common.consul.domain }}" stub_to: "127.0.0.1@8600" - waf_config_sites: "{{ common.sites }}" + caddy_sites: "{{ common.sites }}" ovh_ldp_cluster: "gra2.logs.ovh.com" ovh_ldp_token: ¨{{ secrets.ovh_ldp.token }} roles: - "local_ca_certs" - "consul" - - "waf_install" + - "caddy" # - "keepalived" - - "waf_config" - "unbound" - "ovh_ldp" tags: diff --git a/ansible/roles/waf_install/.gitignore b/ansible/roles/caddy/.gitignore similarity index 100% rename from ansible/roles/waf_install/.gitignore rename to ansible/roles/caddy/.gitignore diff --git a/ansible/roles/waf_install/README.md b/ansible/roles/caddy/README.md similarity index 100% rename from ansible/roles/waf_install/README.md rename to ansible/roles/caddy/README.md diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml new file mode 100644 index 0000000..aa799e8 --- /dev/null +++ b/ansible/roles/caddy/defaults/main.yml @@ -0,0 +1,407 @@ +# Defaults +caddy_config_only: false +caddy_go_version: "1.25.5" +caddy_xcaddy_version: "0.4.5" +caddy_caddy_version: "2.10.2" +caddy_coraza_caddy_version: "2.1.0" # Coraza v3.3.3 +caddy_owasp_crs_version: "4.22.0" +caddy_system_user: "byow" +caddy_system_group: "byow" +caddy_home_dir: "/opt/{{ caddy_system_user }}" +caddy_config_dir: "/etc/{{ caddy_system_user }}" +caddy_log_dir: "/var/log/{{ caddy_system_user }}" + +# Cf.: https://github.com/coreruleset/plugin-registry +caddy_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" + has_after_config: true + - name: "roundcube-rule-exclusions" + version: "1.0.4" + provider: "EsadCetiner" + - name: "sogo-rule-exclusions" + version: "1.0.4" + provider: "EsadCetiner" + +# Required unless not site.custom_cert +caddy_email: null + +# Required if site.bot_barrier +caddy_bot_barrier_secret: null + +# Optionnal +caddy_local_ca_name: null +caddy_ca_root: null + +caddy_sites: [] +# - name: "" +# site: "" +# paths: +# - path: "*" +# addrs: +# - "" +# blacklist: +# - "" +# aliases: +# - "" +# crs_exceptions: +# before_request: +# - "" +# after_response: +# - "" + +caddy_waf_defaults: + redirect_from_aliases: true + custom_cert: false + custom_cert_file: "" + custom_key_file: "" + + # Security headers + header_content_security_policy: + + # https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html#detailed-csp-directives + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy#directives + ## Fetch directives + child-src: "'self'" + connect-src: "'self'" + font-src: "'self'" + img-src: "'self'" + manifest-src: "'self'" + media-src: "'self'" + # Deprecated: prefetch-src: "'self'" + object-src: "'none'" + script-src: "'none'" + script-src-attr: "'none'" + script-src-elem: "'none'" + style-src: "'self'" + style-src-attr: "'self'" + style-src-elem: "'self'" + default-src: "'self'" + ## Document directives + # base-uri + # plugin-types + # sandbox + ## Navigation directives + form-action: "'self'" + frame-ancestors: "'self'" + # https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-src + frame-src: "'self'" + # https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/worker-src + worker-src: "'self'" + # https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/block-all-mixed-content + block-all-mixed-content: + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/fenced-frame-src + fenced-frame-src: "'self'" + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/upgrade-insecure-requests + upgrade-insecure-requests: + + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy + header_cross_origin_embedder_policy: "require-corp" + + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Opener-Policy + header_cross_origin_opener_policy: "same-origin" + + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Resource-Policy + header_cross_origin_resource_policy: "same-site" + + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy + header_permissions_policy: + accelerometer: "()" + ambient-light-sensor: "()" + aria-notify: "()" + attribution-reporting: "()" + autoplay: "()" + bluetooth: "()" + browsing-topics: "()" + camera: "()" + captured-surface-control: "()" + compute-pressure: "()" + cross-origin-isolated: "()" + deferred-fetch: "()" + deferred-fetch-minimal: "()" + display-capture: "()" + encrypted-media: "()" + fullscreen: "()" + gamepad: "()" + geolocation: "()" + gyroscope: "()" + hid: "()" + identity-credentials-get: "()" + idle-detection: "()" + language-detector: "()" + local-fonts: "()" + magnetometer: "()" + microphone: "()" + midi: "()" + on-device-speech-recognition: "()" + otp-credentials: "()" + payment: "()" + picture-in-picture: "()" + private-state-token-issuance: "()" + private-state-token-redemption: "()" + publickey-credentials-create: "()" + publickey-credentials-get: "()" + screen-wake-lock: "()" + serial: "()" + speaker-selection: "()" + storage-access: "()" + translator: "()" + summarizer: "()" + usb: "()" + web-share: "()" + window-management: "()" + xr-spatial-tracking: "()" + + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#floc-federated-learning-of-cohorts + interest-cohort: "()" + + # ??? + battery: "()" + document-domain: "()" + execution-while-not-rendered: "()" + execution-while-out-of-viewport: "()" + navigation-override: "()" + + # https://www.geeksforgeeks.org/computer-networks/http-headers-feature-policy/ + header_feature_policy: + accelerometer: "'none'" + ambient-light-sensor: "'none'" + autoplay: "'none'" + battery: "'none'" + camera: "'none'" + display-capture: "'none'" + document-domain: "'none'" + encrypted-media: "'none'" + fullscreen: "'none'" + geolocation: "'none'" + gyroscope: "'none'" + layout-animation: "'none'" + legacy-image-formats: "'none'" + magnetometer: "'none'" + microphone: "'none'" + midi: "'none'" + oversized-images: "'none'" + payment: "'none'" + picture-in-picture: "'none'" + publickey-credentials-get: "'none'" + sync-xhr: "'none'" + usb: "'none'" + screen-wake-lock: "'none'" + xr-spatial-tracking: "'none'" + + # ??? + execution-while-not-rendered: "'none'" + execution-while-out-of-viewport: "'none'" + navigation-override: "'none'" + speaker-selection: "'none'" + unoptimized-images: "'none'" + unsized-media: "'none'" + web-share: "'none'" + + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy + header_referrer_policy: "strict-origin-when-cross-origin" + + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html + header_strict_transport_security: "max-age=2635200; includeSubDomains" + + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-content-type-options + header_x_content_type_options: "nosniff" + + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-frame-options + header_x_frame_options: "DENY" + + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-robots-tag + header_x_robots_tag: "noindex, nofollow" + + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#content-type + default_content_type: "text/html; charset=UTF-8" + + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#set-cookie + cookies_attributes: "SameSite=None; Secure;" + + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-dns-prefetch-control + # FIXME + + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#server + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-powered-by + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-aspnet-version + # https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-aspnetmvc-version + delete_headers: + - "Server" + - "X-Powered-By" + - "X-AspNet-Version" + - "X-AspNetMvc-Version" + - "Via" + + custom_headers: {} +# - name: +# value: + + # caddy-ratelimit + rate_events: 1000 + rate_window: "1m" + + # steffenbusch/caddy-bot-barrier + bot_barrier: false + + # corazawaf/coraza-caddy + crs_plugins: [] + log_only: false + paranoia_level: 1 + detection_paranoia_level: null + enforce_bodyproc_urlencoded: 1 + critical_anomaly_score: 5 + error_anomaly_score: 4 + warning_anomaly_score: 3 + notice_anomaly_score: 2 + inbound_anomaly_score_threshold: 5 + outbound_anomaly_score_threshold: 4 + reporting_level: 4 + max_num_args: 255 + arg_name_length: 100 + arg_length: 1000 + total_arg_length: 64000 + max_file_size: 10485760 # 10 Mo + combined_file_sizes: 20970520 # 20 Mo + sampling_percentage: 100 + allowed_methods: + - "GET" + - "HEAD" + - "POST" + - "OPTIONS" + allowed_request_content_type: + - "|application/x-www-form-urlencoded|" + - "|multipart/form-data|" + - "|text/xml|" + - "|application/xml|" + - "|application/soap+xml|" + - "|application/json|" + - "|application/reports+json|" + - "|application/csp-report|" + restricted_extensions: + - ".ani/" + - ".asa/" + - ".asax/" + - ".ascx/" + - ".back/" + - ".backup/" + - ".bak/" + - ".bck/" + - ".bk/" + - ".bkp/" + - ".bat/" + - ".cdx/" + - ".cer/" + - ".cfg/" + - ".cmd/" + - ".cnf/" + - ".com/" + - ".compositefont/" + - ".config/" + - ".conf/" + - ".copy/" + - ".crt/" + - ".cs/" + - ".csproj/" + - ".csr/" + - ".dat/" + - ".db/" + - ".dbf/" + - ".dist/" + - ".dll/" + - ".dos/" + - ".dpkg-dist/" + - ".drv/" + - ".gadget/" + - ".hta/" + - ".htr/" + - ".htw/" + - ".ida/" + - ".idc/" + - ".idq/" + - ".inc/" + - ".inf/" + - ".ini/" + - ".jse/" + - ".key/" + - ".licx/" + - ".lnk/" + - ".log/" + - ".mdb/" + - ".msc/" + - ".ocx/" + - ".old/" + - ".pass/" + - ".pdb/" + - ".pfx/" + - ".pif/" + - ".pem/" + - ".pol/" + - ".prf/" + - ".printer/" + - ".pwd/" + - ".rdb/" + - ".rdp/" + - ".reg/" + - ".resources/" + - ".resx/" + - ".sav/" + - ".save/" + - ".scr/" + - ".sct/" + - ".sh/" + - ".shs/" + - ".sql/" + - ".sqlite/" + - ".sqlite3/" + - ".swp/" + - ".sys/" + - ".temp/" + - ".tlb/" + - ".tmp/" + - ".vb/" + - ".vbe/" + - ".vbs/" + - ".vbproj/" + - ".vsdisco/" + - ".vxd/" + - ".webinfo/" + - ".ws/" + - ".wsc/" + - ".wsf/" + - ".wsh/" + - ".xsd/" + - ".xsx/" + restricted_headers_basic: + - "/content-encoding/" + - "/proxy/" + - "/lock-token/" + - "/content-range/" + - "/if/" + - "/x-http-method-override/" + - "/x-http-method/" + - "/x-method-override/" + - "/x-middleware-subrequest/" + - "/expect/" + restricted_headers_extended: + - "/accept-charset/" + allowed_http_versions: + - "HTTP/1.0" + - "HTTP/1.1" + - "HTTP/2" + - "HTTP/2.0" + - "HTTP/3" + - "HTTP/3.0" + allowed_request_content_type_charset: + - "|utf-8|" + - "|iso-8859-1|" + - "|iso-8859-15|" + - "|windows-1252|" diff --git a/ansible/roles/waf_config/files/bot_barrier_template.html b/ansible/roles/caddy/files/bot_barrier_template.html similarity index 100% rename from ansible/roles/waf_config/files/bot_barrier_template.html rename to ansible/roles/caddy/files/bot_barrier_template.html diff --git a/ansible/roles/waf_config/handlers/main.yml b/ansible/roles/caddy/handlers/main.yml similarity index 66% rename from ansible/roles/waf_config/handlers/main.yml rename to ansible/roles/caddy/handlers/main.yml index 117dd8c..1ddbd4b 100644 --- a/ansible/roles/waf_config/handlers/main.yml +++ b/ansible/roles/caddy/handlers/main.yml @@ -1,4 +1,4 @@ - name: Restart Caddy ansible.builtin.service: - name: "caddy" + name: "{{ caddy_system_user }}" state: "restarted" diff --git a/ansible/roles/waf_config/tasks/main.yml b/ansible/roles/caddy/tasks/caddy_config.yml similarity index 58% rename from ansible/roles/waf_config/tasks/main.yml rename to ansible/roles/caddy/tasks/caddy_config.yml index f792aec..e4179c3 100644 --- a/ansible/roles/waf_config/tasks/main.yml +++ b/ansible/roles/caddy/tasks/caddy_config.yml @@ -1,55 +1,55 @@ - name: Create sites config directories - loop: "{{ waf_config_sites }}" + loop: "{{ caddy_sites }}" ansible.builtin.file: - path: "/etc/caddy/sites/{{ item.name }}" + path: "{{ caddy_config_dir }}/sites/{{ item.name }}" state: "directory" - group: "caddy" + group: "{{ caddy_system_group }}" mode: "0750" - name: Generate sites coraza.conf config file - loop: "{{ waf_config_sites }}" + loop: "{{ caddy_sites }}" loop_control: loop_var: "site" ansible.builtin.template: src: "templates/coraza.conf" - dest: "/etc/caddy/sites/{{ site.name }}/coraza.conf" - group: "caddy" + dest: "{{ caddy_config_dir }}/sites/{{ site.name }}/coraza.conf" + group: "{{ caddy_system_group }}" mode: "0640" notify: - "Restart Caddy" - name: Generate sites crs-setup config file - loop: "{{ waf_config_sites }}" + loop: "{{ caddy_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" + dest: "{{ caddy_config_dir }}/sites/{{ site.name }}/crs-setup.conf" + group: "{{ caddy_system_group }}" mode: "0640" notify: - "Restart Caddy" - name: Generate sites exclusions before request file - loop: "{{ waf_config_sites }}" + loop: "{{ caddy_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" + dest: "{{ caddy_config_dir }}/sites/{{ site.name }}/exclusions-request-before.conf" + group: "{{ caddy_system_group }}" mode: "0640" notify: - "Restart Caddy" - name: Generate sites exclusions after response file - loop: "{{ waf_config_sites }}" + loop: "{{ caddy_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" + dest: "{{ caddy_config_dir }}/sites/{{ site.name }}/exclusions-response-after.conf" + group: "{{ caddy_system_group }}" mode: "0640" notify: - "Restart Caddy" @@ -57,8 +57,8 @@ - name: Generate Caddyfile ansible.builtin.template: src: "templates/Caddyfile" - dest: "/etc/caddy/Caddyfile" - group: "caddy" + dest: "{{ caddy_config_dir }}/Caddyfile" + group: "{{ caddy_system_group }}" mode: "0640" notify: - "Restart Caddy" @@ -66,8 +66,8 @@ - name: Copy bot barrier template ansible.builtin.copy: src: "files/bot_barrier_template.html" - dest: "/etc/caddy/bot_barrier_template.html" - group: "caddy" + dest: "{{ caddy_config_dir }}/bot_barrier_template.html" + group: "{{ caddy_system_group }}" mode: "0640" notify: - "Restart Caddy" diff --git a/ansible/roles/waf_install/tasks/main.yml b/ansible/roles/caddy/tasks/caddy_install.yml similarity index 54% rename from ansible/roles/waf_install/tasks/main.yml rename to ansible/roles/caddy/tasks/caddy_install.yml index 0060eaa..58b5803 100644 --- a/ansible/roles/waf_install/tasks/main.yml +++ b/ansible/roles/caddy/tasks/caddy_install.yml @@ -11,49 +11,50 @@ name: - "git" - "libnss3-tools" + - "ssl-cert" - name: Create Go directory ansible.builtin.file: - path: "/usr/local/go-{{ waf_install_go_version }}" + path: "/usr/local/go-{{ caddy_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 }}" + src: "https://go.dev/dl/go{{ caddy_go_version }}.linux-amd64.tar.gz" + dest: "/usr/local/go-{{ caddy_go_version }}" remote_src: true - name: Link Go ansible.builtin.file: - src: "/usr/local/go-{{ waf_install_go_version }}/go/bin/go" + src: "/usr/local/go-{{ caddy_go_version }}/go/bin/go" dest: "/usr/local/bin/go" state: "link" - name: Create xcaddy directory ansible.builtin.file: - path: "/usr/local/xcaddy-{{ waf_install_xcaddy_version }}" + path: "/usr/local/xcaddy-{{ caddy_xcaddy_version }}" state: "directory" mode: "0755" - name: Install xcaddy ansible.builtin.unarchive: - src: "{{ waf_install_xcaddy_download }}/v{{ waf_install_xcaddy_version }}/xcaddy_{{ waf_install_xcaddy_version }}_linux_amd64.tar.gz" - dest: "/usr/local/xcaddy-{{ waf_install_xcaddy_version }}" + src: "{{ caddy_xcaddy_download }}/v{{ caddy_xcaddy_version }}/xcaddy_{{ caddy_xcaddy_version }}_linux_amd64.tar.gz" + dest: "/usr/local/xcaddy-{{ caddy_xcaddy_version }}" remote_src: true - name: Link xcaddy ansible.builtin.file: - src: "/usr/local/xcaddy-{{ waf_install_xcaddy_version }}/xcaddy" + src: "/usr/local/xcaddy-{{ caddy_xcaddy_version }}/xcaddy" dest: "/usr/local/bin/xcaddy" state: "link" - name: Create Caddy build command ansible.builtin.set_fact: - waf_install_build_command: > - xcaddy build v{{ waf_install_caddy_version }} + caddy_build_command: > + xcaddy build v{{ caddy_caddy_version }} --with github.com/pteich/caddy-tlsconsul - --with github.com/corazawaf/coraza-caddy/v2@v{{ waf_install_coraza_caddy_version }} + --with github.com/corazawaf/coraza-caddy/v2@v{{ caddy_coraza_caddy_version }} --with github.com/mholt/caddy-ratelimit --with github.com/steffenbusch/caddy-bot-barrier --output /usr/local/bin/caddy @@ -61,74 +62,74 @@ - name: Test Caddy version ansible.builtin.copy: dest: "/usr/local/bin/.caddy.build" - content: "{{ waf_install_build_command }}" + content: "{{ caddy_build_command }}" mode: "0644" - register: waf_install_test_caddy_version + register: caddy_test_caddy_version - name: Build Caddy # noqa: no-handler - when: "waf_install_test_caddy_version.changed" + when: "caddy_test_caddy_version.changed" ansible.builtin.command: - cmd: "{{ waf_install_build_command }}" + cmd: "{{ caddy_build_command }}" creates: "/usr/local/bin/caddy" notify: - "Restart Caddy" -- name: Create caddy system group +- name: Create Caddy system group ansible.builtin.group: - name: "caddy" + name: "{{ caddy_system_group }}" system: true -- name: Create caddy system user +- name: Create Caddy system user ansible.builtin.user: - name: "caddy" - group: "caddy" + name: "{{ caddy_system_user }}" + group: "{{ caddy_system_group }}" groups: ["ssl-cert"] password: "!" system: true - home: "/opt/caddy" + home: "{{ caddy_home_dir }}" -- name: Create caddy config directories +- name: Create Caddy config directories ansible.builtin.file: path: "{{ item }}" state: "directory" - group: "caddy" + group: "{{ caddy_system_group }}" mode: "0750" loop: - - "/etc/caddy" - - "/etc/caddy/plugins" - - "/etc/caddy/sites" + - "{{ caddy_config_dir }}" + - "{{ caddy_config_dir }}/crs-plugins" + - "{{ caddy_config_dir }}/sites" -- name: Create caddy log dir +- name: Create Caddy log dir ansible.builtin.file: - path: "/var/log/caddy" + path: "{{ caddy_log_dir }}" state: "directory" - group: "caddy" + group: "{{ caddy_system_group }}" 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" + src: "https://github.com/coreruleset/coreruleset/archive/refs/tags/v{{ caddy_owasp_crs_version }}.tar.gz" + dest: "{{ caddy_config_dir }}" + group: "{{ caddy_system_group }}" remote_src: true - name: Install plugins - loop: "{{ waf_install_crs_plugins }}" + loop: "{{ caddy_crs_plugins }}" loop_control: loop_var: "plugin" - ansible.builtin.include_tasks: "install_plugin.yml" + ansible.builtin.include_tasks: "plugin_install.yml" - name: Deploy caddy service file ansible.builtin.template: src: "templates/caddy.service" - dest: "/etc/systemd/system/caddy.service" + dest: "/etc/systemd/system/{{ caddy_system_user }}.service" mode: "0644" notify: - "Restart Caddy" - name: Enable Caddy ansible.builtin.service: - name: "caddy" + name: "{{ caddy_system_user }}" enabled: true daemon_reload: true diff --git a/ansible/roles/caddy/tasks/main.yml b/ansible/roles/caddy/tasks/main.yml new file mode 100644 index 0000000..8c94a87 --- /dev/null +++ b/ansible/roles/caddy/tasks/main.yml @@ -0,0 +1,6 @@ +- name: Install Caddy + when: "not(caddy_config_only)" + ansible.builtin.include_tasks: "caddy_install.yml" + +- name: Config Caddy + ansible.builtin.include_tasks: "caddy_config.yml" diff --git a/ansible/roles/waf_install/tasks/install_plugin.yml b/ansible/roles/caddy/tasks/plugin_install.yml similarity index 62% rename from ansible/roles/waf_install/tasks/install_plugin.yml rename to ansible/roles/caddy/tasks/plugin_install.yml index aa10266..c7dd0e8 100644 --- a/ansible/roles/waf_install/tasks/install_plugin.yml +++ b/ansible/roles/caddy/tasks/plugin_install.yml @@ -3,31 +3,31 @@ 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: "waf_install_plugin_downloaded" + register: "caddy_plugin_downloaded" - name: Deploy plugin {{ plugin.name }} # noqa: no-handler - when: "waf_install_plugin_downloaded.changed" + when: "caddy_plugin_downloaded.changed" block: - name: Get temp dir {{ plugin.name }} ansible.builtin.tempfile: state: "directory" - register: "waf_install_temp_plugin_dir" + register: "caddy_temp_plugin_dir" - name: Unarchive plugin {{ plugin.name }} ansible.builtin.unarchive: src: "/usr/local/src/{{ plugin.name }}-plugin-{{ plugin.version }}.tar.gz" - dest: "{{ waf_install_temp_plugin_dir.path }}" + dest: "{{ caddy_temp_plugin_dir.path }}" remote_src: true - name: Copy plugin files {{ plugin.name }} ansible.builtin.copy: - src: "{{ waf_install_temp_plugin_dir.path }}/{{ plugin.name }}-plugin-{{ plugin.version }}/plugins/" - dest: "/etc/caddy/plugins/" + src: "{{ caddy_temp_plugin_dir.path }}/{{ plugin.name }}-plugin-{{ plugin.version }}/plugins/" + dest: "{{ caddy_config_dir }}/crs-plugins/" mode: "0644" # directory_mode: "0750" - group: "caddy" + group: "{{ caddy_system_group }}" remote_src: true notify: - "Restart Caddy" @@ -36,20 +36,20 @@ when: "not(plugin.has_after_config | default(false))" ansible.builtin.copy: content: "" - dest: "/etc/caddy/plugins/{{ plugin.name }}-after.conf" + dest: "{{ caddy_config_dir }}/crs-plugins/{{ plugin.name }}-after.conf" mode: "0644" - group: "caddy" + group: "{{ caddy_system_group }}" # FIXME: bad hack, why is it required? - - name: Adjust /etc/caddy/plugins permissions + - name: Adjust plugins diretory permissions ansible.builtin.file: - path: "/etc/caddy/plugins" + path: "{{ caddy_config_dir }}/crs-plugins" state: "directory" - group: "caddy" + group: "{{ caddy_system_group }}" mode: "0750" - name: Delete temp dir {{ plugin.name }} - when: "waf_install_temp_plugin_dir.path is defined" + when: "caddy_temp_plugin_dir.path is defined" ansible.builtin.file: - path: "{{ waf_install_temp_plugin_dir.path }}" + path: "{{ caddy_temp_plugin_dir.path }}" state: "absent" diff --git a/ansible/roles/caddy/templates/Caddyfile b/ansible/roles/caddy/templates/Caddyfile new file mode 100644 index 0000000..bfb9b04 --- /dev/null +++ b/ansible/roles/caddy/templates/Caddyfile @@ -0,0 +1,152 @@ +{ + grace_period 10s + order coraza_waf first +{% if caddy_email | default(None) != None %} + email "{{ caddy_email }}" +{% endif %} + +{% if caddy_ca_root | default(None) != None %} + tls { + ca_root "{{ caddy_ca_root }}" + } + +{% endif %} + log waf { + format json + include "http.handlers.waf" + output stdout + } +{% if caddy_local_ca_name | default(None) != None %} + + pki { + ca local { + name "{{ caddy_local_ca_name }}" + } + } +{% endif %} +} + +{% for site in caddy_sites %} +{% if (site.redirect_from_aliases | default(true)) and (site.aliases | default([]) | length) > 0 %} +{{ site.aliases | join(', ') }} { + redir https://{{ site.site }}{uri} +} + +{% else %} +{% for alias in site.aliases | default([]) %} +{{ alias }}, +{% endfor %} +{% endif %} +{{ site.site }} { + encode gzip zstd + +{% for path in site.paths %} + handle {{ path.path | default('*') }} { + reverse_proxy { + to {{ path.addrs | join(' ') }} + header_up X-Real-IP {remote} + lb_policy ip_hash + # Active checks +# health_uri / +# health_interval 10s +# health_timeout 5s +# health_status 200 + # Passive checks + fail_duration 30s + max_fails 3 + unhealthy_latency 2000ms + transport http { +# tls_server_name {host} +# tls_insecure_skip_verify + } + } + } +{% endfor %} + +{% if site.custom_cert | default (false) %} + tls {{ site.custom_cert_file }} {{ site.custom_key_file }} + +{% endif %} +{% if site.blacklist | default([]) | length > 0 %} + @blocked { +{% for ip in site.blacklist | default([]) %} + remote_ip {{ ip }} +{% endfor %} + } + respond @blocked "Access Denied" 403 + +{% endif %} +{% if site.bot_barrier | default (false) %} + bot_barrier { + secret {{ caddy_bot_barrier_secret }} + complexity 18 + valid_for 30m + seed_cookie_name __chall_{{ site.name }}_seed + solution_cookie_name __chall_{{ site.name }}_solution + mac_cookie_name __chall_{{ site.name }}_mac + template {{ caddy_config_dir }}/bot_barrier_template.html + } + +{% endif %} + rate_limit { +# distributed + zone remote_ip { + key {remote.ip} + events {{ site.rate_events | default('1000') }} + window {{ site.rate_window | default('1m') }} + } + } + + coraza_waf { + load_owasp_crs + directives ` + Include "{{ caddy_config_dir }}/sites/{{ site.name }}/coraza.conf" + Include "{{ caddy_config_dir }}/sites/{{ site.name }}/crs-setup.conf" +{% for plugin in site.crs_plugins | default(caddy_waf_defaults.crs_plugins) %} + Include "{{ caddy_config_dir }}/crs-plugins/{{ plugin }}-config.conf" + Include "{{ caddy_config_dir }}/crs-plugins/{{ plugin }}-before.conf" +{% endfor %} + Include "{{ caddy_config_dir }}/sites/{{ site.name }}/exclusions-request-before.conf" + Include "{{ caddy_config_dir }}/coreruleset-{{ caddy_owasp_crs_version }}/rules/*.conf" + Include "{{ caddy_config_dir }}/sites/{{ site.name }}/exclusions-response-after.conf" +{% for plugin in site.crs_plugins | default(caddy_waf_defaults.crs_plugins) %} + Include "{{ caddy_config_dir }}/crs-plugins/{{ plugin }}-after.conf" +{% endfor %} + SecRuleEngine On + ` + } + + header { + Content-Security-Policy "{{ caddy_waf_defaults.header_content_security_policy | combine(site.header_content_security_policy | default({})) | dict2str(sep1=' ', sep2='; ', sep3=';') }}" + Cross-Origin-Embedder-Policy "{{ site.header_cross_origin_embedder_policy | default(caddy_waf_defaults.header_cross_origin_embedder_policy) }}" + Cross-Origin-Opener-Policy "{{ site.header_cross_origin_opener_policy | default(caddy_waf_defaults.header_cross_origin_opener_policy) }}" + Cross-Origin-Resource-Policy "{{ site.header_cross_origin_resource_policy | default(caddy_waf_defaults.header_cross_origin_resource_policy) }}" + Feature-Policy "{{ caddy_waf_defaults.header_feature_policy | combine(site.header_feature_policy | default({})) | dict2str(sep1=' ', sep2='; ', sep3=';') }}" + Permissions-Policy "{{ caddy_waf_defaults.header_permissions_policy | combine(site.header_permissions_policy | default({})) | dict2str(sep2=', ') }}" + Referrer-Policy "{{ site.header_referrer_policy | default(caddy_waf_defaults.header_referrer_policy) }}" + Strict-Transport-Security "{{ site.header_strict_transport_security | default(caddy_waf_defaults.header_strict_transport_security) }}" + X-Content-Type-Options "{{ site.header_x_content_type_options | default(caddy_waf_defaults.header_x_content_type_options) }}" + X-Frame-Options "{{ site.header_x_frame_options | default(caddy_waf_defaults.header_x_frame_options) }}" + X-Robots-Tag "{{ site.header_x_robots_tag | default(caddy_waf_defaults.header_x_robots_tag) }}" + ?Content-Type "{{ site.default_content_type | default(caddy_waf_defaults.default_content_type) }}" + >Set-Cookie "(.*)" "$1; {{ site.cookies_attributes | default(caddy_waf_defaults.cookies_attributes) }}" +{% for header in site.delete_headers | default(caddy_waf_defaults.delete_headers) %} + -{{ header }} +{% endfor %} +{% for header in site.custom_headers | default(caddy_waf_defaults.custom_headers) %} + {{ header.name }} "{{ header.value }}" +{% endfor %} + } + + handle_errors 403 { + header X-Blocked "true" + respond "Your request was blocked." + } + + log { + format json + output file {{ caddy_log_dir }}/site_{{ site.name }}.log + } +} + +{% endfor %} diff --git a/ansible/roles/waf_install/templates/caddy.service b/ansible/roles/caddy/templates/caddy.service similarity index 58% rename from ansible/roles/waf_install/templates/caddy.service rename to ansible/roles/caddy/templates/caddy.service index 079fc3b..62ea5ea 100644 --- a/ansible/roles/waf_install/templates/caddy.service +++ b/ansible/roles/caddy/templates/caddy.service @@ -6,10 +6,10 @@ Requires=network-online.target [Service] Type=notify -User=caddy -Group=caddy -ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile -ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force +User={{ caddy_system_user }} +Group={{ caddy_system_group }} +ExecStart=/usr/local/bin/caddy run --environ --config {{ caddy_config_dir }}/Caddyfile +ExecReload=/usr/local/bin/caddy reload --config {{ caddy_config_dir }}/Caddyfile --force TimeoutStopSec=15s LimitNOFILE=1048576 PrivateTmp=true diff --git a/ansible/roles/waf_config/templates/coraza.conf b/ansible/roles/caddy/templates/coraza.conf similarity index 99% rename from ansible/roles/waf_config/templates/coraza.conf rename to ansible/roles/caddy/templates/coraza.conf index cc30ae9..bbb70c9 100644 --- a/ansible/roles/waf_config/templates/coraza.conf +++ b/ansible/roles/caddy/templates/coraza.conf @@ -126,7 +126,7 @@ SecDataDir /tmp/ # 3: Info # 4-8: Debug # 9: Trace (most verbose) -SecDebugLog /var/log/caddy/coraza-debug.log +SecDebugLog {{ caddy_log_dir }}/coraza-debug.log SecDebugLogLevel 4 diff --git a/ansible/roles/waf_config/templates/crs-setup.conf b/ansible/roles/caddy/templates/crs-setup.conf similarity index 64% rename from ansible/roles/waf_config/templates/crs-setup.conf rename to ansible/roles/caddy/templates/crs-setup.conf index 66bb2f4..e885249 100644 --- a/ansible/roles/waf_config/templates/crs-setup.conf +++ b/ansible/roles/caddy/templates/crs-setup.conf @@ -10,9 +10,9 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - 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 %} + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.blocking_paranoia_level={{ site.paranoia_level | default(caddy_waf_defaults.paranoia_level) }}" +{% if site.detection_paranoia_level | default(caddy_waf_defaults.detection_paranoia_level) != None %} ## Detection paranoia level SecAction \ @@ -22,8 +22,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.detection_paranoia_level={{ site.detection_paranoia_level | default(waf_config_defaults.detection_paranoia_level) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.detection_paranoia_level={{ site.detection_paranoia_level | default(caddy_waf_defaults.detection_paranoia_level) }}" {% endif %} ## Enforce Body Processor URLENCODED @@ -34,8 +34,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.enforce_bodyproc_urlencoded={{ site.enforce_bodyproc_urlencoded | default(waf_config_defaults.enforce_bodyproc_urlencoded) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.enforce_bodyproc_urlencoded={{ site.enforce_bodyproc_urlencoded | default(caddy_waf_defaults.enforce_bodyproc_urlencoded) }}" ## Anomaly Scoring Mode Severity Levels SecAction \ @@ -45,11 +45,11 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - 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) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.critical_anomaly_score={{ site.critical_anomaly_score | default(caddy_waf_defaults.critical_anomaly_score) }},\ + setvar:tx.error_anomaly_score={{ site.error_anomaly_score | default(caddy_waf_defaults.error_anomaly_score) }},\ + setvar:tx.warning_anomaly_score={{ site.warning_anomaly_score | default(caddy_waf_defaults.warning_anomaly_score) }},\ + setvar:tx.notice_anomaly_score={{ site.notice_anomaly_score | default(caddy_waf_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/{{ 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 %}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.inbound_anomaly_score_threshold={% if site.log_only | default(caddy_waf_defaults.log_only) %}1000{% else %}{{ site.inbound_anomaly_score_threshold | default(caddy_waf_defaults.inbound_anomaly_score_threshold) }}{% endif %},\ + setvar:tx.outbound_anomaly_score_threshold={% if site.log_only | default(caddy_waf_defaults.log_only) %}1000{% else %}{{ site.outbound_anomaly_score_threshold | default(caddy_waf_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/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.reporting_level={{ site.reporting_level | default(waf_config_defaults.reporting_level) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.reporting_level={{ site.reporting_level | default(caddy_waf_defaults.reporting_level) }}" ## Early Anomaly Scoring Mode Blocking SecAction \ @@ -98,7 +98,7 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + ver:'OWASP_CRS/{{ caddy_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/{{ waf_config_owasp_crs_version }}',\ + ver:'OWASP_CRS/{{ caddy_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/{{ waf_config_owasp_crs_version }}',\ - setvar:'tx.allowed_methods={{ site.allowed_methods | default(waf_config_defaults.allowed_methods) | join(" ") }}'" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:'tx.allowed_methods={{ site.allowed_methods | default(caddy_waf_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/{{ 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(" ") }}'" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:'tx.allowed_request_content_type={{ site.allowed_request_content_type | default(caddy_waf_defaults.allowed_request_content_type) | join(" ") }}'" ## Allowed HTTP versions SecAction \ @@ -142,8 +142,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - 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(" ") }}'" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:'tx.allowed_http_versions={{ site.allowed_http_versions | default(caddy_waf_defaults.allowed_http_versions) | join(" ") }}'" ## Forbidden file extensions SecAction \ @@ -153,8 +153,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ - setvar:'tx.restricted_extensions={{ site.restricted_extensions | default(waf_config_defaults.restricted_extensions) | join(" ") }}'" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:'tx.restricted_extensions={{ site.restricted_extensions | default(caddy_waf_defaults.restricted_extensions) | join(" ") }}'" ## Restricted request headers SecAction \ @@ -164,8 +164,8 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - 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(" ") }}'" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:'tx.restricted_headers_basic={{ site.restricted_headers_basic | default(caddy_waf_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/{{ waf_config_owasp_crs_version }}',\ - setvar:'tx.restricted_headers_extended={{ site.restricted_headers_extended | default(waf_config_defaults.restricted_headers_extended) | join(" ") }}'" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:'tx.restricted_headers_extended={{ site.restricted_headers_extended | default(caddy_waf_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/{{ 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(" ") }}'" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:'tx.allowed_request_content_type_charset={{ caddy_waf_defaults.allowed_request_content_type_charset | default(caddy_waf_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/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.max_num_args={{ site.max_num_args | default(waf_config_defaults.max_num_args) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.max_num_args={{ site.max_num_args | default(caddy_waf_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/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.arg_name_length={{ site.arg_name_length | default(waf_config_defaults.arg_name_length) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.arg_name_length={{ site.arg_name_length | default(caddy_waf_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/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.arg_length={{ site.arg_length | default(waf_config_defaults.arg_length) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.arg_length={{ site.arg_length | default(caddy_waf_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/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.total_arg_length={{ site.total_arg_length | default(waf_config_defaults.total_arg_length) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.total_arg_length={{ site.total_arg_length | default(caddy_waf_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/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.max_file_size={{ site.max_file_size | default(waf_config_defaults.max_file_size) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.max_file_size={{ site.max_file_size | default(caddy_waf_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/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.combined_file_sizes={{ site.combined_file_sizes | default(waf_config_defaults.combined_file_sizes) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.combined_file_sizes={{ site.combined_file_sizes | default(caddy_waf_defaults.combined_file_sizes) }}" ## Easing In / Sampling Percentage SecAction \ @@ -262,8 +262,8 @@ SecAction \ pass,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.sampling_percentage={{ site.sampling_percentage | default(waf_config_defaults.sampling_percentage) }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.sampling_percentage={{ site.sampling_percentage | default(caddy_waf_defaults.sampling_percentage) }}" ## Check UTF-8 encoding SecAction \ @@ -273,7 +273,7 @@ SecAction \ t:none,\ nolog,\ tag:'OWASP_CRS',\ - ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\ + ver:'OWASP_CRS/{{ caddy_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/{{ waf_config_owasp_crs_version }}',\ +# ver:'OWASP_CRS/{{ caddy_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/{{ waf_config_owasp_crs_version }}',\ - setvar:tx.crs_setup_version={{ waf_config_owasp_crs_version | regex_replace('\.', '') }}" + ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\ + setvar:tx.crs_setup_version={{ caddy_owasp_crs_version | regex_replace('\.', '') }}" diff --git a/ansible/roles/caddy/templates/exclusions-request-before.conf b/ansible/roles/caddy/templates/exclusions-request-before.conf new file mode 100644 index 0000000..22a033f --- /dev/null +++ b/ansible/roles/caddy/templates/exclusions-request-before.conf @@ -0,0 +1,3 @@ +{% for exclusion in site.crs_exceptions.before_request | default([]) %} +{{ exclusion }} +{% endfor %} diff --git a/ansible/roles/caddy/templates/exclusions-response-after.conf b/ansible/roles/caddy/templates/exclusions-response-after.conf new file mode 100644 index 0000000..c376d77 --- /dev/null +++ b/ansible/roles/caddy/templates/exclusions-response-after.conf @@ -0,0 +1,3 @@ +{% for exclusion in site.crs_exceptions.after_response | default([]) %} +{{ exclusion }} +{% endfor %} diff --git a/ansible/roles/caddy/vars/main.yml b/ansible/roles/caddy/vars/main.yml new file mode 100644 index 0000000..a8b2875 --- /dev/null +++ b/ansible/roles/caddy/vars/main.yml @@ -0,0 +1 @@ +caddy_xcaddy_download: "https://github.com/caddyserver/xcaddy/releases/download" diff --git a/ansible/roles/waf_config/defaults/main.yml b/ansible/roles/waf_config/defaults/main.yml deleted file mode 100644 index 14ce3e5..0000000 --- a/ansible/roles/waf_config/defaults/main.yml +++ /dev/null @@ -1,196 +0,0 @@ -# Defaults -waf_config_owasp_crs_version: "4.22.0" - -# Required unless not site.custom_cert -waf_config_email: null - -# Required if site.bot_barrier -waf_config_bot_barrier_secret: null - -# Optionnal -waf_config_local_ca_name: null -waf_config_ca_root: null - -waf_config_sites: [] -# - name: "" -# site: "" -# paths: -# - path: "*" -# addrs: -# - "" -# blacklist: -# - "" -# aliases: -# - "" -# exceptions: -# before_request: -# - "" -# after_response: -# - "" - -waf_config_defaults: - redirect_from_aliases: true - custom_cert: true - custom_cert_file: "" - custom_key_file: "" - plugins: [] - - # caddy-ratelimit - rate_events: 1000 - rate_window: "1m" - - # steffenbusch/caddy-bot-barrier - bot_barrier: false - - # corazawaf/coraza-caddy - log_only: false - paranoia_level: 1 - detection_paranoia_level: null - enforce_bodyproc_urlencoded: 1 - critical_anomaly_score: 5 - error_anomaly_score: 4 - warning_anomaly_score: 3 - notice_anomaly_score: 2 - inbound_anomaly_score_threshold: 5 - outbound_anomaly_score_threshold: 4 - reporting_level: 4 - max_num_args: 255 - arg_name_length: 100 - arg_length: 1000 - total_arg_length: 64000 - max_file_size: 10485760 # 10 Mo - combined_file_sizes: 20970520 # 20 Mo - sampling_percentage: 100 - allowed_methods: - - "GET" - - "HEAD" - - "POST" - - "OPTIONS" - allowed_request_content_type: - - "|application/x-www-form-urlencoded|" - - "|multipart/form-data|" - - "|text/xml|" - - "|application/xml|" - - "|application/soap+xml|" - - "|application/json|" - - "|application/reports+json|" - - "|application/csp-report|" - restricted_extensions: - - ".ani/" - - ".asa/" - - ".asax/" - - ".ascx/" - - ".back/" - - ".backup/" - - ".bak/" - - ".bck/" - - ".bk/" - - ".bkp/" - - ".bat/" - - ".cdx/" - - ".cer/" - - ".cfg/" - - ".cmd/" - - ".cnf/" - - ".com/" - - ".compositefont/" - - ".config/" - - ".conf/" - - ".copy/" - - ".crt/" - - ".cs/" - - ".csproj/" - - ".csr/" - - ".dat/" - - ".db/" - - ".dbf/" - - ".dist/" - - ".dll/" - - ".dos/" - - ".dpkg-dist/" - - ".drv/" - - ".gadget/" - - ".hta/" - - ".htr/" - - ".htw/" - - ".ida/" - - ".idc/" - - ".idq/" - - ".inc/" - - ".inf/" - - ".ini/" - - ".jse/" - - ".key/" - - ".licx/" - - ".lnk/" - - ".log/" - - ".mdb/" - - ".msc/" - - ".ocx/" - - ".old/" - - ".pass/" - - ".pdb/" - - ".pfx/" - - ".pif/" - - ".pem/" - - ".pol/" - - ".prf/" - - ".printer/" - - ".pwd/" - - ".rdb/" - - ".rdp/" - - ".reg/" - - ".resources/" - - ".resx/" - - ".sav/" - - ".save/" - - ".scr/" - - ".sct/" - - ".sh/" - - ".shs/" - - ".sql/" - - ".sqlite/" - - ".sqlite3/" - - ".swp/" - - ".sys/" - - ".temp/" - - ".tlb/" - - ".tmp/" - - ".vb/" - - ".vbe/" - - ".vbs/" - - ".vbproj/" - - ".vsdisco/" - - ".vxd/" - - ".webinfo/" - - ".ws/" - - ".wsc/" - - ".wsf/" - - ".wsh/" - - ".xsd/" - - ".xsx/" - restricted_headers_basic: - - "/content-encoding/" - - "/proxy/" - - "/lock-token/" - - "/content-range/" - - "/if/" - - "/x-http-method-override/" - - "/x-http-method/" - - "/x-method-override/" - - "/x-middleware-subrequest/" - - "/expect/" - restricted_headers_extended: - - "/accept-charset/" - allowed_http_versions: - - "HTTP/1.0" - - "HTTP/1.1" - - "HTTP/2" - - "HTTP/2.0" - - "HTTP/3" - - "HTTP/3.0" - allowed_request_content_type_charset: - - "|utf-8|" - - "|iso-8859-1|" - - "|iso-8859-15|" - - "|windows-1252|" diff --git a/ansible/roles/waf_config/templates/Caddyfile b/ansible/roles/waf_config/templates/Caddyfile deleted file mode 100644 index 3c04643..0000000 --- a/ansible/roles/waf_config/templates/Caddyfile +++ /dev/null @@ -1,152 +0,0 @@ -{ - grace_period 10s - order coraza_waf first -{% if waf_config_email | default(None) != None %} - email "{{ waf_config_email }}" -{% endif %} - -{% if waf_config_ca_root | default(None) != None %} - tls { - ca_root "{{ waf_config_ca_root }}" - } - -{% endif %} - log waf { - format json - include "http.handlers.waf" - output stdout - } -{% if waf_config_local_ca_name | default(None) != None %} - - pki { - ca local { - name "{{ waf_config_local_ca_name }}" - } - } -{% endif %} -} - -{% 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} -} - -{% else %} -{% for alias in site.aliases | default([]) %} -{{ alias }}, -{% endfor %} -{% endif %} -{{ site.site }} { - encode gzip zstd - -{% for path in site.paths %} - handle {{ path.path | default('*') }} { - reverse_proxy { - to {{ path.addrs | join(' ') }} - header_up X-Real-IP {remote} - lb_policy ip_hash - # Active checks -# health_uri / -# health_interval 10s -# health_timeout 5s -# health_status 200 - # Passive checks - fail_duration 30s - max_fails 3 - unhealthy_latency 2000ms - transport http { -# tls_server_name {host} -# tls_insecure_skip_verify - } - } - } -{% endfor %} - -{% if site.custom_cert | default (false) %} - tls {{ site.custom_cert_file }} {{ site.custom_key_file }} - -{% endif %} -{% if site.blacklist | default([]) | length > 0 %} - @blocked { -{% for ip in site.blacklist | default([]) %} - remote_ip {{ ip }} -{% endfor %} - } - respond @blocked "Access Denied" 403 - -{% endif %} -{% if site.bot_barrier | default (false) %} - bot_barrier { - secret {{ waf_config_bot_barrier_secret }} - complexity 18 - valid_for 30m - seed_cookie_name __chall_{{ site.name }}_seed - solution_cookie_name __chall_{{ site.name }}_solution - mac_cookie_name __chall_{{ site.name }}_mac - template /etc/caddy/bot_barrier_template.html - } - -{% endif %} - rate_limit { -# distributed - zone remote_ip { - key {remote.ip} - events {{ site.rate_events | default('1000') }} - window {{ site.rate_window | default('1m') }} - } - } - - coraza_waf { - load_owasp_crs - directives ` - Include "/etc/caddy/sites/{{ site.name }}/coraza.conf" - Include "/etc/caddy/sites/{{ site.name }}/crs-setup.conf" -{% for plugin in site.plugins | default(waf_config_defaults.plugins) %} - Include "/etc/caddy/plugins/{{ plugin }}-config.conf" - Include "/etc/caddy/plugins/{{ plugin }}-before.conf" -{% endfor %} - Include "/etc/caddy/sites/{{ site.name }}/exclusions-request-before.conf" - Include "/etc/caddy/coreruleset-{{ waf_config_owasp_crs_version }}/rules/*.conf" - Include "/etc/caddy/sites/{{ site.name }}/exclusions-response-after.conf" -{% for plugin in site.plugins | default(waf_config_defaults.plugins) %} - Include "/etc/caddy/plugins/{{ plugin }}-after.conf" -{% endfor %} - SecRuleEngine On - ` - } - - # Cf. https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html - header { -# Access-Control-Allow-Origin "https://secure.gravatar.com" - Content-Security-Policy "upgrade-insecure-requests; worker-src 'self' blob:; connect-src 'self'; media-src 'self'; img-src 'self' data: https://secure.gravatar.com/avatar/; font-src 'self' data:; style-src 'self' 'unsafe-inline'; object-src 'none'; frame-src 'self' blob:; child-src 'self'; form-action 'self'; frame-ancestors 'self'" -# Cross-Origin-Embedder-Policy "require-corp" - Cross-Origin-Opener-Policy "same-origin" - Cross-Origin-Resource-Policy "same-site" - Permissions-Policy "geolocation=(), camera=(), microphone=(), interest-cohort=()" - Referrer-Policy "strict-origin-when-cross-origin" - Strict-Transport-Security "max-age=2635200; includeSubDomains" - X-Content-Type-Options "nosniff" - X-Frame-Options "DENY" - X-Robots-Tag "noindex, nofollow" - ?Content-Type "text/html; charset=UTF-8" - >Set-Cookie "(.*)" "$1; SameSite=None; Secure;" - -Server - -Via - -X-Powered-By - -X-AspNet-Version - -X-AspNetMvc-Version - } - - handle_errors 403 { - header X-Blocked "true" - respond "Your request was blocked." - } - - log { - format json - output file /var/log/caddy/site_{{ site.name }}.log - } -} - -{% endfor %} diff --git a/ansible/roles/waf_config/templates/exclusions-request-before.conf b/ansible/roles/waf_config/templates/exclusions-request-before.conf deleted file mode 100644 index aae4a96..0000000 --- a/ansible/roles/waf_config/templates/exclusions-request-before.conf +++ /dev/null @@ -1,3 +0,0 @@ -{% for exclusion in site.exceptions.before_request | default([]) %} -{{ exclusion }} -{% endfor %} diff --git a/ansible/roles/waf_config/templates/exclusions-response-after.conf b/ansible/roles/waf_config/templates/exclusions-response-after.conf deleted file mode 100644 index 7ffde16..0000000 --- a/ansible/roles/waf_config/templates/exclusions-response-after.conf +++ /dev/null @@ -1,3 +0,0 @@ -{% for exclusion in site.exceptions.after_response | default([]) %} -{{ exclusion }} -{% endfor %} diff --git a/ansible/roles/waf_install/defaults/main.yml b/ansible/roles/waf_install/defaults/main.yml deleted file mode 100644 index 39fd8b8..0000000 --- a/ansible/roles/waf_install/defaults/main.yml +++ /dev/null @@ -1,25 +0,0 @@ -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.22.0" - -# 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" - has_after_config: true - - 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/waf_install/handlers/main.yml b/ansible/roles/waf_install/handlers/main.yml deleted file mode 100644 index 500c16c..0000000 --- a/ansible/roles/waf_install/handlers/main.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: Restart Caddy - ansible.builtin.service: - name: "caddy" - state: "restarted" - daemon_reload: true diff --git a/ansible/roles/waf_install/vars/main.yml b/ansible/roles/waf_install/vars/main.yml deleted file mode 100644 index 5db1438..0000000 --- a/ansible/roles/waf_install/vars/main.yml +++ /dev/null @@ -1 +0,0 @@ -waf_install_xcaddy_download: "https://github.com/caddyserver/xcaddy/releases/download" diff --git a/ansible/vars/common.yml b/ansible/vars/common.yml index e942fe1..6337f4c 100644 --- a/ansible/vars/common.yml +++ b/ansible/vars/common.yml @@ -21,11 +21,21 @@ common: custom_cert: true custom_cert_file: "/etc/ssl/certs/test.lab.crt" custom_key_file: "/etc/ssl/private/test.lab.key" - plugins: + crs_plugins: - "wordpress-rule-exclusions" - exceptions: + crs_exceptions: # Cf. https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x) # before_request: # - 'SecRule REQUEST_FILENAME "@endsWith /wp-admin/admin-ajax.php" "id:1,nolog,ctl:ruleRemoveById=932260"' after_response: - 'SecRuleUpdateTargetById 932260 "!ARGS:tag-name"' + header_cross_origin_embedder_policy: "unsafe-none" + header_content_security_policy: + worker-src: "'self' blob:" + img-src: "'self' data: https://secure.gravatar.com/avatar/" + font-src: "'self' data:" + script-src: "'self' 'unsafe-eval'" + script-src-elem: "'self' 'unsafe-inline'" + style-src-attr: "'self' 'unsafe-inline'" + style-src-elem: "'self' 'unsafe-inline'" + frame-src: "'self' blob:"