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
+54
View File
@@ -0,0 +1,54 @@
-- 0002: pentest remediation (A4/W3 privilege reduction, A3 quota default, W1 subscription rows)
BEGIN;
-- ---------------------------------------------------------------------------
-- A4 / W3: authenticated users must NOT be able to UPDATE tunnels directly.
-- All legitimate writes (claim, token rotation, quota/usage, activation) go
-- through service-role server routes / workers. A direct grant let an owner
-- tamper with bytes_used, quota_bytes, is_active, token and subdomain.
-- ---------------------------------------------------------------------------
REVOKE UPDATE ON public.tunnels FROM authenticated;
DROP POLICY IF EXISTS tunnels_update_own ON public.tunnels;
-- SELECT (read-only dashboard) stays intact via tunnels_select_own.
-- ---------------------------------------------------------------------------
-- A3: free-tier quota default 1 TiB -> 2 GiB, and shrink existing rows that
-- still carry the old default so the kill-switch is meaningful.
-- ---------------------------------------------------------------------------
ALTER TABLE public.tunnels ALTER COLUMN quota_bytes SET DEFAULT 2147483648;
UPDATE public.tunnels
SET quota_bytes = 2147483648
WHERE quota_bytes = 1099511627776;
-- ---------------------------------------------------------------------------
-- W1 (enablement): ensure every user has a subscriptions row so the billing
-- webhook activation (PATCH by user) actually targets a row, and backfill
-- existing users.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
INSERT INTO public.users_profile (user_id, email)
VALUES (NEW.id, NEW.email)
ON CONFLICT (user_id) DO NOTHING;
INSERT INTO public.subscriptions (user_id, plan, status)
VALUES (NEW.id, 'free', 'active')
ON CONFLICT (user_id) DO NOTHING;
RETURN NEW;
END;
$$;
REVOKE ALL ON FUNCTION public.handle_new_user() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.handle_new_user() TO supabase_auth_admin;
INSERT INTO public.subscriptions (user_id, plan, status)
SELECT id, 'free', 'active' FROM auth.users
ON CONFLICT (user_id) DO NOTHING;
COMMIT;
@@ -0,0 +1,20 @@
-- 0003: least-privilege table grants (A4/W3 hardening).
-- Supabase's default privileges grant ALL on public tables to anon &
-- authenticated. RLS gates DML, but TRUNCATE bypasses RLS and unauthenticated
-- (anon) should have no direct table rights at all. Reduce to the minimum the
-- app actually needs; service_role (which bypasses RLS) keeps full access.
BEGIN;
REVOKE ALL ON public.tunnels FROM anon, authenticated;
REVOKE ALL ON public.subscriptions FROM anon, authenticated;
REVOKE ALL ON public.usage_samples FROM anon, authenticated;
REVOKE ALL ON public.users_profile FROM anon, authenticated;
-- Authenticated users get read-only dashboard access (still gated by RLS
-- owner policies). users_profile also needs UPDATE (it has an owner policy).
GRANT SELECT ON public.tunnels TO authenticated;
GRANT SELECT ON public.subscriptions TO authenticated;
GRANT SELECT, UPDATE ON public.users_profile TO authenticated;
-- usage_samples: service_role only (no anon/authenticated access).
COMMIT;