Skip to content

feat(deployment): move the hidden variables members sealed for teams onto team keys - #4477

Merged
baktun14 merged 2 commits into
mainfrom
feat/deployment-reseal-team-secrets-backfill
Oct 18, 2026
Merged

baktun14 merged 2 commits into
mainfrom
feat/deployment-reseal-team-secrets-backfill

Conversation

@baktun14

@baktun14 baktun14 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Why

Part of CON-1213. Builds on #4473, #4474 and #4476 (all merged).

With team_data_keys on, new and updated team deployments are sealed under the team's key. Values sealed before that still sit under the key of the member who filed the deployment, and they still go if that member deletes their account. This moves them onto the team's key.

It's a console command rather than a local script. Opening a member's value unwraps that member's data key through the prod KMS key, so it should run in the cluster with the API's own KMS binding, like rekey-user-data-key and rewrap-data-keys. A local script would put prod KMS access and plaintext hidden variables on a laptop.

What

  • reseal-team-deployment-secrets [--dry-run] [--batch-size N] reads every deployment filed in a live (not deleted) team that holds a secret, keyset-paged by id.
  • A value already sealed for the team is counted and skipped, so the command is idempotent and resumable.
  • A member-sealed value is opened through openStored, sealed again with the new sealForTeam (which ignores the flag, since this is an explicit operator action) and written with resealIfUnchanged. A row another writer changed in between is counted, not overwritten.
  • A value whose member key went with the member's account can't be moved. It is reported as deploymentsWhoseMemberKeyIsGone (organizationId/dseq) and doesn't fail the run. Any other failure is logged, the rest still move, and the run exits non-zero.
  • Each page runs in its own execution context, so a member's or team's key is unwrapped once per page.
  • --dry-run opens and counts without sealing or writing.

Rollout

  1. feat(organization): let a team organization hold a data key #4473 and feat(deployment): open hidden variables sealed under a team's data key #4474 are live, and this PR's release is live.
  2. Turn team_data_keys on for everyone. If it's off for some members, their later writes move values back to their own keys.
  3. Size it with the read-only SQL below.
  4. reseal-team-deployment-secrets --dry-run, compare with the SQL.
  5. reseal-team-deployment-secrets. Re-run until deploymentsResealed is 0. The SQL should then show only team key and member key gone.

Read-only sizing SQL: team deployments with hidden variables, grouped by the key their token names.

WITH team_tokens AS (
  SELECT ds.organization_id,
         ds.user_id,
         ds.closed,
         convert_from(
           decode(
             rpad(translate(split_part(ds.sealed_secrets, '.', 1), '-_', '+/'),
                  ((length(split_part(ds.sealed_secrets, '.', 1)) + 3) / 4) * 4, '='),
             'base64'),
           'UTF8')::jsonb AS header
  FROM deployment_settings ds
  JOIN organizations o ON o.id = ds.organization_id AND o.type = 'team' AND o.deleted_at IS NULL
  WHERE ds.sealed_secrets IS NOT NULL
)
SELECT CASE
         WHEN t.header ? 'org' THEN 'team key'
         WHEN dk.id IS NULL THEN 'member key gone'
         WHEN dk.user_id = t.user_id THEN 'filer''s key'
         ELSE 'another member''s key'
       END AS sealed_under,
       count(*) AS deployments,
       count(*) FILTER (WHERE NOT t.closed) AS open_deployments,
       count(DISTINCT t.organization_id) AS teams,
       count(DISTINCT dk.user_id) AS members
FROM team_tokens t
LEFT JOIN data_keys dk ON dk.id::text = t.header ->> 'kid'
GROUP BY 1
ORDER BY 2 DESC;

filer's key is what the command moves. member key gone can't be recovered.

Testing

  • Unit: the reseal service (10 tests) and sealForTeam.
  • Integration against the real DB and the KMS stand-in, end to end:
    • the dry run writes nothing
    • the real run moves a member-sealed team value, which then opens for the team with no filer on the row
    • the value of a departed member, a deleted team's deployment and a personal deployment are left as they were
    • a re-run moves nothing
  • Integration: findStoredSecretsOfTeamsIteratively pages live teams' tokens one row at a time and leaves out rows with no token, personal deployments and deleted teams.
  • The sizing SQL was run against the same seeded data before and after a reseal (filer's key 2 / member key gone 1, then team key 2 / member key gone 1).
  • Mutation on changed lines: 96.2% (50 of 52; the survivors are the logger context and the elapsed time).

@baktun14
baktun14 requested a review from a team as a code owner October 18, 2026 02:43

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 9 billable files and costs up to $2.25.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 25 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: akash-network/console/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 820f063a-c284-4348-afa2-dea1f7f059fa
📥 Commits

Reviewing files that changed from the base of the PR and between 864a4a2 and f72cedd.

📒 Files selected for processing: 9
  • apps/api/src/app/console.ts
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.integration.ts
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts
  • apps/api/src/deployment/services/sdl-secrets/sdl-secrets.service.spec.ts
  • apps/api/src/deployment/services/sdl-secrets/sdl-secrets.service.ts
  • apps/api/src/secret/controllers/team-secrets-reseal/team-secrets-reseal.controller.ts
  • apps/api/src/secret/services/team-secrets-reseal/team-secrets-reseal.service.integration.ts
  • apps/api/src/secret/services/team-secrets-reseal/team-secrets-reseal.service.spec.ts
  • apps/api/src/secret/services/team-secrets-reseal/team-secrets-reseal.service.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor test-coverage suggestion inline. Everything else checks out: I verified the repository iterator is unscoped in the CLI context, the #seal refactor is behavior-preserving, the departed-member classification matches what openStored throws, and the full unit suite, the integration spec (real Postgres + KMS stand-in), tsc --noEmit and eslint all pass.

Reviewed changes

  • Team secrets reseal command — reseal-team-deployment-secrets [--dry-run] [--batch-size N] wired in console.ts through TeamSecretsResealController, keyset-paging every live-team deployment that holds a secret via the new findStoredSecretsOfTeamsIteratively (inner join on Organizations, deleted_at IS NULL, type = 'team', cursor on id).
  • Reseal pass — already-team-sealed values are counted and skipped (idempotent, resumable); member-sealed values are opened through the deployment read path (openStored, so a value bound to another member is refused rather than moved), re-sealed under the team's key, and written through resealIfUnchanged so a concurrent writer's token is never overwritten.
  • SdlSecretsService.sealForTeam — seals under sealerOfOrganization regardless of team_data_keys, with sealForStorage refactored onto a shared #seal (behavior-preserving; covered by the flag-off spec).
  • Error handling — 409 stored_secrets_unavailable (departed member) is reported as deploymentsWhoseMemberKeyIsGone without failing the run; other failures are logged per deployment and fail the run non-zero; each page runs in its own execution context so unwrapped keys are held per page, not per fleet.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GLM-5.3 | 𝕏

@baktun14
baktun14 force-pushed the feat/deployment-seal-team-secrets-under-team-key branch from d01e92e to 3ee3bb8 Compare October 18, 2026 06:11
@baktun14
baktun14 force-pushed the feat/deployment-reseal-team-secrets-backfill branch from 63066e0 to d884935 Compare October 18, 2026 06:11
@github-actions github-actions Bot added size: L and removed size: M labels Oct 18, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run covers the delta since the prior pullfrog review — a single test-only commit landing the repository-level coverage that review asked for.

  • Pinned the team iterator's multi-page behavior against a real database — 3a0f226 adds a "re-sealing the stored secrets filed in teams" describe to deployment-setting.repository.integration.ts: three live-team rows holding secrets are seeded alongside a no-secret team row, a personal row and a deleted-team row, then findStoredSecretsOfTeamsIteratively is iterated with batchSize: 1, asserting every batch is single-row, no id repeats across pages, all three team rows are paged, and the control rows are left out.

Verified on this run: the new test passes against real Postgres, tsc --noEmit is clean, and two mutations confirm the test's teeth — dropping the isNotNull(sealedSecrets) filter and breaking the single-page stop condition each fail their respective assertions.

Pullfrog  | View workflow run | Using GLM-5.3 | 𝕏

@baktun14
baktun14 force-pushed the feat/deployment-seal-team-secrets-under-team-key branch from 3ee3bb8 to ec03cac Compare October 18, 2026 07:27
Base automatically changed from feat/deployment-seal-team-secrets-under-team-key to main October 18, 2026 07:43
…onto team keys

The reseal-team-deployment-secrets command reads every deployment filed in a live
team that holds a secret, opens a value its member sealed through the path a
deployment reads it by, seals it again under the team's key and writes it back
only if the row still holds what was read. A value already sealed for the team is
counted and left alone, so a re-run picks up where an earlier one stopped. A value
whose member key went with the member's account cannot be moved and is reported.
A dry run opens and counts without writing anything.

Part of CON-1213
@baktun14
baktun14 force-pushed the feat/deployment-reseal-team-secrets-backfill branch from 3a0f226 to f72cedd Compare October 18, 2026 07:44
@codecov

codecov Bot commented Oct 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.57143% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.07%. Comparing base (864a4a2) to head (f72cedd).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
apps/api/src/app/console.ts 0.00% 5 Missing ⚠️
...m-secrets-reseal/team-secrets-reseal.controller.ts 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4477      +/-   ##
==========================================
+ Coverage   93.89%   94.07%   +0.18%     
==========================================
  Files        1726     1619     -107     
  Lines       49658    46908    -2750     
  Branches    12165    11617     -548     
==========================================
- Hits        46624    44131    -2493     
+ Misses       2726     2485     -241     
+ Partials      308      292      -16     
Flag Coverage Δ *Carryforward flag
api 95.98% <88.57%> (-0.04%) ⬇️
deploy-web 92.59% <ø> (ø) Carriedforward from 864a4a2
log-collector ?
notifications 95.76% <ø> (ø) Carriedforward from 864a4a2
provider-console 81.68% <ø> (ø) Carriedforward from 864a4a2
provider-inventory ?
provider-proxy 92.21% <ø> (ø) Carriedforward from 864a4a2
tx-signer ?

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...eployment-setting/deployment-setting.repository.ts 99.32% <100.00%> (+0.02%) ⬆️
...oyment/services/sdl-secrets/sdl-secrets.service.ts 100.00% <100.00%> (ø)
...team-secrets-reseal/team-secrets-reseal.service.ts 100.00% <100.00%> (ø)
...m-secrets-reseal/team-secrets-reseal.controller.ts 0.00% <0.00%> (ø)
apps/api/src/app/console.ts 0.00% <0.00%> (ø)

... and 111 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@baktun14
baktun14 added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit e28301c Oct 18, 2026
84 checks passed
@baktun14
baktun14 deleted the feat/deployment-reseal-team-secrets-backfill branch October 18, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant