Skip to content

Commit 1271c42

Browse files
committed
Enhance security by adding automatic scanner blocking and refining probe filtering in NGINX configuration and health monitor script
1 parent 6db672c commit 1271c42

3 files changed

Lines changed: 182 additions & 2 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,13 @@ Example response:
9797
- `DNS_RESOLVER`: DNS resolver servers (default: `8.8.8.8`, space-separated list). Check `cat /etc/resolv.conf` in your container to find the correct value for your environment.
9898
- `STATUS_POLL_INTERVAL`: Seconds between `/status` refreshes (default: `5`)
9999
- `HEALTH_LOG_INTERVAL`: Seconds between periodic upstream health log lines when state is unchanged (default: `300`)
100+
- `AUTO_BLOCK_SCANNERS`: Automatically append probe-source IPs from `/logs/hacks/probes.log` to `/logs/blocked.txt` and live-reload NGINX maps (default: `true`)
100101

101102
### Security Filtering and Blocking
102103

103-
- **Probe filtering**: Requests matching common probing signatures (for example `*.php`, `wp-login.php`, `xmlrpc.php`, `wlwmanifest.xml`, `.env`, `phpmyadmin`, path traversal payloads) are immediately refused with HTTP `403` and are **not** forwarded upstream.
104+
- **Probe filtering**: Requests matching common probing signatures (for example `*.php`, WordPress probe paths like `wp-login.php`, `xmlrpc.php`, `wlwmanifest.xml`, `wp-includes/*`, `.env`, `phpmyadmin`, path traversal payloads) are immediately refused with HTTP `403` and are **not** forwarded upstream.
104105
- **Probe log output**: Refused probe requests are logged to `/logs/hacks/probes.log`, including both raw `X-Forwarded-For` and the extracted left-most client IP.
106+
- **Automatic scanner blocking**: When `AUTO_BLOCK_SCANNERS=true`, newly detected `client_ip` values in `/logs/hacks/probes.log` are appended to `/logs/blocked.txt` (unless already present or whitelisted), and NGINX is reloaded so the block takes effect without container restart.
105107
- **Manual IP blocklist**: Add one IPv4/IPv6 address per line in `/logs/blocked.txt` (comments allowed with `#`).
106108
- **Manual IP whitelist**: Add one IPv4/IPv6 address per line in `/logs/whitelist.txt` (comments allowed with `#`).
107109

@@ -125,7 +127,7 @@ Blocked IP requests return HTTP `403` and are logged to `/logs/hacks/blocked.log
125127

126128
Whitelist entries take precedence over both the blocklist and probe filter.
127129

128-
Blocklist/whitelist entries are loaded when the container starts. If you update `/logs/blocked.txt` or `/logs/whitelist.txt`, restart the container to apply changes.
130+
Blocklist/whitelist entries are watched continuously by the runtime monitor. Updates to `/logs/blocked.txt` or `/logs/whitelist.txt` are converted into map files and applied via `nginx -s reload` within a few seconds.
129131

130132
### Cache Headers
131133

health-monitor.sh

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ STATUS_FILE=${STATUS_FILE:-/var/run/nginx/status.json}
88
NGINX_STATUS_URL=${NGINX_STATUS_URL:-http://127.0.0.1:8080/__nginx_status}
99
STATUS_POLL_INTERVAL=${STATUS_POLL_INTERVAL:-5}
1010
HEALTH_LOG_INTERVAL=${HEALTH_LOG_INTERVAL:-300}
11+
PROBE_LOG=${PROBE_LOG:-/logs/hacks/probes.log}
12+
BLOCKLIST_SOURCE=${BLOCKLIST_SOURCE:-/logs/blocked.txt}
13+
BLOCKLIST_MAP=${BLOCKLIST_MAP:-/etc/nginx/blocked-ips.map}
14+
WHITELIST_SOURCE=${WHITELIST_SOURCE:-/logs/whitelist.txt}
15+
WHITELIST_MAP=${WHITELIST_MAP:-/etc/nginx/whitelisted-ips.map}
16+
AUTO_BLOCK_SCANNERS=${AUTO_BLOCK_SCANNERS:-true}
1117

1218
UPSTREAM_HOST=$(printf '%s' "$UPSTREAM_SERVER" | cut -d: -f1)
1319
UPSTREAM_PORT=$(printf '%s' "$UPSTREAM_SERVER" | cut -d: -f2)
@@ -28,6 +34,9 @@ total_requests=0
2834
hit_requests=0
2935
miss_requests=0
3036
access_log_size=0
37+
probe_log_size=0
38+
last_blocklist_signature=0:0
39+
last_whitelist_signature=0:0
3140

3241
active_connections=
3342
reading_connections=
@@ -64,6 +73,162 @@ get_file_size() {
6473
fi
6574
}
6675

76+
get_file_mtime() {
77+
if [ ! -f "$1" ]; then
78+
printf '0'
79+
return
80+
fi
81+
82+
if mtime=$(stat -c %Y "$1" 2>/dev/null); then
83+
printf '%s' "$mtime"
84+
else
85+
printf '0'
86+
fi
87+
}
88+
89+
get_file_signature() {
90+
file_path="$1"
91+
printf '%s:%s' "$(get_file_mtime "$file_path")" "$(get_file_size "$file_path")"
92+
}
93+
94+
is_truthy() {
95+
value=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
96+
case "$value" in
97+
1|true|yes|on)
98+
return 0
99+
;;
100+
*)
101+
return 1
102+
;;
103+
esac
104+
}
105+
106+
is_valid_ip() {
107+
printf '%s' "$1" | grep -Eq '^[0-9A-Fa-f:.]+$'
108+
}
109+
110+
is_ip_listed() {
111+
source_file="$1"
112+
ip="$2"
113+
114+
if [ ! -f "$source_file" ]; then
115+
return 1
116+
fi
117+
118+
grep -Fqx "$ip" "$source_file" 2>/dev/null
119+
}
120+
121+
prepare_security_paths() {
122+
mkdir -p \
123+
"$(dirname "$PROBE_LOG")" \
124+
"$(dirname "$BLOCKLIST_SOURCE")" \
125+
"$(dirname "$BLOCKLIST_MAP")" \
126+
"$(dirname "$WHITELIST_SOURCE")" \
127+
"$(dirname "$WHITELIST_MAP")"
128+
129+
touch "$PROBE_LOG"
130+
touch "$BLOCKLIST_SOURCE"
131+
touch "$BLOCKLIST_MAP"
132+
touch "$WHITELIST_SOURCE"
133+
touch "$WHITELIST_MAP"
134+
}
135+
136+
generate_ip_map() {
137+
source_file="$1"
138+
target_map="$2"
139+
label="$3"
140+
tmp_map="$(mktemp /tmp/${label}-ips.XXXXXX)"
141+
: > "$tmp_map"
142+
143+
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
144+
line="$(printf '%s' "$raw_line" | tr -d '\r' | sed 's/#.*//;s/^[[:space:]]*//;s/[[:space:]]*$//')"
145+
[ -z "$line" ] && continue
146+
147+
if is_valid_ip "$line"; then
148+
printf '%s 1;\n' "$line" >> "$tmp_map"
149+
else
150+
printf 'Ignoring invalid %s IP entry in %s: %s\n' "$label" "$source_file" "$raw_line" >&2
151+
fi
152+
done < "$source_file"
153+
154+
mv "$tmp_map" "$target_map"
155+
}
156+
157+
reload_nginx() {
158+
if nginx -s reload >/dev/null 2>&1; then
159+
echo "$(date): Applied updated block/whitelist IP maps and reloaded nginx"
160+
else
161+
echo "$(date): WARNING - Failed to reload nginx after IP map update" >&2
162+
fi
163+
}
164+
165+
sync_ip_maps_if_needed() {
166+
blocklist_signature=$(get_file_signature "$BLOCKLIST_SOURCE")
167+
whitelist_signature=$(get_file_signature "$WHITELIST_SOURCE")
168+
169+
if [ "$blocklist_signature" = "$last_blocklist_signature" ] && [ "$whitelist_signature" = "$last_whitelist_signature" ]; then
170+
return
171+
fi
172+
173+
generate_ip_map "$BLOCKLIST_SOURCE" "$BLOCKLIST_MAP" "blocked"
174+
generate_ip_map "$WHITELIST_SOURCE" "$WHITELIST_MAP" "whitelisted"
175+
last_blocklist_signature="$blocklist_signature"
176+
last_whitelist_signature="$whitelist_signature"
177+
reload_nginx
178+
}
179+
180+
update_auto_blocklist_from_probe_log() {
181+
if ! is_truthy "$AUTO_BLOCK_SCANNERS"; then
182+
return
183+
fi
184+
185+
current_size=$(get_file_size "$PROBE_LOG")
186+
187+
if [ "$current_size" -lt "$probe_log_size" ]; then
188+
# Log rotation/truncation: restart tailing from the beginning.
189+
probe_log_size=0
190+
fi
191+
192+
if [ "$current_size" -eq "$probe_log_size" ]; then
193+
return
194+
fi
195+
196+
start_byte=$((probe_log_size + 1))
197+
tmp_chunk="$(mktemp /tmp/probe-log.XXXXXX)"
198+
199+
if ! tail -c +"$start_byte" "$PROBE_LOG" > "$tmp_chunk" 2>/dev/null; then
200+
rm -f "$tmp_chunk"
201+
probe_log_size="$current_size"
202+
return
203+
fi
204+
205+
probe_log_size="$current_size"
206+
added_ip=0
207+
208+
while IFS= read -r line || [ -n "$line" ]; do
209+
ip="$(printf '%s\n' "$line" | sed -n 's/.*client_ip="\([^"]*\)".*/\1/p')"
210+
[ -z "$ip" ] && continue
211+
is_valid_ip "$ip" || continue
212+
213+
# Whitelisted IPs remain exempt even if they trigger probe patterns.
214+
if is_ip_listed "$WHITELIST_SOURCE" "$ip"; then
215+
continue
216+
fi
217+
218+
if ! is_ip_listed "$BLOCKLIST_SOURCE" "$ip"; then
219+
printf '%s\n' "$ip" >> "$BLOCKLIST_SOURCE"
220+
added_ip=1
221+
echo "$(date): Auto-blocked scanner IP: $ip"
222+
fi
223+
done < "$tmp_chunk"
224+
225+
rm -f "$tmp_chunk"
226+
227+
if [ "$added_ip" -eq 1 ]; then
228+
sync_ip_maps_if_needed
229+
fi
230+
}
231+
67232
recount_access_log() {
68233
if [ ! -f "$ACCESS_LOG" ]; then
69234
total_requests=0
@@ -216,15 +381,27 @@ EOF
216381
}
217382

218383
mkdir -p "$STATUS_DIR" "$ACCESS_LOG_DIR"
384+
prepare_security_paths
219385
umask 022
220386

387+
probe_log_size=$(get_file_size "$PROBE_LOG")
388+
last_blocklist_signature=$(get_file_signature "$BLOCKLIST_SOURCE")
389+
last_whitelist_signature=$(get_file_signature "$WHITELIST_SOURCE")
390+
221391
recount_access_log
222392
write_status_file
223393

224394
echo "Monitoring upstream server: $UPSTREAM_HOST:$UPSTREAM_PORT"
395+
if is_truthy "$AUTO_BLOCK_SCANNERS"; then
396+
echo "Auto-blocking scanner IPs from probe log is enabled"
397+
else
398+
echo "Auto-blocking scanner IPs from probe log is disabled"
399+
fi
225400

226401
while true; do
227402
update_access_log_counts
403+
update_auto_blocklist_from_probe_log
404+
sync_ip_maps_if_needed
228405
update_upstream_health
229406
update_connection_stats
230407
write_status_file

nginx.conf.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ http {
2222
default 0;
2323
~*(?:^|/).*\.(?:php\d*|phtml|phar|asp|aspx|jsp|cgi|pl)(?:$|[/?]) 1;
2424
~*(?:^|/)(?:wp-admin|wp-login\.php|xmlrpc\.php|wlwmanifest\.xml|phpmyadmin|pma|adminer|cgi-bin|vendor/phpunit)(?:$|[/?]) 1;
25+
~*(?:^|/)wp-(?:content|includes)(?:$|[/?]) 1;
2526
~*(?:^|/)\.(?:env|git|svn|hg|DS_Store)(?:$|[/?]) 1;
2627
~*(?:\.\./|etc/passwd|proc/self/environ|php://|auto_prepend_file|allow_url_include|%00) 1;
2728
}

0 commit comments

Comments
 (0)