feat(security): Pro env-secrets component for .env encryption#505
feat(security): Pro env-secrets component for .env encryption#505dawsontoth wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the envSecrets component to handle cluster-shared env-secret encryption and decryption. It includes logic to clone the shared private key from the leader node during bootstrapping and registers a decryptor to handle encrypted .env values at runtime. The review feedback highlights two important improvements: first, the cloned private key file should be written with restrictive permissions (0o600) to prevent unauthorized local access; second, the parsed envelope JSON should be validated before accessing its properties to prevent runtime type errors when processing malformed envelopes.
|
Reviewed; no blockers found. |
| }); | ||
| const dir = keysDir(); | ||
| mkdirSync(dir, { recursive: true }); | ||
| writeFileSync(join(dir, ENV_SECRETS_PRIVATE_KEY_NAME), privateKey, { mode: 0o600 }); |
There was a problem hiding this comment.
This seems like the crux of this pair of PRs and the part that undermines the goal of making the secrets inaccessible through the filesystem within the container and inaccessible via the operations API. I think we fundamentally have to have this be Fabric owner and orchestrated to achieve this goal.
Implements the Pro half of Pro-gated .env secret encryption; fills the dormant decrypt hook added in harper core (PR #1528, get via the core submodule once merged). - security/envSecrets.ts: a built-in component that generates/loads a cluster-shared RSA-4096 env-secrets keypair, registers the get_secrets_public_key operation (main thread), and registers a hybrid RSA-OAEP-SHA256 + AES-256-GCM decryptor into core's loadEnv hook (per worker) so enc:v1 .env values decrypt into process.env at runtime. The private key is registered in core's key store so the existing get_key operation can serve it to a cloning node. - cloneNode: cloneEnvSecretsKeys() clones the shared private key from the leader (mirrors cloneJWTKeys) so every node decrypts the same ciphertext. - bin/harper.js: register envSecrets as a built-in component. Gated by component presence: without harper-pro the core hook stays dormant. The cryptography (envelope round-trip, wrong-kid + GCM-tamper rejection) is verified independently of the build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- cloneNode: write the cloned env-secrets private key with mode 0o600 (was default umask / group+world-readable), matching ensureKeypair - envSecrets: validate the decrypted envelope shape (reject null/array/ missing k|iv|ct|tag) so a malformed envelope yields a clean error instead of a cryptic TypeError Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review blocker: the security-critical deny paths had no committed automated tests. Extract the envelope cryptography into a core-free module (security/envSecretCrypto.ts) so it is directly unit-testable without the core submodule, and add unitTests/security/envSecretCrypto.test.mjs covering: - happy-path round-trip (incl. >2KB PEM proving the hybrid envelope, unicode, empty) - wrong-kid rejection - GCM tamper rejection (ciphertext and auth-tag bit-flips) - malformed-envelope rejection (non-JSON, wrong type, missing field) envSecrets.ts now delegates to the pure module (decryptEnvelope / fingerprintOf). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
db6edea to
0ec059d
Compare
Pro companion to harper#1528 (core contract: the dormant
enc:v1:decrypt hook). This PR provides the cryptography, the cluster-shared keypair, and theget_secrets_public_keyoperation that fill that hook.Where this fits (the full picture)
Harper's env-secret feature is one client contract (public key +
enc:v1:envelope) with a swappable provider per deployment tier:.envlocally. Great for air-gapped / hands-off; the private key lives on the container disk.enc:v1:recognition + decrypt hook (Apache).enc:v1client contract.SecretsPublicKey/DeploymentSecrets/ResolveDeploymentEnvops.Important
Depends on harper#1528. It imports
registerEnvSecretDecryptorandENV_ENCRYPTED_PREFIXfrom core, which land in #1528. To build/integration-test, bump thecore/submodule to a commit that includes #1528 (currently pinned older). Left unbumped here to avoid pointing the submodule at an unmerged sha.What this adds
security/envSecrets.ts— a built-in component:keys/dir, and registers the private key in core's key store so the existingget_keyop can serve it to cloning nodes;startOnMainThread()registersget_secrets_public_key(returns public key + fingerprint + scheme — no sensitive material) and the decryptor;start()(per worker) loads the key and registers the hybrid RSA-OAEP-SHA256 + AES-256-GCM decryptor into core'sloadEnvhook, soenc:v1:values decrypt intoprocess.envat runtime.cloneNode/cloneNode.ts—cloneEnvSecretsKeys()clones the shared private key from the leader (mirrorscloneJWTKeys) so every node decrypts the same ciphertext. This is what makes the "dedicated cluster-shared keypair" model work.bin/harper.js— registersenvSecretsas a built-in component.Gating
Component presence (the existing idiomatic Pro gate, like
replication/license/analytics). Without harper-pro, no decryptor is registered and core's hook is dormant — encrypted values are skipped with a warning.Verification
The cryptography — the only novel, must-be-correct logic — is verified end-to-end by a standalone round-trip (client-encrypt per the documented envelope ↔
envSecretsdecrypt):kid;oxlint and prettier pass on the changed files. Full
tsc/integration is pending the core submodule bump (see note above) — the wiring (imports,server.registerOperation,startOnMainThread/start,get_keyclone) is modeled directly onkeyService.ts,usageLicensing.ts, andcloneJWTKeys.Envelope / client format
Documented in
core/docs/env-secret-encryption.md(added in #1528):enc:v1:<base64url(JSON{kid,k,iv,ct,tag})>.🤖 Generated with Claude Code
Related PRs — env-secret encryption
One
enc:v1:client contract across tiers (public key + envelope; values encrypted before they leave the client):.envprotection in the operations API (masking +get_env_keys/set_env_value/delete_env_value)enc:v1:decrypt hook + envelope contract (stacked on #1527)get_secrets_public_key, runtime decryptor)ClusterSecretsops (key/secrets never on the container)enc:v1encryption)Follow-up prototype: #509 hardens the private key + env vars against compromised customer code (plugin / SSR RCE) by keeping the key in host scope and exposing secrets only through a config-declared, per-component
scope.secretsaccessor — grounded in how Harper isolates components (node:vm + SES), so no extra process is needed.