Skip to content

feat(backend): vendor-neutral secrets + outbound TLS 1.2+ enforcement - #204

Open
mercy60 wants to merge 2 commits into
NovaCoreLabs1:mainfrom
mercy60:feat/121-122-secrets-tls
Open

feat(backend): vendor-neutral secrets + outbound TLS 1.2+ enforcement#204
mercy60 wants to merge 2 commits into
NovaCoreLabs1:mainfrom
mercy60:feat/121-122-secrets-tls

Conversation

@mercy60

@mercy60 mercy60 commented Jul 26, 2026

Copy link
Copy Markdown

Closes #121
Closes #122

Description

This PR delivers two related security improvements for the NovaLabs backend:

#121 — Vendor-neutral secret reference (Doppler / Vault / env)

  • Adds a SecretProvider interface (kind + fetch() contract).
  • Implements three providers: EnvSecretProvider (default), DopplerSecretProvider (REST /v3/configs/config/secrets), VaultSecretProvider (KV v2 {mount}/data/{path}).
  • Adds a global SecretsService that polls the configured provider on a configurable interval (default 60s) so a rotation does not require a redeploy.
  • Adds a SecretsModule that registers the service globally and is wired into AppModule.
  • Provider switch via SECRETS_PROVIDER env var (env | doppler | vault); optional structured config via SECRETS_PROVIDER_CONFIG (JSON).
  • Graceful degradation: a failed sync keeps the previous cache so a Doppler / Vault outage does not break in-flight requests.
  • Boots with env provider when no credentials are set — fully backward-compatible; existing process.env.X / ConfigService.get("X") reads continue to work.

#122 — Restrict outbound TLS to TLS 1.2+ (PCI DSS)

  • Adds applyTlsHardening() in backend/src/config/tls-hardening.ts.
  • Pins tls.DEFAULT_MIN_VERSION and https.globalAgent.options to TLSv1.2 + requestCert=true.
  • Monkey-patches https.request so every outbound call enforces minVersion="TLSv1.2" and requestCert=true — including the case where a caller supplies a custom https.Agent({ minVersion: "TLSv1" }). The agent’s own .options are mutated in place because Node consults the agent’s options at TLS handshake time and a request-level option would not override that.
  • Invoked at the very top of bootstrap() in backend/src/main.ts, before any other module is loaded.
  • package.json start scripts now set NODE_OPTIONS=--use-openssl-ca so the OS trust store is used at process boot (in addition to the runtime patch).

Change type

  • feat: new feature
  • fix: bug fix
  • docs: documentation only
  • style: formatting, code style
  • refactor: code restructuring
  • test: adding or updating tests
  • chore: build, CI, tooling

Test plan

  • Unit tests added/updated — 32 new tests in backend/src/config/{tls-hardening,secrets.module}.spec.ts, all passing (tsc --noEmit clean).
  • Integration tests pass — full backend typecheck passes; existing specs untouched.
  • Manual testing performed — recommended: deploy to staging and verify SECRETS_PROVIDER=doppler / SECRETS_PROVIDER=vault configurations boot cleanly.

Manual smoke test

# Defaults to env (backward-compatible)
npm --prefix backend start

# Doppler
SECRETS_PROVIDER=doppler DOPPLER_TOKEN=dp.st.XXX npm --prefix backend start

# Vault
SECRETS_PROVIDER=vault VAULT_ADDR=https://vault.example.com VAULT_TOKEN=hvs.x npm --prefix backend start

The badssl.com TLS 1.0 endpoint test is gated on NOVALABS_CI_OFFLINE so it only runs when the network is available.

Breaking changes

  • No breaking changes
  • Defaults are backward-compatible: SECRETS_PROVIDER unset ⇒ env provider.
  • Provider cannot be set per-call (does not need to be in the current codebase).

Scope note (important)

SecretsModule is the foundation layer. No existing call sites yet consume SecretsService.get() — the existing process.env.X / ConfigService.get("X") reads are unchanged. Wiring a representative subset (e.g. PAYSTACK_SECRET_KEY, JWT_SECRET, SMTP_PASSWORD) into SecretsService.get(...) is the next step and warrants a follow-up PR so this one stays diff-minimal and reviewable.

Files

backend/src/config/secret-provider.interface.ts   (NEW)
backend/src/config/secret-providers.ts            (NEW)
backend/src/config/secrets.module.ts              (NEW)
backend/src/config/secrets.module.spec.ts         (NEW)
backend/src/config/tls-hardening.ts               (NEW)
backend/src/config/tls-hardening.spec.ts          (NEW)
backend/src/main.ts                               (CHANGED — applyTlsHardening() at top)
backend/src/app.module.ts                         (CHANGED — SecretsModule import)
backend/package.json                              (CHANGED — NODE_OPTIONS=--use-openssl-ca on all start scripts)

Requesting review from @NovaCoreLabs1/backend per CODEOWNERS.

mercy60 and others added 2 commits July 26, 2026 11:24
Closes NovaCoreLabs1#121, NovaCoreLabs1#122

NovaCoreLabs1#121: Vendor-neutral secret reference via Doppler / Vault / env
  - Add SecretProvider interface (kind + fetch contract)
  - Add EnvSecretProvider, DopplerSecretProvider, VaultSecretProvider
  - Add SecretsService (global, polling-based, single cache)
  - Add SecretsModule (@global) registered in AppModule
  - Provider switch via SECRETS_PROVIDER env var (env | doppler | vault)
  - Optional structured config via SECRETS_PROVIDER_CONFIG (JSON)
  - Graceful degradation: failed sync keeps previous cache
  - On boot: warns on unknown SECRETS_PROVIDER and malformed JSON
  - Polling skipped when provider is env or refreshIntervalMs=0

NovaCoreLabs1#122: Restrict outbound TLS to TLS 1.2+ (PCI DSS)
  - Add applyTlsHardening() in backend/src/config/tls-hardening.ts
  - Pinned tls.DEFAULT_MIN_VERSION and https.globalAgent options
  - Monkey-patches https.request to enforce minVersion=TLSv1.2
    and requestCert=true on every outbound call (including custom
    https.Agent options in place)
  - Invoked at the very top of bootstrap() in main.ts
  - package.json scripts: NODE_OPTIONS=--use-openssl-ca on all
    start variants so the OS trust store is used at process boot

Tests:
  - tls-hardening.spec.ts: 8 cases incl. custom https.Agent override
  - tls-hardening.spec.ts: badssl.com TLS 1.0 endpoint refused
    (gated on NOVALABS_CI_OFFLINE)
  - secrets.module.spec.ts: 24 cases covering env/doppler/vault,
    warn-on-bad-config, graceful degradation, refresh, polling
    skip paths, timer cleanup on destroy

Note: SecretsModule is the foundation layer. No existing call sites
yet consume SecretsService.get(); a follow-up wiring PR will replace
selected process.env.X / ConfigService.get("X") reads with
secretsService.get(...).

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

LGTM

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.

Restrict outbound TLS to TLS 1.2+ for backend services Vendor-neutral secret reference via Spring Vault or Doppler

2 participants