From ca1e8a1940de232c822e03508e0b7b6f0d4a5ba2 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Sun, 2 Nov 2025 09:05:28 -0500 Subject: [PATCH 1/3] fix: explicitly set pg fail2ban jail backend to auto --- ansible/files/fail2ban_config/jail-postgresql.conf.j2 | 1 + ansible/vars.yml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ansible/files/fail2ban_config/jail-postgresql.conf.j2 b/ansible/files/fail2ban_config/jail-postgresql.conf.j2 index a021035a9..a235c6313 100644 --- a/ansible/files/fail2ban_config/jail-postgresql.conf.j2 +++ b/ansible/files/fail2ban_config/jail-postgresql.conf.j2 @@ -6,3 +6,4 @@ filter = postgresql logpath = /var/log/postgresql/auth-failures.csv maxretry = 3 ignoreip = 192.168.0.0/16 172.17.1.0/20 +backend = auto diff --git a/ansible/vars.yml b/ansible/vars.yml index 8ac0dd57d..eac963118 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -10,9 +10,9 @@ postgres_major: # Full version strings for each major version postgres_release: - postgresorioledb-17: "17.5.1.057-orioledb" - postgres17: "17.6.1.036" - postgres15: "15.14.1.036" + postgresorioledb-17: "17.5.1.058-orioledb-pg-1" + postgres17: "17.6.1.037-pg-1" + postgres15: "15.14.1.037-pg-1" # Non Postgres Extensions pgbouncer_release: 1.19.0 From e86570f486701fef6d65fc955d31a5679e65dcb5 Mon Sep 17 00:00:00 2001 From: samrose Date: Sun, 2 Nov 2025 22:14:04 -0500 Subject: [PATCH 2/3] tests: fail2ban postgres service conf test (#1887) * tests: fail2ban postgres service conf test * chore: undo change on release --- ansible/files/fail2ban_check.py | 257 ++++++++++++++++++++++++++++++++ ansible/playbook.yml | 8 + 2 files changed, 265 insertions(+) create mode 100644 ansible/files/fail2ban_check.py diff --git a/ansible/files/fail2ban_check.py b/ansible/files/fail2ban_check.py new file mode 100644 index 000000000..b14210a63 --- /dev/null +++ b/ansible/files/fail2ban_check.py @@ -0,0 +1,257 @@ +import subprocess +import sys +import os +import re + + +# Expected fail2ban configuration +expected_fail2ban_config = { + "jail": { + "name": "postgresql", + "enabled": True, + "logpath": "/var/log/postgresql/auth-failures.csv", + "filter": "postgresql", + "port": "5432", + "protocol": "tcp", + "maxretry": 3, + "ignoreip": ["192.168.0.0/16", "172.17.1.0/20"], + "backend": "auto", + }, + "filter": { + "failregex": r'^.*,.*,.*,.*,":.*password authentication failed for user.*$', + "ignoreregex": r'^.*,.*,.*,.*,"127\.0\.0\.1.*password authentication failed for user.*$', + # Additional ignoreregex patterns added by Ansible (setup-fail2ban.yml lines 55-62) + "custom_ignoreregex": [ + r'^.*,.*,.*,.*,":.*password authentication failed for user ""supabase_admin".*$', + r'^.*,.*,.*,.*,":.*password authentication failed for user ""supabase_auth_admin".*$', + r'^.*,.*,.*,.*,":.*password authentication failed for user ""supabase_storage_admin".*$', + r'^.*,.*,.*,.*,":.*password authentication failed for user ""authenticator".*$', + r'^.*,.*,.*,.*,":.*password authentication failed for user ""pgbouncer".*$', + ], + }, +} + + +def run_command(command): + """Run a shell command and return the output.""" + try: + process = subprocess.Popen( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + stdout, stderr = process.communicate() + return { + "returncode": process.returncode, + "stdout": stdout, + "stderr": stderr, + "succeeded": process.returncode == 0, + } + except Exception as e: + print(f"Error running command '{command}': {e}") + sys.exit(1) + + +def check_fail2ban_config_syntax(): + """Validate fail2ban configuration syntax using fail2ban-client -d.""" + print("Checking fail2ban configuration syntax...") + + result = run_command("fail2ban-client -d") + + if not result["succeeded"]: + print("fail2ban configuration syntax check failed:") + print(result["stderr"]) + sys.exit(1) + + # Check that postgresql jail appears in the dump + if "postgresql" not in result["stdout"]: + print("postgresql jail not found in fail2ban configuration dump") + sys.exit(1) + + print("✓ fail2ban configuration syntax is valid") + + +def check_fail2ban_filter_regex(): + """Test fail2ban filter regex against the log file.""" + print("Testing fail2ban filter regex...") + + logpath = expected_fail2ban_config["jail"]["logpath"] + filter_path = "/etc/fail2ban/filter.d/postgresql.conf" + + # Check if log file exists + if not os.path.exists(logpath): + print(f"Log file {logpath} does not exist") + print( + "Note: This is expected if PostgreSQL hasn't run yet. Skipping regex test." + ) + return + + # Check if filter file exists + if not os.path.exists(filter_path): + print(f"Filter file {filter_path} does not exist") + sys.exit(1) + + # Run fail2ban-regex to test the filter + result = run_command(f"fail2ban-regex {logpath} {filter_path}") + + if not result["succeeded"]: + print("fail2ban-regex test failed:") + print(result["stderr"]) + sys.exit(1) + + print("✓ fail2ban filter regex test passed") + + +def check_fail2ban_jail_config(): + """Validate jail configuration file contents.""" + print("Checking fail2ban jail configuration...") + + jail_config_path = "/etc/fail2ban/jail.d/postgresql.conf" + + if not os.path.exists(jail_config_path): + print(f"Jail configuration file {jail_config_path} does not exist") + sys.exit(1) + + with open(jail_config_path, "r") as f: + jail_content = f.read() + + expected_jail = expected_fail2ban_config["jail"] + + # Check each expected configuration value + checks = [ + (f"enabled = {str(expected_jail['enabled']).lower()}", "enabled setting"), + (f"port = {expected_jail['port']}", "port setting"), + (f"protocol = {expected_jail['protocol']}", "protocol setting"), + (f"filter = {expected_jail['filter']}", "filter setting"), + (f"logpath = {expected_jail['logpath']}", "logpath setting"), + (f"maxretry = {expected_jail['maxretry']}", "maxretry setting"), + (f"backend = {expected_jail['backend']}", "backend setting"), + ] + + for expected_line, description in checks: + if expected_line not in jail_content: + print(f"Missing or incorrect {description} in {jail_config_path}") + print(f"Expected: {expected_line}") + sys.exit(1) + + # Check ignoreip + for ip_range in expected_jail["ignoreip"]: + if ip_range not in jail_content: + print(f"Missing ignoreip range {ip_range} in {jail_config_path}") + sys.exit(1) + + print("✓ fail2ban jail configuration is correct") + + +def check_fail2ban_filter_config(): + """Validate filter configuration file contents.""" + print("Checking fail2ban filter configuration...") + + filter_config_path = "/etc/fail2ban/filter.d/postgresql.conf" + + if not os.path.exists(filter_config_path): + print(f"Filter configuration file {filter_config_path} does not exist") + sys.exit(1) + + with open(filter_config_path, "r") as f: + filter_content = f.read() + + expected_filter = expected_fail2ban_config["filter"] + + # Check failregex + if expected_filter["failregex"] not in filter_content: + print(f"Missing or incorrect failregex in {filter_config_path}") + print(f"Expected: {expected_filter['failregex']}") + sys.exit(1) + + # Check ignoreregex + if expected_filter["ignoreregex"] not in filter_content: + print(f"Missing or incorrect ignoreregex in {filter_config_path}") + print(f"Expected: {expected_filter['ignoreregex']}") + sys.exit(1) + + # Check custom ignoreregex patterns for Supabase users + for custom_pattern in expected_filter["custom_ignoreregex"]: + if custom_pattern not in filter_content: + print(f"Missing custom ignoreregex pattern in {filter_config_path}") + print(f"Expected: {custom_pattern}") + sys.exit(1) + + print("✓ fail2ban filter configuration is correct") + + +def check_fail2ban_jail_runtime(): + """Validate fail2ban jail is running and monitoring the correct file.""" + print("Checking fail2ban jail runtime status...") + + # Run fail2ban-client status postgresql + result = run_command("fail2ban-client status postgresql") + + if not result["succeeded"]: + print("Failed to get fail2ban postgresql jail status:") + print(result["stderr"]) + sys.exit(1) + + output = result["stdout"] + + # Parse the output + # Expected format: + # Status for the jail: postgresql + # |- Filter + # | |- Currently failed: 0 + # | |- Total failed: X + # | `- File list: /var/log/postgresql/auth-failures.csv + + # Check jail name + if "Status for the jail: postgresql" not in output: + print("postgresql jail is not active") + print(output) + sys.exit(1) + + # Check file list + expected_logpath = expected_fail2ban_config["jail"]["logpath"] + if expected_logpath not in output: + print( + f"postgresql jail is not monitoring the expected log file: {expected_logpath}" + ) + print(output) + sys.exit(1) + + # Extract and display some stats + match = re.search(r"Currently failed:\s+(\d+)", output) + if match: + currently_failed = match.group(1) + print(f" Currently failed IPs: {currently_failed}") + + match = re.search(r"Total failed:\s+(\d+)", output) + if match: + total_failed = match.group(1) + print(f" Total failed attempts: {total_failed}") + + print("✓ fail2ban postgresql jail is active and monitoring correctly") + + +def main(): + print("=" * 60) + print("Supabase Postgres fail2ban Configuration Checker") + print("=" * 60) + + # Static validation (doesn't require fail2ban to be running) + check_fail2ban_jail_config() + check_fail2ban_filter_config() + check_fail2ban_config_syntax() + check_fail2ban_filter_regex() + + # Runtime validation (requires fail2ban to be running) + # This should be called when fail2ban service is started + check_fail2ban_jail_runtime() + + print("=" * 60) + print("All fail2ban configuration checks passed!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/ansible/playbook.yml b/ansible/playbook.yml index 0991a813a..1dfb3f6dd 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -218,6 +218,14 @@ systemctl stop postgresql.service when: stage2_nix + - name: Run fail2ban configuration checks + become: yes + shell: | + systemctl start fail2ban.service + /usr/bin/python3 /tmp/ansible-playbook/ansible/files/fail2ban_check.py + systemctl stop fail2ban.service + when: stage2_nix + - name: Remove osquery become: yes shell: | From 91cc90ceb4c1f4368cb5b0d481b5cbb832f388ff Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 3 Nov 2025 05:41:56 -0500 Subject: [PATCH 3/3] chore: bump version to release --- ansible/vars.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ansible/vars.yml b/ansible/vars.yml index eac963118..691b2fc33 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -10,9 +10,9 @@ postgres_major: # Full version strings for each major version postgres_release: - postgresorioledb-17: "17.5.1.058-orioledb-pg-1" - postgres17: "17.6.1.037-pg-1" - postgres15: "15.14.1.037-pg-1" + postgresorioledb-17: "17.5.1.058-orioledb" + postgres17: "17.6.1.037" + postgres15: "15.14.1.037" # Non Postgres Extensions pgbouncer_release: 1.19.0