From 873863f2b2c42105912aa671ff208f62399129f9 Mon Sep 17 00:00:00 2001 From: Kristian Feldsam Date: Tue, 19 Mar 2024 17:08:29 +0100 Subject: [PATCH 1/2] Show custom 403 page when user ip/network is banned by netfilter Signed-off-by: Kristian Feldsam --- .../Dockerfiles/netfilter/modules/IPTables.py | 24 +- data/conf/nginx/fastcgi_params | 25 ++ data/conf/nginx/includes/site-defaults.conf | 334 +++++++++--------- data/conf/nginx/ip_blacklist.lua | 96 +++++ data/conf/nginx/mime.types | 99 ++++++ data/conf/nginx/site.conf | 1 + data/web/_status.403.html | 39 ++ docker-compose.yml | 6 +- 8 files changed, 460 insertions(+), 164 deletions(-) create mode 100644 data/conf/nginx/fastcgi_params create mode 100644 data/conf/nginx/ip_blacklist.lua create mode 100644 data/conf/nginx/mime.types create mode 100644 data/web/_status.403.html diff --git a/data/Dockerfiles/netfilter/modules/IPTables.py b/data/Dockerfiles/netfilter/modules/IPTables.py index 3d3d43974b..49aefdb01d 100644 --- a/data/Dockerfiles/netfilter/modules/IPTables.py +++ b/data/Dockerfiles/netfilter/modules/IPTables.py @@ -19,6 +19,16 @@ def initChainIPv4(self): rule.target = target if rule not in chain.rules: chain.insert_rule(rule) + + # always allow TCP connections to 80 and 443 ports to show 403 page in case of ban + chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) + rule = iptc.Rule() + rule.create_target("ACCEPT") + match = rule.create_match('multiport') + rule.protocol = 'tcp' + match.dports = '80,443' + if rule not in chain.rules: + chain.insert_rule(rule) def initChainIPv6(self): if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) in iptc.Table6(iptc.Table6.FILTER).chains: @@ -32,6 +42,16 @@ def initChainIPv6(self): rule.target = target if rule not in chain.rules: chain.insert_rule(rule) + + # always allow TCP connections to 80 and 443 ports to show 403 page in case of ban + chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) + rule = iptc.Rule6() + rule.create_target("ACCEPT") + match = rule.create_match('multiport') + rule.protocol = 'tcp' + match.dports = '80,443' + if rule not in chain.rules: + chain.insert_rule(rule) def checkIPv4ChainOrder(self): filter_table = iptc.Table(iptc.Table.FILTER) @@ -98,7 +118,7 @@ def banIPv4(self, source): rule.target = target if rule in chain.rules: return False - chain.insert_rule(rule) + chain.append_rule(rule) return True def banIPv6(self, source): @@ -109,7 +129,7 @@ def banIPv6(self, source): rule.target = target if rule in chain.rules: return False - chain.insert_rule(rule) + chain.append_rule(rule) return True def unbanIPv4(self, source): diff --git a/data/conf/nginx/fastcgi_params b/data/conf/nginx/fastcgi_params new file mode 100644 index 0000000000..28decb9550 --- /dev/null +++ b/data/conf/nginx/fastcgi_params @@ -0,0 +1,25 @@ + +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param SCRIPT_NAME $fastcgi_script_name; +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; +fastcgi_param REQUEST_SCHEME $scheme; +fastcgi_param HTTPS $https if_not_empty; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; + +# PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; diff --git a/data/conf/nginx/includes/site-defaults.conf b/data/conf/nginx/includes/site-defaults.conf index 1d03e93984..6312f50555 100644 --- a/data/conf/nginx/includes/site-defaults.conf +++ b/data/conf/nginx/includes/site-defaults.conf @@ -1,9 +1,11 @@ - include /etc/nginx/mime.types; + include /etc/nginx/conf.d/mime.types; charset utf-8; override_charset on; server_tokens off; + + resolver 127.0.0.11; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; @@ -49,132 +51,136 @@ root /web; location / { - try_files $uri $uri/ @strip-ext; - } - - location /qhandler { - rewrite ^/qhandler/(.*)/(.*) /qhandler.php?action=$1&hash=$2; - } - - location /edit { - rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2; - } - - location @strip-ext { - rewrite ^(.*)$ $1.php last; - } - - location ~ ^/api/v1/(.*)$ { - try_files $uri $uri/ /json_api.php?query=$1&$args; - } - - location ^~ /.well-known/acme-challenge/ { - allow all; - default_type "text/plain"; - } - - # If behind reverse proxy, forwards the correct IP - set_real_ip_from 10.0.0.0/8; - set_real_ip_from 172.16.0.0/12; - set_real_ip_from 192.168.0.0/16; - set_real_ip_from fc00::/7; - real_ip_header X-Forwarded-For; - real_ip_recursive on; - - rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent; - rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent; - - location ^~ /principals { - return 301 /SOGo/dav; - } - - location ^~ /inc/lib/ { - deny all; - return 403; - } - - location ~ \.php$ { - try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9002; - fastcgi_index index.php; - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - fastcgi_read_timeout 3600; - fastcgi_send_timeout 3600; - } - - location /rspamd/ { - location /rspamd/auth { - # proxy_pass is not inherited - proxy_pass http://rspamd:11334/auth; - proxy_intercept_errors on; + access_by_lua_file /etc/nginx/conf.d/ip_blacklist.lua; + + location / { + try_files $uri $uri/ @strip-ext; + } + + + location /qhandler { + rewrite ^/qhandler/(.*)/(.*) /qhandler.php?action=$1&hash=$2; + } + + location /edit { + rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2; + } + + location ~ ^/api/v1/(.*)$ { + try_files $uri $uri/ /json_api.php?query=$1&$args; + } + + location ^~ /.well-known/acme-challenge/ { + allow all; + default_type "text/plain"; + } + + # If behind reverse proxy, forwards the correct IP + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 172.16.0.0/12; + set_real_ip_from 192.168.0.0/16; + set_real_ip_from fc00::/7; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent; + rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent; + + location ^~ /principals { + return 301 /SOGo/dav; + } + + location ^~ /inc/lib/ { + deny all; + return 403; + } + + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9002; + fastcgi_index index.php; + include /etc/nginx/conf.d/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_read_timeout 3600; + fastcgi_send_timeout 3600; + } + + location /rspamd/ { + location /rspamd/auth { + # proxy_pass is not inherited + proxy_pass http://rspamd:11334/auth; + proxy_intercept_errors on; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_redirect off; + error_page 401 /_rspamderror.php; + } + proxy_pass http://rspamd:11334/; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_redirect off; - error_page 401 /_rspamderror.php; } - proxy_pass http://rspamd:11334/; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - proxy_redirect off; - } - - location ~* ^/Autodiscover/Autodiscover.xml { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9002; - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - try_files /autodiscover.php =404; - } - - location ~* ^/Autodiscover/Autodiscover.json { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9002; - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - try_files /autodiscover-json.php =404; - } - - location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9002; - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - try_files /autoconfig.php =404; - } - - location /sogo-auth-verify { - internal; - proxy_set_header X-Original-URI $request_uri; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $http_host; - proxy_set_header Content-Length ""; - proxy_pass http://127.0.0.1:65510/sogo-auth; - proxy_pass_request_body off; - } - - location ^~ /Microsoft-Server-ActiveSync { - include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf; - include /etc/nginx/conf.d/sogo_eas.active; - proxy_connect_timeout 75; - proxy_send_timeout 3600; - proxy_read_timeout 3600; - proxy_buffer_size 128k; - proxy_buffers 64 512k; - proxy_busy_buffers_size 512k; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - client_body_buffer_size 512k; - client_max_body_size 0; - } - - location ^~ /SOGo { - location ~* ^/SOGo/so/.*\.(xml|js|html|xhtml)$ { + + location ~* ^/Autodiscover/Autodiscover.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9002; + include /etc/nginx/conf.d/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autodiscover.php =404; + } + + location ~* ^/Autodiscover/Autodiscover.json { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9002; + include /etc/nginx/conf.d/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autodiscover-json.php =404; + } + + location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9002; + include /etc/nginx/conf.d/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autoconfig.php =404; + } + + location ^~ /Microsoft-Server-ActiveSync { + include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf; + include /etc/nginx/conf.d/sogo_eas.active; + proxy_connect_timeout 75; + proxy_send_timeout 3600; + proxy_read_timeout 3600; + proxy_buffer_size 128k; + proxy_buffers 64 512k; + proxy_busy_buffers_size 512k; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + client_body_buffer_size 512k; + client_max_body_size 0; + } + + location ^~ /SOGo { + location ~* ^/SOGo/so/.*\.(xml|js|html|xhtml)$ { + include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf; + include /etc/nginx/conf.d/sogo.active; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header x-webobjects-server-protocol HTTP/1.0; + proxy_set_header x-webobjects-remote-host $remote_addr; + proxy_set_header x-webobjects-server-name $server_name; + proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; + proxy_set_header x-webobjects-server-port $server_port; + proxy_hide_header Content-Type; + add_header Content-Type text/plain; + break; + } include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf; include /etc/nginx/conf.d/sogo.active; proxy_set_header X-Real-IP $remote_addr; @@ -185,58 +191,66 @@ proxy_set_header x-webobjects-server-name $server_name; proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; proxy_set_header x-webobjects-server-port $server_port; - proxy_hide_header Content-Type; - add_header Content-Type text/plain; + proxy_buffer_size 128k; + proxy_buffers 64 512k; + proxy_busy_buffers_size 512k; + proxy_send_timeout 3600; + proxy_read_timeout 3600; + client_body_buffer_size 128k; + client_max_body_size 0; break; } - include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf; - include /etc/nginx/conf.d/sogo.active; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header x-webobjects-server-protocol HTTP/1.0; - proxy_set_header x-webobjects-remote-host $remote_addr; - proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; - proxy_set_header x-webobjects-server-port $server_port; - proxy_buffer_size 128k; - proxy_buffers 64 512k; - proxy_busy_buffers_size 512k; - proxy_send_timeout 3600; - proxy_read_timeout 3600; - client_body_buffer_size 128k; - client_max_body_size 0; - break; - } - - location ~* /sogo$ { - return 301 $client_req_scheme://$http_host/SOGo; - } - - location /SOGo.woa/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; - } - - location /.woa/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; + + location /SOGo.woa/WebServerResources/ { + alias /usr/lib/GNUstep/SOGo/WebServerResources/; + } + + location /.woa/WebServerResources/ { + alias /usr/lib/GNUstep/SOGo/WebServerResources/; + } + + location /SOGo/WebServerResources/ { + alias /usr/lib/GNUstep/SOGo/WebServerResources/; + } + + location ~* /sogo$ { + return 301 $client_req_scheme://$http_host/SOGo; + } + + include /etc/nginx/conf.d/site.*.custom; + + location ~ ^/cache/(.*)$ { + try_files $uri $uri/ /resource.php?file=$1; + } } - - location /SOGo/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; + + location /sogo-auth-verify { + internal; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_set_header Content-Length ""; + proxy_pass http://127.0.0.1:65510/sogo-auth; + proxy_pass_request_body off; } location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) { + access_by_lua_file /etc/nginx/conf.d/ip_blacklist.lua; + alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; } - include /etc/nginx/conf.d/site.*.custom; + location @strip-ext { + rewrite ^(.*)$ $1.php last; + } error_page 502 @awaitingupstream; + error_page 403 @blocked; location @awaitingupstream { rewrite ^(.*)$ /_status.502.html break; } - location ~ ^/cache/(.*)$ { - try_files $uri $uri/ /resource.php?file=$1; + location @blocked { + rewrite ^(.*)$ /_status.403.html break; } diff --git a/data/conf/nginx/ip_blacklist.lua b/data/conf/nginx/ip_blacklist.lua new file mode 100644 index 0000000000..1994b2e8cb --- /dev/null +++ b/data/conf/nginx/ip_blacklist.lua @@ -0,0 +1,96 @@ +-- original source https://gist.github.com/chrisboulton/6043871 +-- +-- a quick LUA access script for nginx to check IP addresses against an +-- `ip_blacklist` set in Redis, and if a match is found send a HTTP 403. +-- +-- allows for a common blacklist to be shared between a bunch of nginx +-- web servers using a remote redis instance. lookups are cached for a +-- configurable period of time. +-- +-- also requires lua-resty-redis from: +-- https://github.com/agentzh/lua-resty-redis +-- +-- your nginx http context should contain something similar to the +-- below: +-- +-- lua_shared_dict ip_blacklist_cache 10m; +-- +-- you can then use the below (adjust path where necessary) to check +-- against the blacklist in a http, server, location, if context: +-- +-- access_by_lua_file /etc/nginx/conf.d/ip_blacklist.lua; +-- +-- chris boulton, @surfichris + +local redis_host = "redis" +local redis_port = 6379 + +-- connection timeout for redis in ms. don't set this too high! +local redis_timeout = 200 + +-- cache lookups for this many seconds +local cache_ttl = 60 + +-- end configuration + +local ip = ngx.var.remote_addr +local ip_blacklist_cache = ngx.shared.ip_blacklist_cache + +-- setup a local cache +if cache_ttl > 0 then + -- lookup the value in the cache + local cache_result = ip_blacklist_cache:get(ip) + if cache_result then + ngx.log(ngx.DEBUG, "ip_blacklist: found result in cache for "..ip.." -> "..cache_result) + + if cache_result == 0 then + ngx.log(ngx.DEBUG, "ip_blacklist: (cache) no result found for "..ip) + return + end + + ngx.log(ngx.INFO, "ip_blacklist: (cache) "..ip.." is blacklisted") + return ngx.exit(ngx.HTTP_FORBIDDEN) + end +end + +-- helper ip utils +local iputils = require "resty.iputils" +iputils.enable_lrucache() + +-- lookup against redis +local resty = require "resty.redis" +local redis = resty:new() + +redis:set_timeout(redis_timeout) + +local connected, err = redis:connect(redis_host, redis_port) +if not connected then + ngx.log(ngx.ERR, "ip_blacklist: could not connect to redis @"..redis_host..": "..err) + return +end + +-- check for active bans +local result, err = redis:hkeys("F2B_ACTIVE_BANS") +local active_bans = iputils.parse_cidrs(result) +-- load perm bans +local result, err = redis:hkeys("F2B_PERM_BANS") +local perm_bans = iputils.parse_cidrs(result) + +local ban = 0 +if iputils.ip_in_cidrs(ip, active_bans) or iputils.ip_in_cidrs(ip, perm_bans) then + ban = 1 +end + +-- cache the result from redis +if cache_ttl > 0 then + ip_blacklist_cache:set(ip, ban, cache_ttl) +end + +redis:set_keepalive(10000, 2) +if ban == 0 then + ngx.log(ngx.INFO, "ip_blacklist: no result found for "..ip) + return +end + +ngx.log(ngx.INFO, "ip_blacklist: "..ip.." is blacklisted") +return ngx.exit(ngx.HTTP_FORBIDDEN) diff --git a/data/conf/nginx/mime.types b/data/conf/nginx/mime.types new file mode 100644 index 0000000000..1c00d701ae --- /dev/null +++ b/data/conf/nginx/mime.types @@ -0,0 +1,99 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/avif avif; + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/wasm wasm; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf index fb40de879c..16eda7fcf8 100644 --- a/data/conf/nginx/site.conf +++ b/data/conf/nginx/site.conf @@ -1,6 +1,7 @@ proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g; server_names_hash_max_size 512; server_names_hash_bucket_size 128; +lua_shared_dict ip_blacklist_cache 10m; map $http_x_forwarded_proto $client_req_scheme { default $scheme; diff --git a/data/web/_status.403.html b/data/web/_status.403.html new file mode 100644 index 0000000000..2752bb919a --- /dev/null +++ b/data/web/_status.403.html @@ -0,0 +1,39 @@ + + + + Access Restricted + + + +

Access Restricted:
Important Notice!

+

Your Connection Is Temporarily Blocked.

+

We have detected multiple failed login attempts from your IP address or network range, which has triggered a security response. To protect your account and our services, access has been temporarily restricted.

+

Possible Reasons for the Block:

+ +

Immediate Steps You Can Take:

+
    +
  1. Verify Passwords: Ensure that all devices and applications using your email account have the correct password set.
  2. +
  3. Check Network Activity: If you are on a shared network, please check if other users or devices might be causing failed login attempts.
  4. +
+

Please note that the automatic block is typically lifted within 10-15 minutes, so we recommend waiting a short while before attempting to access the services again.

+

Need Further Assistance?

+

Our support team is here to help you regain access and resolve any issues promptly. Please reach out to us if the problem persists or if you need guidance on securing your account:

+

Get support.

+

We appreciate your understanding and cooperation in maintaining the security and reliability of our services.

+ +

Translate this page:

+ +
+ + + + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ea56b4291e..f1c3cea447 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -359,7 +359,7 @@ services: - sogo-mailcow - php-fpm-mailcow - redis-mailcow - image: nginx:mainline-alpine + image: openresty/openresty:alpine-fat dns: - ${IPV4_NETWORK:-172.22.1}.254 command: /bin/sh -c "envsubst < /etc/nginx/conf.d/templates/listen_plain.template > /etc/nginx/conf.d/listen_plain.active && @@ -368,6 +368,8 @@ services: . /etc/nginx/conf.d/templates/server_name.template.sh > /etc/nginx/conf.d/server_name.active && . /etc/nginx/conf.d/templates/sites.template.sh > /etc/nginx/conf.d/sites.active && . /etc/nginx/conf.d/templates/sogo_eas.template.sh > /etc/nginx/conf.d/sogo_eas.active && + mkdir -p /var/log/nginx && + /usr/local/openresty/luajit/bin/luarocks install lua-resty-iputils && nginx -qt && until ping phpfpm -c1 > /dev/null; do sleep 1; done && until ping sogo -c1 > /dev/null; do sleep 1; done && @@ -441,7 +443,7 @@ services: - acme netfilter-mailcow: - image: mailcow/netfilter:1.58 + image: mailcow/netfilter:1.60 stop_grace_period: 30s restart: always privileged: true From e3cf0052c477e0ff59fa55f708eca719622753dd Mon Sep 17 00:00:00 2001 From: Kristian Feldsam Date: Wed, 20 Mar 2024 19:11:46 +0100 Subject: [PATCH 2/2] Added simple self-unban process Just with behavioural check. Signed-off-by: Kristian Feldsam --- data/conf/nginx/includes/site-defaults.conf | 8 ++ data/web/_status.403.html | 4 +- data/web/templates/unban.twig | 116 ++++++++++++++++++++ data/web/unban.php | 105 ++++++++++++++++++ 4 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 data/web/templates/unban.twig create mode 100644 data/web/unban.php diff --git a/data/conf/nginx/includes/site-defaults.conf b/data/conf/nginx/includes/site-defaults.conf index 6312f50555..9cb1dde234 100644 --- a/data/conf/nginx/includes/site-defaults.conf +++ b/data/conf/nginx/includes/site-defaults.conf @@ -254,3 +254,11 @@ location @blocked { rewrite ^(.*)$ /_status.403.html break; } + + location /unban { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9002; + include /etc/nginx/conf.d/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /unban.php =404; + } diff --git a/data/web/_status.403.html b/data/web/_status.403.html index 2752bb919a..249ff9b2c2 100644 --- a/data/web/_status.403.html +++ b/data/web/_status.403.html @@ -18,7 +18,9 @@

Immediate Steps You Can Take:

  • Verify Passwords: Ensure that all devices and applications using your email account have the correct password set.
  • Check Network Activity: If you are on a shared network, please check if other users or devices might be causing failed login attempts.
  • -

    Please note that the automatic block is typically lifted within 10-15 minutes, so we recommend waiting a short while before attempting to access the services again.

    +

    Self-Unban Procedure:

    +

    If you believe this block is a mistake or you've resolved the issue leading to the block, you can initiate the self-unban process. Please click the link below and follow the instructions to regain access immediately:

    +

    Initiate Self-Unban

    Need Further Assistance?

    Our support team is here to help you regain access and resolve any issues promptly. Please reach out to us if the problem persists or if you need guidance on securing your account:

    Get support.

    diff --git a/data/web/templates/unban.twig b/data/web/templates/unban.twig new file mode 100644 index 0000000000..8b5259dd96 --- /dev/null +++ b/data/web/templates/unban.twig @@ -0,0 +1,116 @@ +{% if unban_success %} + + + + Unban Successful + + + +
    +

    Your unban request was successful. Please wait a moment and then refresh the page.

    +
    + + +{% else %} + + + + Unban Challenge + + + + +
    +

    Please wait before clicking the button below to submit your unban request.

    + +
    + +
    + + + + + +
    + + +{% endif %} \ No newline at end of file diff --git a/data/web/unban.php b/data/web/unban.php new file mode 100644 index 0000000000..5fe3a1ba71 --- /dev/null +++ b/data/web/unban.php @@ -0,0 +1,105 @@ +connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); + } + else { + $redis->connect('redis-mailcow', 6379); + } +} +catch (Exception $e) { + exit; +} + +// check if IP is banned +$ip = $_SERVER['REMOTE_ADDR']; +$bans = $redis->hkeys('F2B_ACTIVE_BANS'); + +$banned = false; +foreach($bans as $ban) { + if (ip_in_range( $ip, $ban)) { + $banned = $ban; + break; + } +} + +if(!$banned) { + header('Location: /'); + exit(); +} + +function check(){ + // Check if the honeypot field is filled + if (!empty($_POST['hp'])) { + return false; + } + + // Validate the cryptographic token + if (!hash_equals($_SESSION['token'], $_POST['token'])) { + return false; + } + + // Verify the timing + $startTime = $_SESSION['startTime']; + $endTime = isset($_POST['endTime']) ? $_POST['endTime'] / 1000 : 0; // Convert to seconds + $elapsed = $endTime - $startTime; + + // Ensure the user waited for the randomized time, allowing some leeway + if ($elapsed >= $_SESSION['waitTime'] && $elapsed <= ($_SESSION['waitTime'] + 5)) { + return true; + } + + return false; +} + +$success = null; +if(isset($_POST['unban'])) { + if($success = check()) { + $redis->hSet('F2B_QUEUE_UNBAN', $banned, 1); + } +} + +if(!$success) { + // Generate a random wait time and cryptographic token + $_SESSION['waitTime'] = rand(10, 30); + $_SESSION['startTime'] = microtime(true); + $_SESSION['token'] = bin2hex(random_bytes(32)); +} + + +$template = 'unban.twig'; +$template_data = [ + 'unban_success' => $success, + 'start_time' => round(microtime(true) * 1000), + 'wait_time' => $_SESSION['waitTime'], + 'token' => $_SESSION['token'], +]; + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; \ No newline at end of file