Protect sensitive data with Symfony Secrets

Viktor Pikaev
Boozt Tech
Published in
8 min read3 days ago

Get an overview of three general ways to protect sensitive data and a deep dive into “Symfony Secrects” — a convenient Symfony tool and a step-by-step guide on implementing it. This blog post is based on Viktor Pikaev’s, Senior Software Developer, talk during the “Malmo Symfony Fans” meetup at Foo Cafe in Malmo (Sweden).

Keep secrets in secret

There is various information that can be called sensitive: users’ personal data, company’s proprietary information, source code, passwords, etc.
Let’s start by defining what the sensitive information is and who it should be protected from.

In this post, I will talk about one specific type of sensitive information — passwords, access tokens, secret keys, etc. This information is very important to protect because its compromise can lead to irreversible damage — not only for our products, but for third party products too.

Who should this information be protected from? A short answer: from everyone. But here are some specific cases:

People with access to the repository. In non-IT companies, it’s a common situation when not only developers have access to the VCS repository. It means that if we store sensitive values as plain text, all these people can uncontrollably access it.

Developers. Even they shouldn’t have access to sensitive values. There are two reasons for it:

  • A principle of least privilege. Employees should have only permissions they need to do their job. It’s not only about security.
  • It also helps to avoid accident mistakes. All of us have heard stories about running delete ... or drop table ... SQL queries on the production
  • database accidentally.
  • Data on a developer’s computer can be compromised. Not all developers use strong passwords, disk encryption and lock their computers every time they AFK.

VCS hosting (GitHub/GitLab/etc). It might not be obvious, but we need to protect sensitive data from such services too. Usage of private repositories does not provides enough level of protection.

I suppose you know about an incident with one of AI solution for developers? The company used the code from repositories hosted on GitHub to train the model. In first versions, it provided real passwords and SSH keys from private repositories in its code suggestions. It was fixed quickly. But it’s a great example for us. We should not trust anyone!

How to protect sensitive information?

There are a lot of approaches, but in the end we can summarize them all into the next three options.

Option 1. A separate config file

It’s the most dummy and simple way. We need to split our configuration in two files: common, that contains not sensitive values, and local, that contains sensitive values. We add the local file to .gitignore and don't store that information in the repository. The local file is created and updated on production server manually.

# config.php 
return array_merge(
[
'dbName' => 'my-database',
],
include __DIR__ . '/config-local.php',
);

# config-local.php
return [
'dbLogin' => 'my-login',
'dbPassword' => 'my-password',
];

Advantages

This approach has the only one advantage: it’s simple. You don’t need to set up any extra processes and configure your infrastructure. You just put a file on production and it works.

Disadvantages

But there are several problems. First of all, it’s manual updating. Every time you need to add a new value or change an existing one, you need to do it manually on the production server. It makes configuration maintenance harder and increases error probability.

The second problem: this approach requires write access to the production server for a person, who updates sensitive values. If the person has access to database, it does not mean that this employee should have access to the server. For example, a developer can have access to the database, but not to the production server.

In general, this approach is acceptable for pet projects only. It doesn’t provide enough comfort and automation for commercial development (especially in the team).

Option 2. Environment variables

This option is more complex. Here, sensitive values are stored in environment variables. On the local machine, developers can use an .env file to load these values from it. On production these values are passed through the normal environment variables during deployment process (pipeline).

Production values for these parameters are stored in the CD system’s secrets. During deployment, CD system delivers a new code and set up all sensitive data to environment variables.

# config.php 
return [
'dbName' => 'my-database',
'dbLogin' => getenv('DB_LOGIN'),
'dbPassword' => getenv('DB_PASSWORD'),
];
# .env 
DB_LOGIN=db-login
DB_PASSWORD=db-password

Advantages

This approach is much better, but not perfect. As the first advantage, we can highlight an opportunity to automate configuration updating. It happens automatically without any manual work.

It also allows for secrets rotation. I’m not sure that it’s a common practice, but some companies use secrets rotation. For example, login and password for database connection are changed every month. Storing such credential in CD system’s secrets is the only way to do it. Because it doesn’t require any actions from the developers team. Database administrator sets a new password in the database and also puts a new password to CD system’s secret. With the next deployment, the new value will be provided for the application automatically.

Disadvantages

This approach is good, but it has one annoying problem. It requires someone from developers team to have access to CD system’s secrets to add new or change existing values. In companies with straight hierarchy, it could be hard. Often developers don’t have access to infrastructure. In that case, the developers team will have to add/change secrets through the administrators team. It means tickets, waiting and sometime delays in releases.

Option 3. Encrypted in-repo secrets

The third way is to store sensitive values in the repository but in encrypted view. We need a pair of encryption keys: public for encryption and private for decryption. We store the public key in the repository beside the encrypted values. The private key is stored in the CD system’s secrets and delivered to production during the deployment process. On production, the application uses the private key to decrypt values and use them.

Advantages

This approach allows developers team to add, update and remove secret values without any extra permissions or help. Everything developers need for it — the public key — is already in the repository. In the same time, they can’t decrypt existing values.

Disadvantages

The problem is that it doesn’t allow secrets rotation as the previous approach.

What to choose?

There is no need to choose only one approach. We can combine them to use all advantages and level problems. We can store infrastructure secrets in CD system’s values to make them easy to change by administrators team. All other application related secrets can be stored inside the repository in encrypted view.

Symfony secrets

That’s enough there, let’s go to practice. These approaches can be implemented independently in any project. Symfony though provides a very convenient built-in tool for it. It seamlessly integrates secrets to standard Symfony configuration system. With this tool, the application will treat secrets like normal environment variables — there is no difference for the business code.

How does it work?

To integrate Symfony secrets to the application only five steps should be done:

  1. Generate a pair of encryption keys.
  2. Encrypt secrets with the public key.
  3. Deliver the private key to production
  4. Use the private key on production server to decrypt secrets.
  5. Enjoy safety!

Let’s discuss all of them one by one.

Step 0. Prepare the project

I lied a little when said that there were only five steps. Actually, there are six of them. Before starting with secrets, we need to prepare our application. We need to move all sensitive data from yaml parameter files and PHP constants to environment variables. A standard Symfony application uses a Dotenv package to load values from .env files and environment. So we need to make sure that all sensitive data is loaded that way.

Step 1. Generate the encryption keys

Now we can start with generating a pair of encryption keys: public for encryption and private for decryption. We can do it by running the next commands in terminal:

$ APP_RUNTIME_ENV={env} 
$ php bin/console secrets:generate-keys

It will generate two files:

  • config/secrets/{env}/{env}.decrypt.private.php
  • config/secrets/{env}/{env}.encrypt.public.php

Private key should be immediately added to .gitignore and never be in the repository. We need to repeat this procedure for each environment so each of them uses its own pair of keys. Private keys for not secured environments like "development", "test", etc can be added to the repository. We won't encrypt any production secrets with them, so it doesn't meter.

Important: We shouldn’t change anything in the config/secrets/* directories manually. All files there are auto generated by Symfony. Manual changes can break the whole application.

Step 2. Encrypt the secrets

To add a new or change an existing secret we need to run the next commands in terminal:

$ APP_RUNTIME_ENV={env} 
$ php bin/console secrets:set {name}

Symfony will prompt the value, encrypt it and put into the config/secrets/{env} directory. To remove an existing secret, run the$ php bin/console secrets:remove {name} command in terminal.

The best part of all this system is that we don’t been the private key to do it. So we are able to do it easily by ourselves!

Important: Environment variables (from environment or .env files) have higher priority than secrets. So if we have a secret and an environment variable with the same name, environment variable will override the secret.

Step 3. Deliver the private key to production

The private key should be kept safely somewhere out of the repo. CD system’s secrets is a good place. During the deployment process, CD system should put the key into the config/secrets/prod/prod.decrypt.private.php file. Symfony will use it automatically. We don't need to do anything else.

As alternative, the private key can be put into the SYMFONY_DECRYPTION_SECRET environment variable instead of the file. Symfony will handle it the same way.

Step 4. Decrypt secrets on production

We don’t need to do anything else to decrypt secrets. When the application tries to read a value from the environment variable, at first Symfony will check whether it has a secret with such name. If such secret exists, Symfony will decrypt it and return its value. If there is not such secret, Symfony will look for that value in environment variables.

But here is an optional but strongly recommended advice. It’s better to decrypt all secrets in advance after deployment. To do it CD system needs to run the next command:

$ php bin/console secrets:decrypt-to-local --force

It makes Symfony to decrypt all existing secrets and put them as normal environment values into the .env.prod.local file. It has two benefits:

  • By having the values already decrypted, Symfony doesn’t have to decrypt them every time. It gives us a small performance improvement. Small but nice.
  • It allows Doting package to load these values. So any non-Symfony code will be able to access secrets’ values with the getenv() function. Sometimes it's really necessary.

Important: Doting should be configured and work correctly to load values from the .env.prod.local file properly.

What’s next?

In general, that’s all. Now our sensitive data is protected. We have a flexible, convenient and fast solution. However, there is one more thing we can do.

Trivy is the most popular open source security scanner, reliable, fast, and easy to use. It will be a great idea to add it to the CI pipeline. This tool can scan the source code and check if there are any secrets stored as plain text. By adding such scanning step to the pipeline, we guarantee that all newly added secrets are encrypted and protected.

About the author:

Viktor Pikaev, Senior Software Engineer at Boozt

Boozt: Website, Career page, LinkedIn

--

--