Consolidate encryption keys in platform API#2553
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe Platform API now requires a shared ChangesEncryption key consolidation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Environment
participant LoadConfig
participant PlatformAPI
participant SecretStore
Environment->>LoadConfig: provide ENCRYPTION_KEY and AUTH_JWT_SECRET_KEY
LoadConfig->>PlatformAPI: initialize validated configuration
PlatformAPI->>SecretStore: derive encryption key from ENCRYPTION_KEY
PlatformAPI->>PlatformAPI: encrypt database secrets and subscription tokens
Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ai-workspace-pr-check.yml:
- Line 20: The ENCRYPTION_KEY value in the ai-workspace-pr-check workflow is
invalid and will prevent platform-api from starting. Update the workflow’s
ENCRYPTION_KEY entry to a valid 64-character hex string or a base64-encoded
32-byte value, using the existing ai-workspace-pr-check job/environment setup as
the place to fix it.
In `@distribution/all-in-one/docker-compose.yaml`:
- Line 76: The Postgres deployment in the docker compose setup is still relying
on ENCRYPTION_KEY from the environment, which can default to an ephemeral key
when APIP_DEMO_MODE is enabled. Update the compose configuration around the
Postgres service to require a persisted encryption key source, using the
existing encryption key handling path in the deployment config so
secrets/tokens/sessions survive restarts and startup does not depend on an unset
ENCRYPTION_KEY.
In `@platform-api/internal/handler/auth_login.go`:
- Around line 102-103: The login JWT is using cfg.EncryptionKey as the signing
secret, so the same raw key is shared between AES-GCM encryption and HS256 token
signing. Update the auth login flow around jwt.NewWithClaims and
token.SignedString to use a separate JWT secret or derive purpose-specific
subkeys from the existing key, and make the verifier in server-side JWT parsing
use the matching JWT-specific secret rather than the encryption key.
In `@portals/ai-workspace/README.md`:
- Around line 426-430: The `.env` guidance for `ENCRYPTION_KEY` in the README is
incorrect because `$(openssl rand -hex 32)` will be treated as a literal string,
not executed. Update the Platform API setup note to separate key generation from
configuration: reference the `ENCRYPTION_KEY` example, tell readers to run the
`openssl rand -hex 32` command in a shell to generate a 64-hex value, and then
paste that resulting value into `.env` instead of the command itself.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 03ff1d9b-01d8-4ace-91b7-ef23e67e4efc
📒 Files selected for processing (28)
.github/workflows/ai-workspace-pr-check.ymldistribution/all-in-one/docker-compose.yamldocs/ai-workspace/configuration.mddocs/ai-workspace/features/secrets-management.mdplatform-api/README.mdplatform-api/config/config.goplatform-api/config/config.tomlplatform-api/config/config_test.goplatform-api/internal/handler/auth_login.goplatform-api/internal/integration/harness_test.goplatform-api/internal/repository/api_deployments_test.goplatform-api/internal/repository/subscription_repository.goplatform-api/internal/server/server.goplatform-api/plugins/eventgateway/service/websub_api_hmac_secret.goportals/ai-workspace/README.mdportals/ai-workspace/configs/config-platform-api-template.tomlportals/ai-workspace/configs/config-platform-api.tomlportals/ai-workspace/docker-compose.yamlportals/ai-workspace/production/README.mdportals/developer-portal/README.mdportals/developer-portal/distribution/docker-compose.yamlportals/developer-portal/docker-compose.platform-api.yamlportals/developer-portal/docker-compose.yamlportals/developer-portal/docs/administer/manage-organizations.mdtests/integration-e2e/README.mdtests/integration-e2e/docker-compose.sqlite.yamltests/integration-e2e/docker-compose.sqlserver.yamltests/integration-e2e/docker-compose.yaml
| token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | ||
| signed, err := token.SignedString([]byte(h.cfg.Auth.JWT.SecretKey)) | ||
| signed, err := token.SignedString([]byte(h.cfg.EncryptionKey)) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP --type=go -C3 'func DeriveEncryptionKey'Repository: wso2/api-platform
Length of output: 155
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the login handler, the encryption-key derivation, and the JWT verification path.
git ls-files | rg '(^|/)(auth_login\.go|server\.go|.*\.go)$'
echo '--- auth_login.go ---'
sed -n '1,220p' platform-api/internal/handler/auth_login.go
echo '--- search for DeriveEncryptionKey ---'
rg -n --hidden --glob '*.go' 'DeriveEncryptionKey|EncryptionKey|SignedString|SigningMethodHS256|SecretKey' .
echo '--- server.go if present ---'
sed -n '1,240p' platform-api/internal/server.go 2>/dev/null || trueRepository: wso2/api-platform
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- platform-api/internal/handler/auth_login.go ---'
sed -n '1,180p' platform-api/internal/handler/auth_login.go
echo '--- platform-api/internal/middleware/authenticator.go ---'
sed -n '1,220p' platform-api/internal/middleware/authenticator.go
echo '--- platform-api/internal/middleware/auth.go ---'
sed -n '1,220p' platform-api/internal/middleware/auth.go
echo '--- platform-api/internal/middleware/error_mapper.go ---'
sed -n '1,220p' platform-api/internal/middleware/error_mapper.go
echo '--- DeriveEncryptionKey references ---'
rg -n --hidden --glob '*.go' 'DeriveEncryptionKey|jwt.NewWithClaims|SigningMethodHS256|SecretKey|EncryptionKey' platform-api common gateway gateway-controllerRepository: wso2/api-platform
Length of output: 26395
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- platform-api/internal/utils/subscription_token_crypto.go ---'
sed -n '1,220p' platform-api/internal/utils/subscription_token_crypto.go
echo '--- platform-api/internal/server/server.go (auth/key sections) ---'
sed -n '250,340p' platform-api/internal/server/server.go
echo '--- platform-api/internal/server/server.go (authenticator wiring) ---'
sed -n '480,620p' platform-api/internal/server/server.go
echo '--- platform-api/config/config.go (encryption key validation) ---'
sed -n '372,470p' platform-api/config/config.goRepository: wso2/api-platform
Length of output: 19158
Split the JWT signing secret from the encryption key (platform-api/internal/handler/auth_login.go:102-103).
cfg.EncryptionKey is reused for both AES-256-GCM and this HS256 token, and the verifier in platform-api/internal/server/server.go uses the same raw string. Derive purpose-specific subkeys or add separate config values so the encryption key and login JWT secret aren’t interchangeable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@platform-api/internal/handler/auth_login.go` around lines 102 - 103, The
login JWT is using cfg.EncryptionKey as the signing secret, so the same raw key
is shared between AES-GCM encryption and HS256 token signing. Update the auth
login flow around jwt.NewWithClaims and token.SignedString to use a separate JWT
secret or derive purpose-specific subkeys from the existing key, and make the
verifier in server-side JWT parsing use the matching JWT-specific secret rather
than the encryption key.
Source: Coding guidelines
There was a problem hiding this comment.
A single key is the deliberate design of this consolidation. More decisively, deriving a JWT-specific subkey would break the Developer Portal's local token verification, which shares the raw ENCRYPTION_KEY via APIP_DP_PLATFORMAPI_JWTSECRET (wired that way in this same change) — the signer would use a derived key while the portal verifies with the raw key.
There was a problem hiding this comment.
@thivindu Thanks for the clarification — that's a decisive constraint I missed. Since the Developer Portal's local JWT verification consumes the raw ENCRYPTION_KEY directly via APIP_DP_PLATFORMAPI_JWTSECRET, deriving a purpose-specific subkey on the signing side would indeed break cross-service verification. Withdrawing this comment.
✏️ Learnings added
Learnt from: thivindu
Repo: wso2/api-platform PR: 2553
File: platform-api/internal/handler/auth_login.go:102-103
Timestamp: 2026-07-09T05:06:48.412Z
Learning: In wso2/api-platform, the Platform API consolidates all encryption into a single `Server.EncryptionKey` (`ENCRYPTION_KEY`), which is intentionally reused as-is (not derived into subkeys) for local login JWT signing in `platform-api/internal/handler/auth_login.go`. This is because the Developer Portal verifies these JWTs locally using the same raw key via `APIP_DP_PLATFORMAPI_JWTSECRET`, which is wired to the same `ENCRYPTION_KEY` value. Deriving a JWT-specific subkey from `ENCRYPTION_KEY` for signing would break this cross-service verification, so do not suggest splitting or deriving a separate JWT signing secret from the encryption key.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
There was a problem hiding this comment.
cfg.EncryptionKeyis reused for both AES-256-GCM and thisHS256token, and the verifier inplatform-api/internal/server/server.gouses the same raw string. Derive purpose-specific subkeys or add separate config values so the encryption key and login JWT secret aren’t interchangeable.
I also think we should use at least two keys for internal (signing/encryption) and external (signing/encryption). What I mean as internal vs external is
- External - if you send some values signed/encrypted through an external network which is outside the product boundary. These keys are sensitive very likely to rotate more frequently. And the key rotation should not be a costly operation. Eg: Signed JWT tokens
- Internal - if you keep the encrypted/signed values internally and never sent outside the boundary. These keys are likely to stay longer without rotating. And also the key rotation is very costly. You have to re-encrypt everything when you rotate this key. Eg: Encrypted values store in db - secret manager
So lets not use single key for these two scenarios by any means. This is why we have primary keystore vs internal keystore in APIM on prem product as well. So ideally we should have two configs, but of course, the two configs can point to the same key initially by default. When doing proper production deployments, customers can configure two separate keys.
A single key is the deliberate design of this consolidation. More decisively, deriving a JWT-specific subkey would break the Developer Portal's local token verification, which shares the raw ENCRYPTION_KEY via APIP_DP_PLATFORMAPI_JWTSECRET (wired that way in this same change) — the signer would use a derived key while the portal verifies with the raw key.
Can you clarify this in a bit more detail? I didn't get this properly.
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
There was a problem hiding this comment.
The initial discussion was to consolidate all encryption keys to one.
The AI Workspace and dev portal share the JWT signing key. If we consolidate all keys to one and use that key to derive a JWT key specific for the dev portal only, the dev portal token verification will break. But if we are going to use independent keys for JWT signing and other secret encryption, this would not be as issue. We can set the same JWT signing key to the APIP_DP_PLATFORMAPI_JWTSECRET in devportal and token verification will work.
61fb8b0 to
a14af68
Compare
a14af68 to
cd3ef1b
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/ai-workspace/features/secrets-management.md`:
- Around line 358-359: Replace the truncated ENCRYPTION_KEY example with either
a complete, explicitly non-production 64-character hexadecimal value or a
clearly labeled placeholder; instruct users to paste the full output generated
by the documented openssl command.
In `@platform-api/config/config.go`:
- Around line 408-475: In resolveEncryptionKey, clear cfg.EncryptionKeyFile
before assigning the generated ephemeral key when
loadOrGenerateEncryptionKeyFile fails and the fallback succeeds, so
configuration accurately reflects that no persistent key file backs the active
key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ca212a9f-93e7-4dad-a9ab-e9153fc16de2
📒 Files selected for processing (27)
.github/workflows/ai-workspace-pr-check.ymldistribution/all-in-one/docker-compose.yamldocs/ai-workspace/configuration.mddocs/ai-workspace/features/secrets-management.mdplatform-api/README.mdplatform-api/config/config.goplatform-api/config/config.tomlplatform-api/config/config_test.goplatform-api/internal/integration/harness_test.goplatform-api/internal/repository/api_deployments_test.goplatform-api/internal/repository/subscription_repository.goplatform-api/internal/server/server.goplatform-api/plugins/eventgateway/service/websub_api_hmac_secret.goportals/ai-workspace/README.mdportals/ai-workspace/configs/config-platform-api-template.tomlportals/ai-workspace/configs/config-platform-api.tomlportals/ai-workspace/docker-compose.yamlportals/ai-workspace/production/README.mdportals/developer-portal/README.mdportals/developer-portal/distribution/docker-compose.yamlportals/developer-portal/docker-compose.platform-api.yamlportals/developer-portal/docker-compose.yamlportals/developer-portal/docs/administer/manage-organizations.mdtests/integration-e2e/README.mdtests/integration-e2e/docker-compose.sqlite.yamltests/integration-e2e/docker-compose.sqlserver.yamltests/integration-e2e/docker-compose.yaml
✅ Files skipped from review due to trivial changes (9)
- portals/ai-workspace/production/README.md
- platform-api/internal/repository/api_deployments_test.go
- tests/integration-e2e/README.md
- docs/ai-workspace/configuration.md
- platform-api/plugins/eventgateway/service/websub_api_hmac_secret.go
- portals/ai-workspace/README.md
- portals/developer-portal/docs/administer/manage-organizations.md
- portals/ai-workspace/configs/config-platform-api.toml
- platform-api/internal/integration/harness_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- platform-api/internal/repository/subscription_repository.go
74d995d to
0420d72
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@platform-api/README.md`:
- Around line 244-245: Update the README startup-checks documentation to state
that the server fails to start when demo mode is disabled and
AUTH_JWT_SKIP_VALIDATION=true, matching validateAuthConfig behavior; also add a
period after “required” to correct the run-on sentence.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b60f0366-2ade-4052-9182-27ccb4562edd
📒 Files selected for processing (30)
.github/workflows/ai-workspace-pr-check.ymldistribution/all-in-one/docker-compose.yamldocs/ai-workspace/configuration.mddocs/ai-workspace/features/secrets-management.mdplatform-api/README.mdplatform-api/config/config.goplatform-api/config/config.tomlplatform-api/config/config_test.goplatform-api/internal/integration/harness_test.goplatform-api/internal/repository/api_deployments_test.goplatform-api/internal/repository/subscription_repository.goplatform-api/internal/server/server.goplatform-api/plugins/eventgateway/service/websub_api_hmac_secret.goportals/ai-workspace/README.mdportals/ai-workspace/configs/config-platform-api-template.tomlportals/ai-workspace/configs/config-platform-api.tomlportals/ai-workspace/cypress/e2e/001-providers/003-llm-proxy-secret-management.cy.jsportals/ai-workspace/docker-compose.yamlportals/ai-workspace/production/README.mdportals/developer-portal/README.mdportals/developer-portal/distribution/docker-compose.yamlportals/developer-portal/docker-compose.platform-api.yamlportals/developer-portal/docker-compose.yamlportals/developer-portal/docs/administer/manage-organizations.mdportals/developer-portal/it/docker-compose.test.postgres.yamlportals/developer-portal/it/docker-compose.test.yamltests/integration-e2e/README.mdtests/integration-e2e/docker-compose.sqlite.yamltests/integration-e2e/docker-compose.sqlserver.yamltests/integration-e2e/docker-compose.yaml
✅ Files skipped from review due to trivial changes (9)
- .github/workflows/ai-workspace-pr-check.yml
- tests/integration-e2e/README.md
- platform-api/plugins/eventgateway/service/websub_api_hmac_secret.go
- docs/ai-workspace/configuration.md
- platform-api/config/config.toml
- portals/developer-portal/docs/administer/manage-organizations.md
- portals/developer-portal/README.md
- portals/ai-workspace/configs/config-platform-api.toml
- portals/ai-workspace/configs/config-platform-api-template.toml
🚧 Files skipped from review as they are similar to previous changes (9)
- distribution/all-in-one/docker-compose.yaml
- portals/ai-workspace/docker-compose.yaml
- tests/integration-e2e/docker-compose.sqlite.yaml
- portals/ai-workspace/README.md
- tests/integration-e2e/docker-compose.yaml
- platform-api/internal/integration/harness_test.go
- platform-api/internal/repository/subscription_repository.go
- tests/integration-e2e/docker-compose.sqlserver.yaml
- platform-api/internal/server/server.go
0a1e674 to
f4cc322
Compare
Rework Platform API encryption keys: one
ENCRYPTION_KEYfor at-rest data + a separateAUTH_JWT_SECRET_KEYRelated to - #2425
Summary
The Platform API previously used four overlapping secrets for at-rest encryption and login-token signing, each with its own env var, config key, and fallback chain:
Database.EncryptionKeyDATABASE_ENCRYPTION_KEYDatabase.SubscriptionTokenEncryptionKeyDATABASE_SUBSCRIPTION_TOKEN_ENCRYPTION_KEYDatabase.SecretEncryptionKey(+…KeyFile)PLATFORM_SECRET_ENCRYPTION_KEY/DATABASE_SECRET_ENCRYPTION_KEY_FILEAuth.JWT.SecretKeyAUTH_JWT_SECRET_KEYThis overlapping design had real inconsistencies: the JWT secret was hashed for the vault but used raw for subscription tokens; the different keys had divergent fallbacks; and the requirement/format of each key was inconsistent.
This PR settles on two clearly-scoped keys, both required and never auto-generated:
Server.EncryptionKeyENCRYPTION_KEYDatabase.*keys.Auth.JWT.SecretKeyAUTH_JWT_SECRET_KEYBoth keys are independent (no fallback between them), must be a 32-byte value (64 hex chars or base64→32 bytes), and are required in every mode — they are never generated and there is no key file.
Changes
Config & key resolution (
config/config.go)Added the top-level
Server.EncryptionKey; removed the threeDatabase.*encryption fields and their env mappings.Auth.JWT.SecretKeyis retained as the separate JWT signing key.LoadConfignow validates both keys up front (via a sharedvalid32ByteKeycheck) and fails fast with a clear error:ENCRYPTION_KEYmissingENCRYPTION_KEYmalformedAUTH_JWT_SECRET_KEYmissing (JWT auth enabled)AUTH_JWT_SECRET_KEYmalformedNo key files, no demo auto-generation, no ephemeral keys — the operator must provide both keys (env var or config file).
Call sites —
server.go(secret vault + pluginDBEncryptionKeyuseENCRYPTION_KEY; bothAuthConfig.SecretKeysites useAUTH_JWT_SECRET_KEY),subscription_repository.go(encryption + decryption →ENCRYPTION_KEY),auth_login.go(JWT signing →AUTH_JWT_SECRET_KEY), and the WebSub HMAC service (→ENCRYPTION_KEY). Removed the old SHA-256(JWT)/raw-JWT fallback divergence.Docs & deployment — updated the Platform API README, config TOML templates,
secrets-management.md,configuration.md, AI Workspace / Developer Portal READMEs, the OpenAPI 503 message, and all docker-compose files (e2e ×3, all-in-one, ai-workspace, developer-portal ×3 + IT ×2) plus the AI Workspace CI workflow. Quickstart composes mark both keys required (${VAR:?…}); test/CI/all-in-one composes supply valid 64-hex literals. The Developer Portal's JWT-verify secret (APIP_DP_PLATFORMAPI_JWTSECRET) is fed fromAUTH_JWT_SECRET_KEYso platform-issued login tokens still verify.Breaking changes
DATABASE_ENCRYPTION_KEY,DATABASE_SUBSCRIPTION_TOKEN_ENCRYPTION_KEY,PLATFORM_SECRET_ENCRYPTION_KEY,DATABASE_SECRET_ENCRYPTION_KEY_FILE. UseENCRYPTION_KEYinstead.ENCRYPTION_KEYis required in every mode (it protects DB data even in IDP mode) and is never auto-generated — startup fails if it is missing or malformed.AUTH_JWT_SECRET_KEYis retained but is now format-enforced (must be a 32-byte key — 64 hex / base64) and required when JWT auth is enabled; it is no longer auto-generated in demo mode. A former free-form passphrase will fail at startup.openssl rand -hex 32.Migration
Set
ENCRYPTION_KEYto whatever value your existing at-rest data was encrypted under — for most deployments that is the oldDATABASE_ENCRYPTION_KEY— and existing secrets, subscription tokens, and WebSub HMAC secrets keep decrypting. SetAUTH_JWT_SECRET_KEYto a valid 32-byte key (share the same value with the Developer Portal'sAPIP_DP_PLATFORMAPI_JWTSECRET).Caveats:
ENCRYPTION_KEYcan decrypt all pre-existing data — a one-time re-encryption is required (not included here).AUTH_JWT_SECRET_KEYremain valid only if you keep the same value; otherwise users re-login.Testing
go build ./...,go vet, andgo test ./config/... ./internal/repository/... ./internal/handler/...pass.config_test.gocovers both keys: missing/malformed each → error (proven required even in demo mode), both-valid → success, and thevalid32ByteKeyformat check.