Skip to content

Commit aba3181

Browse files
committed
Enhance security features and logging in NGINX configuration and Docker setup
1 parent 347fe3c commit aba3181

File tree

6 files changed

+179
-6
lines changed

6 files changed

+179
-6
lines changed

.github/workflows/docker.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@ jobs:
3232
DNS_RESOLVER: 8.8.8.8
3333
run: |
3434
envsubst '${UPSTREAM_SERVER} ${CACHE_MAX_SIZE} ${CACHE_STALE_TIME} ${DNS_RESOLVER}' < nginx.conf.template > test-nginx.conf
35-
docker run --rm -v $(pwd)/test-nginx.conf:/tmp/nginx.conf nginx:1.26-alpine nginx -t -c /tmp/nginx.conf
35+
mkdir -p test-logs/hacks
36+
touch test-logs/blocked.txt test-logs/whitelist.txt test-logs/blocked-ips.map test-logs/whitelisted-ips.map
37+
docker run --rm \
38+
-v $(pwd)/test-nginx.conf:/tmp/nginx.conf \
39+
-v $(pwd)/test-logs:/logs \
40+
-v $(pwd)/test-logs/blocked-ips.map:/etc/nginx/blocked-ips.map \
41+
-v $(pwd)/test-logs/whitelisted-ips.map:/etc/nginx/whitelisted-ips.map \
42+
nginx:1.26-alpine nginx -t -c /tmp/nginx.conf
43+
rm -f test-nginx.conf
44+
rm -rf test-logs
3645
3746
- name: Build test Docker image
3847
run: docker build --no-cache . --file Dockerfile --tag test-image
@@ -71,4 +80,4 @@ jobs:
7180
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
7281

7382
- name: Push Docker image
74-
run: docker push virtualflybrain/owl_cache:${{ steps.meta.outputs.tag }}
83+
run: docker push virtualflybrain/owl_cache:${{ steps.meta.outputs.tag }}

Dockerfile

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@ ENV DNS_RESOLVER=8.8.8.8
1111
ARG NGINX_CONF=nginx.conf.template
1212
COPY $NGINX_CONF /etc/nginx/nginx.conf.template
1313
COPY health-monitor.sh /usr/local/bin/health-monitor.sh
14+
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
1415

15-
RUN mkdir -p /var/cache/nginx/owlery && chown -R nginx:nginx /var/cache/nginx && \
16-
chmod +x /usr/local/bin/health-monitor.sh && \
16+
RUN mkdir -p /var/cache/nginx/owlery /logs/hacks && \
17+
touch /logs/blocked.txt /logs/whitelist.txt /etc/nginx/blocked-ips.map /etc/nginx/whitelisted-ips.map && \
18+
chown -R nginx:nginx /var/cache/nginx /logs && \
19+
chmod +x /usr/local/bin/health-monitor.sh /usr/local/bin/docker-entrypoint.sh && \
1720
apk add --no-cache gettext
1821

1922
EXPOSE 80 8080
2023

21-
CMD ["/bin/sh", "-c", "export UPSTREAM_SERVER=$UPSTREAM_SERVER && export CACHE_MAX_SIZE=$CACHE_MAX_SIZE && export CACHE_STALE_TIME=$CACHE_STALE_TIME && export DNS_RESOLVER=\"$DNS_RESOLVER\" && envsubst '${UPSTREAM_SERVER} ${CACHE_MAX_SIZE} ${CACHE_STALE_TIME} ${DNS_RESOLVER}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && /usr/local/bin/health-monitor.sh & nginx -g 'daemon off;'"]
24+
VOLUME ["/var/cache/nginx", "/logs"]
25+
26+
CMD ["/usr/local/bin/docker-entrypoint.sh"]

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
A high-performance caching proxy server that sits in front of OWL reasoning services to dramatically speed up query responses. Built on NGINX Alpine with a 6-month cache TTL, stale-while-revalidate pattern, and 5-year disk retention so a cached response is always available.
66

7+
The proxy also includes security guardrails to refuse common scanner/probing requests before they reach Owlery, with optional IP block and whitelist files under `/logs`.
8+
79
## Usage Examples
810

911
### Basic Usage
@@ -29,6 +31,9 @@ services:
2931
ports:
3032
- "80:80"
3133
- "8080:8080"
34+
volumes:
35+
- /cache:/var/cache/nginx
36+
- /logs:/logs
3237
environment:
3338
- UPSTREAM_SERVER=owl:8080 # For production with owl service
3439
- CACHE_MAX_SIZE=1t # 1TB cache size for high-traffic deployments
@@ -93,6 +98,35 @@ Example response:
9398
- `STATUS_POLL_INTERVAL`: Seconds between `/status` refreshes (default: `5`)
9499
- `HEALTH_LOG_INTERVAL`: Seconds between periodic upstream health log lines when state is unchanged (default: `300`)
95100

101+
### Security Filtering and Blocking
102+
103+
- **Probe filtering**: Requests matching common probing signatures (for example `*.php`, `wp-login.php`, `.env`, `phpmyadmin`, path traversal payloads) are immediately refused with HTTP `403` and are **not** forwarded upstream.
104+
- **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.
105+
- **Manual IP blocklist**: Add one IPv4/IPv6 address per line in `/logs/blocked.txt` (comments allowed with `#`).
106+
- **Manual IP whitelist**: Add one IPv4/IPv6 address per line in `/logs/whitelist.txt` (comments allowed with `#`).
107+
108+
Example `/logs/blocked.txt`:
109+
110+
```txt
111+
203.0.113.10
112+
# office VPN egress
113+
2001:db8::1234
114+
```
115+
116+
Example `/logs/whitelist.txt`:
117+
118+
```txt
119+
203.0.113.50
120+
# trusted monitoring source
121+
2001:db8::beef
122+
```
123+
124+
Blocked IP requests return HTTP `403` and are logged to `/logs/hacks/blocked.log`.
125+
126+
Whitelist entries take precedence over both the blocklist and probe filter.
127+
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.
129+
96130
### Cache Headers
97131

98132
The proxy adds helpful headers to responses:
@@ -151,6 +185,11 @@ docker pull virtualflybrain/owl_cache:latest
151185
mkdir -p /cache
152186
chown -R 101:101 /cache
153187

188+
# Create persistent logs + blocklist file
189+
mkdir -p /logs/hacks
190+
touch /logs/blocked.txt
191+
touch /logs/whitelist.txt
192+
154193
# Deploy with compose
155194
docker-compose up -d
156195

docker-compose.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ services:
99
- UPSTREAM_SERVER=owl:8080
1010
volumes:
1111
- /cache:/var/cache/nginx
12+
- /logs:/logs
1213
networks:
1314
- owlery_network
1415
ports:
@@ -26,4 +27,4 @@ services:
2627

2728
networks:
2829
owlery_network:
29-
driver: bridge
30+
driver: bridge

docker-entrypoint.sh

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
BLOCKLIST_SOURCE="/logs/blocked.txt"
5+
BLOCKLIST_MAP="/etc/nginx/blocked-ips.map"
6+
WHITELIST_SOURCE="/logs/whitelist.txt"
7+
WHITELIST_MAP="/etc/nginx/whitelisted-ips.map"
8+
9+
prepare_log_paths() {
10+
# Ensure required runtime directories exist, including fresh bind mounts/volumes.
11+
mkdir -p \
12+
/logs \
13+
/logs/hacks \
14+
/var/cache/nginx \
15+
/var/cache/nginx/owlery \
16+
/var/log/nginx \
17+
/etc/nginx
18+
19+
touch "$BLOCKLIST_SOURCE"
20+
touch "$WHITELIST_SOURCE"
21+
touch "$BLOCKLIST_MAP"
22+
touch "$WHITELIST_MAP"
23+
}
24+
25+
generate_ip_map() {
26+
source_file="$1"
27+
target_map="$2"
28+
label="$3"
29+
tmp_map="$(mktemp /tmp/${label}-ips.XXXXXX)"
30+
: > "$tmp_map"
31+
32+
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
33+
line="$(printf '%s' "$raw_line" | tr -d '\r' | sed 's/#.*//;s/^[[:space:]]*//;s/[[:space:]]*$//')"
34+
[ -z "$line" ] && continue
35+
36+
if printf '%s' "$line" | grep -Eq '^[0-9A-Fa-f:.]+$'; then
37+
printf '%s 1;\n' "$line" >> "$tmp_map"
38+
else
39+
printf 'Ignoring invalid %s IP entry in %s: %s\n' "$label" "$source_file" "$raw_line" >&2
40+
fi
41+
done < "$source_file"
42+
43+
mv "$tmp_map" "$target_map"
44+
}
45+
46+
export UPSTREAM_SERVER="${UPSTREAM_SERVER:-owl.virtualflybrain.org:80}"
47+
export CACHE_MAX_SIZE="${CACHE_MAX_SIZE:-20g}"
48+
export CACHE_STALE_TIME="${CACHE_STALE_TIME:-6M}"
49+
export DNS_RESOLVER="${DNS_RESOLVER:-8.8.8.8}"
50+
51+
prepare_log_paths
52+
generate_ip_map "$BLOCKLIST_SOURCE" "$BLOCKLIST_MAP" "blocked"
53+
generate_ip_map "$WHITELIST_SOURCE" "$WHITELIST_MAP" "whitelisted"
54+
55+
envsubst '${UPSTREAM_SERVER} ${CACHE_MAX_SIZE} ${CACHE_STALE_TIME} ${DNS_RESOLVER}' \
56+
< /etc/nginx/nginx.conf.template \
57+
> /etc/nginx/nginx.conf
58+
59+
/usr/local/bin/health-monitor.sh &
60+
exec nginx -g 'daemon off;'

nginx.conf.template

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,56 @@ http {
1111
keepalive_timeout 65s;
1212
keepalive_requests 10000;
1313

14+
# Extract the left-most X-Forwarded-For IP (original client) when present.
15+
map $http_x_forwarded_for $client_ip_for_security {
16+
default $remote_addr;
17+
"~^\s*([^,\s]+)" $1;
18+
}
19+
20+
# Block common automated probing paths before they reach the upstream.
21+
map $request_uri $is_probe_request {
22+
default 0;
23+
~*(?:^|/).*\.(?:php\d*|phtml|phar|asp|aspx|jsp|cgi|pl)(?:$|[/?]) 1;
24+
~*(?:^|/)(?:wp-admin|wp-login\.php|xmlrpc\.php|phpmyadmin|pma|adminer|cgi-bin|vendor/phpunit)(?:$|[/?]) 1;
25+
~*(?:^|/)\.(?:env|git|svn|hg|DS_Store)(?:$|[/?]) 1;
26+
~*(?:\.\./|etc/passwd|proc/self/environ|php://|auto_prepend_file|allow_url_include|%00) 1;
27+
}
28+
29+
# Generated at container startup from /logs/whitelist.txt (one IP per line).
30+
map $client_ip_for_security $is_whitelisted_ip {
31+
default 0;
32+
include /etc/nginx/whitelisted-ips*.map;
33+
}
34+
35+
# Generated at container startup from /logs/blocked.txt (one IP per line).
36+
map $client_ip_for_security $is_blocked_ip {
37+
default 0;
38+
include /etc/nginx/blocked-ips*.map;
39+
}
40+
41+
# Whitelist wins over blocklist and probe detection.
42+
map "$is_whitelisted_ip:$is_blocked_ip" $should_block_ip {
43+
default 0;
44+
"0:1" 1;
45+
}
46+
map "$is_whitelisted_ip:$is_probe_request" $should_block_probe {
47+
default 0;
48+
"0:1" 1;
49+
}
50+
1451
log_format cache '$remote_addr - $remote_user [$time_local] "$request" '
1552
'$status $body_bytes_sent "$http_referer" '
1653
'"$http_user_agent" "$http_x_forwarded_for" '
1754
'$upstream_cache_status $request_time $upstream_response_time';
55+
log_format hack '$time_iso8601 client_ip="$client_ip_for_security" '
56+
'remote_addr="$remote_addr" xff="$http_x_forwarded_for" '
57+
'request="$request" status=$status host="$host" '
58+
'ua="$http_user_agent"';
1859

1960
access_log /dev/stdout cache;
2061
access_log /var/log/nginx/cache-access.log cache;
62+
access_log /logs/hacks/probes.log hack if=$should_block_probe;
63+
access_log /logs/hacks/blocked.log hack if=$should_block_ip;
2164
error_log /dev/stderr warn;
2265

2366
proxy_cache_path /var/cache/nginx/owlery levels=1:2 keys_zone=owlery_cache:100m max_size=${CACHE_MAX_SIZE} inactive=5y use_temp_path=off;
@@ -40,6 +83,14 @@ http {
4083
server {
4184
listen 80;
4285

86+
if ($should_block_ip) {
87+
return 403;
88+
}
89+
90+
if ($should_block_probe) {
91+
return 403;
92+
}
93+
4394
location = /status {
4495
access_log off;
4596
default_type application/json;
@@ -95,6 +146,14 @@ http {
95146
server {
96147
listen 8080;
97148

149+
if ($should_block_ip) {
150+
return 403;
151+
}
152+
153+
if ($should_block_probe) {
154+
return 403;
155+
}
156+
98157
location = /status {
99158
access_log off;
100159
default_type application/json;

0 commit comments

Comments
 (0)