Skip to content

Commit f8c523c

Browse files
authored
fix: explicitly set pg fail2ban jail backend to auto (supabase#1886)
* fix: explicitly set pg fail2ban jail backend to auto * tests: fail2ban postgres service conf test (supabase#1887) * tests: fail2ban postgres service conf test * chore: undo change on release * chore: bump version to release
1 parent 6180985 commit f8c523c

File tree

4 files changed

+269
-3
lines changed

4 files changed

+269
-3
lines changed

ansible/files/fail2ban_check.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import subprocess
2+
import sys
3+
import os
4+
import re
5+
6+
7+
# Expected fail2ban configuration
8+
expected_fail2ban_config = {
9+
"jail": {
10+
"name": "postgresql",
11+
"enabled": True,
12+
"logpath": "/var/log/postgresql/auth-failures.csv",
13+
"filter": "postgresql",
14+
"port": "5432",
15+
"protocol": "tcp",
16+
"maxretry": 3,
17+
"ignoreip": ["192.168.0.0/16", "172.17.1.0/20"],
18+
"backend": "auto",
19+
},
20+
"filter": {
21+
"failregex": r'^.*,.*,.*,.*,"<HOST>:.*password authentication failed for user.*$',
22+
"ignoreregex": r'^.*,.*,.*,.*,"127\.0\.0\.1.*password authentication failed for user.*$',
23+
# Additional ignoreregex patterns added by Ansible (setup-fail2ban.yml lines 55-62)
24+
"custom_ignoreregex": [
25+
r'^.*,.*,.*,.*,"<HOST>:.*password authentication failed for user ""supabase_admin".*$',
26+
r'^.*,.*,.*,.*,"<HOST>:.*password authentication failed for user ""supabase_auth_admin".*$',
27+
r'^.*,.*,.*,.*,"<HOST>:.*password authentication failed for user ""supabase_storage_admin".*$',
28+
r'^.*,.*,.*,.*,"<HOST>:.*password authentication failed for user ""authenticator".*$',
29+
r'^.*,.*,.*,.*,"<HOST>:.*password authentication failed for user ""pgbouncer".*$',
30+
],
31+
},
32+
}
33+
34+
35+
def run_command(command):
36+
"""Run a shell command and return the output."""
37+
try:
38+
process = subprocess.Popen(
39+
command,
40+
shell=True,
41+
stdout=subprocess.PIPE,
42+
stderr=subprocess.PIPE,
43+
text=True,
44+
)
45+
stdout, stderr = process.communicate()
46+
return {
47+
"returncode": process.returncode,
48+
"stdout": stdout,
49+
"stderr": stderr,
50+
"succeeded": process.returncode == 0,
51+
}
52+
except Exception as e:
53+
print(f"Error running command '{command}': {e}")
54+
sys.exit(1)
55+
56+
57+
def check_fail2ban_config_syntax():
58+
"""Validate fail2ban configuration syntax using fail2ban-client -d."""
59+
print("Checking fail2ban configuration syntax...")
60+
61+
result = run_command("fail2ban-client -d")
62+
63+
if not result["succeeded"]:
64+
print("fail2ban configuration syntax check failed:")
65+
print(result["stderr"])
66+
sys.exit(1)
67+
68+
# Check that postgresql jail appears in the dump
69+
if "postgresql" not in result["stdout"]:
70+
print("postgresql jail not found in fail2ban configuration dump")
71+
sys.exit(1)
72+
73+
print("✓ fail2ban configuration syntax is valid")
74+
75+
76+
def check_fail2ban_filter_regex():
77+
"""Test fail2ban filter regex against the log file."""
78+
print("Testing fail2ban filter regex...")
79+
80+
logpath = expected_fail2ban_config["jail"]["logpath"]
81+
filter_path = "/etc/fail2ban/filter.d/postgresql.conf"
82+
83+
# Check if log file exists
84+
if not os.path.exists(logpath):
85+
print(f"Log file {logpath} does not exist")
86+
print(
87+
"Note: This is expected if PostgreSQL hasn't run yet. Skipping regex test."
88+
)
89+
return
90+
91+
# Check if filter file exists
92+
if not os.path.exists(filter_path):
93+
print(f"Filter file {filter_path} does not exist")
94+
sys.exit(1)
95+
96+
# Run fail2ban-regex to test the filter
97+
result = run_command(f"fail2ban-regex {logpath} {filter_path}")
98+
99+
if not result["succeeded"]:
100+
print("fail2ban-regex test failed:")
101+
print(result["stderr"])
102+
sys.exit(1)
103+
104+
print("✓ fail2ban filter regex test passed")
105+
106+
107+
def check_fail2ban_jail_config():
108+
"""Validate jail configuration file contents."""
109+
print("Checking fail2ban jail configuration...")
110+
111+
jail_config_path = "/etc/fail2ban/jail.d/postgresql.conf"
112+
113+
if not os.path.exists(jail_config_path):
114+
print(f"Jail configuration file {jail_config_path} does not exist")
115+
sys.exit(1)
116+
117+
with open(jail_config_path, "r") as f:
118+
jail_content = f.read()
119+
120+
expected_jail = expected_fail2ban_config["jail"]
121+
122+
# Check each expected configuration value
123+
checks = [
124+
(f"enabled = {str(expected_jail['enabled']).lower()}", "enabled setting"),
125+
(f"port = {expected_jail['port']}", "port setting"),
126+
(f"protocol = {expected_jail['protocol']}", "protocol setting"),
127+
(f"filter = {expected_jail['filter']}", "filter setting"),
128+
(f"logpath = {expected_jail['logpath']}", "logpath setting"),
129+
(f"maxretry = {expected_jail['maxretry']}", "maxretry setting"),
130+
(f"backend = {expected_jail['backend']}", "backend setting"),
131+
]
132+
133+
for expected_line, description in checks:
134+
if expected_line not in jail_content:
135+
print(f"Missing or incorrect {description} in {jail_config_path}")
136+
print(f"Expected: {expected_line}")
137+
sys.exit(1)
138+
139+
# Check ignoreip
140+
for ip_range in expected_jail["ignoreip"]:
141+
if ip_range not in jail_content:
142+
print(f"Missing ignoreip range {ip_range} in {jail_config_path}")
143+
sys.exit(1)
144+
145+
print("✓ fail2ban jail configuration is correct")
146+
147+
148+
def check_fail2ban_filter_config():
149+
"""Validate filter configuration file contents."""
150+
print("Checking fail2ban filter configuration...")
151+
152+
filter_config_path = "/etc/fail2ban/filter.d/postgresql.conf"
153+
154+
if not os.path.exists(filter_config_path):
155+
print(f"Filter configuration file {filter_config_path} does not exist")
156+
sys.exit(1)
157+
158+
with open(filter_config_path, "r") as f:
159+
filter_content = f.read()
160+
161+
expected_filter = expected_fail2ban_config["filter"]
162+
163+
# Check failregex
164+
if expected_filter["failregex"] not in filter_content:
165+
print(f"Missing or incorrect failregex in {filter_config_path}")
166+
print(f"Expected: {expected_filter['failregex']}")
167+
sys.exit(1)
168+
169+
# Check ignoreregex
170+
if expected_filter["ignoreregex"] not in filter_content:
171+
print(f"Missing or incorrect ignoreregex in {filter_config_path}")
172+
print(f"Expected: {expected_filter['ignoreregex']}")
173+
sys.exit(1)
174+
175+
# Check custom ignoreregex patterns for Supabase users
176+
for custom_pattern in expected_filter["custom_ignoreregex"]:
177+
if custom_pattern not in filter_content:
178+
print(f"Missing custom ignoreregex pattern in {filter_config_path}")
179+
print(f"Expected: {custom_pattern}")
180+
sys.exit(1)
181+
182+
print("✓ fail2ban filter configuration is correct")
183+
184+
185+
def check_fail2ban_jail_runtime():
186+
"""Validate fail2ban jail is running and monitoring the correct file."""
187+
print("Checking fail2ban jail runtime status...")
188+
189+
# Run fail2ban-client status postgresql
190+
result = run_command("fail2ban-client status postgresql")
191+
192+
if not result["succeeded"]:
193+
print("Failed to get fail2ban postgresql jail status:")
194+
print(result["stderr"])
195+
sys.exit(1)
196+
197+
output = result["stdout"]
198+
199+
# Parse the output
200+
# Expected format:
201+
# Status for the jail: postgresql
202+
# |- Filter
203+
# | |- Currently failed: 0
204+
# | |- Total failed: X
205+
# | `- File list: /var/log/postgresql/auth-failures.csv
206+
207+
# Check jail name
208+
if "Status for the jail: postgresql" not in output:
209+
print("postgresql jail is not active")
210+
print(output)
211+
sys.exit(1)
212+
213+
# Check file list
214+
expected_logpath = expected_fail2ban_config["jail"]["logpath"]
215+
if expected_logpath not in output:
216+
print(
217+
f"postgresql jail is not monitoring the expected log file: {expected_logpath}"
218+
)
219+
print(output)
220+
sys.exit(1)
221+
222+
# Extract and display some stats
223+
match = re.search(r"Currently failed:\s+(\d+)", output)
224+
if match:
225+
currently_failed = match.group(1)
226+
print(f" Currently failed IPs: {currently_failed}")
227+
228+
match = re.search(r"Total failed:\s+(\d+)", output)
229+
if match:
230+
total_failed = match.group(1)
231+
print(f" Total failed attempts: {total_failed}")
232+
233+
print("✓ fail2ban postgresql jail is active and monitoring correctly")
234+
235+
236+
def main():
237+
print("=" * 60)
238+
print("Supabase Postgres fail2ban Configuration Checker")
239+
print("=" * 60)
240+
241+
# Static validation (doesn't require fail2ban to be running)
242+
check_fail2ban_jail_config()
243+
check_fail2ban_filter_config()
244+
check_fail2ban_config_syntax()
245+
check_fail2ban_filter_regex()
246+
247+
# Runtime validation (requires fail2ban to be running)
248+
# This should be called when fail2ban service is started
249+
check_fail2ban_jail_runtime()
250+
251+
print("=" * 60)
252+
print("All fail2ban configuration checks passed!")
253+
print("=" * 60)
254+
255+
256+
if __name__ == "__main__":
257+
main()

ansible/files/fail2ban_config/jail-postgresql.conf.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ filter = postgresql
66
logpath = /var/log/postgresql/auth-failures.csv
77
maxretry = 3
88
ignoreip = 192.168.0.0/16 172.17.1.0/20
9+
backend = auto

ansible/playbook.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,14 @@
218218
systemctl stop postgresql.service
219219
when: stage2_nix
220220

221+
- name: Run fail2ban configuration checks
222+
become: yes
223+
shell: |
224+
systemctl start fail2ban.service
225+
/usr/bin/python3 /tmp/ansible-playbook/ansible/files/fail2ban_check.py
226+
systemctl stop fail2ban.service
227+
when: stage2_nix
228+
221229
- name: Remove osquery
222230
become: yes
223231
shell: |

ansible/vars.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ postgres_major:
1010

1111
# Full version strings for each major version
1212
postgres_release:
13-
postgresorioledb-17: "17.5.1.057-orioledb"
14-
postgres17: "17.6.1.036"
15-
postgres15: "15.14.1.036"
13+
postgresorioledb-17: "17.5.1.058-orioledb"
14+
postgres17: "17.6.1.037"
15+
postgres15: "15.14.1.037"
1616

1717
# Non Postgres Extensions
1818
pgbouncer_release: 1.19.0

0 commit comments

Comments
 (0)