Skip to content

feat(env): dormant decrypt hook + enc:v1 contract for env-secret encryption#1528

Merged
kriszyp merged 3 commits into
mainfrom
feat/env-secret-encryption
Jul 1, 2026
Merged

feat(env): dormant decrypt hook + enc:v1 contract for env-secret encryption#1528
kriszyp merged 3 commits into
mainfrom
feat/env-secret-encryption

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1527 (base: feat/protect-env-files). Core (Apache) half of Pro-gated .env secret encryption. The companion harper-pro PR implements the cryptography, keypair, and get_secrets_public_key operation.

What & why

The .env protection in #1527 is editor-only ("smoke and mirrors"). This adds the contract for real encryption: clients encrypt a secret value with a cluster-shared public key, the ciphertext (enc:v1:…) is stored and replicated verbatim, and the runtime decrypts it into process.env. A secret value is then never plaintext to the operations API, logs, replication payloads, or on disk.

The crypto and the private key live entirely in Harper Pro. Core only provides the contract and a dormant hook — so this PR changes no behavior on its own.

Changes

  • utility/envFile.tsENV_ENCRYPTED_PREFIX (enc:v1:) + isEncryptedEnvValue().
  • resources/envSecretDecryptor.ts (new) — registerEnvSecretDecryptor() / getEnvSecretDecryptor() / clearEnvSecretDecryptor(). A dependency-free per-process registry the Pro component fills in.
  • resources/loadEnv.ts — for each enc:v1: value, decrypt via the registered decryptor; if none is registered (no Pro), skip the value with a warning so the app fails on a missing var rather than receiving ciphertext. Plaintext values are unchanged.
  • docs/env-secret-encryption.md (new) — the enc:v1: envelope format (hybrid RSA-OAEP-SHA256 + AES-256-GCM), client flow with a Node reference, key model, and threat model.

Gating

Dormant without Pro: no decryptor registered → loadEnv behaves exactly as before. The Pro env-secrets component (companion PR) registers the decryptor and is gated by component presence (the existing idiomatic Pro gate).

Tests

  • unitTests/utility/envFile.test.jsisEncryptedEnvValue recognition (prefix, case-sensitivity, non-strings).
  • unitTests/resources/loadEnv.test.js — registry register/get/clear; and loadEnv via a fake scope: plaintext passthrough, decrypt-with-decryptor, skip-without-decryptor, skip-on-decryptor-error.

oxlint, prettier, and full tsc all pass; the env + loadEnv suites are green.

Not in this PR (companion harper-pro PR)

Keypair generation/distribution (rides the existing JWT-keypair clone path), the get_secrets_public_key operation, the hybrid decrypt provider that fills this hook, and bin/harper.js component registration.

🤖 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):

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for encrypted environment variables (prefixed with enc:v1:) that are decrypted at runtime using a registered decryptor. The feedback recommends throwing a fatal error instead of silently skipping variables when no decryptor is registered or when decryption fails, to prevent the application from starting up in an insecure or misconfigured state. Corresponding updates to the unit tests and documentation are also suggested.

Comment thread resources/loadEnv.ts Outdated
Comment thread resources/loadEnv.ts
Comment thread unitTests/resources/loadEnv.test.js
Comment thread unitTests/resources/loadEnv.test.js
Comment thread docs/env-secret-encryption.md Outdated
@claude

claude Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@dawsontoth dawsontoth force-pushed the feat/env-secret-encryption branch from 8e0282e to ad6253f Compare June 29, 2026 21:41
@dawsontoth dawsontoth force-pushed the feat/protect-env-files branch from f04f5a8 to b58bbe7 Compare June 29, 2026 21:41

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the idea of reading an ~/harper/.env (independent of app, safe from deploy clobbering) is independent of this PR.

This is an **opt-in, Pro-gated** feature. The cryptography and the private key live entirely in the
Harper Pro **env-secrets** component. Core only:

- recognises the `enc:v1:` value prefix ([`utility/envFile.ts`](../utility/envFile.ts) →

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this.

@kriszyp

kriszyp commented Jul 1, 2026

Copy link
Copy Markdown
Member

Why is this closed?

@kriszyp kriszyp reopened this Jul 1, 2026
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Why is this closed?

I thought this wasn't what you wanted?

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.

@dawsontoth dawsontoth marked this pull request as ready for review July 1, 2026 11:48
@dawsontoth

Copy link
Copy Markdown
Contributor Author

This plus the latent pro feature would enable non-Fabric clusters to use this encryption. And we could sort out an alternative mode that hoists the protection outside the container next. Does that sound like a viable path, @kriszyp ?

dawsontoth and others added 2 commits July 1, 2026 11:04
…yption

Adds the core (Apache) half of Pro-gated .env secret encryption: clients
encrypt secret values with a cluster public key, the ciphertext is stored
and replicated verbatim, and the runtime decrypts it into process.env. The
crypto and private key live entirely in the Harper Pro env-secrets
component; core only provides the contract and a dormant hook:

- utility/envFile.ts: ENV_ENCRYPTED_PREFIX ('enc:v1:') + isEncryptedEnvValue
- resources/envSecretDecryptor.ts: register/get/clear hook for a decryptor
- resources/loadEnv.ts: decrypt enc:v1 values via the registered decryptor;
  with none registered, skip the value with a warning (so the app fails on a
  missing var rather than receiving ciphertext)
- docs/env-secret-encryption.md: envelope format + client flow + threat model

Dormant without Pro: no decryptor is registered, so behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review: make an undecryptable enc:v1 value non-silent by logging at
error level, while keeping load non-fatal (skip). Chosen over a fatal throw
so the node still boots — the bad value remains fixable via set_env_value
(a fatal throw at boot would deadlock remediation), and a non-Pro node isn't
crashed by a replicated encrypted value. Docs updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
env is the first consumer of the enc:v1 hook, not the only one — the
upcoming secrets store and deploy-credential resolution share it. Rename
before this ships in a release so the extension point never needs a
breaking rename.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kriszyp kriszyp deleted the branch main July 1, 2026 23:01
@kriszyp kriszyp closed this Jul 1, 2026
@kriszyp kriszyp reopened this Jul 1, 2026
@kriszyp kriszyp changed the base branch from feat/protect-env-files to main July 1, 2026 23:02
@kriszyp kriszyp merged commit 1b45db9 into main Jul 1, 2026
51 of 54 checks passed
@kriszyp kriszyp deleted the feat/env-secret-encryption branch July 1, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants