The finding
Every secret-bearing file in this project was mode 644 — world-readable by any
account on the machine:
scripts/supabase/edge-function-secrets.json 644 live Stripe secret key,
Stripe webhook secret,
PayPal client secret
.env 644 Supabase SERVICE ROLE key,
Resend API key,
Supabase access token
.env.cloud / .env.local / .env.local-supabase 644
Fixed to 600 on the owner's machine, but nothing enforces it — a fresh clone,
a fork, or a re-created .env lands back at whatever the umask gives, which is 644
by default.
Why gitignore is not the control people think it is
scripts/supabase/edge-function-secrets.example.json says:
"Copy this file to edge-function-secrets.json (gitignored) … NEVER commit the
filled-in copy"
That guidance is correct and incomplete. It addresses git and says nothing about the
filesystem. Gitignore stops a commit; it does nothing about file permissions, about
anything that syncs or backs up a home directory, or about other users and processes
on a shared machine.
Confirmed the git side is genuinely clean: git log --all shows the secrets file was
never committed, and every sk_test_ / whsec_ string in committed docs and in
public/blog/ is a literal placeholder (sk_test_...), not a key.
Why it matters beyond one laptop
docs/FORKING.md hands this pattern to everyone who forks the template, so the
default is world-readable payment credentials for every fork.
Context
Surfaced during the #567 org migration. That migration is also the argument for
keeping secrets outside the Supabase project — Stripe and PayPal were recoverable
after the project was deleted precisely because they lived in this file, while the
Google/GitHub OAuth and Turnstile secrets were lost because they lived only in the
project's config. The pattern is right; the storage is wrong.
Fix
chmod 600 — done on the owner's machine, not enforced anywhere.
- Make
scripts/supabase/set-edge-function-secrets.ts refuse to run when the
config file is group- or world-readable, and say so. A warning is not enough; the
whole point is that nobody looks.
- Longer term, encrypt at rest.
gpg is already installed here (sops/age are
not), so edge-function-secrets.json.gpg decrypted to the setter's stdin keeps
plaintext off disk entirely — and an encrypted file can be committed, so it
survives a dead laptop, which gitignored plaintext does not.
- Update the
_comment and docs/FORKING.md to state the permission requirement,
not just "don't commit it".
Acceptance
- With the config file at 644, the setter exits non-zero and names the file.
Asserting it "warns" is not enough — assert the refusal.
- A fresh
cp edge-function-secrets.example.json edge-function-secrets.json followed
by the setter fails until the mode is fixed.
The finding
Every secret-bearing file in this project was mode 644 — world-readable by any
account on the machine:
Fixed to
600on the owner's machine, but nothing enforces it — a fresh clone,a fork, or a re-created
.envlands back at whatever the umask gives, which is 644by default.
Why gitignore is not the control people think it is
scripts/supabase/edge-function-secrets.example.jsonsays:That guidance is correct and incomplete. It addresses git and says nothing about the
filesystem. Gitignore stops a commit; it does nothing about file permissions, about
anything that syncs or backs up a home directory, or about other users and processes
on a shared machine.
Confirmed the git side is genuinely clean:
git log --allshows the secrets file wasnever committed, and every
sk_test_/whsec_string in committed docs and inpublic/blog/is a literal placeholder (sk_test_...), not a key.Why it matters beyond one laptop
docs/FORKING.mdhands this pattern to everyone who forks the template, so thedefault is world-readable payment credentials for every fork.
Context
Surfaced during the #567 org migration. That migration is also the argument for
keeping secrets outside the Supabase project — Stripe and PayPal were recoverable
after the project was deleted precisely because they lived in this file, while the
Google/GitHub OAuth and Turnstile secrets were lost because they lived only in the
project's config. The pattern is right; the storage is wrong.
Fix
chmod 600— done on the owner's machine, not enforced anywhere.scripts/supabase/set-edge-function-secrets.tsrefuse to run when theconfig file is group- or world-readable, and say so. A warning is not enough; the
whole point is that nobody looks.
gpgis already installed here (sops/agearenot), so
edge-function-secrets.json.gpgdecrypted to the setter's stdin keepsplaintext off disk entirely — and an encrypted file can be committed, so it
survives a dead laptop, which gitignored plaintext does not.
_commentanddocs/FORKING.mdto state the permission requirement,not just "don't commit it".
Acceptance
Asserting it "warns" is not enough — assert the refusal.
cp edge-function-secrets.example.json edge-function-secrets.jsonfollowedby the setter fails until the mode is fixed.