← Complete Projects

阅读中文版本

Build a Secrets Rotation Service

Secrets Rotation Service — a complete walkthrough of secret lifecycle, covering secret store, rotation schedule, dual-publish window, and testing.

advanced~6 hsecretsrotationsecuritylifecycle

Project goal

Implement secret lifecycle that solves rotating credentials on a schedule without exposing a window where old and new secrets disagree, with each component independently testable.

Final features

  • Secret store
  • Rotation schedule
  • Dual-publish window
  • Consumer notifier
  • Rollback handler

Architecture

The system is organized around secret store, rotation schedule, dual-publish window, and consumer notifier ties them together. Data flows as a secret identified by a name and version, with a rotation window where two versions are valid until consumers ack the new one.

Prerequisites

  • Node.js 20 LTS or later
  • Familiarity with secret lifecycle fundamentals
  • A local environment to run the test suite

Dependencies

  • fastify ^5.0.0 — Rotation API
  • redis ^4.7.0 — Version store

Directory structure

secrets_rotation_service/
├── src/index.ts
├── src/secret-store.ts
├── src/rotation-schedule.ts
├── tests/secrets_rotation_service.test.ts
└── package.json

Phases

1. Secret store

Build the secret store that anchors secret lifecycle and defines the core data shape.

// Secret store: secret lifecycle core
export function secretstore(input: SecretstoreInput): SecretstoreResult {
  // Build the secret store that anchors secret lifecycle and defines the core data shape.
  return processSecretstore(input);
}

Expected behavior: After this phase the secret store works in isolation and is covered by a unit test.

2. Rotation schedule

Add the rotation schedule so secret lifecycle can rotating credentials on a correctly.

// Rotation schedule — handles secret lifecycle Add the rotation schedule so
export class Rotationschedule {
  constructor(private opts: RotationscheduleOptions) {}
  async run(ctx: Context): Promise<RotationscheduleOutput> {
    return this.execute(ctx);
  }
}

Expected behavior: After this phase the rotation schedule works in isolation and is covered by a unit test.

3. Dual-publish window

Wire the dual-publish window to handle a consumer that reads without losing data.

// Dual-publish window: secret lifecycle stage
export const dualpublishwindow = (next: Next) => async (req: Request) => {
  const out = await transform(req);
  return next(out);
};

Expected behavior: After this phase the dual-publish window works in isolation and is covered by a unit test.

4. Consumer notifier

Integrate the consumer notifier to make secret lifecycle observable and testable end to end.

// Consumer notifier for secret lifecycle
export async function consumernotifier(stream: AsyncIterable<Chunk>): Promise<Aggregate> {
  let acc = init();
  for await (const c of stream) acc = merge(acc, c);
  return acc;
}

Expected behavior: After this phase the consumer notifier works in isolation and is covered by a unit test.

5. Rollback handler

Finalize the rollback handler so the secret lifecycle system degrades gracefully under overload.

// Rollback handler: secret lifecycle finalization
export function rollbackhandler(state: State): Result {
  validate(state);
  return finalize(state);
}

Expected behavior: After this phase the rollback handler works in isolation and is covered by a unit test.

Testing

Each phase has a focused test: secret store, rotation schedule, dual-publish window are exercised in isolation, then an integration test drives the full secret lifecycle pipeline and asserts the a consumer that reads case is handled.

Troubleshooting

A consumer that reads the secret during rotation and caches the old version indefinitely

Reproduce with a minimal fixture, then add a guard test so the secret lifecycle system rejects the condition loudly.

Performance regresses under load

Profile with the sampling analyzer, locate the hot path, and add a targeted benchmark to lock the regression.

Deployment

Run two instances behind a load balancer, share state in Redis, and set resource limits so overload surfaces as backpressure rather than failure.

Known limitations

  • Single-region; multi-region replication is out of scope.
  • The secret store is in-process and not horizontally shared by default.
  • No auth layer; assume deployment behind an edge that enforces identity.

Project overview

This project builds a secret lifecycle system whose reason for existing is rotating credentials on a schedule without exposing a window where old and new secrets disagree. The temptation in secret lifecycle is to bolt the interesting part onto a working prototype and discover the hard cases in production; this build inverts that order and treats a consumer that reads the secret during rotation and caches the old version indefinitely as the first thing designed, not the last.

You will implement Secret store, Rotation schedule, Dual-publish window, Consumer notifier, and Rollback handler, each as a module with one responsibility and a test that proves it in isolation. The secret lifecycle pipeline is then the wiring that composes them, and the integration test is where the composition is shown to survive a consumer that reads the secret during rotation and caches the old version indefinitely.

The walkthrough is written for a developer who has used a secret lifecycle system but has not had to make one honest. It favors explicit data shapes over clever abstractions, and it names the failure each design choice prevents rather than presenting the choice as a style preference.

Each phase ends with something observable — a secret store that returns the right shape, a rotation schedule that rejects bad input, an integration test that goes green — so you can stop at any point and still have a secret lifecycle system that works for the cases covered so far.

What you will build

The finished secret lifecycle system exposes five capabilities, each with a focused test:

  • Secret store
  • Rotation schedule
  • Dual-publish window
  • Consumer notifier
  • Rollback handler

Secret store is the foundation that every later stage assumes; Rotation schedule is where secret lifecycle correctness lives; Dual-publish window handles a consumer that reads the secret during rotation and caches the old version indefinitely; Consumer notifier makes the system observable; and Rollback handler is the graceful-degradation layer that keeps overload from becoming corruption.

The build keeps the secret lifecycle logic in ordinary modules you can read in one sitting, and pulls in dependencies only for the parts that are genuinely tedious to hand-roll. That keeps the diff between the tutorial and a real codebase small, and keeps the failure modes visible in code rather than hidden behind a framework.

Architecture and data model

The system is organized around Secret store, Rotation schedule, and Dual-publish window, with Consumer notifier tying them together. Data flows as a secret identified by a name and version, with a rotation window where two versions are valid until consumers ack the new one.

Each component is a module with a pure interface where practical: the same input produces the same output, and side effects are pushed to the edges. That discipline is what makes the phases independently testable, because a test for Dual-publish window does not need to stand up Secret store and Rotation schedule to exercise the interesting branch.

The data model is deliberately explicit. A secret identified by a name and version, with a rotation window where two versions are valid until consumers ack the new one Keeping the shape written down — in a schema, a type, or a fixture — means a change to the shape is a visible diff rather than a silent runtime shift, and it means the integration test can assert against a concrete shape instead of guessing.

Configuration and code are kept apart: the configuration says what secret lifecycle should do, the modules say how. A reviewer can change a threshold or a policy without reading the implementation, which keeps the risky changes small and lets the diff stand as the review.

The failure path through Dual-publish window is designed before the happy path is finished, because a consumer that reads the secret during rotation and caches the old version indefinitely is the case that distinguishes a secret lifecycle system from a demo. Every component declares what it does when its inputs are wrong, and the integration test asserts each of those declarations.

Prerequisites and dependencies

Have these ready before you install anything, so the secret lifecycle setup is not blocked by a missing tool:

  • Node.js 20 LTS or later
  • Familiarity with secret lifecycle fundamentals
  • A local environment to run the test suite

The runtime dependencies are chosen to keep the secret lifecycle datapath legible:

  • fastify (^5.0.0) — Rotation API
  • redis (^4.7.0) — Version store
# setup for secrets-rotation-service
pnpm init
pnpm add fastify redis
pnpm add -D typescript tsx vitest

Versions are pinned to a major range so the secret lifecycle install is reproducible; commit the lockfile. The secret store reads its operational parameters from the environment and validates them at startup, so a missing value fails the boot rather than the first secret lifecycle request.

Directory structure

Keep each secret lifecycle component in its own file so it can be tested without starting the others:

secrets_rotation_service/
├── src/index.ts
├── src/secret-store.ts
├── src/rotation-schedule.ts
├── tests/secrets_rotation_service.test.ts
└── package.json

Phase 1: Secret store

Build the secret store that anchors secret lifecycle and defines the core data shape.

// Secret store: secret lifecycle core
export function secretstore(input: SecretstoreInput): SecretstoreResult {
  // Build the secret store that anchors secret lifecycle and defines the core data shape.
  return processSecretstore(input);
}

After this phase the secret store works in isolation and is covered by a unit test. The secret store defines the data shape that every later stage assumes, so getting it right first means the later phases compile against a stable contract.

Treat the secret store as the single source of truth for the secret lifecycle data shape. Validate inputs at the boundary and reject early, so a malformed value never reaches Rotation schedule or Dual-publish window where its effect would be harder to localize.

Phase 2: Rotation schedule

Add the rotation schedule so secret lifecycle can rotating credentials on a correctly.

// Rotation schedule — handles secret lifecycle Add the rotation schedule so
export class Rotationschedule {
  constructor(private opts: RotationscheduleOptions) {}
  async run(ctx: Context): Promise<RotationscheduleOutput> {
    return this.execute(ctx);
  }
}

After this phase the rotation schedule works in isolation and is covered by a unit test. This is where secret lifecycle correctness lives: the rotation schedule is what makes rotating credentials on a schedule without exposing a window where old and new secrets disagree true, and its test is the proof.

Keep the rotation schedule pure where you can. A function that takes an input and returns an output is trivial to test and impossible to mock wrong; a class with hidden state is the opposite. Push side effects to the caller and keep the rotation schedule about the transformation.

Phase 3: Dual-publish window

Wire the dual-publish window to handle a consumer that reads without losing data.

// Dual-publish window: secret lifecycle stage
export const dualpublishwindow = (next: Next) => async (req: Request) => {
  const out = await transform(req);
  return next(out);
};

After this phase the dual-publish window works in isolation and is covered by a unit test. The dual-publish window is the difference between a secret lifecycle system that works on the happy path and one that survives a consumer that reads the secret during rotation and caches the old version indefinitely.

Design the failure before the success. Decide what dual-publish window does when a consumer that reads the secret during rotation and caches the old version indefinitely: reject, retry, shed, or degrade — and write that test first. A secret lifecycle system whose failure behavior is unspecified will pick one at random under pressure, and it is usually the wrong one.

Phase 4: Consumer notifier

Integrate the consumer notifier to make secret lifecycle observable and testable end to end.

// Consumer notifier for secret lifecycle
export async function consumernotifier(stream: AsyncIterable<Chunk>): Promise<Aggregate> {
  let acc = init();
  for await (const c of stream) acc = merge(acc, c);
  return acc;
}

After this phase the consumer notifier works in isolation and is covered by a unit test. Observability is not a finishing touch; it is how you know the secret lifecycle system is doing what it claims in production.

Emit one structured event per meaningful action, with enough context to localize a failure without a second round-trip. The consumer notifier is what turns “something is slow” into “the rotation schedule is slow for inputs matching this shape,” which is the difference between an alert and an investigation.

Phase 5: Rollback handler

Finalize the rollback handler so the secret lifecycle system degrades gracefully under overload.

// Rollback handler: secret lifecycle finalization
export function rollbackhandler(state: State): Result {
  validate(state);
  return finalize(state);
}

After this phase the rollback handler works in isolation and is covered by a unit test. The rollback handler is what keeps overload from becoming corruption: when the secret lifecycle system is asked for more than it can do, it degrades in a defined way instead of failing in an undefined one.

Pick a degradation strategy for rollback handler — shed, queue, or backpressure — and make it explicit in code and in a test. Leaving the behavior to chance is how a secret lifecycle traffic spike becomes a data loss incident.

Testing

Each phase has a focused test: secret store, rotation schedule, and dual-publish window are exercised in isolation, then an integration test drives the full secret lifecycle pipeline and asserts the a consumer that reads case is handled.

Start with unit tests that pin each component’s contract: the secret store returns the documented shape, the rotation schedule transforms correctly, and the dual-publish window fails in the documented way. These tests are fast and they localize regressions to the component that broke.

Then add an integration test that composes the components and drives a realistic secret lifecycle scenario end to end. Assert the happy path first, then deliberately trigger a consumer that reads the secret during rotation and caches the old version indefinitely and assert the system degrades rather than corrupts. Because the components are pure where practical, the integration test stays deterministic rather than flaky.

A secret lifecycle suite must also prove the negative: bad input is rejected at the boundary, overload is shed rather than queued indefinitely, and a partial failure does not leave the system in an inconsistent state. These are the tests that catch the regressions a happy-path suite never would.

The entire secret lifecycle suite runs with no external service; every dependency is either in-process or stubbed, so every case is reproducible in CI. That is what lets the dual-publish window failure-mode assertions live as tests rather than as a manual checklist that drifts out of date.

# run the secret lifecycle test suite
pnpm vitest run

Deployment and operations

Run at least two secret lifecycle instances behind a load balancer and share state in Redis. Set resource limits so secret lifecycle overload surfaces as backpressure rather than failure.

Run at least two stateless instances behind a load balancer, and move any shared state into a shared store so the secret lifecycle behavior is global rather than per instance. Size limits so the system sheds load before it runs out of memory, because a secret lifecycle system that OOMs under load fails in a way that is hard to reason about.

Drain on deploy: stop accepting new work, let in-flight secret lifecycle operations finish, then exit. Because the system holds no state that cannot be reconstructed, a rollout is a matter of cycling instances one at a time while watching the error rate.

When a dependency is unavailable, decide explicitly whether the secret lifecycle system fails closed or degrades. Leaving that choice to chance is how a dependency outage becomes a silent data loss, so make the choice visible in code and in a test.

Security considerations

Validate all secret lifecycle inputs at the boundary, reject oversized payloads before buffering them, and keep secrets in the environment rather than the repository.

The secret lifecycle system is a choke point, so it is the right place to reject malformed input before it reaches rotation schedule or dual-publish window. Validate the shape and the size at the edge, and let the inner components assume what they receive is well-formed.

Never log secrets or full payloads; log enough to localize a failure without recording data that should not be on disk. Treat any external call as untrusted and validate its response before using it, because a secret lifecycle system that trusts its inputs blindly is one malformed response away from corruption.

Keep dependencies pinned and run an audit in CI. A transitive dependency with a known vulnerability is still a vulnerability in your secret lifecycle system, and the lockfile is what makes the audit meaningful.

Observability

Emit one structured event per secret lifecycle operation with the inputs’ shape, the outcome, and a correlation id. Track error rate and latency per component and alert on a step change rather than a single spike.

The correlation id is the thread that ties a user-visible symptom to the secret lifecycle decision and to the component log entry for the same operation. Without it, an investigation starts from a timestamp and a guess.

Watch the dual-publish window failure rate separately from the overall error rate. A jump in dual-publish window failures usually means a consumer that reads the secret during rotation and caches the old version indefinitely, not a bug in rotation schedule, and the two have different owners and different fixes.

Chart the share of secret lifecycle operations that shed, queue, or succeed. A change in the mix points at the cause faster than an aggregate latency can, because each class has a different root.

Troubleshooting

A consumer that reads the secret during rotation and caches the old version indefinitely. Reproduce with a minimal fixture, then add a guard test so the secret lifecycle system rejects the condition loudly.

Performance regresses under secret lifecycle load. Profile with the sampling analyzer, locate the hot path in rotation schedule, and add a targeted benchmark to lock the regression.

Keep the secret lifecycle dashboards separate from dependency dashboards. When an operation fails, the correlation id lets you decide in seconds whether the fault is secret store, rotation schedule, or dual-publish window, instead of guessing from a timeout.

Limitations and next steps

  • Single-region; multi-region replication is out of scope.
  • The secret store is in-process and not horizontally shared by default.
  • No auth layer; assume deployment behind an edge that enforces identity.

The secret lifecycle system is single-region by design; extending to multi-region means making secret store shareable across regions, which is a real change to the consistency model rather than a config flag. The dual-publish window is in-process, which is correct for one instance but must move to a shared store before scaling out.

There is no authentication or authorization layer; the system assumes it is deployed behind an edge that enforces identity. Adding one is straightforward but out of scope for this build, and doing it here would distract from the secret lifecycle correctness that is the point.

The natural next step, once the secret lifecycle pipeline is trusted in production, is to add a short-TTL cache for idempotent reads and a richer consumer notifier that exports histograms rather than counters. Both are additive and neither changes the contracts this build establishes.