Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

supabase_secure

Harden any Supabase project with one command. RLS-by-default, append-only audit log with hash chain, column-level encryption, password hardening, and a built-in security advisor.

License: MIT Postgres Supabase


Install (one line)

DATABASE_URL='postgres://postgres:...@db.<ref>.supabase.co:5432/postgres' \
  bash <(curl -fsSL https://raw.githubusercontent.com/mchawda/supabase_secure/main/install.sh)

That's it. No clone, no Node, no Docker. Just psql.

Don't have DATABASE_URL handy? Run it without and the installer will prompt you for your project ref + DB password.

Or the manual way

git clone https://github.com/mchawda/supabase_secure
cd supabase_secure
DATABASE_URL='postgres://...' ./scripts/apply.sh
./scripts/verify.sh

Or via the Supabase CLI

supabase link --project-ref <your-ref>
cp -r path/to/supabase_secure/supabase/migrations/* supabase/migrations/
supabase db push

What it does

After you run the installer, your Supabase database has:

Feature Detail
🔒 RLS forced on every public table Existing tables hardened; an event trigger turns RLS on automatically for any new table you create.
📜 Append-only audit log with SHA-256 hash chain Every INSERT/UPDATE/DELETE on public.* is captured into security.audit_log. Tamper-evident: select * from security.audit_verify_chain(null) returns 0 rows when the chain is intact.
🔑 Column-level encryption via Supabase Vault security.encrypt_text() / security.decrypt_text() wrap Vault. Plaintext never touches your tables.
👤 Password hardening Auth Hook rejects passwords under 12 chars, no upper/lower/digit/symbol, or in the common-password list.
🚦 Brute-force throttle security.is_login_throttled() for your sign-in API.
🪪 Multi-tenant RBAC helpers security.current_org_id(), security.is_admin(), security.has_role(), security.require_mfa() — all read from the safe app_metadata JWT claim.
🗂️ Storage RLS Default-deny baseline + security.add_org_scoped_bucket_policies('bucket') to wire a tenant in one line.
🩺 Built-in advisor select * from security.advisors shows tables without RLS, views without security_invoker, SECURITY DEFINER leaks in public, broken audit chain, and more.
🔐 Lockdown defaults Revokes ambient PUBLIC grants, hides private schemas, pins search_path, blocks CREATE in public for anon/authenticated.

All of this is idempotent — re-run the installer any time.


Verify

-- One row per misconfiguration. Should be empty (or only false-positives you accept).
select * from security.advisors order by severity;

-- Chain integrity (must return 0).
select count(*) from security.audit_verify_chain(null);

Or run the 10 self-tests:

psql "$DATABASE_URL" -f tests/security_tests.sql
# => OK: 10 tests passed

Day-2 things you should still do

These are settings the SQL kit cannot change for you, but the installer will remind you:

  1. Activate the password hook in Supabase Studio → Authentication → Hooks → Password validation: pg-functions://postgres/security/password_validation_hook
  2. Enable MFA at the Supabase organisation level.
  3. Custom SMTP + CAPTCHA on sign-in / sign-up / reset.
  4. Network restrictions in Database → Settings (IP allow-list).
  5. Put org_id uuid not null on every tenant table; the canonical pattern is in examples/tenant_table_template.sql.

Examples


How it's structured

supabase/migrations/
  20260429000100_security_extensions.sql           ← pgcrypto, vault, pg_stat_statements
  20260429000200_security_schema_and_helpers.sql   ← private security schema + JWT helpers
  20260429000300_security_audit_log.sql            ← append-only audit log + hash chain
  20260429000400_security_audit_triggers.sql       ← generic trigger + auto-attach
  20260429000500_security_encryption.sql           ← Vault-backed encryption helpers
  20260429000600_security_rls_defaults.sql         ← force RLS + auto-RLS event trigger
  20260429000700_security_storage_policies.sql     ← storage default-deny + builders
  20260429000800_security_auth_hardening.sql       ← password hook, throttle, sessions
  20260429000900_security_lockdown.sql             ← revoke PUBLIC grants, pin search_path
  20260429001000_security_advisors.sql             ← security.advisors view
scripts/
  apply.sh        ← apply via psql or supabase CLI
  verify.sh       ← print advisors + chain status
  rollback.sql    ← clean removal (preserves Vault secrets)
install.sh        ← one-line installer (download + apply + verify)
tests/security_tests.sql
examples/

Uninstall

psql "$DATABASE_URL" -f scripts/rollback.sql

Removes the security schema, the audit triggers, and the event triggers. Does not drop supabase_vault — your encrypted secrets stay intact.


FAQ

Will this break my existing app? It enables RLS on tables that didn't have it. If you currently rely on unrestricted access from anon / authenticated, you'll need to add explicit policies. Use select * from security.advisors where kind = 'rls_no_policies' to find such tables. Apply on staging first.

Why not pgsodium for column encryption? Supabase has deprecated pgsodium TCE. Vault is the supported replacement and keeps key material outside the database.

Why is user_metadata not used in any helper? Because raw_user_meta_data is user-editable and unsafe for authorization. Always use app_metadata (set server-side with the service role).

Does this work with the Supabase free tier? Yes. All extensions used are available on every tier.


License

MIT. See LICENSE.

About

Harden any Supabase project with one command. RLS-by-default, append-only audit log with hash chain, Vault-backed column encryption, password hardening, and a built-in security advisor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages