Source Signing Secrets Are Now Encrypted at Rest
Every inbound source signing secret is now stored encrypted with AES-GCM under a key scoped to your organization. Existing secrets are migrated automatically, signature verification is unchanged, and reveal, rotate and export all work the same way. There is nothing for you to change.
The Secret That Can't Be Hashed
A source's signing secret is the credential that proves a webhook really came from Stripe, GitHub, Shopify or whoever sent it. Anyone holding it can sign a payload that your source will accept as genuine. It is the most sensitive value attached to a source.
Passwords and API keys can be stored as one-way hashes, because the server only needs to check that a presented value matches. Signing secrets don't work that way. To verify an HMAC signature, Hookbase has to recompute the signature over the raw request body, and that needs the original secret on every single ingest request. The same goes for header-echo and Basic-auth schemes. Hashing the secret would make verification impossible.
Until now that meant source signing secrets were stored as plaintext in the database. Access to that database is tightly restricted, but defense in depth means a credential shouldn't depend on a single layer. Outbound endpoint secrets were already encrypted at rest. Inbound source secrets now are too.
What Changed
- Encrypted with AES-GCM. Every source signing secret is stored as ciphertext and decrypted only at the point it is used: when an inbound request is verified, when you reveal or export it, or when Hookbase signs a test event for you.
- A key scoped to your organization. Each organization's secrets are encrypted under their own derived key. Ciphertext copied from one organization can't be decrypted as another's.
- Every write path encrypts. Creating, updating, importing and rotating a source, creating one from an integration template, and creating one through the AI assistant all store the secret encrypted.
- Existing secrets are migrated automatically. A background sweep finds secrets stored before this change and encrypts them in place. You don't need to do anything.
- Cached copies are ciphertext too. The ingest path caches source configuration so it doesn't hit the database on every request. That cache now holds the encrypted value.
Nothing for You to Change
Your webhooks keep arriving and verifying exactly as before. The vendor still signs with the same secret, and Hookbase decrypts that secret in memory to check the signature. No secrets were rotated as part of this change, and no vendor settings need updating.
The API surface for sources is unchanged:
GET /api/sources/{id}and the source list still returnhasSigningSecretandsigningSecretLast4, so you can tell which secret is configured without exposing it.GET /api/sources/{id}/reveal-secretstill returns the full plaintext secret, under the same permissions as before: a member, admin or owner role, plus write scope for API keys.POST /api/sources/{id}/rotate-secretstill generates a new secret for providers where Hookbase issues it, and returns it once in the response.- Export with sensitive values included still gives you the plaintext secrets, so an export file stays portable between organizations and environments. Ciphertext never leaves Hookbase.
With the Node SDK, for example, nothing in your code changes:
// Returns the plaintext secret, decrypted server-side
const { signingSecret } = await hookbase.sources.revealSecret(sourceId);
// Generates, stores (encrypted) and returns a new secret
const rotated = await hookbase.sources.rotateSecret(sourceId);
There is one small new validation. When you create, update or import a source, a signing secret that has the shape of Hookbase's stored ciphertext format is rejected with an error telling you to paste the secret itself (a 400 on create and update, a validation error on import). That stops ciphertext from one environment being pasted into another and stored as if it were a real secret.
Failing Closed
The most important design decision was what happens if a stored secret can't be decrypted, for example because a row was corrupted.
The tempting shortcut is to treat that like "no secret configured". On ingest, that would be exactly wrong. A source with signature verification turned on would quietly start accepting unsigned requests: the opposite of what the secret is there for.
So ingest fails closed. If a source verifies signatures and its secret can't be decrypted, the signature check fails, and the event is handled like any other invalid signature. If the source rejects invalid signatures, the request is rejected. The decryption failure is logged so we can see it.
Other paths fail safely in their own way. A source list never errors because of one bad row: that source still shows hasSigningSecret: true, but without the last four characters. An export redacts a secret it can't decrypt instead of failing the whole export or leaking ciphertext. A test send reports a signing error instead of sending an unsigned request.
How the Migration Works
Encrypting existing secrets had to be safe to run against live sources that might be edited or rotated at the same moment. The sweep runs on a schedule. Each pass reads a batch of rows that still hold a plaintext secret, encrypts each one, and writes it back only if the row still holds the plaintext it read. If you rotated or edited a secret in between, the sweep doesn't overwrite your change. That row is either already encrypted by your write, or picked up on the next pass.
Until the sweep reaches a secret, both forms work side by side. A value in the encrypted format is decrypted, and a legacy plaintext value is used as-is. That is why there was no cutover window and no moment when verification depended on the backfill having finished. Once every row was encrypted, each pass became a single query that finds nothing to do.
What This Protects Against, and What It Doesn't
Encryption at rest protects against database-level exposure: a leaked backup, a misdirected query result, or read access to the database without the separately held key. In any of those, the signing secrets are ciphertext.
It doesn't change who can see a secret through Hookbase itself. Revealing or exporting a secret is still gated by role and API-key scope, and those are still the controls to review. If you use IAM policies, the reveal and rotate actions take the same policy checks as before.
It also isn't a substitute for rotating a secret that you think has leaked from somewhere else, such as a CI log or a vendor dashboard screenshot. For secrets Hookbase generates, rotate with rotate-secret. For vendor-issued secrets, regenerate the secret at the vendor and paste the new value into the source.
Related Reading
- Sources guide: signature verification
- Sources API reference, including reveal and rotate
- Enterprise security guide, including source secret rotation