Keys and operations

Store and limit keys

Keep keys out of source and normal logs, use managed key services where appropriate, and grant only required encrypt, decrypt, sign, or verify operations.

Where a key lives and who can use it are part of the cryptographic design. The strongest algorithm cannot protect a key that is lying around. Most real-world crypto failures are key-handling failures, not broken math.

Start with the two obvious exclusions. Keys never go in source control. Keys never go in normal logs. A key committed once lives in git history forever, so deleting the file changes nothing. Treat any committed key as compromised and rotate it.

You can check your own history for leaked private keys:

git log -p --all -S 'BEGIN PRIVATE KEY' -- '*'
# any hit here means the key is burned, even if the file is gone

Where keys should live

There is a ladder, and each step up is better than the one below:

  1. Environment variables injected at deploy time. Better than files in the repo.
  2. A secret manager with access control and an audit log. Better than environment variables.
  3. A KMS or HSM that performs the operation without ever releasing the key. Better still.

That last step changes the model. A managed key service can expose only a sign operation and keep the raw key inside the service. Your code sends bytes and receives a signature. Nothing that runs in your process can copy a key that never enters your process.

Be careful, though. Now the access policy and the audit log are part of your cryptographic system. Misconfigure the policy and the HSM protects an operation that anyone can invoke.

Grant operations, not keys

Permissions should name operations, not just keys. An identity that verifies webhooks needs verify, never sign. A service that encrypts uploads needs encrypt, not decrypt.

Written out, a small system looks like this:

identity            key            allowed
api-server          webhook-hmac   verify
upload-worker       storage-kek    encrypt
restore-job         storage-kek    decrypt
release-pipeline    signing-v3     sign

Notice that no identity can do everything. The upload worker can add encrypted files but cannot read them back. The restore job can read but cannot write.

This matters most in CI. Say a signing key is available as plaintext to the whole CI job, including the formatting and test steps. A compromised test dependency can now copy the same authority you use to sign releases. Scope the key to the one signing step. Or better, keep it in a service that only that step’s identity may call.

Separate keys by environment and by purpose. And keep data and keys under different access paths whenever the architecture allows it. A backup that contains both the ciphertext and its keys protects nothing.

Try this on your own project: build a key inventory with purpose, owner, environment, storage, allowed operations, users, rotation, and revocation. Capture the effective policy and one audit event for an authorized test operation. Then attempt a forbidden operation, like decrypting with a sign-only identity, and confirm you get a denial.

Lesson completed