Girando seu APP_KEY do Laravel sem quebrar tudo

Back-end Criptografia Ferramentas Laravel PHP Tutoriais Webdev
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:

  1. Gerar uma nova chave usando o Artisan
php artisan key:generate --show
  1. Substitua o valor .env APP_KEY pela nova chave base64.
  2. Limpar e reconstruir o cache de configuração
php artisan config:clear
php artisan config:cache
  1. Recarregue os trabalhadores com elegância
# 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 .