Security 12 min read Markdown

How to Rotate a Leaked Laravel APP_KEY Without Breaking Your App

A leaked APP_KEY is not just an encryption problem, it can become remote code execution. But rotating it carelessly logs out every user and can permanently destroy encrypted data. Here is how APP_PREVIOUS_KEYS lets you rotate the key gracefully, what actually breaks, and the exact steps to re-encrypt your data and cut off an attacker.

Matt King
Matt King
July 26, 2026
Last updated: July 26, 2026
How to Rotate a Leaked Laravel APP_KEY Without Breaking Your App

You pushed a commit, opened an old branch, or read a support ticket, and there it is in plain text: your production APP_KEY. Now you have two bad options in front of you. Leave the key in place and hope nobody noticed, or change it and risk logging out every user and shredding your encrypted database columns.

Neither is acceptable, and neither is necessary. Since Laravel 11 there is a clean way to rotate the key that keeps users logged in and keeps your encrypted data readable, as long as you do it in the right order. This guide walks through what the key actually touches, how to measure your blast radius before you change anything, and the exact rotation procedure that cuts off an attacker without taking your app down.

Short version: put the current key into APP_PREVIOUS_KEYS, generate a new APP_KEY, deploy, re-encrypt any stored ciphertext, and then remove the old key. For a confirmed leak you also need to retire that previous key quickly, because it stays valid while it is in the list.


Why a leaked key is not just an encryption problem

It is tempting to think of the APP_KEY as protecting privacy: if it leaks, someone can read your encrypted cookies. That undersells it badly.

Laravel accepts any incoming cookie whose signature verifies against the APP_KEY. The session cookie, and every encrypted cookie, carries a message authentication code the framework checks before it decrypts and deserializes the contents. An attacker who holds your key can produce a cookie that passes that check, containing a serialized PHP object of their choosing. When Laravel unserializes it, that object can kick off a property-oriented gadget chain in a library somewhere in your vendor tree and end in code execution on your server. We covered the mechanics of that in detail in the APP_KEY exposure and RCE write-up.

So a leaked key is not a "rotate it when convenient" secret. It is a live path from your inputs to your shell. Treat the exposure as an incident, and treat the rotation as the fix.


Step one: measure the blast radius

Before you touch anything, find out what actually depends on the key. The answer decides whether this is a five minute job or a careful migration.

The key is used in four places, and they are not equally painful:

What uses APP_KEY What breaks on rotation Recoverable?
Encrypted cookies and the session cookie Users are logged out Yes, automatically, if you use APP_PREVIOUS_KEYS
Crypt::encrypt and encrypted casts Stored ciphertext cannot be decrypted Only if you keep the old key and re-encrypt
Signed and temporary signed URLs Outstanding links (email verification, unsubscribe) stop verifying Yes, links regenerate on next send
Custom encryption you wrote by hand Depends entirely on your code Depends on your code

Notice what is not on the list. Sanctum personal access tokens are hashed with SHA-256, not encrypted with the key, so they are unaffected. Passport signs its tokens with a dedicated RSA key pair in storage, so it is independent of APP_KEY. Password hashes come from bcrypt or Argon2id and never touch the key. If your app has none of the four items above beyond ordinary sessions, rotation is low risk and the worst case is a wave of logouts.

The row that ruins weekends is the second one. Run a quick grep to find it:

grep -rn "Crypt::encrypt\|'encrypted'\|=> 'encrypted:" app/

Also check your model casts for encrypted, encrypted:array, encrypted:json, and encrypted:collection. Every column behind one of those was written with the old key. If you find any, you are doing a migration, not a swap, and the steps below matter.


Why the naive rotation hurts

The instinct is to run one command:

php artisan key:generate

On its own, key:generate overwrites APP_KEY in your .env and throws the old value away. Nothing remembers the previous key. The moment the new key is live:

  • Every session cookie fails its signature check, so every user is logged out.
  • Every value in an encrypted column was written with the old key, which no longer exists anywhere, so Crypt::decrypt throws DecryptException and the data is unreadable.

That second point is the one that turns a routine rotation into data loss. There is no undo once the old key is gone and the ciphertext is all you have left.


APP_PREVIOUS_KEYS: rotate without the outage

Laravel 11 added graceful key rotation, and it is the whole reason this can be painless. There is a previous_keys entry in config/app.php that reads a comma separated list from the environment:

// config/app.php
'previous_keys' => [
    ...array_filter(
        explode(',', env('APP_PREVIOUS_KEYS', ''))
    ),
],

The rule Laravel follows is simple. It always encrypts new data with the current APP_KEY. When it decrypts, it tries the current key first, then walks through each key in APP_PREVIOUS_KEYS until one works. That single behaviour is what lets old cookies and old ciphertext keep working after you introduce a new key, and it is what makes a zero-downtime rotation possible.


The safe rotation procedure

Here is the sequence that keeps users logged in and keeps encrypted data readable. Do it in this order.

1. Copy the current key into the previous keys list. Take the value currently in APP_KEY and set it as APP_PREVIOUS_KEYS. Do this by hand so nothing is lost, since key:generate will not do it for you.

# .env, before generating anything
# APP_KEY=base64:OLDKEYVALUE...
APP_PREVIOUS_KEYS=base64:OLDKEYVALUE...

2. Generate the new key. Now let Laravel mint a fresh APP_KEY:

php artisan key:generate

Your .env now has a new APP_KEY and the old one preserved in APP_PREVIOUS_KEYS.

3. Deploy and clear config cache. Push the change to every server and process that runs your app, including queue workers and scheduler, so they all share the same key material. If you cache config, rebuild it:

php artisan config:clear
php artisan config:cache

At this point new sessions and new encrypted values use the new key, while old cookies and old ciphertext still decrypt through the previous key. Users stay logged in. Nothing has broken.

4. Re-encrypt stored data. Any encrypted column is still sitting there in old-key ciphertext. Rewrite each row so it is re-encrypted with the current key. A one-off command is the safest way, because reading and re-saving an attribute runs it through the current key on the way back in:

// app/Console/Commands/ReEncryptSecrets.php
public function handle(): int
{
    Account::query()->chunkById(500, function ($accounts) {
        foreach ($accounts as $account) {
            // Reading decrypts with the previous key,
            // saving re-encrypts with the current key.
            $account->api_secret = $account->api_secret;
            $account->saveQuietly();
        }
    });

    $this->info('Re-encryption complete.');

    return self::SUCCESS;
}

Run it, verify a sample of rows decrypt cleanly, and only then move on. Do this for every model and column you found in the grep from earlier.

5. Retire the old key. Once every encrypted value has been rewritten and enough time has passed for old session cookies to expire, remove the old key from APP_PREVIOUS_KEYS and deploy again. The rotation is now complete and only the new key exists.


The catch that matters for a real leak

There is a security subtlety in step five that a lot of guides skip, and it is the most important part when the key genuinely leaked rather than being rotated on a schedule.

While the old key sits in APP_PREVIOUS_KEYS, it is still a valid key. Laravel will happily accept a cookie signed with it. That is exactly the behaviour you want for a smooth rotation, but it means an attacker who already has the leaked key can keep forging valid cookies for as long as it remains in the previous list. The graceful window that protects your users also protects the attacker.

So the calculus changes depending on why you are rotating:

  • Planned rotation, no known compromise. Keep the old key in APP_PREVIOUS_KEYS as long as you need to migrate data comfortably. There is no adversary racing you.
  • Confirmed or suspected leak. Migrate fast and drop the previous key as soon as the encrypted data is re-encrypted. If you have no encrypted columns to preserve, do not use APP_PREVIOUS_KEYS at all. Rotate straight to the new key and accept that every user is logged out. A wave of logins is a small price for immediately invalidating every cookie the attacker could forge.

In an incident, forcing everyone to re-authenticate is a feature, not a regression.


If you are still on Laravel 10 or older

APP_PREVIOUS_KEYS does not exist before Laravel 11, so there is no built-in fallback. Your options are to accept the logout and data loss, or to write a migration script that decrypts every encrypted value with the old key explicitly and re-encrypts it with the new one before you flip APP_KEY. You can construct an Illuminate\Encryption\Encrypter instance with a specific key to read the old ciphertext:

$old = new Illuminate\Encryption\Encrypter(
    base64_decode(substr($oldKey, 7)), // strip the "base64:" prefix
    config('app.cipher')
);

$plaintext = $old->decrypt($ciphertext);

Read with the old encrypter, write with the app default, then swap the key. It is more manual, and it is one more reason to get onto a supported Laravel release. The Laravel security checklist for 2026 covers keeping your framework current alongside the other controls that matter here.


Stop the next leak before it happens

Rotating the key fixes today's exposure. It does nothing about the next one, and the next one usually comes from the same handful of mistakes.

Keys leak because a .env file gets committed to a repository, because the file is reachable over HTTP at /.env on a misconfigured server, because a production error page renders the environment in a stack trace, or because the value ends up in a log line or an unencrypted backup. Each of those is preventable. Make sure .env is in your .gitignore, confirm APP_DEBUG is false in production, and verify the file cannot be fetched from the web. We go deeper on the exposure paths in why an exposed .env file is so dangerous and on the cookie side in Laravel session cookie security.

The hard part is knowing whether any of those doors are currently open on your live site, because from the outside is exactly where an attacker looks. StackShield scans your application externally, the same way an attacker would, and flags an exposed .env, a leaking debug page, and the misconfigurations that turn a stray secret into a breach. Run a free scan and find out what your production environment is giving away before someone else does.

Free security check

Is your Laravel app exposed right now?

34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup required. Results in 60 seconds.

Frequently Asked Questions

What does the Laravel APP_KEY actually protect?

The APP_KEY is the secret Laravel uses for symmetric encryption. It encrypts and signs your cookies (including the session cookie), it powers Crypt::encrypt and any Eloquent attribute you mark with the encrypted cast, and it signs temporary and signed URLs. It does not protect Sanctum tokens (hashed with SHA-256), Passport tokens (signed with a separate RSA key pair), or password hashes (produced by bcrypt or Argon2id). Knowing exactly which of these you use tells you what will break when the key changes.

Will rotating APP_KEY log all my users out?

On Laravel 10 and earlier, yes. Session cookies are encrypted with the key, so changing it makes every existing cookie undecryptable and every user is logged out on their next request. On Laravel 11 and newer you can avoid this by moving the old key into APP_PREVIOUS_KEYS before you generate a new one. Laravel encrypts new data with the current key but will still decrypt old cookies using the previous key, so sessions survive the rotation.

Can I lose data by changing APP_KEY?

Yes, and this is the real danger. Anything stored with Crypt::encrypt or an encrypted cast, such as encrypted database columns holding tokens, API secrets, or personal data, was written using the old key. If you change the key with no fallback and no re-encryption step, that ciphertext can no longer be decrypted and the data is effectively gone. Always inventory your encrypted columns first, keep the old key available through APP_PREVIOUS_KEYS, and re-encrypt the data before you retire the old key.

Why is a leaked APP_KEY a remote code execution risk?

Laravel trusts any cookie that carries a valid signature derived from the APP_KEY. An attacker who knows the key can forge a signed, encrypted cookie containing a crafted serialized PHP object. When the framework decrypts and unserializes it, that object can trigger a property-oriented gadget chain in your dependencies and lead to code execution. This is why a leaked key is treated as a critical incident rather than a routine secret rotation.

How do I know if my APP_KEY has leaked?

Check whether your .env file was ever committed to git, exposed over HTTP at a path like /.env, printed on a Laravel debug error page in production, or included in an unencrypted backup or log. Search your git history with git log -p for the APP_KEY string, and confirm your production .env is not reachable from the internet. An external scanner such as StackShield probes for an exposed .env and leaking debug pages automatically, so you find the exposure before an attacker does.

Stay Updated on Laravel Security

Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.