From e1e35dec5582c25000f5afe6bacf05914c938c91 Mon Sep 17 00:00:00 2001 From: halkt Date: Fri, 18 Sep 2026 23:49:12 +0900 Subject: [PATCH] feat: make the severity of the bounce log line configurable The "[Crowdsec] denied '' with ''" line emitted on every bounced request is hardcoded to ngx.ALERT in csmod.Allow(). alert is more severe than error, so nginx writes it at every practical error_log level; the only way to keep it out of the error log is `error_log ... emerg;`, which also silences nginx's own errors and real bouncer failures such as "failed to query LAPI" or "Lua shared dict is full". Since the line is emitted once per bounced request, its volume is unbounded exactly when the bouncer is doing its job, and operators shipping the error log elsewhere have no way to opt out of it. Add a BOUNCING_LOG_LEVEL option taking the nginx error_log severity names. The name is resolved to the matching ngx.* constant once in csmod.init(), so the hot path stays a plain field read. Unknown values fall back to the default with a warning, matching the other enum-like options in config.lua. The default is "alert", i.e. current behaviour, so this is a no-op unless the option is set. --- config_example.conf | 2 + lib/crowdsec.lua | 21 +++++- lib/plugins/crowdsec/config.lua | 11 ++- t/25_bouncing_log_level.t | 67 +++++++++++++++++++ .../25_notice_crowdsec_nginx_bouncer.conf | 9 +++ 5 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 t/25_bouncing_log_level.t create mode 100644 t/conf_t/25_notice_crowdsec_nginx_bouncer.conf diff --git a/config_example.conf b/config_example.conf index f69f2cb..1e820af 100644 --- a/config_example.conf +++ b/config_example.conf @@ -9,6 +9,8 @@ CACHE_EXPIRATION=1 # bounce for all type of remediation that the bouncer can receive from the local API BOUNCING_ON_TYPE=all FALLBACK_REMEDIATION=ban +# severity of the "denied" log line: debug, info, notice, warn, error, crit, alert or emerg +BOUNCING_LOG_LEVEL=alert REQUEST_TIMEOUT=3000 UPDATE_FREQUENCY=10 # By default internal requests are ignored, such as any path affected by rewrite rule. diff --git a/lib/crowdsec.lua b/lib/crowdsec.lua index 1c58eb0..5701b9e 100644 --- a/lib/crowdsec.lua +++ b/lib/crowdsec.lua @@ -35,6 +35,19 @@ local APPSEC_TRANSFER_ENCODING_HEADER = "x-crowdsec-appsec-transfer-encoding" local REMEDIATION_API_KEY_HEADER = 'x-api-key' local METRICS_PERIOD = 900 +-- maps the BOUNCING_LOG_LEVEL config values to the ngx.log severity constants. +-- names are spelled as in nginx's own `error_log` directive. +local LOG_LEVELS = { + debug = ngx.DEBUG, + info = ngx.INFO, + notice = ngx.NOTICE, + warn = ngx.WARN, + error = ngx.ERR, + crit = ngx.CRIT, + alert = ngx.ALERT, + emerg = ngx.EMERG, +} + local METHODS_WITH_BODY = { POST = true, PUT = true, @@ -117,6 +130,10 @@ function csmod.init(configFile, userAgent) runtime.userAgent = userAgent runtime.cache = ngx.shared.crowdsec_cache runtime.fallback = runtime.conf["FALLBACK_REMEDIATION"] + -- severity used for the "denied" line emitted on every bounced request. + -- config.loadConfig() already rejected unknown names, so the lookup cannot miss; + -- the fallback keeps us safe if the bouncer is initialised with a hand-built conf table. + runtime.bouncing_log_level = LOG_LEVELS[runtime.conf["BOUNCING_LOG_LEVEL"]] or ngx.ALERT if runtime.conf["ENABLED"] == "false" then return "Disabled", nil @@ -899,7 +916,7 @@ function csmod.Allow(ip) if not ok then if remediation == "ban" then - ngx.log(ngx.ALERT, "[Crowdsec] denied '" .. ip .. "' with '"..remediation.."' (by " .. flag.Flags[remediationSource] .. ")") + ngx.log(runtime.bouncing_log_level, "[Crowdsec] denied '" .. ip .. "' with '"..remediation.."' (by " .. flag.Flags[remediationSource] .. ")") ban.apply(ret_code) return end @@ -940,7 +957,7 @@ function csmod.Allow(ip) if forcible then ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") end - ngx.log(ngx.ALERT, "[Crowdsec] denied '" .. ip .. "' with '"..remediation.."'") + ngx.log(runtime.bouncing_log_level, "[Crowdsec] denied '" .. ip .. "' with '"..remediation.."'") captcha.apply() return end diff --git a/lib/plugins/crowdsec/config.lua b/lib/plugins/crowdsec/config.lua index e5dd08f..46600a5 100644 --- a/lib/plugins/crowdsec/config.lua +++ b/lib/plugins/crowdsec/config.lua @@ -1,10 +1,12 @@ local config = {} -local valid_params = {'ENABLED', 'ENABLE_INTERNAL', 'API_URL', 'API_KEY', 'BOUNCING_ON_TYPE', 'MODE', 'SECRET_KEY', 'SITE_KEY', 'BAN_TEMPLATE_PATH' ,'CAPTCHA_TEMPLATE_PATH', 'REDIRECT_LOCATION', 'RET_CODE', 'CAPTCHA_RET_CODE', 'EXCLUDE_LOCATION', 'FALLBACK_REMEDIATION', 'CAPTCHA_PROVIDER', 'APPSEC_URL', 'APPSEC_FAILURE_ACTION', 'ALWAYS_SEND_TO_APPSEC', 'APPSEC_DROP_UNREADABLE_BODY', 'SSL_VERIFY', 'USE_TLS_AUTH', 'TLS_CLIENT_CERT', 'TLS_CLIENT_KEY', 'SCENARIOS_CONTAINING', 'SCENARIOS_NOT_CONTAINING'} +local valid_params = {'ENABLED', 'ENABLE_INTERNAL', 'API_URL', 'API_KEY', 'BOUNCING_ON_TYPE', 'MODE', 'SECRET_KEY', 'SITE_KEY', 'BAN_TEMPLATE_PATH' ,'CAPTCHA_TEMPLATE_PATH', 'REDIRECT_LOCATION', 'RET_CODE', 'CAPTCHA_RET_CODE', 'EXCLUDE_LOCATION', 'FALLBACK_REMEDIATION', 'CAPTCHA_PROVIDER', 'APPSEC_URL', 'APPSEC_FAILURE_ACTION', 'ALWAYS_SEND_TO_APPSEC', 'APPSEC_DROP_UNREADABLE_BODY', 'SSL_VERIFY', 'USE_TLS_AUTH', 'TLS_CLIENT_CERT', 'TLS_CLIENT_KEY', 'SCENARIOS_CONTAINING', 'SCENARIOS_NOT_CONTAINING', 'BOUNCING_LOG_LEVEL'} local valid_int_params = {'CACHE_EXPIRATION', 'CACHE_SIZE', 'REQUEST_TIMEOUT', 'UPDATE_FREQUENCY', 'CAPTCHA_EXPIRATION', 'APPSEC_CONNECT_TIMEOUT', 'APPSEC_SEND_TIMEOUT', 'APPSEC_PROCESS_TIMEOUT', 'STREAM_REQUEST_TIMEOUT'} -- CACHE_SIZE is not used in the code, but as is was valid parameter for the configuration file, not removing it now local valid_bouncing_on_type_values = {'ban', 'captcha', 'all'} local valid_truefalse_values = {'false', 'true'} +-- nginx error_log severities, spelled as in the `error_log` directive. +local valid_log_level_values = {'debug', 'info', 'notice', 'warn', 'error', 'crit', 'alert', 'emerg'} local default_values = { ['ENABLED'] = "true", ['ENABLE_INTERNAL'] = "false", @@ -34,6 +36,7 @@ local default_values = { ['TLS_CLIENT_KEY'] = "", ['SCENARIOS_CONTAINING'] = "", ['SCENARIOS_NOT_CONTAINING'] = "", + ['BOUNCING_LOG_LEVEL'] = "alert", } @@ -125,6 +128,12 @@ function config.loadConfig(file, default) ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'stream' instead") value = "stream" end + elseif key == "BOUNCING_LOG_LEVEL" then + value = value:lower() + if not has_value(valid_log_level_values, value) then + ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'alert' instead") + value = "alert" + end elseif key == "FALLBACK_REMEDIATION" then if not has_value({'captcha', 'ban'}, value) then ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'ban' instead") diff --git a/t/25_bouncing_log_level.t b/t/25_bouncing_log_level.t new file mode 100644 index 0000000..c3ad724 --- /dev/null +++ b/t/25_bouncing_log_level.t @@ -0,0 +1,67 @@ +use Test::Nginx::Socket 'no_plan'; + +run_tests(); + +__DATA__ + +=== TEST 1: BOUNCING_LOG_LEVEL lowers the severity of the "denied" line + +--- main_config +load_module /usr/share/nginx/modules/ndk_http_module.so; +load_module /usr/share/nginx/modules/ngx_http_lua_module.so; + +--- http_config + +lua_package_path './lib/?.lua;;'; +lua_shared_dict crowdsec_cache 50m; +lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; + +init_by_lua_block +{ + cs = require "crowdsec" + local ok, err = cs.init("./t/conf_t/25_notice_crowdsec_nginx_bouncer.conf", "crowdsec-nginx-bouncer/v1.0.8") + if ok == nil then + ngx.log(ngx.ERR, "[Crowdsec] " .. err) + error() + end +} + +access_by_lua_block { + local cs = require "crowdsec" + cs.Allow(ngx.var.remote_addr) +} + +server { + listen 8081; + + location = /v1/decisions { + content_by_lua_block { + local args, err = ngx.req.get_uri_args() + if args.ip == "1.1.1.1" then + ngx.say('[{"duration":"1h00m00s","id":4091593,"origin":"CAPI","scenario":"crowdsecurity/vpatch-CVE-2024-4577","scope":"Ip","type":"ban","value":"1.1.1.1"}]') + else + ngx.print('null') + end + } + } +} + +--- config + +location = /t { + set_real_ip_from 127.0.0.1; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + content_by_lua_block { + ngx.say("Hello, world") + } +} + +--- log_level: info +--- more_headers +X-Forwarded-For: 1.1.1.1 +--- request +GET /t +--- error_code: 403 +--- error_log eval +qr/\[notice\] .*\[Crowdsec\] denied '1\.1\.1\.1' with 'ban'/ diff --git a/t/conf_t/25_notice_crowdsec_nginx_bouncer.conf b/t/conf_t/25_notice_crowdsec_nginx_bouncer.conf new file mode 100644 index 0000000..7f3293f --- /dev/null +++ b/t/conf_t/25_notice_crowdsec_nginx_bouncer.conf @@ -0,0 +1,9 @@ +ENABLED=true +API_URL=http://127.0.0.1:8081 +API_KEY=LFrdL+aiecMTSxpGE9vLkx5sGMwdIpgVovpVMfXp3J0 +MODE=live +BOUNCING_ON_TYPE=all +BAN_TEMPLATE_PATH=./t/conf_t/ban +# the mock LAPI below is served by the same nginx, so it must not bounce itself +EXCLUDE_LOCATION=/v1/decisions +BOUNCING_LOG_LEVEL=notice