Skip to Content
EnterpriseSecurityKey Management

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

CategoryPurposeMain control
Application SDK keysAuthenticate one application to supported runtime APIsShow once, store approved hash, rotate, and revoke
OAuth credentialsAuthenticate registered clients and preserve delegated scopeProtect client secrets and refresh tokens
Authentication signing materialIssue or validate supported tokensLimit issuer access and rotate by version
Deployment-context signing keySign the public bootstrap contextKeep the key outside frontend artifacts
Content-protection materialProtect supported captured contentControl access, version, recovery, and deletion
Outbound signing keysSign every request VeriProof sends to customer endpointsPrivate keys only on the apps that sign; public keys published for receivers
Webhook secretsSign requests for webhooks that choose HMACShown once, stored encrypted, 24-hour overlap on rotation
Proof-provider credentialsSubmit or verify Solana or Azure SQL Ledger proofsSeparate submission authority from verifier access
Third-party integration secretsConnect approved external servicesScope 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:

SettingHolds
OutboundSigning__Keys__N__KeyIdA stable key ID, such as vp-sig-2026-09
OutboundSigning__Keys__N__PublicKeyThe 32-byte public key, base64url without padding
OutboundSigning__Keys__N__PrivateKeyThe 32-byte private seed, base64url without padding
OutboundSigning__Keys__N__Signtrue 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:

  1. 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.
  2. Activate. After the publish wait (two hours by default, longer than the one-hour key-set cache), both slots sign. Requests carry two signatures.
  3. 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 90

Use -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.json

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

  1. Add the new key with Sign set to false. Wait at least an hour so receivers’ cached key sets include it.
  2. Set Sign to true on the new key. Requests now carry two signatures, one from each key.
  3. Once in-flight retries have drained, set Sign to false on 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.

SecretHoldsRead by
encryption-master-keyAES-256 key that protects stored credentials and integration settingsCustomer Portal API, Customer Workflows, Ingest API, MCP server
ingest-content-pepper-v1Key that protects captured contentCustomer Portal API, Customer Workflows, Ingest API, MCP server
ingest-content-pepper-v2The next content key version, created ahead of rotationCustomer Portal API, Customer Workflows, Ingest API, MCP server
session-evidence-revision-keyAES-256 key for session evidence revisionsCustomer Portal API, Customer Workflows, Ingest API, MCP server
attestation-master-keyAES-256 key for stored attestationsIngest API
jwt-secretSigns portal session tokensCustomer Portal API, MCP server
deploymentContext--signingKeySigns the deployment contextCustomer Portal API, Customer Workflows
EvidencePackage--SigningPrivateKeyPemECDSA P-256 key that signs evidence packagesCustomer 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:

  1. Confirm ingest-content-pepper-v2 exists and is enabled, and that every reader in the table above can read it.
  2. Set ContentProtection__ActivePepperVersion to 2 on 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.
  3. After the restart, confirm new sessions record key version 2 and that earlier sessions still open.
  4. 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:

  1. Identify every service that creates or uses it.
  2. Check how those services select a version and whether overlap is supported.
  3. Create and test the replacement through the approved workflow.
  4. Move consumers to the new version and confirm that new requests use it.
  5. Check whether existing records still need the old version for decryption or verification.
  6. Revoke or retire the old version only when its documented dependencies permit it.
  7. 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.

Last updated on