Girando seu APP_KEY do Laravel sem quebrar tudo

A APP_KEY do Laravel não é apenas mais um valor de configuração — é a chave criptográfica raiz do seu aplicativo. Ela criptografa sessões e cookies e assina URLs geradas pelo seu aplicativo.
Se essa chave vazar, um invasor pode falsificar cookies, sequestrar sessões ou descriptografar blobs confidenciais. Infelizmente, muitas equipes a geram uma vez e nunca mais a acessam . Isso não é sustentável e é uma receita para riscos ocultos.
A rotação manual (Big-Bang)
A maneira mais simples de rotacionar sua chave é a abordagem “big-bang”: você substitui a chave e reinicia tudo.
Como funciona:
- Gerar uma nova chave usando o Artisan
php artisan key:generate --show
-
Substitua o valor .env
APP_KEY
pela nova chave base64. - Limpar e reconstruir o cache de configuração
php artisan config:clear
php artisan config:cache
# Restart classic queue workers
php artisan queue:restart # Restart Horizon supervisors (graceful)
php artisan horizon:terminate # (Optional) pause/continue around Horizon restart for zero-downtime
php artisan horizon:pause
php artisan horizon:terminate
php artisan horizon:continue
O que quebra:
- Todos os usuários estão desconectados.
- Os cookies “Lembrar de mim” param de funcionar.
- URLs assinadas falham instantaneamente.
- Todos os campos criptografados do banco de dados se tornam ilegíveis.
Essa abordagem é boa para aplicativos pequenos ou emergências, mas é prejudicial na produção.
A maneira mais segura: chaveiro com chaves antigas
Uma solução melhor é usar um conjunto de chaves: uma lista de chaves onde as mais novas criptografam dados e as mais antigas ainda são aceitas para descriptografia.
Na prática, você coloca a chave atual
APP_KEY
e mantém as chaves mais antigas em uma variável separada, como
APP_KEYS_OLD
.
# Current key (always encrypts/signs)
APP_KEY=base64:NEW_CURRENT # Comma-separated history of old keys (decrypt-only)
APP_KEYS_OLD="base64:OLD_1,base64:OLD_2"
KeyRingEncrypter
use Illuminate\Contracts\Encryption\Encrypter as Contract;
use Illuminate\Encryption\Encrypter; class KeyRingEncrypter implements Contract {
public function __construct(private array $keys, private string $cipher) {} public function encrypt($value, $serialize = true)
{
return (new Encrypter($this->keys[0], $this->cipher))->encrypt($value, $serialize);
} public function decrypt($payload, $unserialize = true)
{
foreach ($this->keys as $key) {
try {
return (new Encrypter($key, $this->cipher))->decrypt($payload, $unserialize);
} catch (\Throwable) {
// try next key
}
} throw new \RuntimeException('Decryption failed with all keys.');
} public function encryptString($value) { return $this->encrypt($value, false); }
public function decryptString($payload) { return $this->decrypt($payload, false); }
}
Este criptografador personalizado encapsula o criptografador integrado do Laravel, mas aceita uma matriz de chaves. Ele sempre criptografa com a primeira chave (atual) e tenta cada chave em ordem ao descriptografar. Isso torna os valores antigos ainda legíveis, enquanto os novos valores são gravados com a chave mais recente.
Provedor de serviços de aplicativo
use Illuminate\Support\Str;
use App\Support\KeyRingEncrypter; public function register(): void {
$this->app->singleton('encrypter', function () {
$cipher = config('app.cipher', 'AES-256-CBC'); $encoded = array_filter([
env('APP_KEY'),
...array_map('trim', explode(',', (string) env('APP_KEYS_OLD', ''))),
]); $keys = array_map(fn ($k) =>
Str::startsWith($k, 'base64:') ? base64_decode(substr($k, 7)) : $k,
$encoded
); return new KeyRingEncrypter($keys, $cipher);
});
}
Isso substitui a vinculação padrão do criptografador do Laravel no contêiner. Ele carrega as chaves atuais e antigas do seu .env, as decodifica e as passa para o KeyRingEncrypter. Com isso implementado, cada chamada para o Crypt se beneficia automaticamente do suporte ao keyring.
Uso em código
use Illuminate\Support\Facades\Crypt; // Encrypts with the NEW key $encrypted = Crypt::encryptString('secret'); // Decrypts with NEW, or OLD if necessary $plain = Crypt::decryptString($encrypted);
Não são necessárias alterações no código do seu aplicativo. Você pode continuar usando Crypt::encryptString e Crypt::decryptString normalmente — a criptografia ocorre com a nova chave, e a descriptografia funciona de forma transparente tanto com as chaves novas quanto com as antigas.
Trabalho de recriptografia em segundo plano
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB; class ReencryptColumn {
public function handle(string $table, string $column, string $pk = 'id', int $chunk = 500): void {
DB::table($table)->orderBy($pk)->whereNotNull($column)
->chunkById($chunk, function ($rows) use ($table, $column, $pk) {
foreach ($rows as $row) {
try {
$plain = Crypt::decryptString($row->{$column}); // NEW or OLD $fresh = Crypt::encryptString($plain); // always NEW if ($fresh !== $row->{$column}) {
DB::table($table)->where($pk, $row->{$pk})
->update([$column => $fresh]);
}
} catch (\Throwable $e) {
report($e);
}
}
});
}
}
Este trabalho é um exemplo de uma maneira segura de migrar colunas criptografadas do banco de dados para a nova chave. Ele descriptografa valores (usando qualquer chave no anel) e os grava novamente criptografados com a chave mais recente. Após a conclusão deste trabalho, você pode remover com segurança a chave antiga do
APP_KEYS_OLD
.
Com esta configuração:
-
Novas criptografias sempre usam
APP_KEY
. - A descriptografia tenta primeiro a chave atual e depois retorna para a lista antiga.
- Com o tempo, à medida que as sessões e os valores criptografados são atualizados, as chaves antigas são naturalmente desativadas.
Isso significa que os usuários permanecem conectados, os cookies permanecem válidos e as colunas criptografadas continuam legíveis até que você as criptografe completamente novamente.
Quando descartar chaves antigas
Um chaveiro só funciona se você eventualmente o podar. Manter chaves velhas por perto para sempre só aumenta sua superfície de ataque. Você está pronto para se livrar de uma chave velha quando todas as seguintes condições forem verdadeiras:
- Sessões e cookies: O TTL máximo da sessão e o TTL para lembrar de mim já passaram.
- URLs assinadas: todos os links criados antes da rotação expiraram (a menos que você tenha criado a verificação de várias chaves).
- Campos de banco de dados criptografados: os trabalhos de nova criptografia em segundo plano foram concluídos.
- Monitoramento: Nenhum erro de descriptografia aparece nos logs por pelo menos um ciclo completo.
Quando essas condições forem atendidas, remova a chave antiga de APP_KEYS_OLD, reimplante-a e destrua-a.
👉 Para manuais de rotação avançados — incluindo como responder a uma violação e como lidar com demissões de funcionários — leia a Parte 2: Manuais de rotação do Laravel APP_KEY .