A Credential Rotation Policy: When and How Without Breaking Things

Rotate database passwords, API keys, and payment tokens at least every 90 days, using a written inventory of where each secret lives and two overlapping keys so nothing breaks, with immediate rotation when someone leaves or a .env file leaks.
A working credential rotation policy rests on four rules: a written inventory of every secret, where it is used, and who owns it; a 90-day maximum cycle for database passwords, API keys, and payment gateway tokens; rotation with two overlapping keys so the old and new secrets work side by side for a short window and nothing stops; and immediate rotation within hours when someone with access leaves or a .env file leaks. SSH keys belong to people rather than servers, so they are removed the day their owner leaves. Without the inventory you will rotate a secret and find out two days later that a forgotten script on another server broke because it still uses the old one, and that exact fear is why most companies never rotate anything.
This guide explains what to rotate, how often, how to do it step by step without downtime, and what to do in the first hour after a leak.
What Gets Rotated, and When
Secrets are not equally dangerous, and the cadence should follow the impact of a leak rather than convenience. This table is a reasonable starting point for a company running a custom system, a payment gateway, and corporate email:
| Secret | Routine cadence | Rotate immediately when | Impact of a leak |
|---|---|---|---|
| Payment gateway secret key | Every 90 days | Any suspicion, a developer leaves | Refunds issued or transaction data exposed |
| Database password | Every 90 days | .env leak, a developer leaves | All customer data read or deleted |
| Third-party API keys (SMS, WhatsApp, storage) | Every 90 days | .env or repository leak | Surprise bills, messages sent in your name |
| Personal SSH keys | Yearly or on device change | Owner leaves, device lost | Full root access |
| Laravel APP_KEY | Rarely and carefully | Confirmed leak | Forged sessions, decrypted data |
| Control panel passwords | Every 180 days with 2FA | Anyone who knows it leaves | Full control of the service |
Ninety days is not a magic number, but it is a practical balance. Anything shorter wears the team out until people start working around it, and anything longer means a stolen secret stays valid for a full quarter. A note on Laravel's APP_KEY: changing it invalidates sessions and anything encrypted with Crypt, so if you must rotate it, use the APP_PREVIOUS_KEYS setting available since Laravel 11 so older data stays readable during the transition.
Inventory First: Where Every Secret Lives
Before any rotation, build a simple table, even an encrypted text file or a vault in a shared password manager, listing for each secret its name, the service that issued it, every place it is read from (production server, queue server, CI, a developer laptop), its responsible owner, and the date it was last rotated. The result usually surprises people. A typical inventory for a company with a single ERP turns up 25 to 40 secrets spread across 4 to 6 locations.
Here is a quick way to find forgotten locations on your servers:
# Find any file holding a distinctive fragment of the old key
grep -rl "sk_live_4f9a" /var/www /etc /home /root 2>/dev/null
# Check cron and systemd environment entries
grep -r "DB_PASSWORD\|API_KEY" /etc/cron.d /etc/systemd/system 2>/dev/null
# And the repository history (secrets committed by mistake)
git log -p | grep -n "sk_live_\|password=" | head
If a secret shows up in git history, treat it as leaked and rotate it. Removing it in the latest commit does not remove it from history, and anyone who cloned the repository already has it.
Zero-Downtime Rotation With Overlapping Keys
Rotation breaks things when people swap the secret at the source and in the application at the same moment, so any delay between the two means failed requests. The fix is four phases in which both secrets work together:
- Create the new secret while keeping the old one active. Most payment gateways and API providers allow two active keys at once.
- Deploy the new secret to every location in the inventory, then restart long-running processes such as queue workers.
- Watch for 24 to 48 hours to see whether anything still uses the old key. Most providers show a "last used" time per key.
- Revoke the old secret once its usage drops to zero.
Example: a MySQL Password
MySQL 8.0.14 and later support two passwords for the same user, a feature designed for exactly this:
-- Phase 1: add the new password and keep the current one
ALTER USER 'app'@'10.0.0.%' IDENTIFIED BY 'New#Pass2026' RETAIN CURRENT PASSWORD;
-- Phase 2: update .env on every server, then
-- php artisan config:cache && php artisan queue:restart
-- Phase 4: once confirmed, drop the old one
ALTER USER 'app'@'10.0.0.%' DISCARD OLD PASSWORD;
Example: SSH Keys
An SSH key is replaced rather than rotated. Add the new key to authorized_keys, test logging in with it from a new terminal before closing the current session, then delete the old one. More importantly, each person should have their own key with their name in the comment instead of one key shared by the whole team:
ssh-ed25519 AAAAC3Nz... ahmed@laptop-2026
ssh-ed25519 AAAAC3Nz... deploy@ci
Removing someone's access then becomes a single line instead of changing a key five people depend on.
When Someone Leaves: The First-Hour Checklist
A departure is not necessarily hostile, but the personal laptop and local clones of the repository leave with the person. Run this list on their last day, not a week later:
- Remove their key from
authorized_keyson every server, using the inventory to know where. - Disable their accounts in Git, the cloud console, the payment gateway dashboard, and email.
- Rotate every secret they could read, including database passwords and API keys in any .env that sat on their machine.
- Change shared panel passwords if any exist, and better yet stop sharing them entirely.
- Review login logs for the last 30 days for anything unusual.
When a .env File Leaks: No Time for the Normal Cycle
A .env file can leak through an accidental push to a public repository, a web server serving it as a static file, or a breach. In that situation, order the work by direct financial damage:
- Payment gateway keys within the first hour, since they turn into financial loss fastest.
- Keys for billed services such as SMS, storage, and AI APIs. One SMS is cheap, but 100,000 of them are not.
- The database password, making sure the user is restricted to an IP address rather than
%. - APP_KEY and the remaining secrets.
Then prevent a repeat by making sure the web server never serves hidden files:
location ~ /\.(?!well-known) { deny all; }
Test from outside with curl -I https://example.sa/.env. You should get 403 or 404, never 200.
Where Secrets Live Between Rotations
Rotation loses its value if the new secret gets sent over a WhatsApp chat or pasted into a shared Google Drive file. Pick one approved home for secrets and treat every other copy as derived rather than original. For small companies a shared password manager with a vault for the technical team and per-person permissions is enough, while larger teams benefit from a tool like HashiCorp Vault or the CI platform's own secrets store, where the secret is injected into the deployment environment at runtime and never written to the repository.
Three simple rules prevent most leaks:
- Put
.envin.gitignorefrom the very first commit, alongside an.env.examplefile that holds variable names with empty values. - Set the file permissions on the server to
640with the application user as owner, so no other user on the same machine can read it. - Run a scanner like
gitleaksbefore every push, which stops the secret before it ever leaves a developer's laptop:
gitleaks protect --staged --redact
# Or a one-off full scan of the repository history
gitleaks detect --source . --redact
Across many teams, the first full gitleaks scan of a two-year-old repository turns up between 3 and 10 old secrets everyone had forgotten, and each one deserves immediate rotation because it has been exposed the whole time to anyone with repository access.
Reduce the Number of Secrets in the First Place
The best secret is one that never needs rotating. Restrict the database user to your application server's IP address so a leaked password is useless from outside. Turn on IP allowlists in payment gateway and API provider dashboards where they exist. Each cloud server has a fixed address, and if you need to separate a service onto its own address, an additional IP costs 400 SAR a year. Keep the database listening on a private network rather than a public address, and close port 3306 at the firewall. With these layers in place, a leaked password becomes an incident you handle calmly instead of a disaster.
Start This Week
- Write the inventory with every secret, its locations, its owner, and its last rotation date.
- Set a recurring 90-day calendar reminder assigned to a named person, not "the team".
- Rotate one low-risk secret using the overlapping-key pattern to test the process.
- Write the departure checklist and the leak checklist in one file everyone knows how to find.
- If you are planning to move your system to a Saudi server, use the move as a chance to rotate every secret at once, and review pricing and plans to choose the right size.


