Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .tests/supavisor-logs/config.yaml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions .tests/supavisor-logs/parser.assert
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions .tests/supavisor-logs/scenario.assert
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
results[0].Overflow.Alert.Source.IP == "192.168.1.100"
results[1].Overflow.Alert.Source.IP == "10.0.0.50"
20 changes: 20 additions & 0 deletions .tests/supavisor-logs/supavisor-logs.log
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions collections/crowdsecurity/supabase-supavisor.md
Original file line number Diff line number Diff line change
@@ -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/)
6 changes: 6 additions & 0 deletions collections/crowdsecurity/supabase-supavisor.yaml
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions parsers/s01-parse/crowdsecurity/supavisor-logs.md
Original file line number Diff line number Diff line change
@@ -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
```
94 changes: 94 additions & 0 deletions parsers/s01-parse/crowdsecurity/supavisor-logs.yaml
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions scenarios/crowdsecurity/supavisor-bf.md
Original file line number Diff line number Diff line change
@@ -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
```
17 changes: 17 additions & 0 deletions scenarios/crowdsecurity/supavisor-bf.yaml
Original file line number Diff line number Diff line change
@@ -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