Created
November 24, 2025 19:23
-
-
Save willvincent/d721ee1066b51619d260bd9efe9596df to your computer and use it in GitHub Desktop.
Artisan command to rotate app key for production
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| declare(strict_types=1); | |
| namespace App\Console\Commands; | |
| use Illuminate\Console\Command; | |
| use Illuminate\Encryption\Encrypter; | |
| use Illuminate\Support\Str; | |
| final class RotateAppKeyCommand extends Command | |
| { | |
| protected $signature = 'app:rotate-key {--force : Skip confirmation}'; | |
| protected $description = 'Locally rotate APP_KEY → update .env.production → re-encrypt the file automatically'; | |
| public function handle(): int | |
| { | |
| // 1. Must be local only | |
| if (! app()->isLocal()) { | |
| $this->error('This command can only be run in the local environment.'); | |
| return self::FAILURE; | |
| } | |
| // 2. Confirm unless --force | |
| if (! $this->option('force') && ! $this->confirm('Rotate APP_KEY and re-encrypt .env.production now?')) { | |
| $this->info('Cancelled.'); | |
| return self::SUCCESS; | |
| } | |
| $envPath = base_path('.env.production'); | |
| if (! file_exists($envPath)) { | |
| $this->error('.env.production not found! Create it first.'); | |
| return self::FAILURE; | |
| } | |
| // Load current values from the real .env.production file (not cached config) | |
| $currentContent = file_get_contents($envPath); | |
| $currentKey = $this->extractEnvValue($currentContent, 'APP_KEY'); | |
| $previousKeys = $this->extractEnvValue($currentContent, 'APP_PREVIOUS_KEYS'); | |
| if (! $currentKey || ! Str::startsWith($currentKey, 'base64:')) { | |
| $this->error('Invalid or missing APP_KEY in .env.production'); | |
| return self::FAILURE; | |
| } | |
| $this->info('Generating new key...'); | |
| $newKey = 'base64:'.base64_encode(Encrypter::generateKey(config('app.cipher'))); | |
| // Build new APP_PREVIOUS_KEYS (append current key) | |
| $newPrevious = $previousKeys ? trim($previousKeys).','.$currentKey : $currentKey; | |
| // Update the actual file content | |
| $newContent = preg_replace( | |
| '/^APP_KEY=.*/m', | |
| 'APP_KEY='.$newKey, | |
| $currentContent | |
| ); | |
| $newContent = preg_replace( | |
| '/^APP_PREVIOUS_KEYS=.*/m', | |
| 'APP_PREVIOUS_KEYS='.$newPrevious, | |
| $newContent | |
| ); | |
| // If APP_PREVIOUS_KEYS line still doesn’t exist, insert it | |
| if (! str_contains($newContent, 'APP_PREVIOUS_KEYS=')) { | |
| $newContent = preg_replace( | |
| '/(APP_KEY=.*)/', | |
| "$1\nAPP_PREVIOUS_KEYS={$newPrevious}", | |
| $newContent | |
| ); | |
| } | |
| // Write back the updated plain .env.production | |
| file_put_contents($envPath, $newContent); | |
| $this->info('Updated .env.production with new key and previous keys'); | |
| // Encrypt .env.production | |
| $keyPath = base_path('.encryption_key'); | |
| if (! file_exists($keyPath)) { | |
| $this->newLine(); | |
| $this->warn('.encryption_key file not found.'); | |
| $encryptionKey = null; | |
| } else { | |
| $encryptionKey = trim(file_get_contents($keyPath)); | |
| } | |
| $this->info('Re-encrypting .env.production → .env.production.encrypted'); | |
| $this->call('env:encrypt', [ | |
| '--env' => 'production', | |
| '--key' => $encryptionKey, | |
| '--force' => true, | |
| ]); | |
| $this->newLine(); | |
| $this->components->info('APP_KEY rotation complete!'); | |
| $this->line(" Old key added to APP_PREVIOUS_KEYS"); | |
| $this->line(" New key: <fg=green>{$newKey}</>"); | |
| $this->line(" Encrypted file: <fg=green>.env.production.encrypted</> (ready to commit)"); | |
| return self::SUCCESS; | |
| } | |
| private function extractEnvValue(string $content, string $key): ?string | |
| { | |
| if (! preg_match("/^{$key}=(.*)$/m", $content, $matches)) { | |
| return null; | |
| } | |
| return trim($matches[1]); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment