Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 10 additions & 0 deletions .tests/supavisor-logs/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
parsers:
- 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
26 changes: 26 additions & 0 deletions .tests/supavisor-logs/parser.assert
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Wrong password auth failure - first entry (192.168.1.100)
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"

# Wrong password auth failure - second attacker (10.0.0.50) - entry index 6
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"

# SSL required error - entry index 12
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"

# Bad startup payload (no source_ip) - entry index 13
results["s01-parse"]["crowdsecurity/supavisor-logs"][13].Success == true
results["s01-parse"]["crowdsecurity/supavisor-logs"][13].Evt.Meta.log_type == "supavisor_bad_startup"

# User not found (no source_ip) - entry index 16
results["s01-parse"]["crowdsecurity/supavisor-logs"][16].Success == true
results["s01-parse"]["crowdsecurity/supavisor-logs"][16].Evt.Meta.log_type == "supavisor_user_not_found"
7 changes: 7 additions & 0 deletions .tests/supavisor-logs/scenario.assert
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Brute force scenario should trigger for IP 192.168.1.100 (6 auth failures)
results["scenarios"]["crowdsecurity/supavisor-bf"][0].Success == true
results["scenarios"]["crowdsecurity/supavisor-bf"][0].Evt.Overflow.Alert.Source.IP == "192.168.1.100"

# Brute force scenario should trigger for IP 10.0.0.50 (6 auth failures)
results["scenarios"]["crowdsecurity/supavisor-bf"][1].Success == true
results["scenarios"]["crowdsecurity/supavisor-bf"][1].Evt.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
220 changes: 220 additions & 0 deletions collections/crowdsecurity/supabase-supavisor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Supabase Supavisor Collection

Detect and block attacks against self-hosted [Supabase](https://supabase.com/) deployments using the [Supavisor](https://github.com/supabase/supavisor) connection pooler.

## Why This Collection?

Modern Supabase deployments use Supavisor (an Elixir-based connection pooler) instead of PgBouncer. This changes where you need to monitor for attacks:

```
┌─────────────────────────────────────────────────────────────┐
│ Attack Flow │
├─────────────────────────────────────────────────────────────┤
│ Attacker ──→ Supavisor ──→ PostgreSQL │
│ │ │ │ │
│ Real IP Sees real IP Only sees Supavisor's IP │
│ ✅ Monitor here ❌ Useless for blocking │
└─────────────────────────────────────────────────────────────┘
```

**PostgreSQL logs are useless** in this architecture - they only show Supavisor's internal container IP, not the attacker's IP. This collection monitors Supavisor directly where the real client IPs are visible.

## Installation

```bash
cscli collections install crowdsecurity/supabase-supavisor
```

## Acquisition Configuration

Create `/etc/crowdsec/acquis.d/supavisor.yaml`:

### Standard Supabase docker-compose

```yaml
source: docker
container_name:
- supabase-supavisor
labels:
type: supavisor
```

### Coolify / Dynamic Container Names

Coolify and similar platforms add random suffixes to container names. Use regex matching:

```yaml
source: docker
container_name_regexp:
- "supabase-supavisor-.*"
labels:
type: supavisor
```

### Multiple Patterns (Recommended)

For maximum compatibility:

```yaml
source: docker
container_name_regexp:
- "supabase-supavisor-.*"
- ".*supavisor.*"
labels:
type: supavisor
```

## Requirements

### Docker Socket Access

CrowdSec needs access to the Docker socket to read container logs:

```yaml
# In your CrowdSec docker-compose.yml
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
```

### Network Access

CrowdSec must be able to reach the Supavisor container. If using Docker networks:

```yaml
networks:
- your-supabase-network
```

## 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` | Extracts fields from Supavisor logs |
| Scenario | `crowdsecurity/supavisor-bf` | Brute force detection (5 failures/30s) |

## Testing

### Generate Test Alerts

```bash
# Run 6+ failed login attempts (exceeds threshold of 5)
for i in {1..7}; do
psql 'postgresql://postgres.your_tenant:wrongpassword@your-host:5432/postgres' -c '\q' 2>/dev/null
echo "Attempt $i"
sleep 2
done
```

### Verify Detection

```bash
# Check alerts
cscli alerts list

# Check decisions (bans)
cscli decisions list

# View metrics
cscli metrics
```

### Unban Yourself (or add your IP to the whitelist)

If you accidentally ban your own IP during testing:

```bash
cscli decisions delete --ip YOUR_IP
```

whitelist config for your IP:

```yaml
# /opt/crowdsec/config/parsers/s02-enrich/my-whitelist.yaml
name: custom/my-whitelist
description: "Whitelist my IPs"
whitelist:
reason: "My personal/office IPs"
ip:
- "123.123.123.123"
```

## Remediation

After detection, you need a bouncer to actually block the IPs:

### Firewall Bouncer (iptables/nftables)

```bash
apt install crowdsec-firewall-bouncer-iptables
# or
apt install crowdsec-firewall-bouncer-nftables
```

### Traefik Bouncer

If using Traefik as reverse proxy:

```bash
# See: https://github.com/fbonalair/traefik-crowdsec-bouncer
```

## Troubleshooting

### No Logs Being Read

```bash
# Check if CrowdSec sees the container
docker exec crowdsec cscli metrics | grep docker

# Check acquisition config
docker exec crowdsec cat /etc/crowdsec/acquis.yaml

# Verify container name matches regex
docker ps --format '{{.Names}}' | grep -i supavisor
```

### Parser Not Matching

```bash
# Enable debug mode in parser
# Edit: /etc/crowdsec/parsers/s01-parse/crowdsecurity/supavisor-logs.yaml
# Set: debug: true

# Restart and check logs
docker restart crowdsec
docker logs crowdsec 2>&1 | grep -i supavisor
```

### Scenario Not Triggering

```bash
# Check if parser is setting log_type
docker logs crowdsec 2>&1 | grep "log_type"

# Check scenario is loaded
cscli scenarios list | grep supavisor
```

## 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
86 changes: 86 additions & 0 deletions parsers/s01-parse/crowdsecurity/supavisor-logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Supavisor Logs Parser

Parses [Supavisor](https://github.com/supabase/supavisor) connection pooler logs to detect authentication failures.

## Overview

Supavisor is Supabase's cloud-native, multi-tenant PostgreSQL connection pooler written in Elixir. It replaced PgBouncer in the Supabase stack and is now the default connection pooler.

**Important**: In modern Supabase deployments using Supavisor, the PostgreSQL container only sees connections from Supavisor's internal IP - not the real client IPs. This makes monitoring at the Supavisor level essential for detecting and blocking attacks.

## Supported Log Formats

### Authentication Failures (with peer_ip - can be blocked)

```
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
```

### SSL Required Errors (with peer_ip)

```
05:44:32.395 project=dev_tenant user=postgres region=local mode=transaction type=single app_name=psql peer_ip=123.123.123.123 [error] ClientHandler: Tenant is not allowed to connect without SSL, user postgres
```

### Bad Startup Payload (NO peer_ip - monitoring only)

```
08:31:53.782 region=local [error] ClientHandler: Client startup message error: :bad_startup_payload
```

### User Not Found (NO peer_ip - monitoring only)

```
06:06:31.740 region=local [error] ClientHandler: User not found: "Either external_id or sni_hostname must be provided" {:single, "postgres", nil}
```

## Limitation

Some Supavisor error types do NOT include `peer_ip`, making blocking impossible:

| Log Type | Has peer_ip | Can Block |
|----------|-------------|-----------|
| Wrong password | ✅ Yes | ✅ Yes |
| SSL required | ✅ Yes | ✅ Yes |
| Bad startup payload | ❌ No | ❌ No |
| User not found | ❌ No | ❌ No |

## Parsed Fields

| Field | Description |
|-------|-------------|
| `source_ip` | Client IP address (when `peer_ip` present) |
| `project` | Supavisor tenant/project identifier |
| `db_user` | Database user attempting connection |
| `pool_mode` | Connection pool mode (transaction/session) |
| `pool_type` | Pool type (single/pooled) |
| `app_name` | Client application name |
| `log_type` | Event classification |

## Acquisition

This parser requires Docker socket 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-.*"
- ".*supavisor.*"
labels:
type: supavisor
```

## Related

- Scenario: `crowdsecurity/supavisor-bf`
- Collection: `crowdsecurity/supabase-supavisor`
Loading