tools.vault and the process boots without loading the AWS SDK. Development omits vault unless VAULT_PROVIDER=local. Once that is set, VAULT_KMS_CAPABILITY and VAULT_LOCAL_MASTER_KEY are required, with no fallback. The same capability is required in every environment where vault is enabled, including test.
Authorization
There is no
vault:use scope and no GraphQL or HTTP decrypt path. A harness “Test key” action, when built, calls use() on the consumer.
recordVaultJob is the only writer of vault_jobs. A fabricated job id fails job_not_found. Another org’s job fails job_org_mismatch. Neither calls Decrypt.
Envelope
One data key per secret. The provider only wraps and unwraps that key. Shared code builds one AES-256-GCM envelope (v, alg, wrappedDataKey, iv, authTag, ciphertext). Additional authenticated data is the canonical encryption context {org_id, name, secret_id}. AWS KMS receives the same context. A blob moved to another org or row fails closed with decrypt_failed.
local is allowed only when NODE_ENV is development or test. Production and staging refuse to boot if the provider is local. Local wrap still honors the process capability gate.
Event matrix
use() passes a Buffer to the callback and zeroes it in finally. That wipe is best effort once the caller copies the bytes into a string. The vault module does not coerce the full secret to a string. last4 is the last four bytes only. Values shorter than 8 bytes are value_too_short.
Key policy
IAM is documented here and is not provisioned by this repo. Use one CMK with automatic rotation on. Require encryption contextorg_id. Split the principals:
- Webapp:
kms:GenerateDataKeyonly. - Consumer:
kms:Decryptonly.
kms:GenerateDataKey and do not grant it kms:Decrypt.
Alerts and incidents
- Metric
vault.operationwith tagsactionandoutcomeonly. Alert on an unusual volume ofaction=use. - CloudTrail alarm on
Decryptfor the key ARN: a spike, andAccessDenied. - Logs run through vault redaction (
value,plaintext,secret,ciphertext,wrappedDataKey,Plaintext,CiphertextBlob).
Legacy keys
After a verified migration, runmigrateOrgSecretsToVault with --finalize. Finalize purges legacy ciphertext from the journal. Then delete ORG_SECRETS_ENCRYPTION_KEY and ORG_SECRETS_ENCRYPTION_KEY_RING from Railway. Revert restores the legacy row only before finalize. After finalize, --revert exits journal_finalized.
Refract has no organization_secrets table. The script logs a skip and exits 0.