aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Tuomi <jan@jantuomi.fi>2025-12-29 23:16:22 +0200
committerJan Tuomi <jan@jantuomi.fi>2025-12-29 23:16:22 +0200
commit533bc95c751337d5f2952252b0378848cf078357 (patch)
tree2bf4ccadc31baf02d2a586fe7357af99787265dc
parent39feba1c79c1a0c2743932af35e157ba29d089d1 (diff)
Add crawler tarpit
-rw-r--r--tasks/jail_ingress.yml42
-rw-r--r--templates/ingress/etc_pf.conf.j25
-rw-r--r--templates/ingress/nginx_snippet_ban.inc17
-rw-r--r--templates/ingress/pf-ban-socket.py72
-rw-r--r--templates/ingress/usr_local_etc_nginx_nginx.conf.j24
-rw-r--r--templates/ingress/usr_local_etc_rc.d_pf_ban_socket43
6 files changed, 182 insertions, 1 deletions
diff --git a/tasks/jail_ingress.yml b/tasks/jail_ingress.yml
index 2844c12..1848177 100644
--- a/tasks/jail_ingress.yml
+++ b/tasks/jail_ingress.yml
@@ -39,6 +39,24 @@
shell: jexec ingress sysctl net.inet.ip.forwarding=1
when: ingress_ip_forwarding_enabled.rc != 0
+- name: Install pf-ban-socket.py
+ copy:
+ src: templates/ingress/pf-ban-socket.py
+ dest: /usr/local/jails/containers/ingress/usr/local/bin/pf-ban-socket.py
+ owner: root
+ group: wheel
+ mode: "0755"
+ register: ingress_pf_ban_socket_py
+
+- name: Install pf-ban-socket service
+ copy:
+ src: templates/ingress/usr_local_etc_rc.d_pf_ban_socket
+ dest: /usr/local/jails/containers/ingress/usr/local/etc/rc.d/pf_ban_socket
+ owner: root
+ group: wheel
+ mode: "0755"
+ register: ingress_pf_ban_socket_service
+
- name: Check if pf enabled
shell: jexec ingress sysrc pf_enable | grep -q "YES"
register: ingress_pf_enabled
@@ -58,6 +76,12 @@
shell: service -j ingress pf start
when: ingress_pf_status.rc != 0
+- name: Enable pf-ban-socket service
+ shell: |
+ service -j ingress pf_ban_socket enable
+ service -j ingress pf_ban_socket restart
+ when: ingress_pf_ban_socket_service.changed or ingress_pf_ban_socket_py.changed
+
- name: Copy acme-dns-auth.py
copy:
src: templates/ingress/acme-dns-auth.py
@@ -90,6 +114,22 @@
mode: "0644"
register: nginx_conf
+- name: Create nginx snippets directory
+ file:
+ path: "/usr/local/jails/containers/ingress/usr/local/etc/nginx/snippets"
+ state: directory
+ owner: root
+ group: wheel
+ mode: "0755"
+
+- name: Include ban.inc
+ template:
+ src: ingress/nginx_snippet_ban.inc
+ dest: /usr/local/jails/containers/ingress/usr/local/etc/nginx/snippets/ban.inc
+ owner: root
+ group: wheel
+ mode: "0644"
+
- name: Create static directories
loop: "{{ ingress_routes | selectattr('static', 'defined') | map(attribute='static') | unique | list }}"
file:
@@ -109,7 +149,7 @@
- name: Enable and start nginx
shell: |
service -j ingress nginx enable
- service -j ingress nginx onestart
+ service -j ingress nginx restart
when: ingress_nginx_enabled.rc != 0 or nginx_conf.changed
- name: Install hetzner_ddns.sh
diff --git a/templates/ingress/etc_pf.conf.j2 b/templates/ingress/etc_pf.conf.j2
index feb35d5..c0528e1 100644
--- a/templates/ingress/etc_pf.conf.j2
+++ b/templates/ingress/etc_pf.conf.j2
@@ -5,6 +5,8 @@ lan = "epl{{ jail.num }}b"
wan = "epw{{ jail.num }}b"
lan_net = "{{ lan_ipv4_network }}"
+table <blocked> persist
+
# Keep PF out of loopback, drop by default if you add blocks later
set skip on lo0
set block-policy drop
@@ -12,6 +14,9 @@ set block-policy drop
# NAT: translate LAN traffic to the WAN interface address
nat on $wan from $lan_net to any -> ($wan)
+# Block traffic from IPs in the blocked table
+block in quick from <blocked> to any
+
# Allow all outbound traffic from the jail and LAN via both interfaces
# NAT will be applied automatically when source is in $lan_net and going out $wan
pass out on $wan all keep state
diff --git a/templates/ingress/nginx_snippet_ban.inc b/templates/ingress/nginx_snippet_ban.inc
new file mode 100644
index 0000000..029b965
--- /dev/null
+++ b/templates/ingress/nginx_snippet_ban.inc
@@ -0,0 +1,17 @@
+# Trap route
+location ^~ /wp-admin/ {
+ proxy_set_header X-IP $remote_addr;
+ proxy_method POST;
+ proxy_pass http://unix:/var/run/pfban/ban.sock:/ban;
+
+ proxy_connect_timeout 50ms;
+ proxy_send_timeout 50ms;
+ proxy_read_timeout 50ms;
+
+ # If the socket isn't up yet, still return something
+ error_page 500 502 503 504 = @ban_fallback;
+}
+
+location @ban_fallback {
+ return 204;
+}
diff --git a/templates/ingress/pf-ban-socket.py b/templates/ingress/pf-ban-socket.py
new file mode 100644
index 0000000..cbcd80e
--- /dev/null
+++ b/templates/ingress/pf-ban-socket.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+import http.server
+import os
+import re
+import socketserver
+import subprocess
+
+SOCK_PATH = "/var/run/pfban/ban.sock"
+PF_TABLE = "blocked"
+
+# Simple, conservative filter to avoid junk / injection
+IP_RE = re.compile(r"^[0-9A-Fa-f:.]{3,}$")
+
+
+def ensure_socket_dir(path: str) -> None:
+ os.makedirs(os.path.dirname(path), mode=0o755, exist_ok=True)
+
+
+class BanHandler(http.server.BaseHTTPRequestHandler):
+ # Silence default logging
+ def log_message(self, format, *args):
+ return
+
+ def do_POST(self):
+ if self.path != "/ban":
+ self.send_response(404)
+ self.end_headers()
+ return
+
+ ip = self.headers.get("X-IP", "").strip()
+
+ if ip and IP_RE.match(ip):
+ subprocess.run(
+ ["/sbin/pfctl", "-t", PF_TABLE, "-T", "add", ip],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ check=False,
+ )
+
+ self.send_response(204)
+ self.end_headers()
+
+ def do_GET(self):
+ self.send_response(405)
+ self.end_headers()
+
+
+class ThreadingUnixHTTPServer(
+ socketserver.ThreadingMixIn,
+ socketserver.UnixStreamServer,
+):
+ daemon_threads = True
+
+
+def main() -> None:
+ ensure_socket_dir(SOCK_PATH)
+
+ # Remove stale socket if present
+ try:
+ os.unlink(SOCK_PATH)
+ except FileNotFoundError:
+ pass
+
+ with ThreadingUnixHTTPServer(SOCK_PATH, BanHandler) as httpd:
+ # Allow nginx workers (www) to connect
+ os.chmod(SOCK_PATH, 0o660)
+
+ httpd.serve_forever()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 b/templates/ingress/usr_local_etc_nginx_nginx.conf.j2
index 456a607..086d7f9 100644
--- a/templates/ingress/usr_local_etc_nginx_nginx.conf.j2
+++ b/templates/ingress/usr_local_etc_nginx_nginx.conf.j2
@@ -22,6 +22,8 @@ http {
listen 80 default_server;
server_name _;
+ include /usr/local/etc/nginx/snippets/ban.inc;
+
location / {
return 404;
}
@@ -32,6 +34,8 @@ http {
server_name {{ route.host }};
http2 on;
+ include /usr/local/etc/nginx/snippets/ban.inc;
+
listen 443 ssl;
listen [::]:443 ssl;
diff --git a/templates/ingress/usr_local_etc_rc.d_pf_ban_socket b/templates/ingress/usr_local_etc_rc.d_pf_ban_socket
new file mode 100644
index 0000000..d21bc2d
--- /dev/null
+++ b/templates/ingress/usr_local_etc_rc.d_pf_ban_socket
@@ -0,0 +1,43 @@
+#!/bin/sh
+#
+# PROVIDE: pf_ban_socket
+# REQUIRE: NETWORKING pf
+# KEYWORD: shutdown
+#
+# Enable in /etc/rc.conf:
+# pf_ban_socket_enable="YES"
+#
+. /etc/rc.subr
+
+name="pf_ban_socket"
+rcvar=pf_ban_socket_enable
+
+load_rc_config $name
+
+: ${pf_ban_socket_command:=/usr/local/bin/pf-ban-socket.py}
+: ${pf_ban_socket_log:=/var/log/${name}.log}
+
+start_cmd="${name}_start"
+stop_cmd="${name}_stop"
+status_cmd="${name}_status"
+
+extra_commands="status"
+
+pf_ban_socket_start() {
+ /usr/local/bin/logto ${pf_ban_socket_log} ${pf_ban_socket_command} &
+}
+
+pf_ban_socket_status() {
+ if pgrep -f "pf-ban-socket.py"; then
+ echo "pf_ban_socket is running"
+ else
+ echo "pf_ban_socket is not running"
+ exit 1
+ fi
+}
+
+pf_ban_socket_stop() {
+ pkill "pf-ban-socket.py"
+}
+
+run_rc_command "$1"