Skip to content

Instantly share code, notes, and snippets.

@willvincent
Created November 24, 2025 19:23
Show Gist options
  • Select an option

  • Save willvincent/d721ee1066b51619d260bd9efe9596df to your computer and use it in GitHub Desktop.

Select an option

Save willvincent/d721ee1066b51619d260bd9efe9596df to your computer and use it in GitHub Desktop.
Artisan command to rotate app key for production
<?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