Skip to main content
Vault is opt-in. Omit 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 context org_id. Split the principals:
  • Webapp: kms:GenerateDataKey only.
  • Consumer: kms:Decrypt only.
Give the webapp the same condition on kms:GenerateDataKey and do not grant it kms:Decrypt.

Alerts and incidents

  • Metric vault.operation with tags action and outcome only. Alert on an unusual volume of action=use.
  • CloudTrail alarm on Decrypt for the key ARN: a spike, and AccessDenied.
  • Logs run through vault redaction (value, plaintext, secret, ciphertext, wrappedDataKey, Plaintext, CiphertextBlob).
Incident: disable the IAM access keys, confirm CloudTrail, rotate the CMK, then re-wrap data keys. Switching provider later only re-wraps the data key. The legacy migration script is the only path that decrypts old AES plaintext.

Legacy keys

After a verified migration, run migrateOrgSecretsToVault 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.