security: harden remote-access stack (pentest remediation R1-R4)

App layer (R1): bind frps NewProxy to token-owned subdomain (anti-hijack),
default-deny unknown webhook ops, HMAC-verify stripe-stub billing webhook,
enforce bandwidth quota kill-switch (Ping op), least-privilege table grants
(migrations 0002/0003), GOTRUE_PASSWORD_MIN_LENGTH=12.

Infra/net (R2): unpublish internal host ports (kong/pooler/analytics/frps-dash),
read-only docker-socket-proxy for vector (no host breakout), on-demand-TLS
allow-list authorizer, edge-block machine-only webhooks, no-new-privileges on
custom containers.

Secrets (R3): rotate Postgres password (all roles) + frps dashboard; replace
predictable supavisor defaults; secrets externalized to gitignored .env.

Med/Low (R4): security response headers (HSTS/XCTO/XFO/Referrer/Permissions/COOP),
restrict frp proxy_type to http (no open relay), disable destructive redis
commands, tighten frps.toml perms.

No secrets committed; rotated values live only in gitignored .env files.
This commit is contained in:
2026-05-30 10:45:07 +02:00
parent a8593afa61
commit 50ab46dbe1
12 changed files with 259 additions and 33 deletions
+49 -12
View File
@@ -52,9 +52,8 @@ services:
restart: unless-stopped
# https://unix.stackexchange.com/a/294837
entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
ports:
- ${KONG_HTTP_PORT}:8000/tcp
- ${KONG_HTTPS_PORT}:8443/tcp
# SECURITY (R2/F2/I3): host ports unpublished. Kong is reached only via Caddy
# over the internal docker network (api.linumiq.net -> supabase-kong:8000).
depends_on:
analytics:
condition: service_healthy
@@ -113,6 +112,7 @@ services:
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
GOTRUE_PASSWORD_MIN_LENGTH: ${GOTRUE_PASSWORD_MIN_LENGTH}
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
@@ -315,6 +315,7 @@ services:
# TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786
VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
REDIS_URL: ${REDIS_URL}
STRIPE_STUB_WEBHOOK_SECRET: ${STRIPE_STUB_WEBHOOK_SECRET}
volumes:
- ./volumes/functions:/home/deno/functions:Z
command:
@@ -361,8 +362,7 @@ services:
# Uncomment to use Big Query backend for analytics
# GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID}
# GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER}
ports:
- 4000:4000
# SECURITY (R2/F2/I3): host port 4000 unpublished. Reached internally as analytics:4000.
# Comment out everything below this point if you are using an external Postgres database
db:
@@ -412,6 +412,41 @@ services:
# Volume to persist pgsodium decryption key between restarts
- ./volumes/db-config:/etc/postgresql-custom
# SECURITY (R2/I1): read-only Docker API gateway. Vector no longer mounts the
# raw docker.sock (a `:ro` bind on a unix socket does NOT restrict the Docker
# API, so a compromised vector could create privileged containers = host
# escape). This haproxy-based proxy exposes ONLY read endpoints (GET
# containers/events/ping); all POST/exec/create/start are denied.
docker-socket-proxy:
container_name: supabase-docker-socket-proxy
image: tecnativa/docker-socket-proxy:0.3.0
restart: unless-stopped
environment:
CONTAINERS: 1
EVENTS: 1
PING: 1
INFO: 0
POST: 0
EXEC: 0
IMAGES: 0
NETWORKS: 0
VOLUMES: 0
BUILD: 0
COMMIT: 0
CONFIGS: 0
DISTRIBUTION: 0
NODES: 0
PLUGINS: 0
SECRETS: 0
SERVICES: 0
SESSION: 0
SWARM: 0
SYSTEM: 0
TASKS: 0
AUTH: 0
volumes:
- ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro
vector:
container_name: supabase-vector
image: timberio/vector:0.28.1-alpine
@@ -429,9 +464,11 @@ services:
timeout: 5s
interval: 5s
retries: 3
depends_on:
docker-socket-proxy:
condition: service_started
volumes:
- ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro
- ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro
environment:
LOGFLARE_API_KEY: ${LOGFLARE_API_KEY}
command: [ "--config", "etc/vector/vector.yml" ]
@@ -455,9 +492,8 @@ services:
- -c
- /app/bin/migrate && /app/bin/supavisor eval "$$(cat /etc/pooler/pooler.exs)" && /app/bin/server
restart: unless-stopped
ports:
- ${POSTGRES_PORT}:${POSTGRES_PORT}
- ${POOLER_PROXY_PORT_TRANSACTION}:${POOLER_PROXY_PORT_TRANSACTION}
# SECURITY (R2/F1/I5): host ports 5432/6543 unpublished. Postgres/pooler are
# reached only over the internal docker network; no public DB exposure.
environment:
- PORT=4000
- POSTGRES_PORT=${POSTGRES_PORT}
@@ -465,13 +501,14 @@ services:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- DATABASE_URL=ecto://postgres:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/_supabase
- CLUSTER_POSTGRES=true
- SECRET_KEY_BASE=UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
- VAULT_ENC_KEY=your-encryption-key-32-chars-min
# SECURITY (R3): replaced predictable upstream-default supavisor secrets.
- SECRET_KEY_BASE=${SUPAVISOR_SECRET_KEY_BASE}
- VAULT_ENC_KEY=${SUPAVISOR_VAULT_ENC_KEY}
- API_JWT_SECRET=${JWT_SECRET}
- METRICS_JWT_SECRET=${JWT_SECRET}
- REGION=local
- ERL_AFLAGS=-proto_dist inet_tcp
- POOLER_TENANT_ID=your-tenant-id
- POOLER_TENANT_ID=${POOLER_TENANT_ID}
- POOLER_DEFAULT_POOL_SIZE=${POOLER_DEFAULT_POOL_SIZE}
- POOLER_MAX_CLIENT_CONN=${POOLER_MAX_CLIENT_CONN}
- POOLER_POOL_MODE=transaction