Laravel APP_KEY rotation policy for app security
If you have an existing laravel app running or you do fresh laravel installation, you will notice in your app's .env
file (in case of new installations it's in .env.example
file), there is a key called APP_KEY
. It is a 32 characters long string. This is the laravel application key.
Laravel has an artisan command php artisan key:generate
which generates a new APP_KEY
for you. If you have installed Laravel via Composer or the Laravel installer, this key will be set for you automatically by the key:generate
command.
Why your app key is important?
The APP_KEY
is used to keep your user sessions and other encrypted data secure! If the application key is not set, your user sessions and other encrypted data will not be secure. Believe it or not it is a big security risk.
To give you more specific context, earlier laravel had a security issue :
If your application's encryption key is in the hands of a malicious party, that party could craft cookie values using the encryption key and exploit vulnerabilities inherent to PHP object serialization / unserialization, such as calling arbitrary class methods within your application.
Do not worry, laravel later released a security update which disabled all serialization and unserialization of cookie values using APP_KEY
. All Laravel cookies are encrypted and signed, cookie values can be considered safe from client tampering. clich here
to see details about this security update.
Before Update :
If used to serialize all values during encryption.
$encryptedValue = \openssl_encrypt(serialize($value), $this->cipher, $this->key, 0, $iv);
After Update :
It will serialize only if you pass second parameter as true
while calling encrypt function.
$encryptedValue = \openssl_encrypt(
$serialize ? serialize($value) : $value,
$this->cipher, $this->key, 0, $iv
);
Okay, to sum it up, APP_KEY
is important and any backdoor associated with it which leads to compromising app security should be closed.
Passwords and APP_KEY :
When we consider parts of the app which have encryption, first thing comes to mind is the user passwords. Let's take a minute to differentiate both :
- Encryption :
Encryption is when you have a data which you want to safeguard. So you take original data, encrypt it using key and ciphers so it turns into gibrish string. This gribrish string has no meaning and hence it can not be interpreter directly toits original data meaning. When you need, you can decrypt this encrypted value to retrive it in original state.
Laravel has Crypt
facade which helps us implement encryption and decryption.
In this case, the key
and ciphers
are important because those are used in decryption. And should NOT be explosed.
- Hashing :
Hashing in simple terms is one way encryption. Once you encrypt you can NOT decrypt it in original state. You can verify if the hash matches a plain value to check if its the original value or not. But that's all you can do. It is way more secure in terms of sensitive information like user passwords.
Laravel has Hash
facade which helps us implement one way hashing encryption.
Now, one of the main thing you should understand : APP_KEY is NOT
used in Hashing and it is used in encryption. So your passwords security is NOT dependent on the APP_KEY. Whereas any data of your app which you have encrypted do have
dependency on APP_KEY.
Affects of changing APP_KEY :
Before we dive into how to change APP_KEY, it is important to know what will happen if we change it. When APP_KEY is changed in an existing app :
- Existing user sessions will be invalidated. Hence, all your currently active logged in users will be logged out.
- Any data in your app which you have encrypted using
Crypt
facade orencrypt()
helper function will no longer be decrypted as the encryption uses APP_KEY. - Any user passwords will NOT be affected so no need to worry about that.
- If you have more than one app servers running, all should be replaced with the same APP_KEY.
Let's handle the headache first :
As seen above, you can imagine most headache in changing APP_KEY is handling the data which is encrypted using old app key. For that you need to first decrypt the data using old APP_KEY and then re-encrypt using new APP_KEY. Damn!
Don't worry! Here is a simple helper function you can use :
/**
* Function to re-encrypt when APP_KEY is rotated/changed
*
* @param string $oldAppKey
* @param mixed $value
*/
function reEncrypt($oldAppKey, $value)
{
// Get cipher of encryption
$cipher = config('app.cipher');
// Get newly generated app_key
$newAppKey = config('app.key');
// Verify old app Key
if (Str::startsWith($oldAppKey, 'base64:')) {
$oldAppKey = base64_decode(substr($oldAppKey, 7));
}
// Verify new app Key
if (Str::startsWith($newAppKey, 'base64:')) {
$newAppKey = base64_decode(substr($newAppKey, 7));
}
// Initialize encryptor instance for old app key
$oldEncryptor = new Illuminate\Encryption\Encrypter((string)$oldAppKey, $cipher);
// Initialize encryptor instance for new app key
$newEncryptor = new Illuminate\Encryption\Encrypter((string)$newAppKey, $cipher);
// Decrypt the value and re-encrypt
return $newEncryptor->encrypt($oldEncryptor->decrypt($value));
}
Let's imagine we have a column called bank_account_number
in users
table which is stored as encrypted string. I have another column called old_bank_account_number
in the users
table to store old value as a backup before we save newly re-encrypted value. We can create a command php artisan encryption:rotate
:
<?php
namespace App\Console\Commands;
use App\User;
use Illuminate\Console\Command;
use Illuminate\Encryption\Encrypter;
class EncryptionRotateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'encryption:rotate {--oldappkey= : Old app key}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Re-encrypt when APP_KEY is rotated';
/**
* Create a new command instance.
*
*/
public function __construct()
{
parent::__construct();
}
/**
* Function to re-encrypt when APP_KEY is rotated/changed
*
* @param string $oldAppKey
* @param mixed $value
*/
public function handle()
{
// Get the old app key
$oldAppKey = $this->option('oldappkey');
// Get cipher of encryption
$cipher = config('app.cipher');
// Get newly generated app_key
$newAppKey = config('app.key');
// Verify old app Key
if (Str::startsWith($oldAppKey, 'base64:')) {
$oldAppKey = base64_decode(substr($oldAppKey, 7));
}
// Verify new app Key
if (Str::startsWith($newAppKey, 'base64:')) {
$newAppKey = base64_decode(substr($newAppKey, 7));
}
// Initialize encryptor instance for old app key
$oldEncryptor = new Encrypter((string)$oldAppKey, $cipher);
// Initialize encryptor instance for new app key
$newEncryptor = new Encrypter((string)$newAppKey, $cipher);
User::all()->each(function($user) use($oldEncryptor, newEncryptor){
// Stored value in a backup column
$user->old_bank_account_number = $user->bank_account_number;
// Decrypt the value and re-encrypt
$user->bank_account_number = $newEncryptor->encrypt($oldEncryptor->decrypt($user->bank_account_number));
$user->save();
});
$this->info('Encryption completed with newly rotated key');
}
}
Update :
I have pushed a new package which helps to simplify above implementation. click here
to view the Laravel package.
Rotating the key :
Finally, lets rotate the key.
- Run `php artisan down` on all instances so that no user can interact until this is done
- Go to terminal on one of the instance and open your `.env` file.
- Copy the APP_KEY value which is the old(existing) app key
- Run `php artisan key:generate` which will generate a new APP_KEY
- Run the helper command as we created above to re-encrypt the values `php artisan encryption:rotate --oldappkey=your_old_app_key_value`
- Replace the APP_KEY key on all remaining instances
- Run `php artisan config:clear` on all instances
- Run `php artisan cache:clear` on all instances
- Run `php artisan view:clear` on all instances
- Run `php artisan view:clear` on all instances
- Run `php artisan up` on all instances
And.. It's done! You can do this based on key rotation frequency you are comfortable in. I would recommend at-least once in 3 months.