Skip to content

feat(security): Pro env-secrets component for .env encryption#505

Draft
dawsontoth wants to merge 3 commits into
mainfrom
feat/env-secret-encryption
Draft

feat(security): Pro env-secrets component for .env encryption#505
dawsontoth wants to merge 3 commits into
mainfrom
feat/env-secret-encryption

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Pro companion to harper#1528 (core contract: the dormant enc:v1: decrypt hook). This PR provides the cryptography, the cluster-shared keypair, and the get_secrets_public_key operation 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:

  • Self-hosted (this PR + core): the node holds an on-disk keypair and decrypts .env locally. Great for air-gapped / hands-off; the private key lives on the container disk.
  • Fabric (managed): the control plane holds the key and the secrets outside the container and injects decrypted values at container start — the container holds neither the key nor the ciphertext (closes the on-disk-key gap). Same enc:v1 client contract.
    • HarperFast/central-manager#405 — Fabric secret store, per-cluster keypair, SecretsPublicKey / DeploymentSecrets / ResolveDeploymentEnv ops.
    • HarperFast/host-manager#128 — injects the decrypted env into the container at start.

Important

Depends on harper#1528. It imports registerEnvSecretDecryptor and ENV_ENCRYPTED_PREFIX from core, which land in #1528. To build/integration-test, bump the core/ 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:
    • generates a cluster-shared RSA-4096 env-secrets keypair on first boot (leader), persists it to the keys/ dir, and registers the private key in core's key store so the existing get_key op can serve it to cloning nodes;
    • startOnMainThread() registers get_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's loadEnv hook, so enc:v1: values decrypt into process.env at runtime.
  • cloneNode/cloneNode.tscloneEnvSecretsKeys() clones the shared private key from the leader (mirrors cloneJWTKeys) so every node decrypts the same ciphertext. This is what makes the "dedicated cluster-shared keypair" model work.
  • bin/harper.js — registers envSecrets as 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 ↔ envSecrets decrypt):

  • round-trips short values, a >2 KB PEM (proves the hybrid envelope is required), unicode, embedded quotes, and empty;
  • rejects a wrong kid;
  • rejects a tampered ciphertext (GCM auth).

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_key clone) is modeled directly on keyService.ts, usageLicensing.ts, and cloneJWTKeys.

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


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.secrets accessor — grounded in how Harper isolates components (node:vm + SES), so no extra process is needed.

@gemini-code-assist gemini-code-assist 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

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.

Comment thread cloneNode/cloneNode.ts Outdated
Comment thread security/envSecrets.ts Outdated
Comment thread cloneNode/cloneNode.ts Outdated
@claude

claude Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

Comment thread security/envSecrets.ts Outdated
Comment thread security/envSecrets.ts
});
const dir = keysDir();
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, ENV_SECRETS_PRIVATE_KEY_NAME), privateKey, { mode: 0o600 });

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.

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 closed this Jun 30, 2026
@dawsontoth dawsontoth reopened this Jul 1, 2026
@dawsontoth dawsontoth marked this pull request as ready for review July 1, 2026 11:46
@dawsontoth dawsontoth requested a review from a team as a code owner July 1, 2026 11:46
dawsontoth and others added 3 commits July 1, 2026 11:06
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>
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