source-cooperative / source-cooperative/source.coop

feat(data-connections): store backend credentials in Cloudflare Secrets Store instead of DynamoDB

Open
#485 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
TypeScript
Stars
26
Forks
9
Avg merge
1d 7h
Merged PRs (30d)
42

Description

Problem

S3AccessKeyAuthenticationSchema (src/types/data-connection.ts) holds access_key_id and secret_access_key inline, and the whole authentication object is persisted on the data connection row in DynamoDB. src/components/features/data-connections/redact.ts strips it before anything crosses into a client component, which is the right guard for the RSC boundary — but the credential is still sitting in plaintext at rest, in table scans, in backups, and one careless select away from a response body.

This gets more pressing as S3-compatible backends without a federation story come online. Cloudflare R2 in particular has no AssumeRoleWithWebIdentity equivalent, so s3_access_key is the only way to reach it — key-based auth is not a legacy path being wound down, it's the path forward for R2.

Ask

Move backend credential material out of DynamoDB and into Cloudflare Secrets Store. source.coop is write-only against the store: it creates, rotates, and deletes secrets, and never reads a value back. The data proxy (source-cooperative/data.source.coop) holds the read side. Companion issue: source-cooperative/data.source.coop#210.

The capability split is enforced by Cloudflare's own token model rather than by convention:

  • source.coop gets an API token with Account Secrets Store Edit only.
  • The value cannot be read back through the API by anyone — Account Secrets Store Read grants metadata only, never the value. Values are only consumable through a Worker binding.

That last point is a feature here: it makes "source.coop cannot read the credential it wrote" a property of the platform, not something to review for.

Consequences for the UI

Edit becomes write-only-replace. The form cannot pre-fill or display an existing key, so the secret fields render empty with a "leave blank to keep the current credential" affordance. redact.ts gets simpler in the process — S3AccessKey already redacts to a bare { type }, and once the value never enters the record there is nothing left to strip.

⚠️ The part that needs a decision first

Secrets Store secrets are bound to a Worker statically, by name, at deploy time (secrets_store_secrets in Wrangler config, read as await env.BINDING.get()). There is no runtime lookup by name. So "one secret per data connection" means the proxy needs a new binding and a redeploy for every connection created — plus the account limits bite: one store per account in beta, 100 secrets per account, which becomes a hard ceiling on how many key-based connections the platform can ever hold.

The alternative is envelope encryption: one long-lived key in Secrets Store bound to the proxy, used to encrypt each connection's credentials. DynamoDB stores only the ciphertext, and the API serves it as part of authentication. That keeps plaintext out of the database (the actual goal), scales to any number of connections, and needs no redeploy on connection create.

Recommendation: envelope encryption, with an asymmetric keypair. source.coop holds the public half (encrypt only), the proxy the private half (decrypt only) — the read/write split then holds by construction rather than by IAM scoping alone. Wire shape:

{ "type": "s3_access_key", "ciphertext": "<base64>", "key_id": "dataconn-v1" }

key_id names the bound secret so rotation is a config change plus a re-encrypt sweep, not a flag day.

Work

  • Settle the shape (one-secret-per-connection vs. envelope) jointly with the proxy issue — the authentication wire shape depends on it.
  • Provision the store and a scoped write token; document the naming convention (e.g. dataconn-{data_connection_id}) and the workers scope required for the proxy to consume it.
  • Write on connection create; rotate on update; delete on connection delete. Handle the partial-failure case (secret written, row write failed → orphaned secret; row written, secret failed → connection that cannot authenticate).
  • Extend the create/update actions and the /api/v1/data-connections routes so authentication never round-trips a plaintext credential.
  • Update DataConnectionForm for write-only-replace, and simplify redact.ts accordingly.
  • Migration for existing s3_access_key connections; verify none are left holding plaintext afterwards.
  • Extend Azure SAS token the same way, or state explicitly that it is out of scope for now.

Acceptance

  • Creating an R2 data connection through the UI stores no credential material in DynamoDB.
  • Reading the connection back — API, RSC payload, or admin UI — never yields the secret.
  • The proxy can serve signed reads from that connection (validated jointly with the companion issue).

Related: #432 (GitHub Actions OIDC epic — same "who may act on a product" territory), source-cooperative/data.source.coop#207.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

First resolve the one-secret-per-connection versus envelope-encryption shape with the companion proxy issue, since it determines the authentication wire format. Then inspect src/types/data-connection.ts, the create/update actions, /api/v1/data-connections routes, DataConnectionForm, and src/components/features/data-connections/redact.ts; done means no credential material remains in DynamoDB or responses and the proxy can serve signed reads.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, cloud, databases, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.