WIP
This commit is contained in:
195
ansible/roles/waf_config/defaults/main.yml
Normal file
195
ansible/roles/waf_config/defaults/main.yml
Normal file
@@ -0,0 +1,195 @@
|
||||
# Defaults
|
||||
waf_config_owasp_crs_version: "4.21.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:
|
||||
# - ""
|
||||
# plugins: FIXME:TODO
|
||||
# - ""
|
||||
# exceptions:
|
||||
# before_request:
|
||||
# - ""
|
||||
# after_response:
|
||||
# - ""
|
||||
|
||||
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
|
||||
|
||||
# CRS defaults
|
||||
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/"
|
||||
allowed_http_versions:
|
||||
- "HTTP/1.0"
|
||||
- "HTTP/1.1"
|
||||
- "HTTP/2"
|
||||
- "HTTP/2.0"
|
||||
- "HTTP/3"
|
||||
- "HTTP/3.0"
|
||||
restricted_headers_extended:
|
||||
- "/accept-charset/"
|
||||
allowed_request_content_type_charset:
|
||||
- "|utf-8|"
|
||||
- "|iso-8859-1|"
|
||||
- "|iso-8859-15|"
|
||||
- "|windows-1252|"
|
||||
71
ansible/roles/waf_config/files/bot_barrier_template.html
Normal file
71
ansible/roles/waf_config/files/bot_barrier_template.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Security Check</title>
|
||||
<style nonce="{{ .CSPNonce }}">
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f9;
|
||||
color: #333;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.4em 0 0.3em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
margin: 2em 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid #ccc;
|
||||
border-top: 5px solid #333;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- English Section -->
|
||||
<h1>Just a moment...</h1>
|
||||
<p>A small mathematical challenge is running<br>
|
||||
in the background - fully automatic to confirm<br>
|
||||
you're not a bot 🔒.</p>
|
||||
|
||||
<!-- Spinner -->
|
||||
<div class="spinner"></div>
|
||||
|
||||
<p>⏳ You'll be redirected shortly.</p>
|
||||
</div>
|
||||
|
||||
<script nonce="{{ .CSPNonce }}">{{ .Script }}</script>
|
||||
</body>
|
||||
</html>
|
||||
4
ansible/roles/waf_config/handlers/main.yml
Normal file
4
ansible/roles/waf_config/handlers/main.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
- name: Reload Caddy
|
||||
ansible.builtin.service:
|
||||
name: "caddy"
|
||||
state: "reloaded"
|
||||
75
ansible/roles/waf_config/tasks/main.yml
Normal file
75
ansible/roles/waf_config/tasks/main.yml
Normal file
@@ -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"
|
||||
127
ansible/roles/waf_config/templates/Caddyfile
Normal file
127
ansible/roles/waf_config/templates/Caddyfile
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
# storage redis
|
||||
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 file /var/log/caddy/waf.log
|
||||
}
|
||||
{% 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"
|
||||
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-{{ 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
|
||||
`
|
||||
}
|
||||
|
||||
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 %}
|
||||
158
ansible/roles/waf_config/templates/coraza.conf
Normal file
158
ansible/roles/waf_config/templates/coraza.conf
Normal file
@@ -0,0 +1,158 @@
|
||||
# -- Rule engine initialization ----------------------------------------------
|
||||
|
||||
# Enable Coraza, attaching it to every transaction. Use detection
|
||||
# only to start with, because that minimises the chances of post-installation
|
||||
# disruption.
|
||||
SecRuleEngine DetectionOnly
|
||||
|
||||
|
||||
# -- Request body handling ---------------------------------------------------
|
||||
|
||||
# Allow Coraza to access request bodies. If you don't, Coraza
|
||||
# won't be able to see any POST parameters, which opens a large security
|
||||
# hole for attackers to exploit.
|
||||
SecRequestBodyAccess On
|
||||
|
||||
# Enable XML request body parser.
|
||||
# Initiate XML Processor in case of xml content-type
|
||||
SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \
|
||||
"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
|
||||
|
||||
# Enable JSON request body parser.
|
||||
# Initiate JSON Processor in case of JSON content-type; change accordingly
|
||||
# if your application does not use 'application/json'
|
||||
SecRule REQUEST_HEADERS:Content-Type "^application/json" \
|
||||
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
|
||||
|
||||
# Enable JSON request body parser for more subtypes.
|
||||
# Adapt this rule if you want to engage the JSON Processor for "+json" subtypes
|
||||
SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \
|
||||
"id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
|
||||
|
||||
# Maximum request body size we will accept for buffering. If you support
|
||||
# file uploads, this value must has to be as large as the largest file
|
||||
# you are willing to accept.
|
||||
SecRequestBodyLimit 13107200
|
||||
|
||||
# Maximum request body size that Coraza will store in memory. If the body
|
||||
# size exceeds this value, it will be saved to a temporary file on disk.
|
||||
SecRequestBodyInMemoryLimit 131072
|
||||
|
||||
# Maximum request body size we will accept for buffering, with files excluded.
|
||||
# You want to keep that value as low as practical.
|
||||
# Note: SecRequestBodyNoFilesLimit is currently NOT supported by Coraza
|
||||
# SecRequestBodyNoFilesLimit 131072
|
||||
|
||||
# What to do if the request body size is above our configured limit.
|
||||
# Keep in mind that this setting will automatically be set to ProcessPartial
|
||||
# when SecRuleEngine is set to DetectionOnly mode in order to minimize
|
||||
# disruptions when initially deploying Coraza.
|
||||
# Warning: Setting this directive to ProcessPartial introduces a potential bypass
|
||||
# risk, as attackers could prepend junk data equal to or greater than the inspected body size.
|
||||
SecRequestBodyLimitAction Reject
|
||||
|
||||
# Verify that we've correctly processed the request body.
|
||||
# As a rule of thumb, when failing to process a request body
|
||||
# you should reject the request (when deployed in blocking mode)
|
||||
# or log a high-severity alert (when deployed in detection-only mode).
|
||||
SecRule REQBODY_ERROR "!@eq 0" \
|
||||
"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"
|
||||
|
||||
# By default be strict with what we accept in the multipart/form-data
|
||||
# request body. If the rule below proves to be too strict for your
|
||||
# environment consider changing it to detection-only.
|
||||
# Do NOT remove it, as it will catch many evasion attempts.
|
||||
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
|
||||
"id:'200003',phase:2,t:none,log,deny,status:400, \
|
||||
msg:'Multipart request body failed strict validation.'"
|
||||
|
||||
# -- Response body handling --------------------------------------------------
|
||||
|
||||
# Allow Coraza to access response bodies.
|
||||
# You should have this directive enabled in order to identify errors
|
||||
# and data leakage issues.
|
||||
# Do keep in mind that enabling this directive does increases both
|
||||
# memory consumption and response latency.
|
||||
SecResponseBodyAccess On
|
||||
|
||||
# Which response MIME types do you want to inspect? You should adjust the
|
||||
# configuration below to catch documents but avoid static files
|
||||
# (e.g., images and archives).
|
||||
SecResponseBodyMimeType text/plain text/html text/xml
|
||||
|
||||
# Buffer response bodies of up to 512 KB in length.
|
||||
SecResponseBodyLimit 524288
|
||||
|
||||
# What happens when we encounter a response body larger than the configured
|
||||
# limit? By default, we process what we have and let the rest through.
|
||||
# That's somewhat less secure, but does not break any legitimate pages.
|
||||
SecResponseBodyLimitAction ProcessPartial
|
||||
|
||||
|
||||
# -- Filesystem configuration ------------------------------------------------
|
||||
|
||||
# The location where Coraza will keep its persistent data. This default setting
|
||||
# is chosen due to all systems have /tmp available however, it
|
||||
# too should be updated to a place that other users can't access.
|
||||
SecDataDir /tmp/
|
||||
|
||||
|
||||
# -- File uploads handling configuration -------------------------------------
|
||||
|
||||
# The location where Coraza stores intercepted uploaded files. This
|
||||
# location must be private to Coraza. You don't want other users on
|
||||
# the server to access the files, do you?
|
||||
#SecUploadDir /opt/coraza/var/upload/
|
||||
|
||||
# If On, the WAF will store the uploaded files in the SecUploadDir
|
||||
# directory.
|
||||
# Note: SecUploadKeepFiles is currently NOT supported by Coraza
|
||||
#SecUploadKeepFiles Off
|
||||
|
||||
# Uploaded files are by default created with permissions that do not allow
|
||||
# any other user to access them. You may need to relax that if you want to
|
||||
# interface Coraza to an external program (e.g., an anti-virus).
|
||||
# Note: SecUploadFileMode is currently NOT supported by Coraza
|
||||
#SecUploadFileMode 0600
|
||||
|
||||
|
||||
# -- Debug log configuration -------------------------------------------------
|
||||
|
||||
# Default debug log path
|
||||
# Debug levels:
|
||||
# 0: No logging (least verbose)
|
||||
# 1: Error
|
||||
# 2: Warn
|
||||
# 3: Info
|
||||
# 4-8: Debug
|
||||
# 9: Trace (most verbose)
|
||||
SecDebugLog /var/log/caddy/coraza-debug.log
|
||||
SecDebugLogLevel 4
|
||||
|
||||
|
||||
# -- Audit log configuration -------------------------------------------------
|
||||
|
||||
# Log the transactions that are marked by a rule, as well as those that
|
||||
# trigger a server error (determined by a 5xx or 4xx, excluding 404,
|
||||
# level response status codes).
|
||||
SecAuditEngine RelevantOnly
|
||||
SecAuditLogRelevantStatus "^(?:(5|4)(0|1)[0-9])$"
|
||||
|
||||
# Define which parts of the transaction are going to be recorded in the audit log
|
||||
SecAuditLogParts ABIJDEFHZ
|
||||
|
||||
# Use a single file for logging. This is much easier to look at, but
|
||||
# assumes that you will use the audit log only occasionally.
|
||||
SecAuditLogType Serial
|
||||
|
||||
# The format used to write the audit log.
|
||||
# Can be one of JSON|JsonLegacy|Native|OCSF
|
||||
SecAuditLogFormat JSON
|
||||
|
||||
# The following settings are not supported by Coraza
|
||||
# SecCookieFormat 0
|
||||
# SecArgumentSeparator &
|
||||
# SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \
|
||||
# "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
|
||||
# SecRule TX:/^COR_/ "!@streq 0" \
|
||||
# "id:'200005',phase:2,t:none,deny,msg:'Coraza internal error flagged: %{MATCHED_VAR_NAME}'"
|
||||
305
ansible/roles/waf_config/templates/crs-setup.conf
Normal file
305
ansible/roles/waf_config/templates/crs-setup.conf
Normal file
@@ -0,0 +1,305 @@
|
||||
## Log destination
|
||||
SecDefaultAction "phase:1,log,auditlog,pass"
|
||||
SecDefaultAction "phase:2,log,auditlog,pass"
|
||||
|
||||
## Paranoia level
|
||||
SecAction \
|
||||
"id:900000,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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 %}
|
||||
|
||||
## Detection paranoia level
|
||||
SecAction \
|
||||
"id:900001,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
{% endif %}
|
||||
|
||||
## Enforce Body Processor URLENCODED
|
||||
SecAction \
|
||||
"id:900010,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
|
||||
## Anomaly Scoring Mode Severity Levels
|
||||
SecAction \
|
||||
"id:900100,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
|
||||
## Anomaly Scoring Mode Blocking Threshold Levels
|
||||
SecAction \
|
||||
"id:900110,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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 %}"
|
||||
|
||||
## Application Specific Rule Exclusions
|
||||
# FIXME
|
||||
# In CRS 4, these are no longer part of the CRS itself, but they are available
|
||||
# as "CRS plugins". Some plugins improve support for web applications, and others
|
||||
# may bring new functionality. Plugins are not installed by default, but can be
|
||||
# downloaded from the plugin registry:
|
||||
# https://github.com/coreruleset/plugin-registry
|
||||
# For detailed information about using and installing plugins, please see:
|
||||
# https://coreruleset.org/docs/concepts/plugins/
|
||||
|
||||
## Anomaly Score Reporting Level
|
||||
# 0 - Reporting disabled
|
||||
# 1 - Reporting for requests with a blocking anomaly score >= a threshold
|
||||
# 2 - Reporting for requests with a detection anomaly score >= a threshold
|
||||
# 3 - Reporting for requests with a blocking anomaly score greater than 0
|
||||
# 4 - Reporting for requests with a detection anomaly score greater than 0
|
||||
# 5 - Reporting for all requests
|
||||
SecAction \
|
||||
"id:900115,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
|
||||
## Early Anomaly Scoring Mode Blocking
|
||||
SecAction \
|
||||
"id:900120,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
t:none,\
|
||||
nolog,\
|
||||
tag:'OWASP_CRS',\
|
||||
ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\
|
||||
setvar:tx.early_blocking=0"
|
||||
|
||||
## Initialize Default Collections
|
||||
SecAction \
|
||||
"id:900130,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
t:none,\
|
||||
nolog,\
|
||||
tag:'OWASP_CRS',\
|
||||
ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\
|
||||
setvar:tx.enable_default_collections=1"
|
||||
|
||||
## HTTP Policy Settings
|
||||
SecAction \
|
||||
"id:900200,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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(" ") }}'"
|
||||
|
||||
# Content-Types that a client is allowed to send in a request
|
||||
SecAction \
|
||||
"id:900220,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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(" ") }}'"
|
||||
|
||||
## Allowed HTTP versions
|
||||
SecAction \
|
||||
"id:900230,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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(" ") }}'"
|
||||
|
||||
## Forbidden file extensions
|
||||
SecAction \
|
||||
"id:900240,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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(" ") }}'"
|
||||
|
||||
## Restricted request headers
|
||||
SecAction \
|
||||
"id:900250,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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(" ") }}'"
|
||||
|
||||
## Extended restricted request headers (forbidden at a higher paranoia level)
|
||||
SecAction \
|
||||
"id:900255,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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(" ") }}'"
|
||||
|
||||
# Content-Types charsets that a client is allowed to send in a request
|
||||
SecAction \
|
||||
"id:900280,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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(" ") }}'"
|
||||
|
||||
# Block request if number of arguments is too high
|
||||
SecAction \
|
||||
"id:900300,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
|
||||
# Block request if the length of any argument name is too high
|
||||
SecAction \
|
||||
"id:900310,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
|
||||
# Block request if the length of any argument value is too high
|
||||
SecAction \
|
||||
"id:900320,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
|
||||
# Block request if the total length of all combined arguments is too high
|
||||
SecAction \
|
||||
"id:900330,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
|
||||
# Block request if the file size of any individual uploaded file is too high
|
||||
SecAction \
|
||||
"id:900340,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
|
||||
# Block request if the total size of all combined uploaded files is too high
|
||||
SecAction \
|
||||
"id:900350,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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) }}"
|
||||
|
||||
## Easing In / Sampling Percentage
|
||||
SecAction \
|
||||
"id:900400,\
|
||||
phase:1,\
|
||||
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) }}"
|
||||
|
||||
## Check UTF-8 encoding
|
||||
SecAction \
|
||||
"id:900950,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
t:none,\
|
||||
nolog,\
|
||||
tag:'OWASP_CRS',\
|
||||
ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\
|
||||
setvar:tx.crs_validate_utf8_encoding=1"
|
||||
|
||||
## Skip Checking Responses
|
||||
# CRS will perform analysis of the response contents if this is enabled and you have
|
||||
# the directive `SecResponseBodyAccess On`.
|
||||
# Warning: this feature is _enabled_ by default, but depending on your applications
|
||||
# you might be targeted in a Request Filter Denial of Service (RFDoS) attack.
|
||||
# References: https://blog.sicuranext.com/response-filter-denial-of-service-a-new-way-to-shutdown-a-website/
|
||||
# Uncomment this rule to _skip checking responses_.
|
||||
#SecAction \
|
||||
# "id:900500,\
|
||||
# phase:1,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# nolog,\
|
||||
# tag:'OWASP_CRS',\
|
||||
# ver:'OWASP_CRS/{{ waf_config_owasp_crs_version }}',\
|
||||
# setvar:tx.crs_skip_response_analysis=1"
|
||||
|
||||
## End of setup
|
||||
SecAction \
|
||||
"id:900990,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
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('\.', '') }}"
|
||||
@@ -0,0 +1,3 @@
|
||||
{% for exclusion in site.exceptions.before_request | default([]) %}
|
||||
{{ exclusion }}
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% for exclusion in site.exceptions.after_response | default([]) %}
|
||||
{{ exclusion }}
|
||||
{% endfor %}
|
||||
Reference in New Issue
Block a user