Are you tired of storing a file with your application secrets in a password manager and need to copy it to your CI/CD environment everytime you change it to deploy your application in a security compliant way?
Spin up Symfony
Create a docker-compose.yml in your project root and add the following:
(See PHP Docker development with XDEBUG explained here)
Spin up the service with docker-compose up and install Symfony 5 to get the app running:
docker-compose exec php bash -c 'composer create-project symfony/website-skeleton project && mv project/* . && rm -rf project'
Now you can browse http://localhost:8080 and should see this:
Store secrets in a vault
Since Symfony 4.4 you get a secrets management out of the box that helps to store all secrets encrypted in a vault. You can decrypt your secrets with a private key or a passphrase. We can securely save our DB connection string, which currently lives in the .env file with this command:
php bin/console secrets:set DATABASE_URL
As we can see the command created a vault, a key pair and tells you not to commit the decryption key. We can check the files that were generated in config/secrets/dev:
We see our key, a private decryption key and a public encryption key, that can be committed. There is also a listing php file that contains the following content to list all encrypted keys:
This is used for php bin/console secrets:list and shows you the following:
As you might noticed Symfony stores the secrets divided on the environment. So to not block other developers in your team just commit the dev files and your good to go. For production vault files you shouldn’t commit the private decryption key. Adding the dev secrets for now works like this:
git add config/secrets
When we try to run a database query it should just work, because Symfony fetches %env% params if they are not exposed as environment variables from the vault. In the doctrine package configuration file you see the following:
We can test if the connection works:
php bin/console doctrine:query:sql "SHOW VARIABLES LIKE 'max_join_size'"
When you get no error and a 0 printed, everything is fine and the connection worked.
To save the DATABASE_URL param in the production vault we run this command:
php bin/console secrets:set --env=prod DATABASE_URL
This generates a production vault with all the necessary keys and sets the DATABASE_URL with your provided value. We need to be aware to not add the private decryption key to our versioning. That was already handled by installing Symfony with composer, so we see the following line already in the .gitignore file:
Symfony is so smart!
Reveal the secret values
To list all values we added to the vault we know to use the secrets:list command. To see the secrets cleartext value you can pass an option called — reveal:
For production values use:
php bin/console secrets:list --env=prod --reveal
Now we have version controlled secrets for multiple environments. That’s pretty awesome!
We have only one sensitive value and that’s the private decryption key. So during the deployment we need to add the private decryption file to the project or deploy with an environment variable called SYMFONY_DECRYPTION_SECRET.
When we want to use the SYMFONY_DECRYPTION_SECRET variable we need to provide the private decryption key value encoded with base64 e.g.:
php -r "echo base64_encode(require 'config/secrets/prod/prod.decrypt.private.php');"
When you’re using a build system like Jenkins or Gitlab you can inject this as a environment variable to your build and hand it over to your dockerized build or you add the whole file to the build files and deploy them. Jenkins offers the secret file feature for this:
But wait there’s another method to even don’t add the private decryption key to your code files that get deployed. You can use this command on your build system to generate a .env file that you can deploy with your application files:
php bin/console secrets:decrypt-to-local --force --env=prod
This command generates a .env.prod.local file. So your Symfony app will use the env variables from this file and there will be no performance loss because of decrypting secrets at runtime. Amazing job and it makes no difference regarding security. If someone has access to your server he can copy the private decryption key or just copy the .env.prod.local file.
Local vault for overrides during development
How can I override secrets during development? Here is the local vault that we can use to override the DATABASE_URL to check something on another DB. Add a local value like this:
php bin/console secrets:set DATABASE_URL --local
But what happend now? The local credential overrides are stored in .env.dev.local! So no additional vault and of course Symfony already added a line in the .gitignore file to prevent you from committing it. Also .env file entries are overriden so you are safe to go with this way. The only important thing you see is that Symfony looks first for env variables and after that for secrets. So with env variables you can always override secret values.
- Env variables first, secrets second
- secret:set command solves dev, local and prod env handling automatically
- Symfony handles possible git issues with .gitignore entries