Skip to content

Payment and service-role secrets are stored world-readable (644): .env and edge-function-secrets.json #614

Description

@TortoiseWolfe

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

  1. chmod 600 — done on the owner's machine, not enforced anywhere.
  2. 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.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions