Private
Public Access
2
0
This commit is contained in:
2026-01-20 17:40:16 +03:00
parent cda556eec6
commit b78da80550
83 changed files with 108 additions and 82 deletions

View File

@@ -0,0 +1,154 @@
{
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 file {{ caddy_log_dir }}/waf.log
}
{% 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') }}
}
}
{% if site.enable_crs | default(caddy_waf_defaults.enable_crs) %}
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
`
}
{% endif %}
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 %}

View File

@@ -0,0 +1,20 @@
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
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
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target

View 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 {{ caddy_log_dir }}/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}'"

View 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/{{ 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 \
"id:900001,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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
SecAction \
"id:900010,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900100,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900110,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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
# 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/{{ caddy_owasp_crs_version }}',\
setvar:tx.reporting_level={{ site.reporting_level | default(caddy_waf_defaults.reporting_level) }}"
## Early Anomaly Scoring Mode Blocking
SecAction \
"id:900120,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_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/{{ caddy_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/{{ 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 \
"id:900220,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900230,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900240,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900250,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900255,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900280,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900300,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900310,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900320,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900330,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900340,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900350,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900400,\
phase:1,\
pass,\
nolog,\
tag:'OWASP_CRS',\
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 \
"id:900950,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_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/{{ caddy_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/{{ caddy_owasp_crs_version }}',\
setvar:tx.crs_setup_version={{ caddy_owasp_crs_version | regex_replace('\.', '') }}"

View File

@@ -0,0 +1,3 @@
{% for exclusion in site.crs_exceptions.before_request | default([]) %}
{{ exclusion }}
{% endfor %}

View File

@@ -0,0 +1,3 @@
{% for exclusion in site.crs_exceptions.after_response | default([]) %}
{{ exclusion }}
{% endfor %}