Skip to content

feat(core)!: fail closed on field-level encryption in deployed stages - #663

Open
seanspeaks wants to merge 4 commits into
nextfrom
fix/encryption-fail-closed
Open

seanspeaks wants to merge 4 commits into
nextfrom
fix/encryption-fail-closed

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Field-level encryption now fails closed in deployed stages. Frigg used to skip encryption whenever STAGE was dev, test or local, and it silently wrote plaintext when no key was configured. Because frigg deploy defaults to --stage dev, a plain deploy stored OAuth tokens and API keys unencrypted.

Encryption now depends on where the code runs, not on the stage name:

Runtime Key Opt-out (FRIGG_ENCRYPTION_DISABLED=true) Result
Deployed (any stage, including dev) KMS_KEY_ARN any KMS (an opt-out is ignored with a warning, so existing ciphertext stays readable)
Deployed AES_KEY_ID + AES_KEY any AES
Deployed AES_KEY_ID without AES_KEY no EncryptionConfigurationError
Deployed none no EncryptionConfigurationError
Deployed none yes plaintext, with a warning on every cold start
Local, dev/test/local stage any any skipped (unchanged)
Local, other stage key set / none any encrypts / plaintext with warning (unchanged)

Deployed means running in AWS Lambda (AWS_LAMBDA_FUNCTION_NAME, LAMBDA_TASK_ROOT or AWS_EXECUTION_ENV=AWS_Lambda_*) and not under serverless-offline (IS_OFFLINE), serverless invoke local (IS_LOCAL) or Jest. Stage names alone aren't enough, because serverless-offline sets AWS_LAMBDA_FUNCTION_NAME itself, so frigg start keeps working. CloudShell, CodeBuild and ECS count as local, so CLI commands that open a Prisma client in CI aren't affected.

Changes

  • packages/core/database/encryption/encryption-config.js holds the rule in one place. prisma.js, the DocumentDB encryption service, integration-mapping encryption and the encryption health check all use it; the DocumentDB service previously had its own copy of the stage check.
  • Prisma client creation throws EncryptionConfigurationError in the deployed no-key case. The message says what's missing and how to fix it, and the failed client isn't cached. A failed encryption-extension setup now re-throws instead of continuing unencrypted.
  • Explicit opt-out: encryption: { fieldLevelEncryptionMethod: 'none' } in the app definition (new schema enum value) sets FRIGG_ENCRYPTION_DISABLED=true. That variable is kept out of SSM offload, because Prisma reads it before SSM values load.
  • /health/detailed reports a deployed stage with no key as unhealthy, and an opt-out as disabled. BYPASS_ENCRYPTION_STAGE was only ever read by the health check, and is no longer read.
  • Docs: CLAUDE.md, the core encryption README, the DocumentDB encryption doc, the devtools infrastructure README, the frigg and canary skills, and the website assistant's knowledge.

Breaking change (for the 2.0.1 release notes, see #660)

A deployed stage with no key refuses to start: database access returns 500 and the logs show EncryptionConfigurationError with the fix. To fix it, add encryption: { fieldLevelEncryptionMethod: 'kms' } and redeploy (Frigg creates or discovers the key on every stage), provide AES_KEY_ID and AES_KEY, or opt out explicitly. Rows already written in plaintext stay readable and are encrypted the next time they're written.

Known limits

  • A key reference that is set but invalid (e.g. an empty discovered KMS key ID) passes the startup check and fails on the first KMS call.
  • Non-Lambda hosts (ECS and similar) count as local, so the fail-closed check doesn't cover them.
  • The schema still annotates fieldLevelEncryptionMethod with default: "aes", which nothing applies. It's left unchanged here.

Test plan

  • New encryption-config.test.js runtime matrix: offline, invoke-local, Jest, script, Lambda, ECS and CloudShell, each with KMS, AES, none and opt-out
  • New prisma.encryption.test.js: startup throws and nothing is cached; KMS on dev installs the extension; opt-out warns; offline bypasses; a failed extension setup throws
  • DocumentDB service, health use case, environment-builder and schema cases added
  • Core: 2,571 pass / 64 fail. The same 64 tests fail on next.
  • devtools infrastructure: 89 failures before and after, none new. Schemas 126/126.

🤖 Generated with Claude Code

📦 Published PR as canary version: 2.0.0--canary.663.9683f8b.0

✨ Test out this PR locally via:

npm install @friggframework/admin-scripts@2.0.0--canary.663.9683f8b.0
npm install @friggframework/core@2.0.0--canary.663.9683f8b.0
npm install @friggframework/devtools@2.0.0--canary.663.9683f8b.0
npm install @friggframework/eslint-config@2.0.0--canary.663.9683f8b.0
npm install @friggframework/prettier-config@2.0.0--canary.663.9683f8b.0
npm install @friggframework/schemas@2.0.0--canary.663.9683f8b.0
npm install @friggframework/serverless-plugin@2.0.0--canary.663.9683f8b.0
npm install @friggframework/test@2.0.0--canary.663.9683f8b.0
npm install @friggframework/ui@2.0.0--canary.663.9683f8b.0
# or 
yarn add @friggframework/admin-scripts@2.0.0--canary.663.9683f8b.0
yarn add @friggframework/core@2.0.0--canary.663.9683f8b.0
yarn add @friggframework/devtools@2.0.0--canary.663.9683f8b.0
yarn add @friggframework/eslint-config@2.0.0--canary.663.9683f8b.0
yarn add @friggframework/prettier-config@2.0.0--canary.663.9683f8b.0
yarn add @friggframework/schemas@2.0.0--canary.663.9683f8b.0
yarn add @friggframework/serverless-plugin@2.0.0--canary.663.9683f8b.0
yarn add @friggframework/test@2.0.0--canary.663.9683f8b.0
yarn add @friggframework/ui@2.0.0--canary.663.9683f8b.0

seanspeaks and others added 4 commits October 2, 2026 16:01
Field-level encryption used to be skipped whenever STAGE was dev, test
or local, and silently turned off (with a warning) when no key was
configured. `frigg deploy` defaults to --stage dev, so a plain deploy
stored credentials in plaintext.

The rule now keys off where the code runs, not the stage name, and
lives in one place (database/encryption/encryption-config.js) shared by
the Prisma client, DocumentDBEncryptionService, the integration-mapping
helpers and the encryption health check:

- Deployed (in Lambda, not serverless-offline / invoke local / Jest):
  KMS_KEY_ARN or AES_KEY_ID+AES_KEY encrypts on every stage, dev
  included. No key throws EncryptionConfigurationError at Prisma client
  creation, naming the fix, unless FRIGG_ENCRYPTION_DISABLED=true opts
  out explicitly (warned once per cold start). A configured key wins
  over the opt-out.
- Local (frigg start, tests, scripts): unchanged. dev/test/local skip
  encryption; other stages encrypt when a key is set.

The Prisma client also stops continuing without encryption when the
extension fails to initialise. /health/detailed reports a deployed stage
without a key as unhealthy and an opt-out as disabled; it no longer
reads BYPASS_ENCRYPTION_STAGE, which never switched encryption off.

BREAKING CHANGE: a deployed stage with no KMS_KEY_ARN and no
AES_KEY_ID/AES_KEY now refuses to start instead of writing plaintext,
including stages named dev, test or local. Set
encryption.fieldLevelEncryptionMethod: 'kms' (or provide AES keys), or
opt out with FRIGG_ENCRYPTION_DISABLED=true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`encryption: { fieldLevelEncryptionMethod: 'none' }` is the declarative
way to run a deployed stage without field-level encryption now that core
refuses to start without a key. The environment builder turns it into
FRIGG_ENCRYPTION_DISABLED=true on every function, and the SSM offload
blocklist keeps that variable in the Lambda environment, since Prisma
reads it at INIT before SSM values load.

The KMS wiring already ignores the stage name, so an app with
fieldLevelEncryptionMethod: 'kms' deployed with --stage dev gets
KMS_KEY_ARN and encrypts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the "dev, test and local stages bypass encryption" guidance in
CLAUDE.md, the core encryption README, the DocumentDB encryption design
note, the frigg and canary skills, the infrastructure README and the
website assistant prompt with the deployed/local rule, the explicit
opt-out, and the startup error and its fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@seanspeaks seanspeaks added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Oct 2, 2026

@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.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, comment @claude review on this pull request, or push a new commit, to trigger a review.

@netlify

netlify Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for friggframework-org ready!

Name Link
🔨 Latest commit 9683f8b
🔍 Latest deploy log https://app.netlify.com/projects/friggframework-org/deploys/6ac00dee3913cd000824fc3d
😎 Deploy Preview https://deploy-preview-663--friggframework-org.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@sonarqubecloud

sonarqubecloud Bot commented Oct 2, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease This change is available in a prerelease. release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant