Skip to content

Provision a dedicated R2 bucket and identity for the wedding database backupΒ #3251

Closed

Description

@devantler

πŸ€– Generated by the Agentic Engineer

Evidence

wedding-db's backup consumes the shared platform backup identity. Measured on main:

  • k8s/bases/apps/wedding-app/external-secret-db-backup.yaml reads OpenBao
    infrastructure/backup/r2 β€” the same path Velero, vault-backup and umami-db read.
  • k8s/bases/apps/wedding-app/object-store.yaml writes to s3://${r2_bucket}/cnpg/wedding-db,
    i.e. a prefix inside the shared bucket.

Cloudflare permanent R2 identities are bucket-scoped, not prefix-scoped, so no change on our side
can narrow the shared credential to that prefix. A dedicated destination is required before the
tenant's access can be reduced.

Why this is its own child

This child isolates the Cloudflare account dependency from the downstream manifest changes.
Account authentication and any required approval for the exact credential scope come first; the
agent can then inspect and carry out the scoped setup and encrypted handoff. It is not a standing
rule that the maintainer must manually provision the destination or handle a raw secret.

Expected outcome

A dedicated production R2 bucket for the wedding database backup, plus an Object Read and Write
identity scoped to that bucket only, with no access to the shared backup bucket.

Acceptance criteria

  • A dedicated R2 bucket exists for the wedding database backup.
  • An Object Read and Write identity exists that is scoped to only that bucket.
  • The identity has no read or write access to the shared backup bucket (verified by attempting
    access and observing denial).
  • The credential is handed over through the existing encrypted per-environment bootstrap flow so
    the next child can seed it declaratively.

Verified account setup β€” September 8, 2026

The exact account-token grant was approved in the session. The dedicated token is now created with Object Read and Write permission restricted to the wedding backup bucket.

A real S3 API probe successfully wrote, read, listed and deleted a synthetic object in that bucket. Read, list and write attempts against the shared backup bucket each returned 403 AccessDenied. The synthetic object was removed and its absence verified. R2 ignored the conditional-delete header, so cleanup used the reviewed ownership-checking fallback; this is not evidence of conditional-delete enforcement.

The credentials were passed directly into SOPS and saved in the production bootstrap source. The encrypted handoff is being reviewed for delivery under #3252. That final acceptance item remains open until the source handoff is published; seed/projection verification, backup cutover and restore proof remain engineering work. The token grant itself is complete.

Delivery status β€” September 8: public publication of this encrypted credential file with the existing production recipient was explicitly approved in the session. The reviewed handoff is now published in PR3690, with all 20 committed files independently matched to the reviewed candidate. Hosted checks and source integration remain in progress, so the handoff acceptance item stays open until delivery. No personal action or repeated token approval is required.

Part of #2867.

Activity

  1. coderabbitai commented on Aug 19, 2026

    @coderabbitai
    Contributor
    πŸ”— Related PRs

    #3203 - security(vault-backup): disposition the last two CKV_K8S_40 findings as one deferred exception [merged]


    πŸ“ Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    πŸ§ͺ Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    πŸ’¬ Have feedback or questions? Drop into our discord!

  2. moved this to πŸ“₯ Backlog in 🌊 Project Boardon Aug 19, 2026
  3. devantler commented on Sep 3, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    This has been blocked for 15 days and nobody has actually asked. Asking now.

    @devantler β€” this issue is the only thing holding the wedding-backup isolation chain
    (#3251 β†’ #3252 β†’ #3253, under #2867). Its parent's blocker line has been re-verified as recently as
    2026-09-02, so the loop has been checking it daily while never raising it. That is my defect, not
    yours; the general fix is tracked at
    monorepo#3171.

    Why an agent cannot do this one. I checked rather than assumed: this platform holds Cloudflare
    credentials for DNS only, and its Crossplane providers are provider-aws-iam, provider-family-aws,
    provider-github and provider-upjet-unifi. There is no Cloudflare provider, so no declarative path
    to an R2 bucket exists today. R2 permanent identities are also bucket-scoped rather than
    prefix-scoped, so no manifest change can narrow the existing shared credential to the
    cnpg/wedding-db prefix. The bucket has to exist first, and creating it is an account action.

    What is needed, as small as I can make it:

    1. A dedicated production R2 bucket for the wedding database backup, separate from the shared backup
      destination.
    2. A permanent R2 API token scoped to Object Read and Write on that bucket only β€” with no access
      to the shared backup bucket. That non-access is the whole point of the issue.
    3. The resulting endpoint, access key id and secret handed over through whichever private channel you
      prefer. Do not paste them here β€” this repository is public.

    Once those exist, everything downstream is mechanical and I can take it from there: the seeding path
    (vault-seed/push-secret-seed-r2-credentials.yaml β†’ OpenBao β†’ ExternalSecret) already exists and
    #3252 is an additive use of it, with #3253 covering the restore proof, the denial check against the
    shared destination, retiring the old access, and the DR runbook updates.

    If you would rather not add a bucket, the alternative worth considering is accepting the shared
    identity and closing #2867 with that reasoning recorded β€” that is a legitimate outcome and better
    than this sitting open indefinitely. Either answer unblocks the chain; only silence does not.

  4. devantler commented on Sep 9, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Final acceptance criterion delivered β€” closing. The encrypted per-environment handoff was
    published in #3690, which merged 2026-09-08T22:39:13Z (8e11a1b321). Verified on main rather
    than inferred from the merge:

    • k8s/clusters/prod/bootstrap/secret-wedding-db-backup-r2.enc.yaml and its local counterpart both
      exist, each carrying 3 ENC[AES256_GCM…] values β€” the credential material is encrypted at rest,
      with sops.encrypted_regex ^(data|stringData)$, an age recipient, and sops 3.13.3.
    • Both are registered in their cluster's bootstrap/kustomization.yaml, so the flow actually
      consumes them rather than merely containing them.
    • The seeding path the next child needs is present too:
      k8s/bases/infrastructure/vault-seed/push-secret-seed-wedding-db-backup-r2.yaml plus the dedicated
      external-secret-db-backup-dedicated.yaml.

    ⚠️ One measurement note so nobody re-derives a false negative: the .enc.yaml files are JSON, not
    block YAML, so a ^sops:-anchored probe reports the sops block ABSENT on a perfectly valid file. It is
    present as a "sops" object. YAML is a JSON superset, so kustomize and sops both parse it.

    What this issue does and does not now assert. All four acceptance criteria are met: the dedicated
    bucket and bucket-scoped Object Read and Write identity exist, denial against the shared bucket was
    observed directly (403 on read, list and write), and the encrypted handoff is delivered. It does not
    assert that the tenant has been cut over
    β€” seed and projection verification, the backup cutover, and
    restore proof are #3252 and #3253's engineering work, and the shared credential is still what
    wedding-db uses until those land. The isolation is available, not yet in force.

    Part of #2867, whose remaining children are #3252 and #3253.

  5. moved this from πŸ“₯ Backlog to βœ… Done in 🌊 Project Boardon Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Fields

    No fields configured for Security.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions