Secrets Rotation Without a Vault Cluster: Automation for Symfony and Next.js

#secrets rotation automation
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Founder & Lead Developer

Expert in software development and legacy code optimization

The database password in production at one SaaS company we reviewed had not changed since the service launched in 2021. It was in the deployment config, in three engineers' shell histories, in a Slack message from the original migration, and in a screenshot in a Confluence runbook. When one of those engineers left, nobody rotated it, because rotating it meant touching four services and nobody was confident which ones would break. That is the real reason secrets rotation automation gets skipped: not that teams disagree it matters, but that the tooling feels heavy and the blast radius feels unknowable. Secrets rotation automation does not require a HashiCorp Vault cluster and a dedicated platform engineer. This post shows a pragmatic rotation pipeline for Symfony and Next.js that you can run with the infrastructure you already have.

The goal here is not maximalist. A three-person team serving European customers does not need dynamic per-request credentials and a sidecar agent on every pod. They need their long-lived secrets to change on a schedule, the change to happen without an outage, and an audit trail that proves it happened. That is achievable with a scheduled job, your existing secret store, and a small amount of discipline about how your applications read configuration.

Why Secrets Rotation Automation Matters More Than a Fancier Vault

The security value of rotation is bounded but real. If a credential leaks through a log, a stale laptop, a compromised CI run, or a departed contractor, rotation limits how long that leaked value remains useful. A secret that rotates every 30 days turns an indefinite compromise into a 30-day window. Auditors know this, which is why SOC 2, ISO 27001, and increasingly NIS2-driven supplier questionnaires ask specifically how often you rotate credentials and whether the process is automated or manual. Manual rotation that depends on someone remembering is treated, correctly, as no rotation at all.

The trap teams fall into is believing rotation requires dynamic secrets: short-lived credentials minted on demand by a Vault that brokers every database connection. Dynamic secrets are excellent and they are also a large operational commitment. You are now running a clustered, highly available secrets broker whose downtime takes down every service that depends on it. For most mid-size SaaS teams that is the wrong trade. Scheduled rotation of static secrets gives you most of the security benefit for a fraction of the operational cost, and it is the right place to start. You can graduate to dynamic secrets later if your threat model justifies it.

The Core Pattern: Two-Slot Rotation

Every safe rotation scheme relies on one idea: at the moment of rotation, both the old and the new secret must be valid simultaneously, so that running processes never hit a window where their credential is wrong. This is the two-slot, or overlapping-validity, pattern, and it applies to almost every secret type.

The rotation runs in four ordered phases. First, create the new secret at the provider while the old one still works. For a database user that means creating a second password or a second user; for an API key it means issuing a new key alongside the existing one. Second, publish the new secret to your secret store and roll it out to running services so they start using it. Third, verify that traffic is actually flowing on the new secret. Fourth, and only then, revoke the old secret. The window between phase one and phase four is the overlap window, and getting it right is the whole game. Skip the overlap and you get the classic rotation outage: the new secret is live, half your pods still hold the old one, and you are paged at 2am for connection failures you caused yourself.

Where Secrets Live: Pick One Store

Before automating rotation you need a single source of truth for secrets that your applications read at deploy or boot time. You almost certainly already have one. If you deploy on AWS, use Secrets Manager, which has rotation hooks built in. On GCP, use Secret Manager. If you are on a single VPS or a small fleet, SOPS with age encryption, committed to your repository and decrypted at deploy, is a legitimate and lightweight choice. The Symfony Secrets vault (secrets:set, secrets:decrypt-to-local) is fine for application-level secrets but is not where you want rotation logic to live.

The hard rule: applications read secrets from the store, never from a value baked into an image or a long-lived .env committed somewhere. If a secret is hardcoded in a built artifact, rotation means a rebuild and redeploy, and that friction is exactly why rotation gets skipped. Reading configuration at process start from environment variables that your platform injects from the store is enough; you do not need hot reloading for a 30-day cadence.

Rotating the Database Password in Symfony

For a Symfony application using Doctrine, the database credential is the highest-value secret and the best place to prove the pattern. PostgreSQL and MySQL both let a single role hold a password, so the cleanest two-slot approach is to alternate between two application users, app_user_a and app_user_b, rather than juggling two passwords on one user.

The rotation job, run as a scheduled task, does the following. It determines which user is currently active by reading the store. It sets a fresh strong password on the inactive user with ALTER ROLE app_user_b WITH PASSWORD '...' and confirms that user has the same grants as the active one. It writes the inactive user's DSN into the store as the new active value. It triggers a rolling restart of the application so each instance picks up the new DSN from its environment at boot. It then runs a health check that opens a connection and executes a trivial query as the new user. Once that passes across all instances, the previously active user becomes the standby, and its password can be scrambled on the next cycle. Because Doctrine opens connections lazily and a rolling restart never takes all instances down at once, there is no moment where live traffic has no working credential.

The one Symfony-specific detail worth flagging: if you use a connection pool such as PgBouncer in front of Postgres, existing pooled connections keep using the old credential until they are recycled. Set a server_lifetime short enough that the pool drains within your overlap window, or issue an explicit pool reload as part of the rollout. Skipping this is the most common cause of a rotation that looks successful but leaves stale connections alive.

Rotating API Keys and JWT Signing Keys in Next.js

A Next.js application typically holds two kinds of sensitive material: outbound API keys for third-party services, and signing keys for the sessions or tokens it issues itself. They rotate differently.

Outbound API keys follow the same overlap pattern. Most providers (Stripe, SendGrid, payment and email vendors) let you hold multiple active keys. The job mints a new key through the provider's API or dashboard automation, writes it to the store, redeploys so server-side code reads the new value, verifies a real call succeeds, then revokes the old key. Keep these keys strictly server-side; a key that has ever been exposed to the browser bundle is compromised by definition and rotation will not save it. This is the same principle we cover in our guide to API security for B2B SaaS: the secret must never cross to the client.

Signing keys for JWTs need overlap on the verification side. You rotate by introducing a new signing key while keeping the old key in the set of accepted verification keys, identified by a key ID (kid) in the token header. New tokens are signed with the new key; tokens already in the wild still verify against the old key until they expire. Once the maximum token lifetime has passed, the old verification key is retired. This is exactly the JWKS rotation model, and you do not need an identity provider to implement it: a small ordered list of keys in your store, with the newest used for signing and all of them accepted for verification, is sufficient.

Make It a Job, Not a Ritual

The automation is deliberately boring. A scheduled task, run by your CI platform's cron, a systemd timer, or a cloud scheduler, executes a rotation script per secret type on a cadence. Each run is idempotent: if it fails partway, rerunning it either completes or safely no-ops, because each phase checks current state before acting. Crucially, the job emits a structured log of what it rotated and when, and that log is your audit evidence. When a customer's security team or an auditor asks for proof of rotation, you point at the log, not at someone's memory.

Build in a kill switch. A ROTATION_PAUSED flag in the store that the job checks first lets you stop rotation during an incident or a freeze without disabling the scheduler. And alert on the job not running, not only on it failing; a rotation job that silently stopped firing three months ago is the failure mode that auditors find and you do not.

Where to Draw the Line

Resist scope creep. Rotate the credentials that would do real damage if leaked: database users, third-party API keys, signing keys, and any service-to-service credentials. Do not try to rotate things that are not really secrets, like public configuration, or things whose rotation requires human coordination you cannot automate yet. Start with the database password and one API key, prove the overlap pattern works end to end including the rollback path, then extend. A rotation pipeline covering your three highest-value secrets, running reliably every month, beats an ambitious dynamic-secrets platform that the team is too nervous to turn on.

If you are weighing whether your current secret handling would survive an enterprise security review, or you want help wiring a rotation pipeline into an existing Symfony or Next.js stack without downtime, this is the kind of pragmatic hardening work we do. Our code quality consulting and custom software development engagements regularly include exactly this. Email hello@wolf-tech.io or visit wolf-tech.io to talk it through.