Key and secret management
Start with the key and secret inventory in the release runbook. Assign an owner to each item and identify every service that uses it.
Use Azure Key Vault and managed identities where supported. Follow the runbook’s reference format rather than copying secret values into service configuration.
Secret categories
| Category | Purpose | Main control |
|---|---|---|
| Application SDK keys | Authenticate one application to supported runtime APIs | Show once, store approved hash, rotate, and revoke |
| OAuth credentials | Authenticate registered clients and preserve delegated scope | Protect client secrets and refresh tokens |
| Authentication signing material | Issue or validate supported tokens | Limit issuer access and rotate by version |
| Deployment-context signing key | Sign the public bootstrap context | Keep the key outside frontend artifacts |
| Content-protection material | Protect supported captured content | Control access, version, recovery, and deletion |
| Outbound signing keys | Sign every request VeriProof sends to customer endpoints | Private keys only on the apps that sign; public keys published for receivers |
| Webhook secrets | Sign requests for webhooks that choose HMAC | Shown once, stored encrypted, 24-hour overlap on rotation |
| Proof-provider credentials | Submit or verify Solana or Azure SQL Ledger proofs | Separate submission authority from verifier access |
| Third-party integration secrets | Connect approved external services | Scope to the integration and rotate independently |
Outbound signing keys
Each deployment, whether shared SaaS or Private Enterprise, has its own ed25519
keys. VeriProof uses them to sign outcome callbacks and event webhooks, and
receivers check those signatures against the public keys the deployment
publishes at /.well-known/veriproof-webhook-keys.json. See
How VeriProof signs requests
for the receiver side.
Each key is configured by index N:
| Setting | Holds |
|---|---|
OutboundSigning__Keys__N__KeyId | A stable key ID, such as vp-sig-2026-09 |
OutboundSigning__Keys__N__PublicKey | The 32-byte public key, base64url without padding |
OutboundSigning__Keys__N__PrivateKey | The 32-byte private seed, base64url without padding |
OutboundSigning__Keys__N__Sign | true to sign with this key, false to publish it only |
Only the Customer Portal API and Customer Workflows sign, so only they get
PrivateKey. The Ingest API and MCP server load public keys only. The Ingest
API and Customer Portal API both serve the key set, which lists every
configured public key, including keys that don’t sign yet.
In Azure, each key lives in Key Vault as two secrets,
outbound-signing--{keyId}--public-key and
outbound-signing--{keyId}--private-key, and the app settings use Key Vault
references. Deployment creates each key once and never overwrites it. For
local development, ./vdev.ps1 secrets init-outbound-signing -Env local
creates a key in the ignored config/secrets.json.
A Private Enterprise installation has two fixed key slots, vp-sig-1 and
vp-sig-2. The installer creates both keys in your Key Vault, publishes both,
and signs with vp-sig-1. Here the key ID names the slot: rotation writes a
new key into a slot rather than adding one. Both slots are always published, so
a rotation never changes the installation’s role assignments.
Rotate a signing key
Private Enterprise. Invoke-PrivateEnterpriseSigningKeyRotation.ps1 in the
release’s installation folder advances rotation by one step when that step is
due, and verifies it before it finishes:
- Prepare. Writes a new key version into the slot that isn’t signing, reloads the four Function apps, and confirms that the Customer Portal API and Ingest API publish the new public key.
- Activate. After the publish wait (two hours by default, longer than the one-hour key-set cache), both slots sign. Requests carry two signatures.
- Retire. After the overlap (24 hours by default), the previous slot stops signing. Its key stays published until the next rotation re-keys that slot.
Every earlier key version stays enabled in Key Vault, so signatures from retired keys can still be checked. After each step the command confirms that every workload is ready, every signing Key Vault reference resolves, and both key sets list the expected keys. A failed check stops the rotation where it is.
pwsh ./installation/Invoke-PrivateEnterpriseSigningKeyRotation.ps1 `
-KeyVaultResourceId <key-vault-resource-id> `
-ManagedResourceGroupId <managed-resource-group-id> `
-RotationIntervalDays 90Use -Action Status to see the slots and when the next step is due, and
-WhatIf to see what would change. Rotation state is kept as tags on the slot
secrets, so you can run the command by hand, on a schedule, or both.
To schedule it, deploy installation/customer-signing-key-rotation-schedule.json
into one of your own resource groups. It creates an Azure Automation account and
a daily schedule. Then publish the runbook:
az deployment group create `
--resource-group <operations-resource-group> `
--template-file ./installation/customer-signing-key-rotation-schedule.json `
--parameters automationAccountName=<name> `
keyVaultResourceId=<key-vault-resource-id> `
functionAppResourceIds='<four Function app resource IDs as a JSON array>'
pwsh ./installation/Register-PrivateEnterpriseSigningKeyRotationSchedule.ps1 `
-AutomationAccountResourceId <automation-account-resource-id> `
-InstallationEvidencePath ./installation-evidence.jsonThe Automation account’s managed identity can only list versions of and write new versions to the four signing secrets, and read, update, and restart the four Function apps. It can’t read, delete, or purge a secret value. Each run writes its result, without key material, to the job output; route the account’s job streams to your Log Analytics workspace to keep that record.
If a private key may have been exposed, run Prepare, Activate, and Retire with
-Action and -IgnoreWaitPeriods, then run Prepare once more so the exposed key
leaves the published set. SDK receivers refetch the key set when no key
verifies a message.
Shared SaaS and other deployments. Rotate by adding a key slot:
- Add the new key with
Signset tofalse. Wait at least an hour so receivers’ cached key sets include it. - Set
Signtotrueon the new key. Requests now carry two signatures, one from each key. - Once in-flight retries have drained, set
Signtofalseon the old key, then remove it from configuration.
If a private key may have been exposed, move through these steps without waiting for retries to drain. SDK receivers stop trusting a removed key when their cached key set refreshes, within an hour.
Private Enterprise runtime secrets
The Private Enterprise installer also creates the secrets the Function apps need at runtime. It writes each one into your Key Vault the first time and never replaces it, so a redeployment or upgrade keeps the existing value. Each Function app gets Key Vault Secrets User on only the secrets it reads.
| Secret | Holds | Read by |
|---|---|---|
encryption-master-key | AES-256 key that protects stored credentials and integration settings | Customer Portal API, Customer Workflows, Ingest API, MCP server |
ingest-content-pepper-v1 | Key that protects captured content | Customer Portal API, Customer Workflows, Ingest API, MCP server |
ingest-content-pepper-v2 | The next content key version, created ahead of rotation | Customer Portal API, Customer Workflows, Ingest API, MCP server |
session-evidence-revision-key | AES-256 key for session evidence revisions | Customer Portal API, Customer Workflows, Ingest API, MCP server |
attestation-master-key | AES-256 key for stored attestations | Ingest API |
jwt-secret | Signs portal session tokens | Customer Portal API, MCP server |
deploymentContext--signingKey | Signs the deployment context | Customer Portal API, Customer Workflows |
EvidencePackage--SigningPrivateKeyPem | ECDSA P-256 key that signs evidence packages | Customer Portal API |
The apps reference encryption-master-key by version, so adding a version
doesn’t change the key they use until you redeploy. The others resolve to their
current version.
If one of these secrets is disabled, or deleted but still recoverable, the installation stops and names it. Enable or recover it, then run the deployment again. Don’t delete them: records protected by a key can’t be read without it. The installer doesn’t rotate these secrets.
Content key versions
Captured content is protected with a numbered key version, and each record
keeps the version it was written with. Like the two signing slots, the
installer creates the next version, ingest-content-pepper-v2, at install time
and grants the same apps access to it.
The Ingest API writes new content with the version in
ContentProtection__ActivePepperVersion. When the setting is absent it writes
with v1. Every app reads a record with the version stored on that record, so
content written with any provisioned version stays readable after the active
version changes.
To move new content to v2:
- Confirm
ingest-content-pepper-v2exists and is enabled, and that every reader in the table above can read it. - Set
ContentProtection__ActivePepperVersionto2on the Ingest API Function app. The app reads the setting at startup and refuses to start if the value isn’t a whole number from 1 to 255. - After the restart, confirm new sessions record key version 2 and that earlier sessions still open.
- Keep
ingest-content-pepper-v1. Records written with it can’t be read without it.
In Private Enterprise, the deployment owns the Function app settings, so a
redeployment resets a manual change. Activating v2 there is delivered with a
VeriProof upgrade, which needs no new secret and no role change.
Customer-managed Azure ownership
The customer owns Key Vault policy, recovery settings, network access, managed-identity assignments, monitoring, and emergency access.
VeriProof documents which service needs each secret and which reference format the release supports.
Rotation
Follow the workflow for the specific credential or key:
- Identify every service that creates or uses it.
- Check how those services select a version and whether overlap is supported.
- Create and test the replacement through the approved workflow.
- Move consumers to the new version and confirm that new requests use it.
- Check whether existing records still need the old version for decryption or verification.
- Revoke or retire the old version only when its documented dependencies permit it.
- Retain the change record and validation results.
Not every key supports two active versions. Ending authentication with an old credential is also different from deleting a key needed to read retained evidence.
Deletion and recovery
Secret deletion can make data or integrations unavailable. Confirm legal holds, recovery windows, backup policy, provider behavior, and affected records before authorizing deletion.
Do not use a Key Vault purge command as a substitute for the product’s scoped deletion workflow.
Monitoring
Alert on failed secret resolution, unexpected version use, repeated authentication failures, privileged Key Vault changes, provider credential errors, and emergency access.
In a Private Enterprise installation, the health summary reports signing key age, expiry, and the next rotation step for your own monitoring.
Never place secret values in documentation, source control, command history, screenshots, diagnostic reports, evidence packs, or application logs.