Skip to content

Key Rotation Tool

By Romi Fadli, S.Kom. 8 min read Security & Secrets
On this page
Four identical grey keys on the left each become a key of its own colour on the right, every new key marked with a green check; below, a purple key stays behind a red bar

Key Rotation Tool is a single command that replaces the internal credentials of one environment, key by key, and proves each new credential works before it moves on to the next. By default it only shows its plan; when it does run, it takes a backup first, gives every key a value of its own, and puts any key it cannot prove back the way it was.

Rotating a password looks like editing a configuration file. For a database, an object store, or a search engine it is not, because the real password lives inside their data, and the file only matters on the day the data directory is empty.

At a glance

FormCommand-line script
LanguageBash with embedded Python for JSON configuration, driving Docker and openssl
StatusStable, in production
Source codePrivate
What the tool is, what it is built with, and its state.

What it does

A. Rotates internal credentials, each to its own value

It covers every internal credential of the stack: databases, caches, object storage, and the search engine. Every key receives a fresh random value of its own, so a rotation also ends any password that several services used to share. The values are generated in a form that needs no escaping in a URL, an environment file, or a shell. Groups can be rotated together or one at a time.

B. Proves every new credential before moving on

For each group the order is fixed: change the credential where it really lives, write it into the files of the owner and of every consumer, recreate the containers, and then connect with the new credential. Each file is copied before it is written, and the copies are restored on failure or dropped on success. The database login is made the way a real client makes it, so a wrong password cannot pass by accident. In the search engine the superuser is changed last, because it authenticates every other change. A group whose container is not running is skipped, not failed. If the connection with the new credential fails, the old password is set again, the files are restored, and the services are restarted on their old values. One failed group does not stop the others; the summary lists what succeeded, what failed, and what was skipped.

C. Backs up first, in the cheapest safe mode

Before changing anything it runs the backup toolkit, and a failed backup cancels the rotation. It chooses a full backup when there is none yet, when the latest one is older than a threshold, or when its age cannot be read, and an incremental one otherwise, and prints the reason for its choice. The backup can be skipped with a flag labelled as not recommended, for the day the backup system itself is what is broken.

D. Shows the plan without showing a secret

Without --apply nothing changes; the command prints the backup mode it would use and why, and each group with a short hash of its old and new value. Secrets never appear in the tool's own output or in its log, yet two runs can still be compared.

How I use it

bash
rotate-keys                          # dry run: the plan only
rotate-keys --apply                  # back up, then rotate everything
rotate-keys --apply --only storage   # one group
rotate-keys --list                   # available groups
StoreWhere the change is madeHow it is proven
PostgreSQLALTER USER inside the databasea query that logs in with the new password
Redisits configuration, applied on restartan authenticated PING
Object storageits root credentials, applied on restartlisting buckets with the new key, before its consumers restart
Search engineits security API, per user, the superuser lastan authenticated request with the superuser's new password
Shared encryption keyboth services at onceboth containers running again
How each kind of credential is changed and proven.
Exit codeMeaning
0every group that ran was rotated and proven, or nothing was asked to run
1the backup failed and nothing was rotated, at least one group failed and was rolled back, or the environment folder does not exist
2an unknown argument
How the command reports its outcome.

Design decisions

A dry run is the default, and a backup comes first. Touching production credentials by accident has to be impossible, so the command does nothing until it is told to. A rotation that works can still leave a service unable to read what it wrote, so it never starts without a fresh backup unless I skip it on purpose. The backup mode is chosen, not fixed: when every rotation forced a full backup, two runs 38 minutes apart produced two nearly identical full copies of about 294 MB, and with a fixed retention they pushed older restore points out. An incremental run of the same data took 66 MB, and it restores as a chain of one full and one incremental with every table intact. The operation meant to protect the data had been eroding its history.

It works from an allowlist, never by searching for the old value. Two unrelated secrets can hold the same value, so a rotation that searched for the old value and replaced it could break an integration it never meant to touch. So the command touches only the keys it names, in the files it names, and gives each of them a different value, which ends the sharing for good. A password stored inside a connection URL is URL-encoded, so a plain search would not even find it; the tool takes the URL apart, replaces the password field, and puts it back together, including a URL nested deep inside a JSON file.

A credential counts as rotated only when something has logged in with it. An exit code of zero from the command that changed it proves nothing. The one exception is the shared encryption key, which nothing logs in with; there the proof is that both services start again. Containers are recreated, not merely restarted, because a service whose configuration is mounted from the host would otherwise keep running with a connection pool that still uses the old password, and report its database as down while its file looks correct. The object store's consumers are restarted only after the new key has been proven, so a failed attempt never leaves them holding a key the store does not accept. Each group stands alone: one that fails is rolled back to its old password and files, and the rest carry on.

Knowing what not to rotate is part of the tool. Some secrets cannot be replaced by a random value without consequences that go beyond a restart, so each of those decisions carries its reason in the code, and nobody changes one without understanding the cost. The shared encryption key is rotated only when no queued job still holds a value encrypted with the old one; otherwise it is skipped and named, to be run again once the queue is empty.

Small reads and writes are made carefully, because one bad one is silent. Values are read with carriage returns and surrounding quotes stripped. A key is rewritten by itself, in place, so the file keeps its owner and mode, and a filter that finds nothing no longer ends the run under strict error handling, as it once did without a word. The list of groups lives in a variable that does not shadow a read-only one built into the shell, after the listing once printed a user id instead of the groups.

Specifications

InputsAn environment (production by default); optionally specific groups, a forced full backup, a skipped backup, or a different full-backup age threshold, also settable from the environment
OutputsNew credentials applied in each store and in every consumer's configuration, and a summary of what succeeded, failed, or was skipped
SafetyDry run by default; backup before rotating; allowlist of keys and files; per-group verification and rollback; secrets shown only as short hashes
DowntimeA short restart of each service whose credentials change
RequirementsDocker, openssl, and Python 3 on the host; the backup toolkit, writing to a backup path built from the project name; network access to pull the storage client's image
Inputs, outputs, and guarantees of the tool.

The story of rotating every internal credential without breaking a single service.

The toolkit that takes the backup every rotation starts with.

The release procedure that keeps production changing safely; rotation runs beside it on its own schedule.

The script follows the shape I give every small tool, in its Bash variant.

Why a rotation never starts without a fresh point to return to.

Why the telemetry user has a password of its own, which is rotated with the rest.

Terms

Credential rotation
Replacing a password or key with a new value on a schedule, so that a leaked old value stops being useful.
Dry run
Running a command so that it reports what it would change without changing anything.
Allowlist
An explicit list of what may be touched; anything not on it is left alone.
Rollback
Returning a change to its previous state when it could not be completed or proven.
Fingerprint
A short hash of a value, enough to tell two values apart without revealing either.

Technologies covered

  • Bash
  • Elasticsearch
  • PostgreSQL
  • Redis
  • Docker
  • MinIO

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

A purple root circle linked to a blue and an orange intermediate circle, which lead to a signed document with a green seal and to a clock.

Aurograf

DevOps

The digital signature platform of the City Government of South Tangerang: signers prove who they are, sign PDFs to PAdES B-LTA with certificates from the platform's own authorities, and anyone can verify the result — version 2, which I designed, built and operate.

10 min read Read more →

Related Articles

A stack of Markdown documents and an orange arrow leading to a browser page with a side menu, a heading, paragraphs, a dark code panel, and a table of contents with its active entry highlighted

A single script that turns a project's Markdown documentation into a static site that opens from a folder with no server or internet: a menu, a table of contents that follows the reader, tables that never truncate, code with a copy button, and links that keep working.

7 min read Read more →
An old layout with a grey banner and a new structure with a green banner, each holding a blue database, a purple document store, and an orange telemetry store; three coloured paths pass through one export package marked with a green check

Four commands that move a stack from its old layout into a new structure across major versions: export without changing the source's data, import each store the way its version allows, verify what really arrived, and rename modules while moving their data along.

11 min read Read more →
Three image layer stacks, each with a version tag of its own colour, blue, orange, and green, each leading to an archive file banded in the same colour

Commands that version every module of a stack on its own, build its image under a tag that means exactly one build, refuse to reuse a version or swallow a failed compile, and archive images by name and tag so versions never overwrite each other.

7 min read Read more →