diff --git a/.tests/supavisor-logs/config.yaml b/.tests/supavisor-logs/config.yaml new file mode 100644 index 00000000000..2f5852c48b6 --- /dev/null +++ b/.tests/supavisor-logs/config.yaml @@ -0,0 +1,11 @@ +parsers: + - crowdsecurity/syslog-logs + - crowdsecurity/dateparse-enrich + - ./parsers/s01-parse/crowdsecurity/supavisor-logs.yaml +scenarios: + - ./scenarios/crowdsecurity/supavisor-bf.yaml +postoverflows: + - "" +log_file: supavisor-logs.log +log_type: supavisor +ignore_parsers: false diff --git a/.tests/supavisor-logs/parser.assert b/.tests/supavisor-logs/parser.assert new file mode 100644 index 00000000000..fc3636e7fef --- /dev/null +++ b/.tests/supavisor-logs/parser.assert @@ -0,0 +1,17 @@ +results["s01-parse"]["crowdsecurity/supavisor-logs"][0].Success == true +results["s01-parse"]["crowdsecurity/supavisor-logs"][0].Evt.Meta.source_ip == "192.168.1.100" +results["s01-parse"]["crowdsecurity/supavisor-logs"][0].Evt.Meta.log_type == "supavisor_auth_fail" +results["s01-parse"]["crowdsecurity/supavisor-logs"][0].Evt.Meta.service == "supavisor" +results["s01-parse"]["crowdsecurity/supavisor-logs"][0].Evt.Parsed.project == "dev_tenant" +results["s01-parse"]["crowdsecurity/supavisor-logs"][0].Evt.Parsed.db_user == "postgres" +results["s01-parse"]["crowdsecurity/supavisor-logs"][6].Success == true +results["s01-parse"]["crowdsecurity/supavisor-logs"][6].Evt.Meta.source_ip == "10.0.0.50" +results["s01-parse"]["crowdsecurity/supavisor-logs"][6].Evt.Meta.log_type == "supavisor_auth_fail" +results["s01-parse"]["crowdsecurity/supavisor-logs"][6].Evt.Parsed.db_user == "admin" +results["s01-parse"]["crowdsecurity/supavisor-logs"][12].Success == true +results["s01-parse"]["crowdsecurity/supavisor-logs"][12].Evt.Meta.source_ip == "172.16.0.25" +results["s01-parse"]["crowdsecurity/supavisor-logs"][12].Evt.Meta.log_type == "supavisor_ssl_required" +results["s01-parse"]["crowdsecurity/supavisor-logs"][13].Success == true +results["s01-parse"]["crowdsecurity/supavisor-logs"][13].Evt.Meta.log_type == "supavisor_bad_startup" +results["s01-parse"]["crowdsecurity/supavisor-logs"][16].Success == true +results["s01-parse"]["crowdsecurity/supavisor-logs"][16].Evt.Meta.log_type == "supavisor_user_not_found" diff --git a/.tests/supavisor-logs/scenario.assert b/.tests/supavisor-logs/scenario.assert new file mode 100644 index 00000000000..272e5a8cf50 --- /dev/null +++ b/.tests/supavisor-logs/scenario.assert @@ -0,0 +1,2 @@ +results[0].Overflow.Alert.Source.IP == "192.168.1.100" +results[1].Overflow.Alert.Source.IP == "10.0.0.50" \ No newline at end of file diff --git a/.tests/supavisor-logs/supavisor-logs.log b/.tests/supavisor-logs/supavisor-logs.log new file mode 100644 index 00000000000..8c0b55d02f5 --- /dev/null +++ b/.tests/supavisor-logs/supavisor-logs.log @@ -0,0 +1,20 @@ +18:37:23.568 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=192.168.1.100 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +18:38:08.977 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=192.168.1.100 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +18:38:11.207 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=192.168.1.100 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +18:38:13.394 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=192.168.1.100 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +18:38:15.581 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=192.168.1.100 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +18:38:17.778 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=192.168.1.100 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +19:33:35.083 project=dev_tenant user=admin region=local mode=transaction type=single app_name=psql peer_ip=10.0.0.50 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +19:33:37.329 project=dev_tenant user=admin region=local mode=transaction type=single app_name=psql peer_ip=10.0.0.50 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +19:33:39.530 project=dev_tenant user=admin region=local mode=transaction type=single app_name=psql peer_ip=10.0.0.50 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +19:33:41.717 project=dev_tenant user=admin region=local mode=transaction type=single app_name=psql peer_ip=10.0.0.50 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +19:33:43.903 project=dev_tenant user=admin region=local mode=transaction type=single app_name=psql peer_ip=10.0.0.50 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +19:33:45.100 project=dev_tenant user=admin region=local mode=transaction type=single app_name=psql peer_ip=10.0.0.50 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +05:44:32.395 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=172.16.0.25 [error] ClientHandler: Tenant is not allowed to connect without SSL, user postgres +08:31:53.782 region=local [error] ClientHandler: Client startup message error: :bad_startup_payload +08:31:54.123 region=local [error] ClientHandler: Client startup message error: :bad_startup_payload +08:31:54.293 region=local [error] ClientHandler: Client startup message error: :bad_startup_payload +06:06:31.740 region=local [error] ClientHandler: User not found: "Either external_id or sni_hostname must be provided" {:single, "postgres", nil} +06:06:31.767 region=local [error] ClientHandler: User not found: "Either external_id or sni_hostname must be provided" {:single, "postgres", nil} +18:32:01.852 request_id=GICLZXgj-0m5cLcAAUTh region=local [info] GET /api/health +18:32:01.853 request_id=GICLZXgj-0m5cLcAAUTh region=local [info] Sent 204 in 413µs diff --git a/collections/crowdsecurity/supabase-supavisor.md b/collections/crowdsecurity/supabase-supavisor.md new file mode 100644 index 00000000000..af5665a80f8 --- /dev/null +++ b/collections/crowdsecurity/supabase-supavisor.md @@ -0,0 +1,54 @@ +# Supabase Supavisor Collection + +Detect brute force attacks against self-hosted [Supabase](https://supabase.com/) deployments using the [Supavisor](https://github.com/supabase/supavisor) connection pooler. + +## Acquisition + +```yaml +source: docker +container_name: + - supabase-supavisor +labels: + type: supavisor +``` + +For dynamic container names (Coolify, etc.): + +```yaml +source: docker +container_name_regexp: + - "supabase-supavisor" + - "supabase-supavisor-.*" +labels: + type: supavisor +``` + +## What Gets Detected + +### ✅ Detectable (has peer_ip) +| Attack Type | Log Pattern | Action | +|-------------|-------------|--------| +| Wrong password | `Exchange error: "Wrong password"` | Block after 5 attempts | +| SSL required bypass | `Tenant is not allowed to connect without SSL` | Block after 5 attempts | + +### ❌ Not Detectable (no peer_ip in logs) + +| Log Type | Reason | +|----------|--------| +| Bad startup payload | Supavisor doesn't log client IP | +| User not found | Supavisor doesn't log client IP | + +This is a Supavisor logging limitation, not a CrowdSec limitation. + +## Included Components + +| Type | Name | Description | +|------|------|-------------| +| Parser | `crowdsecurity/supavisor-logs` | Parses Supavisor logs | +| Scenario | `crowdsecurity/supavisor-bf` | Brute force detection | + +## Related + +- [Supabase Self-Hosting Guide](https://supabase.com/docs/guides/self-hosting/docker) +- [Supavisor Repository](https://github.com/supabase/supavisor) +- [CrowdSec Documentation](https://docs.crowdsec.net/) diff --git a/collections/crowdsecurity/supabase-supavisor.yaml b/collections/crowdsecurity/supabase-supavisor.yaml new file mode 100644 index 00000000000..d83d6b62c7a --- /dev/null +++ b/collections/crowdsecurity/supabase-supavisor.yaml @@ -0,0 +1,6 @@ +name: crowdsecurity/supabase-supavisor +description: "Detect attacks against Supabase PostgreSQL via Supavisor connection pooler" +parsers: + - crowdsecurity/supavisor-logs +scenarios: + - crowdsecurity/supavisor-bf diff --git a/parsers/s01-parse/crowdsecurity/supavisor-logs.md b/parsers/s01-parse/crowdsecurity/supavisor-logs.md new file mode 100644 index 00000000000..345e3dfe25b --- /dev/null +++ b/parsers/s01-parse/crowdsecurity/supavisor-logs.md @@ -0,0 +1,28 @@ +# Supavisor Logs Parser + +Parses [Supavisor](https://github.com/supabase/supavisor) connection pooler logs to detect authentication failures. + +## Log Format + +``` +18:38:17.778 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=123.123.123.123 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +``` + +## Parsed Fields + +| Field | Description | +|-------|-------------| +| `source_ip` | Client IP address | +| `project` | Tenant/project identifier | +| `db_user` | Database user | +| `log_type` | Event classification | + +## Acquisition + +```yaml +source: docker +container_name: + - supabase-supavisor +labels: + type: supavisor +``` diff --git a/parsers/s01-parse/crowdsecurity/supavisor-logs.yaml b/parsers/s01-parse/crowdsecurity/supavisor-logs.yaml new file mode 100644 index 00000000000..8fb3db99a66 --- /dev/null +++ b/parsers/s01-parse/crowdsecurity/supavisor-logs.yaml @@ -0,0 +1,94 @@ +name: crowdsecurity/supavisor-logs +description: "Parse Supavisor connection pooler logs for authentication failures" +filter: "evt.Parsed.program == 'supavisor'" +onsuccess: next_stage +debug: false + +# Supavisor uses Elixir Logger format with metadata +# Real log example: +# 18:38:17.778 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=123.123.123.123 [error] ClientHandler: Exchange error: "Wrong password" when method :auth_query +pattern_syntax: + SUPAVISOR_TS: '%{TIME:timestamp}\.%{INT:timestamp_ms}' + SUPAVISOR_LEVEL: '\[%{WORD:log_level}\]' + SUPAVISOR_META_FULL: 'project=%{DATA:project}\s+user=%{DATA:db_user}\s+region=%{DATA:region}\s+mode=%{DATA:pool_mode}\s+type=%{DATA:pool_type}\s+app_name=%{DATA:app_name}\s+peer_ip=%{IP:source_ip}' + SUPAVISOR_META_PARTIAL: 'region=%{DATA:region}' + +nodes: + - grok: + pattern: '%{SUPAVISOR_TS}\s+%{SUPAVISOR_META_FULL}\s+%{SUPAVISOR_LEVEL}\s+ClientHandler:\s+Exchange error:\s+"Wrong password"%{GREEDYDATA}' + apply_on: message + statics: + - meta: log_type + value: supavisor_auth_fail + - meta: service + value: supavisor + - meta: source_ip + expression: evt.Parsed.source_ip + - target: evt.StrTime + expression: evt.Parsed.timestamp + + - grok: + pattern: '%{SUPAVISOR_TS}\s+%{SUPAVISOR_META_FULL}\s+%{SUPAVISOR_LEVEL}\s+ClientHandler:\s+Tenant is not allowed to connect without SSL%{GREEDYDATA}' + apply_on: message + statics: + - meta: log_type + value: supavisor_ssl_required + - meta: service + value: supavisor + - meta: source_ip + expression: evt.Parsed.source_ip + - target: evt.StrTime + expression: evt.Parsed.timestamp + + - grok: + pattern: '%{SUPAVISOR_TS}\s+%{SUPAVISOR_META_FULL}\s+%{SUPAVISOR_LEVEL}\s+ClientHandler:\s+Exchange error:%{GREEDYDATA:error_detail}' + apply_on: message + statics: + - meta: log_type + value: supavisor_auth_fail + - meta: service + value: supavisor + - meta: source_ip + expression: evt.Parsed.source_ip + - target: evt.StrTime + expression: evt.Parsed.timestamp + + - grok: + pattern: '%{SUPAVISOR_TS}\s+%{SUPAVISOR_META_FULL}\s+%{SUPAVISOR_LEVEL}\s+%{GREEDYDATA:error_message}' + apply_on: message + filter: "evt.Parsed.log_level == 'error'" + statics: + - meta: log_type + value: supavisor_error_with_ip + - meta: service + value: supavisor + - meta: source_ip + expression: evt.Parsed.source_ip + - target: evt.StrTime + expression: evt.Parsed.timestamp + + - grok: + pattern: '%{SUPAVISOR_TS}\s+%{SUPAVISOR_META_PARTIAL}\s+%{SUPAVISOR_LEVEL}\s+ClientHandler:\s+Client startup message error:\s+:bad_startup_payload' + apply_on: message + statics: + - meta: log_type + value: supavisor_bad_startup + - meta: service + value: supavisor + - target: evt.StrTime + expression: evt.Parsed.timestamp + + - grok: + pattern: '%{SUPAVISOR_TS}\s+%{SUPAVISOR_META_PARTIAL}\s+%{SUPAVISOR_LEVEL}\s+ClientHandler:\s+User not found:%{GREEDYDATA:enum_detail}' + apply_on: message + statics: + - meta: log_type + value: supavisor_user_not_found + - meta: service + value: supavisor + - target: evt.StrTime + expression: evt.Parsed.timestamp + +statics: + - meta: service + value: supavisor diff --git a/scenarios/crowdsecurity/supavisor-bf.md b/scenarios/crowdsecurity/supavisor-bf.md new file mode 100644 index 00000000000..cda187d2942 --- /dev/null +++ b/scenarios/crowdsecurity/supavisor-bf.md @@ -0,0 +1,35 @@ +# Supavisor Brute Force Detection + +Detects brute force attacks against PostgreSQL databases through the Supavisor connection pooler. + +## Description + +This scenario triggers when multiple authentication failures are detected from the same IP address. It detects wrong password attempts via Supavisor's `auth_query` authentication method. + +## Behavior + +| Parameter | Value | Description | +|-----------|-------|-------------| +| `capacity` | 5 | Failed attempts before triggering | +| `leakspeed` | 30s | Time window for counting | +| `blackhole` | 5m | Cooldown after trigger | + +## Labels + +| Label | Value | +|-------|-------| +| `confidence` | 3 | +| `spoofable` | 0 | +| `classification` | attack.T1110 | +| `remediation` | true | + +## Acquisition + +```yaml +source: docker +container_name_regexp: + - "supabase-supavisor" + - "supabase-supavisor-.*" +labels: + type: supavisor +``` diff --git a/scenarios/crowdsecurity/supavisor-bf.yaml b/scenarios/crowdsecurity/supavisor-bf.yaml new file mode 100644 index 00000000000..8ececc5e2e2 --- /dev/null +++ b/scenarios/crowdsecurity/supavisor-bf.yaml @@ -0,0 +1,17 @@ +type: leaky +name: crowdsecurity/supavisor-bf +description: "Detect brute force attacks against PostgreSQL via Supavisor connection pooler" +filter: evt.Meta.log_type == 'supavisor_auth_fail' +groupby: evt.Meta.source_ip +capacity: 5 +leakspeed: 30s +blackhole: 5m +labels: + service: supavisor + confidence: 3 + spoofable: 0 + classification: + - attack.T1110 + behavior: "database:bruteforce" + label: "Supavisor bruteforce" + remediation: true