Encrypting sensitive data creates a long-term dependency that is easy to overlook: the application must retain the right decryption capability for as long as the ciphertext remains useful. Replacing an encryption key without planning for that dependency can make old data unreadable. Keeping one key forever avoids that immediate problem but makes future key changes harder and can increase the amount of data tied to one key.

The practical solution is key versioning. Each ciphertext records which key version protects it. New encryption uses the current key, while decryption can temporarily use older versions for data that has not yet been migrated.

This article explains that model, how to rotate keys without a risky all-at-once rewrite, what rotation does and does not accomplish, and how to verify that an old key can eventually be retired.

Treat encryption and decryption as different lifecycle decisions

A common design assumes there is simply “the encryption key”:

plaintext -> encrypt with K -> ciphertext
ciphertext -> decrypt with K -> plaintext

That works until K must change.

Suppose a service has encrypted customer records for two years with key K1. The team creates K2 and immediately deletes K1. New writes work, but every existing record protected by K1 is now unreadable.

The mistake is not rotating the key. The mistake is treating the key used for new encryption and the keys still required for decryption as if they must be the same set.

A safer lifecycle separates those decisions:

new encryption:  K2

decryption:
  version 1 -> K1
  version 2 -> K2

K1 is no longer active for new encryption, but it remains available for authorized decryption until its dependent ciphertext has been migrated or expired.

This gives key rotation a useful invariant: changing the active encryption key must not silently remove the ability to decrypt data that the application still needs.

Put a key identifier beside the ciphertext

The application needs a reliable way to select the correct decryption key. Do not discover it by trying keys until one succeeds. Store a non-secret key identifier with the encrypted record.

A simplified record might look like this:

key_version: 2
nonce:       ...
ciphertext:  ...

The key version is metadata, not the key itself. A value such as 2 can map to a protected key held by a key-management component:

1 -> K1
2 -> K2
3 -> K3

On a new write, the application asks for the current encryption version and stores that version with the resulting ciphertext. On a read, it uses the stored version to request the corresponding decryption capability.

This explicit mapping matters operationally. Without it, a key change can leave the application unable to tell which historical key a record needs. It also makes migration measurable because the system can count how much ciphertext still references each version.

The exact identifier format is implementation-specific. A monotonically increasing version, a key-management-service identifier, or another stable opaque identifier can all work. The important property is that the identifier unambiguously selects the intended key and remains available for the lifetime of the ciphertext.

Rotate by changing writes first

For ordinary planned rotation, the lowest-risk transition is usually to change new writes before rewriting old data.

Assume K1 is the current key and the team wants to introduce K2.

First, provision K2 in the protected key store and make sure the application can use it. Do not retire K1 yet.

Next, make K2 the active key for encryption. From this point forward:

new record -> encrypt with K2 -> store version 2
old record -> still version 1 -> decrypt with K1

The system is now rotated for new writes even though historical data still depends on K1. This is useful because the key change no longer requires every existing record to be rewritten during one deployment window.

After the write path is stable, historical ciphertext can be migrated separately. For each eligible record, the system decrypts it with its recorded old key and encrypts the plaintext again with the current key, using the encryption scheme’s required fresh parameters such as a nonce. The record is updated only after the new ciphertext has been produced successfully.

This is a decrypt-and-re-encrypt migration. It temporarily exposes plaintext inside the trusted component that already has authority to decrypt the record. Production designs should keep that boundary as narrow as practical and avoid writing plaintext to migration logs, queues, temporary files, or error messages.

Make migration restartable rather than heroic

Large migrations fail for ordinary reasons: processes restart, records are concurrently updated, dependencies time out, or deployment windows end. A key-rotation design should expect interruption.

Versioned ciphertext makes the migration naturally restartable. A worker can select records whose key_version is older than the current version, migrate a bounded batch, and continue later. Records already on the current version need no work.

The difficult case is concurrent modification. Imagine a migration worker reads version 1 of a record while a user updates that same record and creates new ciphertext. If the worker later writes its migrated copy without checking, it can overwrite the user’s newer data.

Use the application’s normal concurrency-control mechanism to prevent that outcome. For example, an update can require that the record revision or ciphertext version still matches what the worker originally read. If it changed, the worker discards its result and retries from the current record.

The security lesson is broader than any particular database feature: cryptographic migration does not exempt a job from normal data-consistency rules.

Retire an old key only after proving it has no required dependents

Deleting an old key is the irreversible part of rotation. Treat it as a separate operation with stronger evidence than “the migration job finished.”

Before retiring K1, determine whether any required ciphertext still references version 1. That check should include every place covered by the key lifecycle, not only the primary application table. Depending on the system, encrypted data may also exist in archives, asynchronous jobs, exports, retained snapshots, or other stores.

Backups deserve explicit treatment. A database backup containing ciphertext from version 1 may require K1 after restoration. Deleting the key while retaining that backup can turn an apparently successful restore into permanent data loss.

There are several valid policies. A system might retain old decryption keys for the lifetime of corresponding backups, re-encrypt backup material under a different hierarchy, or allow old backups and their keys to expire together. The correct choice depends on recovery requirements and the key-management architecture. What matters is that key retirement and data retention are designed together.

A useful retirement condition is:

retire key version N only when
no data the organization still intends to decrypt depends on N

Verify that condition with inventory and restore testing rather than assumption.

Rotation reduces some risks, not every encryption risk

Planned rotation limits how long one key is used for new data and provides a practiced mechanism for changing keys. That operational capability is valuable when a key must later be replaced.

Rotation does not undo past exposure. If an attacker copied K1 and ciphertext protected by K1, deleting the application’s copy of the key does not erase the attacker’s copy. Re-encrypting retained data under K2 reduces future dependence on K1, but it cannot make previously exposed plaintext or key material secret again.

Rotation also does not compensate for an attacker who still controls the application and can ask it to decrypt data with authorized keys. That threat requires controls around application access, key-use authorization, isolation, monitoring, and incident response.

The threat model therefore matters. Routine rotation is mainly a lifecycle and exposure-management control. Rotation after suspected compromise is an incident-response action and may require additional steps, such as identifying what data and key material were accessible, removing the attacker’s access, and deciding which ciphertext must be re-encrypted.

Keep key state explicit

A useful key-management model has more than “exists” and “deleted.” Even if the underlying platform uses different terminology, applications benefit from distinguishing states such as:

active       -> allowed for new encryption and decryption
legacy       -> allowed for decryption, not new encryption
retired      -> no longer available to the application

This reduces accidental reuse of an old key. Once K1 becomes legacy, a configuration error should not quietly make it the default for new writes again.

For higher-impact systems, separate permissions can reinforce the model. A normal write path may need access only to the current encryption key, while a migration or compatibility path may need controlled access to legacy decryption keys. Whether that separation is worth the operational complexity depends on the application and key-management service.

Avoid embedding raw encryption keys in source code or ordinary application configuration merely to make versioning convenient. Versioning identifies keys; it does not change the requirement to protect the key material itself.

Test the lifecycle, not just encryption

A unit test that encrypts and decrypts with one key proves very little about rotation. Test transitions that resemble the real lifecycle.

A useful integration test can create data under version 1, switch new writes to version 2, verify that both generations remain readable, migrate a version-1 record, and verify that it now references version 2. It should also confirm that new writes never fall back to version 1 after the switch.

Before actual retirement, test recovery using representative backups or snapshots and the keys they require. The goal is to discover hidden dependencies while the old key still exists.

Monitoring should make the migration observable. Useful signals include the number of records remaining on each legacy version, migration failures, attempts to request unknown key identifiers, and unexpected encryption with a legacy version. These signals help distinguish a slow migration from a broken one.

Avoid rotation schedules that exceed their purpose

More frequent rotation is not automatically more protective. Every rotation introduces operational work: provisioning keys, changing active versions, migrating or retaining old ciphertext, testing recovery, and eventually retiring keys. A schedule so aggressive that teams bypass controls or accumulate untracked legacy keys can make the system harder to reason about.

Choose rotation triggers from the threat model and operational requirements. A simple application with short-lived encrypted data may need a modest lifecycle. A system with long-lived sensitive records, strict key-use boundaries, or credible compromise concerns may justify more automation and stronger separation.

Regardless of frequency, the important capability is predictable replacement. Teams should know how to introduce a new key, stop using the old one for writes, identify remaining dependencies, migrate where necessary, test recovery, and retire the old key deliberately.

Conclusion

Encryption key rotation is not a single key swap. It is a controlled transition between generations of ciphertext.

Store a stable key identifier with each encrypted record. Use one current key for new encryption while retaining only the legacy decryption capabilities that existing data still requires. Migrate old ciphertext in restartable batches, account for concurrent updates and backups, and retire a key only after verifying that no data you intend to recover still depends on it.

That model turns rotation from a risky maintenance event into a normal part of the encryption lifecycle.