security: public Deploy härten (Reverse-Proxy, GoTrue-Rate-Limit, RLS-Revoke)
Für die öffentlich erreichbare Instanz (dev.openbureau.ch): 1. Reverse-Proxy nur /auth/* durchreichen — /rest, /storage, /realtime raus. PostgREST /rest/v1/ gab die komplette DB-Schema-Beschreibung (OpenAPI) preis; der Browser nutzt Supabase nur fürs Login, Daten laufen über /api/*. (Caddy-Block in create-openbureau-lxc.sh + proxmox/README.md angepasst.) 2. GoTrue GOTRUE_RATE_LIMIT_TOKEN_REFRESH=100 — bremst Brute-Force aufs /token, das public direkt gegen GoTrue läuft (nicht übers Node-Rate-Limit). 3. db/schema.sql: revoke all from anon/authenticated auf posts/comments/forums/ threads; grants nur noch service_role. RLS bleibt so auch bei künftigen Policies dicht (Defense-in-Depth statt "RLS ohne Policy"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -161,6 +161,14 @@ Eingebaute Schutzmaßnahmen (Stand: Härtungs-Pass):
|
|||||||
- **Non-root**: der CMS-Container läuft als `node` (uid 1000).
|
- **Non-root**: der CMS-Container läuft als `node` (uid 1000).
|
||||||
- **Port-Binding** über `BIND_ADDR` (Standard `127.0.0.1`), DB nur auf localhost.
|
- **Port-Binding** über `BIND_ADDR` (Standard `127.0.0.1`), DB nur auf localhost.
|
||||||
- **CORS** am Kong-Gateway auf die eigene `SITE_URL`-Origin beschränkt (kein `*`).
|
- **CORS** am Kong-Gateway auf die eigene `SITE_URL`-Origin beschränkt (kein `*`).
|
||||||
|
- **Reverse-Proxy nur `/auth/*`**: bei einem Domain-Deploy gehört nur das Login
|
||||||
|
(GoTrue) public — `/rest`, `/storage`, `/realtime` nicht durchreichen (PostgREST
|
||||||
|
`/rest/v1/` würde sonst die DB-Schema-Beschreibung preisgeben). Siehe Caddy-Block
|
||||||
|
in [`../proxmox/README.md`](../proxmox/README.md).
|
||||||
|
- **Login-Rate-Limit** an GoTrue (`GOTRUE_RATE_LIMIT_TOKEN_REFRESH`), weil das
|
||||||
|
öffentliche Login direkt aufs `/token` geht (nicht übers Node-Limit).
|
||||||
|
- **Keine Tabellenrechte für `anon`/`authenticated`** (`revoke` in `db/schema.sql`):
|
||||||
|
RLS bleibt so auch bei künftigen Policies dicht; nur `service_role` (Node) liest.
|
||||||
|
|
||||||
### Migration eines bestehenden Containers
|
### Migration eines bestehenden Containers
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -31,6 +31,7 @@ create index if not exists posts_section_idx on public.posts (section);
|
|||||||
-- RLS aktivieren; die api nutzt den Service-Key (umgeht RLS). Wenn das
|
-- RLS aktivieren; die api nutzt den Service-Key (umgeht RLS). Wenn das
|
||||||
-- Frontend später direkt liest, hier gezielte Policies ergänzen.
|
-- Frontend später direkt liest, hier gezielte Policies ergänzen.
|
||||||
alter table public.posts enable row level security;
|
alter table public.posts enable row level security;
|
||||||
|
revoke all on public.posts from anon, authenticated;
|
||||||
|
|
||||||
-- ── Dialog / Diskussionen ───────────────────────────────────────────────
|
-- ── Dialog / Diskussionen ───────────────────────────────────────────────
|
||||||
-- Thread = Pfad des Beitrags (z.B. /library/software/stack/). Flache Wortmeldungen
|
-- Thread = Pfad des Beitrags (z.B. /library/software/stack/). Flache Wortmeldungen
|
||||||
@@ -50,7 +51,12 @@ create table if not exists public.comments (
|
|||||||
alter table public.comments add column if not exists author_role text;
|
alter table public.comments add column if not exists author_role text;
|
||||||
create index if not exists comments_thread_idx on public.comments (thread, created_at);
|
create index if not exists comments_thread_idx on public.comments (thread, created_at);
|
||||||
alter table public.comments enable row level security;
|
alter table public.comments enable row level security;
|
||||||
grant all on public.comments to anon, authenticated, service_role;
|
-- Nur die Node-API (service_role) greift auf die Tabellen zu; der Browser geht
|
||||||
|
-- ausschliesslich über /api/*. anon/authenticated bekommen KEINE Tabellenrechte,
|
||||||
|
-- damit das öffentlich erreichbare /rest/v1 auch bei künftigen RLS-Policies dicht
|
||||||
|
-- bleibt (Defense-in-Depth, nicht nur "RLS ohne Policy").
|
||||||
|
grant all on public.comments to service_role;
|
||||||
|
revoke all on public.comments from anon, authenticated;
|
||||||
|
|
||||||
-- Aggregat je Thread (Anzahl + letzte Aktivität). Spart der API den Full-Table-
|
-- Aggregat je Thread (Anzahl + letzte Aktivität). Spart der API den Full-Table-
|
||||||
-- Scan + JS-Aggregation bei jedem Forum-Aufruf; Postgres zählt direkt.
|
-- Scan + JS-Aggregation bei jedem Forum-Aufruf; Postgres zählt direkt.
|
||||||
@@ -75,7 +81,8 @@ create table if not exists public.forums (
|
|||||||
created_at timestamptz not null default now()
|
created_at timestamptz not null default now()
|
||||||
);
|
);
|
||||||
alter table public.forums enable row level security;
|
alter table public.forums enable row level security;
|
||||||
grant all on public.forums to anon, authenticated, service_role;
|
grant all on public.forums to service_role;
|
||||||
|
revoke all on public.forums from anon, authenticated;
|
||||||
|
|
||||||
-- ── Threads (Diskussionen) ──────────────────────────────────────────────
|
-- ── Threads (Diskussionen) ──────────────────────────────────────────────
|
||||||
-- key = stabiler Bezeichner, den comments.thread referenziert:
|
-- key = stabiler Bezeichner, den comments.thread referenziert:
|
||||||
@@ -96,7 +103,8 @@ create table if not exists public.threads (
|
|||||||
);
|
);
|
||||||
create index if not exists threads_forum_idx on public.threads (forum_id);
|
create index if not exists threads_forum_idx on public.threads (forum_id);
|
||||||
alter table public.threads enable row level security;
|
alter table public.threads enable row level security;
|
||||||
grant all on public.threads to anon, authenticated, service_role;
|
grant all on public.threads to service_role;
|
||||||
|
revoke all on public.threads from anon, authenticated;
|
||||||
|
|
||||||
-- Seed-Kategorien (idempotent; im Admin umbenenn-/erweiterbar).
|
-- Seed-Kategorien (idempotent; im Admin umbenenn-/erweiterbar).
|
||||||
insert into public.forums (slug, name, sort, kind) values
|
insert into public.forums (slug, name, sort, kind) values
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ services:
|
|||||||
# Single-Author: Self-Signup aus. User wird per Admin-API angelegt
|
# Single-Author: Self-Signup aus. User wird per Admin-API angelegt
|
||||||
# (Kommando steht im README / LXC-Output).
|
# (Kommando steht im README / LXC-Output).
|
||||||
GOTRUE_DISABLE_SIGNUP: "true"
|
GOTRUE_DISABLE_SIGNUP: "true"
|
||||||
|
# Brute-Force-Bremse aufs /token: das öffentliche Login läuft direkt gegen
|
||||||
|
# GoTrue (nicht über das Node-Rate-Limit), daher hier kappen — max. 100
|
||||||
|
# Token-Anfragen / 5 Min. Reichlich für einen Autor, bremst Rateversuche.
|
||||||
|
GOTRUE_RATE_LIMIT_TOKEN_REFRESH: "100"
|
||||||
GOTRUE_JWT_ADMIN_ROLES: service_role
|
GOTRUE_JWT_ADMIN_ROLES: service_role
|
||||||
GOTRUE_JWT_AUD: authenticated
|
GOTRUE_JWT_AUD: authenticated
|
||||||
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||||
|
|||||||
@@ -192,8 +192,11 @@ cat <<EOF
|
|||||||
|
|
||||||
Öffentlich: https://${SITE_DOMAIN} (sobald der Reverse-Proxy auf ${IPADDR:-<ip>} zeigt)
|
Öffentlich: https://${SITE_DOMAIN} (sobald der Reverse-Proxy auf ${IPADDR:-<ip>} zeigt)
|
||||||
Caddy-Block: ${SITE_DOMAIN} {
|
Caddy-Block: ${SITE_DOMAIN} {
|
||||||
@sb path /auth/* /rest/* /storage/* /realtime/*
|
# Nur /auth/* muss public ans Gateway (Browser-Login). Daten
|
||||||
reverse_proxy @sb ${IPADDR:-<ip>}:8000
|
# laufen über /api/* (Node spricht kong intern an). /rest, /storage,
|
||||||
|
# /realtime NICHT exponieren — unnötige Angriffsfläche.
|
||||||
|
@auth path /auth/*
|
||||||
|
reverse_proxy @auth ${IPADDR:-<ip>}:8000
|
||||||
reverse_proxy ${IPADDR:-<ip>}:8080
|
reverse_proxy ${IPADDR:-<ip>}:8080
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
+6
-2
@@ -69,8 +69,12 @@ Den passenden Reverse-Proxy-Eintrag gibt das Skript am Ende selbst aus. Für
|
|||||||
|
|
||||||
```caddy
|
```caddy
|
||||||
dev.openbureau.ch {
|
dev.openbureau.ch {
|
||||||
@sb path /auth/* /rest/* /storage/* /realtime/*
|
# Nur /auth/* muss public ans Supabase-Gateway (Browser-Login). Alle Daten
|
||||||
reverse_proxy @sb 192.168.1.134:8000
|
# laufen über /api/* (Node spricht kong intern an). /rest, /storage,
|
||||||
|
# /realtime bewusst NICHT exponieren — sonst gibt /rest/v1/ die ganze
|
||||||
|
# DB-Schema-Beschreibung preis (PostgREST-OpenAPI).
|
||||||
|
@auth path /auth/*
|
||||||
|
reverse_proxy @auth 192.168.1.134:8000
|
||||||
reverse_proxy 192.168.1.134:8080
|
reverse_proxy 192.168.1.134:8080
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user