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