Skip to content

[FrameworkBundle] Add secrets management. #31101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from

Conversation

jderusse
Copy link
Member

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #27351
License MIT
Doc PR symfony/symfony-docs/pull/11396

During the Foss Hackathon, we worked 2 wall days to talk and read RFC comment, other framework implementations (laravel, rails, ...)
We tried to cover every used case, building a simple and flexible solution that could be improved in the future or 3rd party bundles.
Here are the choice we tooks and why:

  1. How it works:

Have a look to the draft of the documentation. it may help you to understand how it work

In short default usage:

# config/packages/secrets.yaml
framwork:
    secrets:
        encrypted_secrets_dir: '%kernel.project_dir%/config/secrets/%kernel.environment%/'
        encryption_key: '%kernel.project_dir%/config/secrets/encryption_%kernel.environment%.key'
APP_ENV=prod ./bin/console secrets:generate-key

APP_ENV=prod ./bin/console secrets:add DATABASE_URL my-secret

APP_ENV=prod ./bin/console secrets:list --reveal

 -------------- ------------------
  key            plaintext secret 
 -------------- ------------------
  DATABASE_URL   my-secret
 -------------- ------------------
# config/packages/doctrine.yaml
doctrine:
    dbal:
        password: '%env(default::secret:DATABASE_PASSWORD)%'
        # url: '%env(DATABASE_URL_WITHOUT_PASSWORD)'
  1. Use %env()% syntax even if it's not an Environement Variable.

Even if it's not related to "environement variables"", secrets are related to the the environement. using %env()%` make perfect sense and is exactly what we need: executed at runtime + many EnvProcessors already defined

  1. List of Storage implementation

The build-in version in symfony will only store secret in a file.

If users want something else: They could implements their own Storage with SecretStorageInterface.

Some example of storage that are out of scope for now:

  • ChainStorage: lookup for secret in multiple storages
  • Vault
  • AWS Secret management
  • FileStorage with another encryption algorithm
  1. By default secrets and key are compartimentized per env

The build-in storage stores secrets and key are stored in a path that vary with the kernel.environement variable.
Thuse will - BY DESIGN - avoid users from being tempted to share the production key with other developpers/CI/...

  1. Use lib sodium By default:

Because modern and easy to use (avoid mistake by using wrong combinaison of algorithme)

  1. Symetric algorithm

In oposition with everything I thought before, @tobias conviced me to use a symetric algorithm. (Which mean that everybody who's able to generate a secret would also be able to read the secrets)

When using asymetric keys, the private key should be deployed on production and be accessible by few OPS
When using asymetric keys, the keys are not easily renewable (the private key is required to decrypt with the old key and recrypt with the new key)

Because even with asymetric algorithm, the key could be compromised (if an OPS is fired for instance) and not easily renewable,
We recommand to use symetric key (owned by few people) and renew that key (and secrets of course) everytime an owner leaves the project.

A big thank you to @Tobion, @mpdude and @nicolas-grekas .

Copy link
Member

@fabpot fabpot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just CS comments for now

@sstok
Copy link
Contributor

sstok commented Apr 14, 2019

Minor comment, it's environment not environement 👍

@jderusse jderusse force-pushed the encrypted-secrets-jd branch from f164b9b to a17c165 Compare April 14, 2019 09:23
@nicolas-grekas nicolas-grekas added this to the next milestone Apr 14, 2019
@jderusse jderusse force-pushed the encrypted-secrets-jd branch 3 times, most recently from 826fb3c to bef764e Compare April 14, 2019 20:10
Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first round (wording only)

Copy link
Contributor

@noniagriconomie noniagriconomie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice feature :)

private $secretsStorage;
private $encoder;

public function __construct(EncoderInterface $encoder, MutableSecretStorageInterface $secretsStorage)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm right, we don't use prefixes like Mutable and Immutable in Symfony code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would drop the prefix as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming things are hard... :(

Here I wanted to distingquihed "readonly storage" from "writable storage" do you have any idea of naming?

note: we have a Symfony\Component\Security\Acl\Model\MutableAclInterface

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecretStorageInterface {
    get(): string;
    set(): void;
    list(): iterable;
}

would be enough IMHO

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👎 don't just name it get, set etc without "secret". It should be obvious from the method name that this is confidential data, not a generic data store.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given:

  • At first, users only need to fetch secrets from the storage. That's the purpose of the SecretStorageInterface.
  • Symfony provides a default storage (the FileSecretStorage) and a convenient way to add new secrets in it (the SecretsAddCommand). Maybe I should inject the FileSecretStorage and remove that interface?

We have 2 interface for reasons:

  1. From our point of view, it didn't make sens to use symfony's command to manage 3rd party tools like vault. Users could use their favorite tool to add/delete secrets from them.

  2. I think that a futur implementation of SecretStorageInterface could be a ChainSecretStorage that iterate over a list of SecretStorageInterface. Implementing setSecret in a chain wouldn't make sens.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the ChainSecretStorage which will improve the DX by allowing developper/3rd party to create new SecretStorages

@jderusse jderusse force-pushed the encrypted-secrets-jd branch 4 times, most recently from 68d5fb9 to 29351bb Compare April 15, 2019 14:50
@Tobion
Copy link
Contributor

Tobion commented Jul 19, 2019

I haven't see that one before. Thanks for the link. Why did he create the PR on his own fork instead of symfony?

@jderusse jderusse force-pushed the encrypted-secrets-jd branch from 09709e6 to 072b311 Compare July 20, 2019 09:12
@jderusse jderusse changed the base branch from master to 4.4 July 20, 2019 09:12
@jderusse
Copy link
Member Author

neither do I. There are a lot of common concept in both PR:

  • commands to add/update secrets
  • rotate keys
  • envProcessor

which let me think, we are on the right way.

But also many differences.

  • an edit command which open an editor to edit secrets
  • concret JwtHandler implementation compare to EncoderInterface + SecretStorageInterface
  • JWT Impl with async alg compare to raw sodium (in default impl)
  • all secrets in a single file compare to 1 file per secret (in default impl)

Using interface in this PR leave the possibility open to add a JwtEncoder and a SingleFileStorage.

PR rebased, and ready for review

@jderusse jderusse force-pushed the encrypted-secrets-jd branch from 072b311 to 7ecd091 Compare July 20, 2019 09:26
@ro0NL
Copy link
Contributor

ro0NL commented Jul 21, 2019

would a JSON based secret storage be useful? to group multiple secrets in a single file ...

edit: oh you mentioned SingleFileStorage

@jderusse jderusse force-pushed the encrypted-secrets-jd branch from 7ecd091 to 2083c18 Compare July 22, 2019 06:26
@jderusse
Copy link
Member Author

hello @ro0NL even if this PR leaves the possibility to implement a SingleFileStorage I don't see the usecase for such feature. Will it be better than 1 file per secret?

Note: storing all secrets in 1 file will have a performance drawback : reading/deconding the entier file for fetching a single secret

@ro0NL
Copy link
Contributor

ro0NL commented Jul 22, 2019

@jderusse im not sure yet :) i must say Docker makes it pretty easy to ship secret files into /run/secrets, but yes, im worried about the maintenance part in case of a "file per secret" long term.

I think security is a bigger issue then performance actually, as fetching one value reveals all (using the same key).

@surelygroup
Copy link

I I understand the one file concept right, when using one file only the json keys are revealed. Each secret (under each key) is encrypted independently.

@Tobion
Copy link
Contributor

Tobion commented Jul 22, 2019

From performance and security perspective it doesn't matter if it's a single file or one file per secret.
The only difference is usuability. To me several files makes it easier because

  1. it avoids conflicts: if several developers try to add a secret in a single file, the conflict is not resovable because the data is binary junk
  2. nobody needs to worry about the format of several secrets in a file (one secret per line or json etc) and how to access individual secrets

@javiereguiluz
Copy link
Member

Just a friendly reminder that Symfony 4.4/5.0 will enter its "feature freeze" period in 11 days, on October 1st. It'd be nice to have this feature in. I hope you can do a last-ditch effort to finish this. Thanks!

@jderusse jderusse force-pushed the encrypted-secrets-jd branch from 2083c18 to c2d8a52 Compare September 19, 2019 16:23
@jderusse
Copy link
Member Author

PR rebased and RFR.

Unless I missed a point all comments are resolved. (or could be resolved in a Future dedicated PR, because the current interfaces in this PR are flexible enougth to allow such evolution (ie. SingleFileStorage or other encoders)

Please tell me if I missed a point.

@fabpot
Copy link
Member

fabpot commented Sep 19, 2019

What about creating a Secret component instead of storing everything in the frameworkbundle, which should stay as an integration point for components?

@jderusse
Copy link
Member Author

Depend of the point of view on the feature.

This PR is about providing a "secured" way to store configuration. The encryption thing is an implementation detail.

Moving to a component means focusing on loading/storing secret, probably providing/maintaining several backend (vault, KMS, ...).

At the time I opened the PR I wasn't sure it worth it. We talked about it with @nicolas-grekas which convince me that we should provide a single backend implementation in order to offer the simplest and best experience for the user.

@jderusse jderusse force-pushed the encrypted-secrets-jd branch from c2d8a52 to afd2f0f Compare September 19, 2019 23:57
@fabpot
Copy link
Member

fabpot commented Sep 20, 2019

Having a component does not necessarily mean having to provide more than one backend. Dotenv only provides one (.env file) and the Profiler only provides a filesystem backend. We just need to be very clear in the documentation in the interfaces/classes.

interface EncoderInterface
{
/**
* Generate the keys and material necessary for its operation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generates

public function generateKeys(bool $override = false): array;

/**
* Encrypt a secret.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encrypts

public function encrypt(string $secret): string;

/**
* Decrypt a secret.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decrypts

$this->encryptionKey = null;

$encryptionKey = sodium_crypto_stream_keygen();
(new Filesystem())->dumpFile($this->encryptionKeyPath, $encryptionKey);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we want to depend on Filesystem

@jderusse
Copy link
Member Author

closing in favor of #33997

@jderusse jderusse closed this Oct 16, 2019
fabpot added a commit that referenced this pull request Oct 20, 2019
…ecret:...)%` processor to deal with secrets seamlessly (Tobion, jderusse, nicolas-grekas)

This PR was merged into the 4.4 branch.

Discussion
----------

[FrameworkBundle] Add `secrets:*` commands and `%env(secret:...)%` processor to deal with secrets seamlessly

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #27351
| License       | MIT
| Doc PR        | symfony/symfony-docs/pull/11396

This PR continues #31101, please see there for previous discussions. The attached patch has been fine-tuned on nicolas-grekas#33 with @jderusse.

This PR is more opinionated and thus a lot simpler than #31101: only Sodium is supported to encrypt/decrypt (polyfill possible), and only local filesystem is available as a storage, with little to no extension point. That's on purpose: the goal here is to provide an experience, not software building blocks. In 5.1, this might be extended and might lead to a new component, but we'd first need reports from real-world needs. Having this straight-to-the-point in 4.4 will allow gathering these needs (if they exist) and will immediately provide a nice workflow for the need we do want to solve now: forwarding secrets from dev to prod using git in a secure way.

The workflow this will allow is the following:
- public/private key pairs are generated in the `config/secrets/%kernel.environment%/` folder using `bin/console secrets:generate-keys`
- for the prod env, the corresponding private key should be deployed to the server using whatever means the hosting provider allows - this key MUST NOT be committed
- the public key is used to encrypt secrets and thus *may* be committed in the git repository to allow anyone *that can commit* to add secrets - this is done using `bin/console secrets:set`

DI configuration can reference secrets using `%env(secret:...)%` in e.g `services.yaml`.
There is also `bin/console secrets:remove` and `bin/console debug:secrets` to complete the toolbox.

In terms of design, vs #31101, this groups the dual "encoder" + "storage" concepts in a single "vault" one. That's part of what makes this PR simpler.

That's all folks :)

Commits
-------

c4653e1 Restrict secrets management to sodium+filesystem
02b5d74 Add secrets management
8c8f623 Proof of concept for encrypted secrets
@nicolas-grekas nicolas-grekas modified the milestones: next, 4.4 Oct 27, 2019
@jderusse jderusse deleted the encrypted-secrets-jd branch March 5, 2020 20:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.