@zigrivers/scaffold 3.15.0 → 3.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -12
- package/content/knowledge/backend/backend-fintech-broker-integration.md +244 -0
- package/content/knowledge/backend/backend-fintech-compliance.md +181 -0
- package/content/knowledge/backend/backend-fintech-data-modeling.md +210 -0
- package/content/knowledge/backend/backend-fintech-ledger.md +226 -0
- package/content/knowledge/backend/backend-fintech-observability.md +151 -0
- package/content/knowledge/backend/backend-fintech-order-lifecycle.md +213 -0
- package/content/knowledge/backend/backend-fintech-risk-management.md +150 -0
- package/content/knowledge/backend/backend-fintech-testing.md +197 -0
- package/content/knowledge/core/automated-review-tooling.md +31 -26
- package/content/knowledge/core/multi-model-review-dispatch.md +30 -55
- package/content/knowledge/core/multi-service-api-contracts.md +634 -0
- package/content/knowledge/core/multi-service-architecture.md +492 -0
- package/content/knowledge/core/multi-service-auth.md +706 -0
- package/content/knowledge/core/multi-service-data-ownership.md +539 -0
- package/content/knowledge/core/multi-service-observability.md +545 -0
- package/content/knowledge/core/multi-service-resilience.md +710 -0
- package/content/knowledge/core/multi-service-task-decomposition.md +615 -0
- package/content/knowledge/core/multi-service-testing.md +728 -0
- package/content/methodology/backend-fintech.yml +46 -0
- package/content/methodology/custom-defaults.yml +6 -0
- package/content/methodology/deep.yml +6 -0
- package/content/methodology/multi-service-overlay.yml +103 -0
- package/content/methodology/mvp.yml +6 -0
- package/content/pipeline/architecture/service-ownership-map.md +83 -0
- package/content/pipeline/quality/cross-service-auth.md +96 -0
- package/content/pipeline/quality/cross-service-observability.md +104 -0
- package/content/pipeline/quality/integration-test-plan.md +106 -0
- package/content/pipeline/specification/inter-service-contracts.md +95 -0
- package/content/tools/post-implementation-review.md +36 -7
- package/content/tools/review-code.md +33 -8
- package/content/tools/review-pr.md +79 -95
- package/dist/cli/commands/adopt.cli-flags.test.js +20 -0
- package/dist/cli/commands/adopt.cli-flags.test.js.map +1 -1
- package/dist/cli/commands/adopt.d.ts.map +1 -1
- package/dist/cli/commands/adopt.js +11 -3
- package/dist/cli/commands/adopt.js.map +1 -1
- package/dist/cli/commands/complete.d.ts +1 -0
- package/dist/cli/commands/complete.d.ts.map +1 -1
- package/dist/cli/commands/complete.js +26 -8
- package/dist/cli/commands/complete.js.map +1 -1
- package/dist/cli/commands/dashboard.d.ts +1 -0
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +19 -6
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/decisions.d.ts +1 -0
- package/dist/cli/commands/decisions.d.ts.map +1 -1
- package/dist/cli/commands/decisions.js +18 -4
- package/dist/cli/commands/decisions.js.map +1 -1
- package/dist/cli/commands/info.d.ts +1 -0
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +25 -3
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init-from.test.d.ts +2 -0
- package/dist/cli/commands/init-from.test.d.ts.map +1 -0
- package/dist/cli/commands/init-from.test.js +315 -0
- package/dist/cli/commands/init-from.test.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +239 -129
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +20 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/next.d.ts +1 -0
- package/dist/cli/commands/next.d.ts.map +1 -1
- package/dist/cli/commands/next.js +40 -4
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/cli/commands/next.test.js +151 -0
- package/dist/cli/commands/next.test.js.map +1 -1
- package/dist/cli/commands/reset.d.ts +1 -0
- package/dist/cli/commands/reset.d.ts.map +1 -1
- package/dist/cli/commands/reset.js +77 -29
- package/dist/cli/commands/reset.js.map +1 -1
- package/dist/cli/commands/rework.d.ts +1 -0
- package/dist/cli/commands/rework.d.ts.map +1 -1
- package/dist/cli/commands/rework.js +16 -2
- package/dist/cli/commands/rework.js.map +1 -1
- package/dist/cli/commands/run.d.ts +1 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +65 -13
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/run.test.js +192 -3
- package/dist/cli/commands/run.test.js.map +1 -1
- package/dist/cli/commands/skip.d.ts +1 -0
- package/dist/cli/commands/skip.d.ts.map +1 -1
- package/dist/cli/commands/skip.js +24 -7
- package/dist/cli/commands/skip.js.map +1 -1
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +51 -4
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/status.test.js +128 -0
- package/dist/cli/commands/status.test.js.map +1 -1
- package/dist/cli/guards-coverage.test.d.ts +2 -0
- package/dist/cli/guards-coverage.test.d.ts.map +1 -0
- package/dist/cli/guards-coverage.test.js +26 -0
- package/dist/cli/guards-coverage.test.js.map +1 -0
- package/dist/cli/guards-integration.test.d.ts +2 -0
- package/dist/cli/guards-integration.test.d.ts.map +1 -0
- package/dist/cli/guards-integration.test.js +178 -0
- package/dist/cli/guards-integration.test.js.map +1 -0
- package/dist/cli/guards.d.ts +13 -0
- package/dist/cli/guards.d.ts.map +1 -0
- package/dist/cli/guards.js +70 -0
- package/dist/cli/guards.js.map +1 -0
- package/dist/cli/guards.test.d.ts +2 -0
- package/dist/cli/guards.test.d.ts.map +1 -0
- package/dist/cli/guards.test.js +136 -0
- package/dist/cli/guards.test.js.map +1 -0
- package/dist/cli/init-flag-families.d.ts +1 -1
- package/dist/cli/init-flag-families.d.ts.map +1 -1
- package/dist/cli/init-flag-families.js +4 -1
- package/dist/cli/init-flag-families.js.map +1 -1
- package/dist/cli/init-flag-families.test.js +10 -0
- package/dist/cli/init-flag-families.test.js.map +1 -1
- package/dist/cli/shutdown.d.ts +2 -3
- package/dist/cli/shutdown.d.ts.map +1 -1
- package/dist/cli/shutdown.js +14 -11
- package/dist/cli/shutdown.js.map +1 -1
- package/dist/cli/shutdown.test.js +2 -4
- package/dist/cli/shutdown.test.js.map +1 -1
- package/dist/config/schema.d.ts +12122 -288
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +74 -79
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +230 -1
- package/dist/config/schema.test.js.map +1 -1
- package/dist/config/validators/backend.d.ts +4 -0
- package/dist/config/validators/backend.d.ts.map +1 -0
- package/dist/config/validators/backend.js +14 -0
- package/dist/config/validators/backend.js.map +1 -0
- package/dist/config/validators/browser-extension.d.ts +4 -0
- package/dist/config/validators/browser-extension.d.ts.map +1 -0
- package/dist/config/validators/browser-extension.js +24 -0
- package/dist/config/validators/browser-extension.js.map +1 -0
- package/dist/config/validators/cli.d.ts +4 -0
- package/dist/config/validators/cli.d.ts.map +1 -0
- package/dist/config/validators/cli.js +14 -0
- package/dist/config/validators/cli.js.map +1 -0
- package/dist/config/validators/data-pipeline.d.ts +4 -0
- package/dist/config/validators/data-pipeline.d.ts.map +1 -0
- package/dist/config/validators/data-pipeline.js +14 -0
- package/dist/config/validators/data-pipeline.js.map +1 -0
- package/dist/config/validators/game.d.ts +4 -0
- package/dist/config/validators/game.d.ts.map +1 -0
- package/dist/config/validators/game.js +14 -0
- package/dist/config/validators/game.js.map +1 -0
- package/dist/config/validators/index.d.ts +7 -0
- package/dist/config/validators/index.d.ts.map +1 -0
- package/dist/config/validators/index.js +27 -0
- package/dist/config/validators/index.js.map +1 -0
- package/dist/config/validators/library.d.ts +4 -0
- package/dist/config/validators/library.d.ts.map +1 -0
- package/dist/config/validators/library.js +25 -0
- package/dist/config/validators/library.js.map +1 -0
- package/dist/config/validators/ml.d.ts +4 -0
- package/dist/config/validators/ml.d.ts.map +1 -0
- package/dist/config/validators/ml.js +31 -0
- package/dist/config/validators/ml.js.map +1 -0
- package/dist/config/validators/mobile-app.d.ts +4 -0
- package/dist/config/validators/mobile-app.d.ts.map +1 -0
- package/dist/config/validators/mobile-app.js +14 -0
- package/dist/config/validators/mobile-app.js.map +1 -0
- package/dist/config/validators/registry.test.d.ts +2 -0
- package/dist/config/validators/registry.test.d.ts.map +1 -0
- package/dist/config/validators/registry.test.js +26 -0
- package/dist/config/validators/registry.test.js.map +1 -0
- package/dist/config/validators/research.d.ts +4 -0
- package/dist/config/validators/research.d.ts.map +1 -0
- package/dist/config/validators/research.js +24 -0
- package/dist/config/validators/research.js.map +1 -0
- package/dist/config/validators/research.test.d.ts +2 -0
- package/dist/config/validators/research.test.d.ts.map +1 -0
- package/dist/config/validators/research.test.js +44 -0
- package/dist/config/validators/research.test.js.map +1 -0
- package/dist/config/validators/types.d.ts +19 -0
- package/dist/config/validators/types.d.ts.map +1 -0
- package/dist/config/validators/types.js +2 -0
- package/dist/config/validators/types.js.map +1 -0
- package/dist/config/validators/validators.test.d.ts +2 -0
- package/dist/config/validators/validators.test.d.ts.map +1 -0
- package/dist/config/validators/validators.test.js +25 -0
- package/dist/config/validators/validators.test.js.map +1 -0
- package/dist/config/validators/web-app.d.ts +4 -0
- package/dist/config/validators/web-app.d.ts.map +1 -0
- package/dist/config/validators/web-app.js +31 -0
- package/dist/config/validators/web-app.js.map +1 -0
- package/dist/core/assembly/context-gatherer.d.ts.map +1 -1
- package/dist/core/assembly/context-gatherer.js +4 -2
- package/dist/core/assembly/context-gatherer.js.map +1 -1
- package/dist/core/assembly/cross-reads.d.ts +58 -0
- package/dist/core/assembly/cross-reads.d.ts.map +1 -0
- package/dist/core/assembly/cross-reads.js +185 -0
- package/dist/core/assembly/cross-reads.js.map +1 -0
- package/dist/core/assembly/cross-reads.test.d.ts +2 -0
- package/dist/core/assembly/cross-reads.test.d.ts.map +1 -0
- package/dist/core/assembly/cross-reads.test.js +383 -0
- package/dist/core/assembly/cross-reads.test.js.map +1 -0
- package/dist/core/assembly/overlay-loader-structural.test.d.ts +2 -0
- package/dist/core/assembly/overlay-loader-structural.test.d.ts.map +1 -0
- package/dist/core/assembly/overlay-loader-structural.test.js +114 -0
- package/dist/core/assembly/overlay-loader-structural.test.js.map +1 -0
- package/dist/core/assembly/overlay-loader.d.ts +17 -3
- package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
- package/dist/core/assembly/overlay-loader.js +75 -0
- package/dist/core/assembly/overlay-loader.js.map +1 -1
- package/dist/core/assembly/overlay-resolver.d.ts +2 -2
- package/dist/core/assembly/overlay-resolver.d.ts.map +1 -1
- package/dist/core/assembly/overlay-resolver.js.map +1 -1
- package/dist/core/assembly/overlay-resolver.test.js.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.d.ts +5 -0
- package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.js +41 -1
- package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.test.js +262 -0
- package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
- package/dist/core/assembly/update-mode.d.ts +1 -0
- package/dist/core/assembly/update-mode.d.ts.map +1 -1
- package/dist/core/assembly/update-mode.js +17 -9
- package/dist/core/assembly/update-mode.js.map +1 -1
- package/dist/core/dependency/eligibility.d.ts +10 -1
- package/dist/core/dependency/eligibility.d.ts.map +1 -1
- package/dist/core/dependency/eligibility.js +19 -1
- package/dist/core/dependency/eligibility.js.map +1 -1
- package/dist/core/dependency/eligibility.test.js +82 -0
- package/dist/core/dependency/eligibility.test.js.map +1 -1
- package/dist/core/dependency/graph.d.ts +4 -1
- package/dist/core/dependency/graph.d.ts.map +1 -1
- package/dist/core/dependency/graph.js +7 -1
- package/dist/core/dependency/graph.js.map +1 -1
- package/dist/core/dependency/graph.test.js +29 -0
- package/dist/core/dependency/graph.test.js.map +1 -1
- package/dist/core/pipeline/global-steps.d.ts +7 -0
- package/dist/core/pipeline/global-steps.d.ts.map +1 -0
- package/dist/core/pipeline/global-steps.js +18 -0
- package/dist/core/pipeline/global-steps.js.map +1 -0
- package/dist/core/pipeline/resolver.d.ts +1 -0
- package/dist/core/pipeline/resolver.d.ts.map +1 -1
- package/dist/core/pipeline/resolver.js +51 -6
- package/dist/core/pipeline/resolver.js.map +1 -1
- package/dist/core/pipeline/types.d.ts +5 -1
- package/dist/core/pipeline/types.d.ts.map +1 -1
- package/dist/e2e/cross-service-references.test.d.ts +22 -0
- package/dist/e2e/cross-service-references.test.d.ts.map +1 -0
- package/dist/e2e/cross-service-references.test.js +188 -0
- package/dist/e2e/cross-service-references.test.js.map +1 -0
- package/dist/e2e/multi-service-pipeline.test.d.ts +10 -0
- package/dist/e2e/multi-service-pipeline.test.d.ts.map +1 -0
- package/dist/e2e/multi-service-pipeline.test.js +185 -0
- package/dist/e2e/multi-service-pipeline.test.js.map +1 -0
- package/dist/e2e/project-type-overlays.test.js +68 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -1
- package/dist/e2e/service-execution.test.d.ts +15 -0
- package/dist/e2e/service-execution.test.d.ts.map +1 -0
- package/dist/e2e/service-execution.test.js +219 -0
- package/dist/e2e/service-execution.test.js.map +1 -0
- package/dist/e2e/service-manifest.test.d.ts +19 -0
- package/dist/e2e/service-manifest.test.d.ts.map +1 -0
- package/dist/e2e/service-manifest.test.js +166 -0
- package/dist/e2e/service-manifest.test.js.map +1 -0
- package/dist/project/__frozen-schemas__/schema-v3.9.2.d.ts +224 -224
- package/dist/project/frontmatter.d.ts.map +1 -1
- package/dist/project/frontmatter.js +11 -0
- package/dist/project/frontmatter.js.map +1 -1
- package/dist/project/frontmatter.test.js +71 -0
- package/dist/project/frontmatter.test.js.map +1 -1
- package/dist/state/completion.d.ts +1 -1
- package/dist/state/completion.d.ts.map +1 -1
- package/dist/state/completion.js +10 -8
- package/dist/state/completion.js.map +1 -1
- package/dist/state/decision-logger.d.ts +3 -2
- package/dist/state/decision-logger.d.ts.map +1 -1
- package/dist/state/decision-logger.js +12 -11
- package/dist/state/decision-logger.js.map +1 -1
- package/dist/state/ensure-v3-migration.d.ts +9 -0
- package/dist/state/ensure-v3-migration.d.ts.map +1 -0
- package/dist/state/ensure-v3-migration.js +35 -0
- package/dist/state/ensure-v3-migration.js.map +1 -0
- package/dist/state/lock-manager.d.ts +5 -4
- package/dist/state/lock-manager.d.ts.map +1 -1
- package/dist/state/lock-manager.js +11 -11
- package/dist/state/lock-manager.js.map +1 -1
- package/dist/state/rework-manager.d.ts +1 -2
- package/dist/state/rework-manager.d.ts.map +1 -1
- package/dist/state/rework-manager.js +4 -5
- package/dist/state/rework-manager.js.map +1 -1
- package/dist/state/state-manager.d.ts +25 -1
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +86 -12
- package/dist/state/state-manager.js.map +1 -1
- package/dist/state/state-manager.test.js +278 -0
- package/dist/state/state-manager.test.js.map +1 -1
- package/dist/state/state-migration-v3.d.ts +22 -0
- package/dist/state/state-migration-v3.d.ts.map +1 -0
- package/dist/state/state-migration-v3.js +82 -0
- package/dist/state/state-migration-v3.js.map +1 -0
- package/dist/state/state-migration-v3.test.d.ts +2 -0
- package/dist/state/state-migration-v3.test.d.ts.map +1 -0
- package/dist/state/state-migration-v3.test.js +196 -0
- package/dist/state/state-migration-v3.test.js.map +1 -0
- package/dist/state/state-migration.d.ts.map +1 -1
- package/dist/state/state-migration.js +11 -6
- package/dist/state/state-migration.js.map +1 -1
- package/dist/state/state-migration.test.js +47 -2
- package/dist/state/state-migration.test.js.map +1 -1
- package/dist/state/state-path-resolver.d.ts +23 -0
- package/dist/state/state-path-resolver.d.ts.map +1 -0
- package/dist/state/state-path-resolver.js +36 -0
- package/dist/state/state-path-resolver.js.map +1 -0
- package/dist/state/state-path-resolver.test.d.ts +2 -0
- package/dist/state/state-path-resolver.test.d.ts.map +1 -0
- package/dist/state/state-path-resolver.test.js +78 -0
- package/dist/state/state-path-resolver.test.js.map +1 -0
- package/dist/state/state-version-dispatch.d.ts +17 -0
- package/dist/state/state-version-dispatch.d.ts.map +1 -0
- package/dist/state/state-version-dispatch.js +27 -0
- package/dist/state/state-version-dispatch.js.map +1 -0
- package/dist/state/state-version-dispatch.test.d.ts +2 -0
- package/dist/state/state-version-dispatch.test.d.ts.map +1 -0
- package/dist/state/state-version-dispatch.test.js +40 -0
- package/dist/state/state-version-dispatch.test.js.map +1 -0
- package/dist/types/config.d.ts +25 -3
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.test.js +13 -1
- package/dist/types/config.test.js.map +1 -1
- package/dist/types/dependency.d.ts +5 -0
- package/dist/types/dependency.d.ts.map +1 -1
- package/dist/types/frontmatter.d.ts +5 -0
- package/dist/types/frontmatter.d.ts.map +1 -1
- package/dist/types/lock.d.ts +1 -1
- package/dist/types/lock.d.ts.map +1 -1
- package/dist/types/state.d.ts +1 -1
- package/dist/types/state.d.ts.map +1 -1
- package/dist/utils/artifact-path.d.ts +19 -0
- package/dist/utils/artifact-path.d.ts.map +1 -0
- package/dist/utils/artifact-path.js +95 -0
- package/dist/utils/artifact-path.js.map +1 -0
- package/dist/utils/artifact-path.test.d.ts +2 -0
- package/dist/utils/artifact-path.test.d.ts.map +1 -0
- package/dist/utils/artifact-path.test.js +138 -0
- package/dist/utils/artifact-path.test.js.map +1 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +5 -2
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/user-errors.d.ts +46 -0
- package/dist/utils/user-errors.d.ts.map +1 -0
- package/dist/utils/user-errors.js +76 -0
- package/dist/utils/user-errors.js.map +1 -0
- package/dist/utils/user-errors.test.d.ts +2 -0
- package/dist/utils/user-errors.test.d.ts.map +1 -0
- package/dist/utils/user-errors.test.js +74 -0
- package/dist/utils/user-errors.test.js.map +1 -0
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +16 -0
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.test.js +48 -0
- package/dist/validation/index.test.js.map +1 -1
- package/dist/validation/state-validator.d.ts +5 -2
- package/dist/validation/state-validator.d.ts.map +1 -1
- package/dist/validation/state-validator.js +18 -20
- package/dist/validation/state-validator.js.map +1 -1
- package/dist/validation/state-validator.test.js +31 -2
- package/dist/validation/state-validator.test.js.map +1 -1
- package/dist/wizard/copy/backend.d.ts.map +1 -1
- package/dist/wizard/copy/backend.js +12 -0
- package/dist/wizard/copy/backend.js.map +1 -1
- package/dist/wizard/flags.d.ts +1 -0
- package/dist/wizard/flags.d.ts.map +1 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +5 -1
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +45 -2
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +23 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +85 -47
- package/dist/wizard/wizard.js.map +1 -1
- package/dist/wizard/wizard.test.js +186 -1
- package/dist/wizard/wizard.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: multi-service-auth
|
|
3
|
+
description: Mutual TLS, service tokens, zero-trust architecture, and audience scoping
|
|
4
|
+
topics: [mtls, service-tokens, zero-trust, audience-scoping, token-rotation]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Service-to-service authentication is distinct from user authentication. Services are long-running workloads with machine identities, not human users. Relying on network perimeter security ("inside the firewall means trusted") is insufficient for modern multi-service architectures.
|
|
10
|
+
|
|
11
|
+
**Zero-trust principles:** Every service-to-service call must be authenticated and authorized regardless of source. All traffic is encrypted. Services request only minimum required permissions. Assume any service may be compromised and limit lateral movement.
|
|
12
|
+
|
|
13
|
+
**Two complementary layers:**
|
|
14
|
+
- **mTLS (transport layer):** Both services present certificates signed by a trusted internal CA. Provides encryption and cryptographic identity verification. A service mesh (Istio/Linkerd) can handle mTLS transparently without application code changes.
|
|
15
|
+
- **Service JWTs (application layer):** Short-lived tokens (5 min TTL) carrying claims: `iss` (caller), `sub` (caller identity), `aud` (target service), `scope` (authorized operations), and optional propagated user context.
|
|
16
|
+
|
|
17
|
+
**Audience scoping:** Tokens must include the target service's identifier in the `aud` claim. Without this, a stolen token can be replayed against any service.
|
|
18
|
+
|
|
19
|
+
**SPIFFE/SPIRE:** Standard workload identity framework for multi-cloud/multi-cluster environments where a service mesh is insufficient.
|
|
20
|
+
|
|
21
|
+
**Secret rotation:** mTLS certificates every 24–90 days; JWT signing keys every 30–90 days; database passwords every 30–90 days. Use zero-downtime rotation via JWKS endpoints that serve both old and new public keys during the transition window.
|
|
22
|
+
|
|
23
|
+
## Deep Guidance
|
|
24
|
+
|
|
25
|
+
## Zero-Trust Architecture Principles
|
|
26
|
+
|
|
27
|
+
### Never Trust the Network
|
|
28
|
+
|
|
29
|
+
The traditional security perimeter model assumes that traffic inside the network is trusted. Zero-trust inverts this: every request must be authenticated and authorized regardless of its source.
|
|
30
|
+
|
|
31
|
+
**Core zero-trust rules:**
|
|
32
|
+
1. Every service-to-service call is authenticated — no implicit trust based on network location.
|
|
33
|
+
2. Every service-to-service call is authorized — the caller must have explicit permission to call the specific endpoint.
|
|
34
|
+
3. All traffic is encrypted — no plaintext HTTP between services, even within a private VPC.
|
|
35
|
+
4. Least privilege — services request only the permissions they need, scoped to the minimum audience.
|
|
36
|
+
5. Assume breach — design for the case where one service is compromised. Lateral movement must be limited.
|
|
37
|
+
|
|
38
|
+
**Trade-offs (zero-trust):**
|
|
39
|
+
- (+) Compromised service cannot access all other services — blast radius is limited by explicit authorization.
|
|
40
|
+
- (+) Insider threat mitigation — a rogue actor who gains network access cannot impersonate services.
|
|
41
|
+
- (+) Audit trail — every service call has authenticated identity attached, enabling forensic analysis.
|
|
42
|
+
- (-) Operational complexity — certificates, tokens, and rotation must be managed as infrastructure.
|
|
43
|
+
- (-) Latency overhead — mTLS handshake and token validation add a few milliseconds per request.
|
|
44
|
+
- (-) Requires consistent adoption — a single service using HTTP without auth is a gap in the model.
|
|
45
|
+
|
|
46
|
+
### Identity for Workloads
|
|
47
|
+
|
|
48
|
+
Every service must have a cryptographic identity that other services can verify. The two primary models:
|
|
49
|
+
|
|
50
|
+
**Certificate-based identity (mTLS):** The service presents a TLS client certificate. The certificate's Subject or SAN identifies the service. Verification is done at the TLS layer.
|
|
51
|
+
|
|
52
|
+
**Token-based identity (JWT):** The service presents a signed token in the `Authorization` header. The token contains claims identifying the service. Verification is done at the application layer.
|
|
53
|
+
|
|
54
|
+
Use mTLS and JWTs together: mTLS at the transport layer for encryption and certificate-based authentication; JWTs at the application layer for fine-grained authorization claims. A service mesh (Istio, Linkerd) can handle mTLS transparently, leaving application-layer JWT for business authorization logic.
|
|
55
|
+
|
|
56
|
+
## Mutual TLS (mTLS)
|
|
57
|
+
|
|
58
|
+
### How mTLS Works
|
|
59
|
+
|
|
60
|
+
In standard TLS, the client verifies the server's certificate. In mutual TLS, both sides present certificates:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Client (Service A) Server (Service B)
|
|
64
|
+
| |
|
|
65
|
+
|--- ClientHello -----------------------> |
|
|
66
|
+
|<-- ServerHello + Server Cert ----------- |
|
|
67
|
+
|--- Client Cert + ClientKeyExchange ----> | (A proves identity to B)
|
|
68
|
+
|<-- Finished (session established) ------ |
|
|
69
|
+
|--- Encrypted request ------------------> | (B verifies A's cert)
|
|
70
|
+
|<-- Encrypted response ------------------ |
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Both sides verify that the certificate was signed by a trusted Certificate Authority (CA). The CA can be internal (managed by your organization) or a public CA.
|
|
74
|
+
|
|
75
|
+
### Certificate Management with Internal CA
|
|
76
|
+
|
|
77
|
+
For service meshes and internal APIs, an internal CA is the standard approach:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Create internal CA using OpenSSL
|
|
81
|
+
openssl genrsa -out ca-key.pem 4096
|
|
82
|
+
openssl req -new -x509 -days 3650 -key ca-key.pem -out ca-cert.pem \
|
|
83
|
+
-subj "/C=US/O=Acme Corp/CN=Acme Internal CA"
|
|
84
|
+
|
|
85
|
+
# Generate service certificate (for order-service)
|
|
86
|
+
openssl genrsa -out order-service-key.pem 2048
|
|
87
|
+
openssl req -new -key order-service-key.pem -out order-service.csr \
|
|
88
|
+
-subj "/C=US/O=Acme Corp/CN=order-service"
|
|
89
|
+
|
|
90
|
+
# Sign with internal CA — certificate expires in 90 days (rotate frequently)
|
|
91
|
+
openssl x509 -req -days 90 -in order-service.csr \
|
|
92
|
+
-CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial \
|
|
93
|
+
-out order-service-cert.pem \
|
|
94
|
+
-extfile <(printf "subjectAltName=DNS:order-service,DNS:order-service.production.svc.cluster.local")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### mTLS Configuration in Node.js
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import https from 'https';
|
|
101
|
+
import fs from 'fs';
|
|
102
|
+
import tls from 'tls';
|
|
103
|
+
|
|
104
|
+
// Server: require client certificates
|
|
105
|
+
const serverOptions: https.ServerOptions = {
|
|
106
|
+
key: fs.readFileSync('/etc/certs/order-service-key.pem'),
|
|
107
|
+
cert: fs.readFileSync('/etc/certs/order-service-cert.pem'),
|
|
108
|
+
ca: fs.readFileSync('/etc/certs/ca-cert.pem'),
|
|
109
|
+
requestCert: true, // Request client certificate
|
|
110
|
+
rejectUnauthorized: true, // Reject if client cert is not trusted by CA
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const server = https.createServer(serverOptions, (req, res) => {
|
|
114
|
+
const clientCert = (req.socket as tls.TLSSocket).getPeerCertificate();
|
|
115
|
+
|
|
116
|
+
if (!clientCert || !clientCert.subject) {
|
|
117
|
+
res.writeHead(401);
|
|
118
|
+
res.end('Client certificate required');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Extract service identity from certificate subject
|
|
123
|
+
const callerServiceName = clientCert.subject.CN;
|
|
124
|
+
|
|
125
|
+
// Authorize: check if this service is allowed to call this endpoint
|
|
126
|
+
if (!isAuthorized(callerServiceName, req.method, req.url)) {
|
|
127
|
+
res.writeHead(403);
|
|
128
|
+
res.end(`Service ${callerServiceName} not authorized for this endpoint`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Attach caller identity to request context for audit logging
|
|
133
|
+
(req as any).callerService = callerServiceName;
|
|
134
|
+
handleRequest(req, res);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Client: present certificate when calling other services
|
|
138
|
+
const clientOptions: https.RequestOptions = {
|
|
139
|
+
key: fs.readFileSync('/etc/certs/order-service-key.pem'),
|
|
140
|
+
cert: fs.readFileSync('/etc/certs/order-service-cert.pem'),
|
|
141
|
+
ca: fs.readFileSync('/etc/certs/ca-cert.pem'), // Trust internal CA only
|
|
142
|
+
rejectUnauthorized: true,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
async function callInventoryService(path: string): Promise<unknown> {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const req = https.request(
|
|
148
|
+
{ hostname: 'inventory-service', port: 443, path, ...clientOptions },
|
|
149
|
+
(res) => {
|
|
150
|
+
let data = '';
|
|
151
|
+
res.on('data', chunk => data += chunk);
|
|
152
|
+
res.on('end', () => resolve(JSON.parse(data)));
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
req.on('error', reject);
|
|
156
|
+
req.end();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### mTLS in Kubernetes with Istio
|
|
162
|
+
|
|
163
|
+
If using a service mesh, mTLS is handled transparently at the sidecar layer — application code does not need to manage certificates:
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
# Istio PeerAuthentication — require mTLS for all services in namespace
|
|
167
|
+
apiVersion: security.istio.io/v1beta1
|
|
168
|
+
kind: PeerAuthentication
|
|
169
|
+
metadata:
|
|
170
|
+
name: default
|
|
171
|
+
namespace: production
|
|
172
|
+
spec:
|
|
173
|
+
mtls:
|
|
174
|
+
mode: STRICT # STRICT = require mTLS; PERMISSIVE = allow both (for migration)
|
|
175
|
+
---
|
|
176
|
+
# Istio AuthorizationPolicy — control which services can call order-service
|
|
177
|
+
apiVersion: security.istio.io/v1beta1
|
|
178
|
+
kind: AuthorizationPolicy
|
|
179
|
+
metadata:
|
|
180
|
+
name: order-service-authz
|
|
181
|
+
namespace: production
|
|
182
|
+
spec:
|
|
183
|
+
selector:
|
|
184
|
+
matchLabels:
|
|
185
|
+
app: order-service
|
|
186
|
+
rules:
|
|
187
|
+
- from:
|
|
188
|
+
- source:
|
|
189
|
+
principals:
|
|
190
|
+
# Only allow calls from these service identities (SPIFFE URIs)
|
|
191
|
+
- "cluster.local/ns/production/sa/checkout-service"
|
|
192
|
+
- "cluster.local/ns/production/sa/fulfillment-service"
|
|
193
|
+
to:
|
|
194
|
+
- operation:
|
|
195
|
+
methods: ["GET", "POST"]
|
|
196
|
+
paths: ["/orders", "/orders/*"]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Trade-offs (mTLS):**
|
|
200
|
+
- (+) Encryption and mutual authentication in one mechanism — transport and identity together.
|
|
201
|
+
- (+) Certificate-based identity is cryptographically strong. Cannot be forged without the private key.
|
|
202
|
+
- (+) Service mesh handles certificate rotation automatically (Istio rotates every 24 hours by default).
|
|
203
|
+
- (-) Certificate management complexity — requires a CA, cert distribution, rotation automation.
|
|
204
|
+
- (-) TLS handshake adds latency (~1-5ms for the initial connection).
|
|
205
|
+
- (-) Debugging TLS issues requires familiarity with certificate tooling (openssl, istioctl).
|
|
206
|
+
|
|
207
|
+
## Service-to-Service JWT Patterns
|
|
208
|
+
|
|
209
|
+
### Why JWTs for Inter-Service Auth
|
|
210
|
+
|
|
211
|
+
While mTLS provides transport-level identity, JWTs provide application-level claims that carry richer context:
|
|
212
|
+
|
|
213
|
+
- Which service is calling (subject)
|
|
214
|
+
- Which service is the intended recipient (audience)
|
|
215
|
+
- What permissions the caller has (scopes)
|
|
216
|
+
- The user context being acted on behalf of (propagated user identity)
|
|
217
|
+
|
|
218
|
+
This enables fine-grained authorization at the application layer, independent of network topology.
|
|
219
|
+
|
|
220
|
+
### Token Issuance
|
|
221
|
+
|
|
222
|
+
An internal token issuer (auth service or a shared library) signs JWTs with a private key. Other services verify signatures using the corresponding public key.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import jwt from 'jsonwebtoken';
|
|
226
|
+
import fs from 'fs';
|
|
227
|
+
|
|
228
|
+
const PRIVATE_KEY = fs.readFileSync('/etc/secrets/service-signing-key.pem');
|
|
229
|
+
const TOKEN_TTL_SECONDS = 300; // 5 minutes — short-lived for internal tokens
|
|
230
|
+
|
|
231
|
+
interface ServiceTokenClaims {
|
|
232
|
+
iss: string; // Issuer: which service issued the token
|
|
233
|
+
sub: string; // Subject: the calling service's identity
|
|
234
|
+
aud: string; // Audience: the specific target service
|
|
235
|
+
iat: number; // Issued at
|
|
236
|
+
exp: number; // Expiration
|
|
237
|
+
jti: string; // JWT ID — unique token identifier (for replay prevention)
|
|
238
|
+
scope: string[]; // Authorized scopes for this token
|
|
239
|
+
// Optional: propagate user context
|
|
240
|
+
user_id?: string;
|
|
241
|
+
user_roles?: string[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function issueServiceToken(options: {
|
|
245
|
+
callerService: string;
|
|
246
|
+
targetService: string;
|
|
247
|
+
scopes: string[];
|
|
248
|
+
userContext?: { userId: string; roles: string[] };
|
|
249
|
+
}): string {
|
|
250
|
+
const claims: ServiceTokenClaims = {
|
|
251
|
+
iss: options.callerService,
|
|
252
|
+
sub: options.callerService,
|
|
253
|
+
aud: options.targetService,
|
|
254
|
+
iat: Math.floor(Date.now() / 1000),
|
|
255
|
+
exp: Math.floor(Date.now() / 1000) + TOKEN_TTL_SECONDS,
|
|
256
|
+
jti: crypto.randomUUID(),
|
|
257
|
+
scope: options.scopes,
|
|
258
|
+
...(options.userContext && {
|
|
259
|
+
user_id: options.userContext.userId,
|
|
260
|
+
user_roles: options.userContext.roles,
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return jwt.sign(claims, PRIVATE_KEY, { algorithm: 'RS256' });
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Token Validation
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import jwt, { JwtPayload } from 'jsonwebtoken';
|
|
272
|
+
|
|
273
|
+
// Load public keys from a JWKS endpoint or static file
|
|
274
|
+
// In production: use a JWKS endpoint so keys can rotate without redeployment
|
|
275
|
+
const PUBLIC_KEYS = loadPublicKeys('/etc/certs/service-signing-pub.pem');
|
|
276
|
+
|
|
277
|
+
interface ValidationOptions {
|
|
278
|
+
expectedAudience: string; // This service's identity
|
|
279
|
+
allowedIssuers: string[]; // Which services are authorized to call
|
|
280
|
+
requiredScopes?: string[]; // Scopes required for this endpoint
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function validateServiceToken(
|
|
284
|
+
token: string,
|
|
285
|
+
options: ValidationOptions,
|
|
286
|
+
): Promise<ServiceTokenClaims> {
|
|
287
|
+
let claims: JwtPayload;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
claims = jwt.verify(token, PUBLIC_KEYS, {
|
|
291
|
+
algorithms: ['RS256'],
|
|
292
|
+
audience: options.expectedAudience,
|
|
293
|
+
clockTolerance: 5, // Allow 5 seconds of clock skew
|
|
294
|
+
}) as JwtPayload;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error instanceof jwt.TokenExpiredError) {
|
|
297
|
+
throw new AuthError('TOKEN_EXPIRED', 'Service token has expired');
|
|
298
|
+
}
|
|
299
|
+
if (error instanceof jwt.JsonWebTokenError) {
|
|
300
|
+
throw new AuthError('TOKEN_INVALID', 'Service token signature is invalid');
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Verify issuer is an authorized caller
|
|
306
|
+
if (!options.allowedIssuers.includes(claims.iss as string)) {
|
|
307
|
+
throw new AuthError('UNAUTHORIZED_ISSUER', `Service ${claims.iss} is not authorized`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Verify audience matches this service (prevents token reuse across services)
|
|
311
|
+
if (claims.aud !== options.expectedAudience) {
|
|
312
|
+
throw new AuthError('AUDIENCE_MISMATCH', 'Token was not issued for this service');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Verify required scopes are present
|
|
316
|
+
if (options.requiredScopes) {
|
|
317
|
+
const tokenScopes = new Set<string>(claims.scope as string[]);
|
|
318
|
+
const missingScopes = options.requiredScopes.filter(s => !tokenScopes.has(s));
|
|
319
|
+
if (missingScopes.length > 0) {
|
|
320
|
+
throw new AuthError('INSUFFICIENT_SCOPE', `Missing scopes: ${missingScopes.join(', ')}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return claims as unknown as ServiceTokenClaims;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Express middleware
|
|
328
|
+
function requireServiceAuth(requiredScopes?: string[]) {
|
|
329
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
330
|
+
const authHeader = req.headers.authorization;
|
|
331
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
332
|
+
return res.status(401).json({ error: { code: 'MISSING_TOKEN' } });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const token = authHeader.slice(7);
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const claims = await validateServiceToken(token, {
|
|
339
|
+
expectedAudience: 'order-service',
|
|
340
|
+
allowedIssuers: ['checkout-service', 'fulfillment-service', 'admin-service'],
|
|
341
|
+
requiredScopes,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
req.callerService = claims.sub;
|
|
345
|
+
req.callerScopes = claims.scope;
|
|
346
|
+
if (claims.user_id) req.propagatedUserId = claims.user_id;
|
|
347
|
+
|
|
348
|
+
next();
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (error instanceof AuthError) {
|
|
351
|
+
return res.status(401).json({ error: { code: error.code, message: error.message } });
|
|
352
|
+
}
|
|
353
|
+
next(error);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Trade-offs (service JWTs):**
|
|
360
|
+
- (+) Rich authorization claims — scopes, propagated user context, audience constraints.
|
|
361
|
+
- (+) Stateless validation — verifying a JWT requires only the public key, no database lookup.
|
|
362
|
+
- (+) Short TTL limits the window for a stolen token to be used.
|
|
363
|
+
- (-) Cannot revoke individual tokens before expiry (use short TTLs to limit damage).
|
|
364
|
+
- (-) Public key distribution requires a JWKS endpoint or coordinated rotation.
|
|
365
|
+
- (-) Token forwarding risk — a compromised service could forward its tokens to other services.
|
|
366
|
+
|
|
367
|
+
## Audience Scoping
|
|
368
|
+
|
|
369
|
+
### Why Audience Scoping Matters
|
|
370
|
+
|
|
371
|
+
Without audience scoping, a token issued for service A can be used to call service B. If an attacker steals a token from service A's request, they can replay it against any service.
|
|
372
|
+
|
|
373
|
+
**The attack without audience scoping:**
|
|
374
|
+
```
|
|
375
|
+
Attacker intercepts token from A → B request
|
|
376
|
+
Attacker replays token to call C (which A has no business calling)
|
|
377
|
+
C accepts the token because it's validly signed
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**With audience scoping:**
|
|
381
|
+
```
|
|
382
|
+
Token for A → B includes aud: "service-B"
|
|
383
|
+
Attacker replays token to call C
|
|
384
|
+
C rejects: aud "service-B" does not match "service-C"
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Audience Scoping Patterns
|
|
388
|
+
|
|
389
|
+
**Per-service audiences:** Each service has a unique audience identifier. Tokens are issued with a specific target service's audience.
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
// Token issued by checkout-service to call order-service
|
|
393
|
+
const token = issueServiceToken({
|
|
394
|
+
callerService: 'checkout-service',
|
|
395
|
+
targetService: 'order-service', // aud: "order-service"
|
|
396
|
+
scopes: ['orders:create', 'orders:read'],
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// This token cannot be used to call inventory-service
|
|
400
|
+
// inventory-service expects aud: "inventory-service"
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Per-operation audiences:** For even tighter scoping, include the operation in the audience:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
// Token scoped to a specific endpoint
|
|
407
|
+
const token = issueServiceToken({
|
|
408
|
+
callerService: 'billing-service',
|
|
409
|
+
targetService: 'order-service:GET:/orders/:id',
|
|
410
|
+
scopes: ['orders:read'],
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Trade-offs (audience scoping):**
|
|
415
|
+
- (+) Stolen tokens cannot be replayed against arbitrary services.
|
|
416
|
+
- (+) Defense-in-depth: limits blast radius if one service is compromised.
|
|
417
|
+
- (-) More tokens to manage — each service pair requires separate token issuance.
|
|
418
|
+
- (-) Token caching becomes harder — a token cached for A→B cannot be reused for A→C.
|
|
419
|
+
|
|
420
|
+
### Token Caching for Performance
|
|
421
|
+
|
|
422
|
+
Short-lived tokens (5 minutes) require frequent issuance. Cache tokens with a safety margin:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
class ServiceTokenCache {
|
|
426
|
+
private cache = new Map<string, { token: string; expiresAt: number }>();
|
|
427
|
+
private readonly SAFETY_MARGIN_MS = 30_000; // Refresh 30s before expiry
|
|
428
|
+
|
|
429
|
+
async getToken(callerService: string, targetService: string, scopes: string[]): Promise<string> {
|
|
430
|
+
const cacheKey = `${callerService}:${targetService}:${scopes.sort().join(',')}`;
|
|
431
|
+
const cached = this.cache.get(cacheKey);
|
|
432
|
+
|
|
433
|
+
if (cached && cached.expiresAt - Date.now() > this.SAFETY_MARGIN_MS) {
|
|
434
|
+
return cached.token;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const token = await issueServiceToken({ callerService, targetService, scopes });
|
|
438
|
+
const decoded = jwt.decode(token) as JwtPayload;
|
|
439
|
+
|
|
440
|
+
this.cache.set(cacheKey, {
|
|
441
|
+
token,
|
|
442
|
+
expiresAt: (decoded.exp ?? 0) * 1000,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return token;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## SPIFFE/SPIRE Identity Framework
|
|
451
|
+
|
|
452
|
+
### What SPIFFE Provides
|
|
453
|
+
|
|
454
|
+
SPIFFE (Secure Production Identity Framework for Everyone) is an open standard for workload identity in multi-cloud and multi-cluster environments. SPIRE is its reference implementation.
|
|
455
|
+
|
|
456
|
+
**Core concept:** Every workload gets a SPIFFE Verifiable Identity Document (SVID) — a short-lived X.509 certificate or JWT containing a SPIFFE ID:
|
|
457
|
+
|
|
458
|
+
```
|
|
459
|
+
spiffe://trust-domain/path/to/workload
|
|
460
|
+
|
|
461
|
+
# Examples:
|
|
462
|
+
spiffe://acme.com/ns/production/sa/order-service
|
|
463
|
+
spiffe://acme.com/ns/production/sa/checkout-service
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### SPIRE Architecture
|
|
467
|
+
|
|
468
|
+
```
|
|
469
|
+
SPIRE Server (runs once per cluster/region)
|
|
470
|
+
↓ issues SVIDs via workload API
|
|
471
|
+
SPIRE Agent (runs on every node as DaemonSet)
|
|
472
|
+
↓ attests workload identity
|
|
473
|
+
Workload (your service)
|
|
474
|
+
↓ presents SVID for mTLS or JWT auth
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**SPIRE agent configuration:**
|
|
478
|
+
|
|
479
|
+
```hcl
|
|
480
|
+
# spire-agent.conf
|
|
481
|
+
agent {
|
|
482
|
+
data_dir = "/var/lib/spire/agent"
|
|
483
|
+
log_level = "INFO"
|
|
484
|
+
trust_domain = "acme.com"
|
|
485
|
+
server_address = "spire-server.spire.svc.cluster.local"
|
|
486
|
+
server_port = 8081
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
plugins {
|
|
490
|
+
KeyManager "disk" {
|
|
491
|
+
plugin_data {
|
|
492
|
+
directory = "/var/lib/spire/agent/keys"
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
NodeAttestor "k8s_psat" {
|
|
497
|
+
plugin_data {
|
|
498
|
+
cluster = "production-cluster"
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
WorkloadAttestor "k8s" {
|
|
503
|
+
plugin_data {
|
|
504
|
+
skip_kubelet_verification = false
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**SPIRE registration entry (maps workload to SPIFFE ID):**
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
# Register order-service workload
|
|
514
|
+
spire-server entry create \
|
|
515
|
+
-spiffeID spiffe://acme.com/ns/production/sa/order-service \
|
|
516
|
+
-parentID spiffe://acme.com/spire/agent/k8s_psat/production-cluster/node1 \
|
|
517
|
+
-selector k8s:ns:production \
|
|
518
|
+
-selector k8s:sa:order-service \
|
|
519
|
+
-ttl 3600
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Trade-offs (SPIFFE/SPIRE):**
|
|
523
|
+
- (+) Automated workload identity — no manual certificate management per service.
|
|
524
|
+
- (+) Short-lived SVIDs (hours, not years) — compromise window is narrow.
|
|
525
|
+
- (+) Works across clouds, clusters, and on-premises — not tied to one provider.
|
|
526
|
+
- (-) Operational overhead — SPIRE Server and Agent fleet must be highly available.
|
|
527
|
+
- (-) Learning curve — SPIFFE concepts and SPIRE configuration are non-trivial.
|
|
528
|
+
- (-) Not necessary for single-cluster deployments where Istio/Linkerd already provide workload identity.
|
|
529
|
+
|
|
530
|
+
**Use SPIFFE/SPIRE when:** Multi-cloud or multi-cluster architectures where workload identity must span infrastructure boundaries, or when you need a provider-agnostic identity standard.
|
|
531
|
+
|
|
532
|
+
## Secret Rotation Strategies
|
|
533
|
+
|
|
534
|
+
### Rotation Principles
|
|
535
|
+
|
|
536
|
+
Secrets that never rotate are a liability: a leaked secret is valid forever. All credentials — certificates, tokens, API keys, database passwords — should be rotated on a schedule shorter than your expected breach detection time.
|
|
537
|
+
|
|
538
|
+
**Target rotation windows:**
|
|
539
|
+
- mTLS certificates: 24–90 days (Istio default: 24 hours)
|
|
540
|
+
- Service JWT signing keys: 30–90 days
|
|
541
|
+
- Database passwords: 30–90 days
|
|
542
|
+
- API keys (third-party): 90–365 days (limited by provider)
|
|
543
|
+
- Secrets in Kubernetes: sync with source on every deployment
|
|
544
|
+
|
|
545
|
+
### Zero-Downtime Key Rotation
|
|
546
|
+
|
|
547
|
+
Rotating JWT signing keys requires a transition period where both old and new keys are valid:
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
// JWKS endpoint — serves multiple active keys
|
|
551
|
+
// Tokens signed with old key are still valid until they expire
|
|
552
|
+
// New tokens are signed with the new key
|
|
553
|
+
app.get('/.well-known/jwks.json', (req, res) => {
|
|
554
|
+
res.json({
|
|
555
|
+
keys: [
|
|
556
|
+
// New key (primary — used for signing new tokens)
|
|
557
|
+
{
|
|
558
|
+
kid: 'key-2026-04',
|
|
559
|
+
kty: 'RSA',
|
|
560
|
+
use: 'sig',
|
|
561
|
+
alg: 'RS256',
|
|
562
|
+
n: newKeyPublicN,
|
|
563
|
+
e: 'AQAB',
|
|
564
|
+
},
|
|
565
|
+
// Old key (secondary — only for verifying tokens signed before rotation)
|
|
566
|
+
// Remove this entry after all old tokens have expired (after max TTL)
|
|
567
|
+
{
|
|
568
|
+
kid: 'key-2026-01',
|
|
569
|
+
kty: 'RSA',
|
|
570
|
+
use: 'sig',
|
|
571
|
+
alg: 'RS256',
|
|
572
|
+
n: oldKeyPublicN,
|
|
573
|
+
e: 'AQAB',
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Validator: try all active public keys — succeeds if any key verifies the token
|
|
580
|
+
async function verifyWithAnyActiveKey(token: string): Promise<JwtPayload> {
|
|
581
|
+
const header = jwt.decode(token, { complete: true })?.header;
|
|
582
|
+
const kid = header?.kid;
|
|
583
|
+
|
|
584
|
+
// Try the specific key if kid is present
|
|
585
|
+
if (kid) {
|
|
586
|
+
const key = getKeyById(kid);
|
|
587
|
+
if (!key) throw new AuthError('UNKNOWN_KEY_ID', `Key ${kid} not found`);
|
|
588
|
+
return jwt.verify(token, key, { algorithms: ['RS256'] }) as JwtPayload;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// No kid — try all active keys
|
|
592
|
+
for (const key of getActivePublicKeys()) {
|
|
593
|
+
try {
|
|
594
|
+
return jwt.verify(token, key, { algorithms: ['RS256'] }) as JwtPayload;
|
|
595
|
+
} catch {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
throw new AuthError('TOKEN_INVALID', 'No active key could verify this token');
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Secret Management Infrastructure
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
// Kubernetes Secret with rotation via external-secrets-operator
|
|
608
|
+
// Syncs from AWS Secrets Manager / HashiCorp Vault on a schedule
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
```yaml
|
|
612
|
+
# external-secrets-operator ExternalSecret
|
|
613
|
+
apiVersion: external-secrets.io/v1beta1
|
|
614
|
+
kind: ExternalSecret
|
|
615
|
+
metadata:
|
|
616
|
+
name: order-service-secrets
|
|
617
|
+
namespace: production
|
|
618
|
+
spec:
|
|
619
|
+
refreshInterval: 1h # Re-sync from source every hour
|
|
620
|
+
secretStoreRef:
|
|
621
|
+
name: aws-secretsmanager
|
|
622
|
+
kind: ClusterSecretStore
|
|
623
|
+
target:
|
|
624
|
+
name: order-service-secrets # Kubernetes Secret name
|
|
625
|
+
creationPolicy: Owner
|
|
626
|
+
template:
|
|
627
|
+
engineVersion: v2
|
|
628
|
+
data:
|
|
629
|
+
DATABASE_URL: "{{ .db_url }}"
|
|
630
|
+
JWT_SIGNING_KEY: "{{ .jwt_private_key }}"
|
|
631
|
+
data:
|
|
632
|
+
- secretKey: db_url
|
|
633
|
+
remoteRef:
|
|
634
|
+
key: production/order-service/database
|
|
635
|
+
property: url
|
|
636
|
+
- secretKey: jwt_private_key
|
|
637
|
+
remoteRef:
|
|
638
|
+
key: production/order-service/jwt
|
|
639
|
+
property: private_key
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
**Trade-offs (rotation):**
|
|
643
|
+
- (+) Limits the damage window if a secret is leaked — the leaked value expires.
|
|
644
|
+
- (+) Forces secret hygiene — stale secrets are cleaned up on a schedule.
|
|
645
|
+
- (-) Requires automation — manual rotation at scale is error-prone and skipped.
|
|
646
|
+
- (-) Rotation bugs cause outages — test rotation in staging before enabling in production.
|
|
647
|
+
|
|
648
|
+
## Propagating User Context
|
|
649
|
+
|
|
650
|
+
When a user's request causes service A to call service B, service B may need to know who the original user is (for authorization, audit logging, or personalization). The user context must be propagated securely.
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
// Service A: propagate user context in the outgoing service token
|
|
654
|
+
const token = issueServiceToken({
|
|
655
|
+
callerService: 'checkout-service',
|
|
656
|
+
targetService: 'order-service',
|
|
657
|
+
scopes: ['orders:create'],
|
|
658
|
+
userContext: {
|
|
659
|
+
userId: req.user.id,
|
|
660
|
+
roles: req.user.roles,
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Service B: extract and use the propagated user context
|
|
665
|
+
app.post('/orders', requireServiceAuth(['orders:create']), async (req, res) => {
|
|
666
|
+
const userId = req.propagatedUserId; // Set by auth middleware from JWT claims
|
|
667
|
+
|
|
668
|
+
// Use for authorization: can this user create an order?
|
|
669
|
+
const user = await userService.getUser(userId);
|
|
670
|
+
if (!user.canPlaceOrders()) {
|
|
671
|
+
return res.status(403).json({ error: { code: 'USER_NOT_AUTHORIZED' } });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Use for audit logging: attribute the action to the user, not the service
|
|
675
|
+
logger.info({ userId, callerService: req.callerService }, 'Creating order on behalf of user');
|
|
676
|
+
|
|
677
|
+
const order = await createOrder({ userId, ...req.body });
|
|
678
|
+
res.status(201).json(order);
|
|
679
|
+
});
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
**Trade-offs:**
|
|
683
|
+
- (+) Service B has full context for authorization and audit — the user identity is not lost in the call chain.
|
|
684
|
+
- (-) Never use the propagated user context without also verifying the caller service is authorized. A malicious caller could forge user context claims if the token itself is valid.
|
|
685
|
+
|
|
686
|
+
## Common Pitfalls
|
|
687
|
+
|
|
688
|
+
**Network trust without mTLS.** Assuming that traffic inside a VPC or Kubernetes cluster is trusted. A compromised pod can make requests to any other pod. Fix: enforce mTLS between all services. Use a service mesh with STRICT mTLS mode.
|
|
689
|
+
|
|
690
|
+
**Long-lived service credentials.** API keys or static tokens that never expire. A leaked credential is valid indefinitely. Fix: use short-lived JWTs (5-15 minutes) and automate rotation of all longer-lived credentials.
|
|
691
|
+
|
|
692
|
+
**No audience validation.** Tokens are validated for signature but not audience. A token issued for service A can be replayed against service B. Fix: always validate the `aud` claim matches the expected audience for this service.
|
|
693
|
+
|
|
694
|
+
**Broad token scopes.** Issuing tokens with `scope: ["*"]` or all permissions. A compromised caller can do anything its target service allows. Fix: request and grant minimum required scopes per operation.
|
|
695
|
+
|
|
696
|
+
**Secret sprawl.** Secrets hard-coded in environment variables, config files, or source code. Rotation requires redeployment. Fix: use a secrets manager (Vault, AWS Secrets Manager) and inject secrets at runtime via external-secrets-operator or equivalent.
|
|
697
|
+
|
|
698
|
+
**Missing token replay prevention.** The same JWT can be presented multiple times within its validity window. Fix: for high-value operations, maintain a token revocation list keyed by `jti` and reject tokens whose `jti` has been seen within the validity window.
|
|
699
|
+
|
|
700
|
+
**Certificate pinning without rotation plan.** Hard-coding a certificate's fingerprint in a service for "extra security." When the certificate rotates, the pinned service breaks. Fix: pin the CA certificate, not the leaf certificate — or use the JWKS endpoint pattern for key distribution.
|
|
701
|
+
|
|
702
|
+
## See Also
|
|
703
|
+
|
|
704
|
+
- [multi-service-architecture](./multi-service-architecture.md) — Service discovery and networking topology
|
|
705
|
+
- [multi-service-api-contracts](./multi-service-api-contracts.md) — API versioning, retries, and idempotency
|
|
706
|
+
- [security-best-practices](./security-best-practices.md) — OWASP Top 10, secrets management, and threat modeling
|