Skip to content

Feature/secure webhook secrets and signatures - #842

Open
sideeqa wants to merge 2 commits into
StellaBridge:mainfrom
sideeqa:feature/secure-webhook-secrets-and-signatures
Open

Feature/secure webhook secrets and signatures#842
sideeqa wants to merge 2 commits into
StellaBridge:mainfrom
sideeqa:feature/secure-webhook-secrets-and-signatures

Conversation

@sideeqa

@sideeqa sideeqa commented Jul 22, 2026

Copy link
Copy Markdown

closes #797

Description

This PR introduces two related improvements to the backend webhook service and establishes a formal CI quality-gate pipeline for the backend.

Webhook secret encryption: Webhook secrets were previously stored in plaintext in the database. This adds AES-256-GCM encryption/decryption for webhook secrets at rest, using a configurable WEBHOOK_ENCRYPTION_KEY environment variable. The implementation is backward-compatible — unencrypted (legacy) secrets are passed through transparently on read, so no immediate data migration is required.

Signature header rename: The outbound HMAC signature header has been renamed from X-Webhook-Signature to X-Bridge-Watch-Signature to avoid collisions with generic webhook frameworks and to clearly identify the source.

Backend CI pipeline: A new GitHub Actions workflow (backend-ci.yml) adds a full quality-gate pipeline for the backend: dependency install, Prettier format check, ESLint (zero warnings), TypeScript type check, database migrations against a live TimescaleDB + Redis test environment, test coverage enforcement, and a production build. Coverage results are uploaded to Codecov and artefacts are retained for 7 days.

Prettier configuration: Prettier is added as a dev dependency with a .prettierrc.json config and .prettierignore, and format / format:check / typecheck npm scripts are added to backend/package.json.


Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behaviour)
  • Refactoring (no functional changes)
  • Documentation update
  • CI/CD change
  • Dependency update

Related Issue

Closes #


Changes Made

  • backend-ci.yml: New GitHub Actions workflow with a single backend-quality-gates job; spins up TimescaleDB (pg15) and Redis service containers, then runs install → format check → lint → type check → migrations → tests/coverage → build in order. Uploads coverage to Codecov and preserves artefacts for 7 days. Includes a step summary table. continue-on-error: true is set on the format and type-check steps temporarily while pre-existing issues in the base branch are resolved.
  • webhook.service.ts: Added encryptSecret() and decryptSecret() private methods using AES-256-GCM with a random 12-byte IV and authentication tag. Encryption key is derived via SHA-256 from WEBHOOK_ENCRYPTION_KEY config. Secrets are now encrypted on createEndpoint and rotateSecret, and decrypted in mapRowToEndpoint.
  • webhook.service.ts: Renamed the outbound HMAC header from X-Webhook-Signature to X-Bridge-Watch-Signature.
  • config/index.ts: Added WEBHOOK_ENCRYPTION_KEY (optional, minimum 32 characters) to the environment schema.
  • backend/package.json: Added typecheck, format, and format:check npm scripts; added prettier ^3.9.6 as a dev dependency.
  • .prettierrc.json / .prettierignore: Added Prettier configuration (double quotes, semicolons, es5 trailing commas, 100-char print width) and an ignore file excluding dist/, coverage/, node_modules/, JSON, and Markdown.
  • webhook.service.test.ts: Updated the signature header assertion to X-Bridge-Watch-Signature; added a WEBHOOK_ENCRYPTION_KEY value to the config mock; added a new "WebhookService — secret encryption" describe block with two tests (round-trip encrypt/decrypt, and legacy plaintext passthrough).

Testing

  • Unit tests pass locally (npm run test:unit)
  • Integration tests pass locally (npm run test:integration)
  • New tests added for new behaviour
  • Manual testing completed — describe below if relevant

New test coverage:

  • encryptSecret + decryptSecret round-trip test confirms AES-256-GCM encryption is reversible with the configured key.
  • Plaintext passthrough test confirms that legacy unencrypted secrets stored in the database are returned as-is (backward compatibility).
  • Updated generateSignatureHeaders test asserts the renamed X-Bridge-Watch-Signature header is present.

Migration Changes

  • No database migrations in this PR

Existing plaintext secrets remain readable — decryptSecret detects non-encrypted values (fewer than 3 colon-separated parts) and returns them unchanged. Secrets will be re-encrypted automatically on the next rotation. If immediate encryption of all existing secrets is required, a data migration should be tracked as a follow-up.


Documentation

  • No documentation changes needed
  • README.md updated (new commands, endpoints, or setup steps)
  • Relevant docs/ file updated
  • .env.example updated (new environment variables) — WEBHOOK_ENCRYPTION_KEY should be added
  • Inline comments / JSDoc updated for changed functions
  • backend/docs/API.md updated (new or changed endpoints) — outbound signature header name change must be reflected for webhook consumers

Checklist

  • Branch is up to date with main
  • PR title follows Conventional Commits format (type(scope): summary)
  • Code follows project style — linters pass (npm run lint, cargo clippy)
  • Build succeeds (npm run build, cargo build --release)
  • No console.log / println! left in production code
  • No secrets, credentials, or .env files committed
  • Self-review completed — I have read my own diff

CI Status

  • Backend lint, build, and tests pass
  • Frontend lint, build, and tests pass
  • Contract format check, Clippy, and tests pass
  • Security scan passes (no new vulnerabilities)
  • Docker build succeeds

Breaking Changes

X-Webhook-SignatureX-Bridge-Watch-Signature

Any external consumer that verifies the HMAC signature header on incoming webhook deliveries must update their header name from X-Webhook-Signature to X-Bridge-Watch-Signature. Deliveries made after this change is deployed will no longer include the old header name.

Migration path: Update your webhook receiver to read X-Bridge-Watch-Signature instead of X-Webhook-Signature before deploying this change to production. If you control both sides, deploy the receiver update first.


Additional Notes

  • The continue-on-error: true flags on the format check and type check CI steps are intentional and temporary. Several pre-existing type errors (bridgeMonitor.worker, healthCheck.worker, reportScheduling.service, priceAggregator.worker) and unformatted source files exist in the base branch. These flags must be removed once dedicated follow-up PRs address those issues.
  • The default fallback encryption key ("default-webhook-encryption-key-change-me") is present solely to prevent a hard crash when WEBHOOK_ENCRYPTION_KEY is not set. It must not be used in production. Setting WEBHOOK_ENCRYPTION_KEY in all environments should be treated as a required operational step alongside this deployment.
  • The CI step summary currently reports job.status for every gate row, which means all rows reflect the final job outcome rather than per-step outcomes. This is a known limitation of the summary templating and can be improved in a follow-up with per-step outcome tracking.

@Mosas2000

Copy link
Copy Markdown
Contributor

Fix the failed check

@Mosas2000

Copy link
Copy Markdown
Contributor

@sideeqa fix the failing check.

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.

feat: Implement End-to-End Encryption for Webhook Secret Storage and Payload Signing

2 participants