10000 feature #33997 [FrameworkBundle] Add `secrets:*` commands and `%env(s… · symfony/symfony@16d5285 · GitHub
[go: up one dir, main page]

Skip to content

Commit 16d5285

Browse files
committed
feature #33997 [FrameworkBundle] Add secrets:* commands and %env(secret:...)% 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
2 parents c50d0b6 + c4653e1 commit 16d5285

21 files changed

+1279
-1
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ install:
207207
208208
if [[ ! $deps ]]; then
209209
php .github/build-packages.php HEAD^ src/Symfony/Bridge/PhpUnit src/Symfony/Contracts
210+
composer remove --dev --no-update paragonie/sodium_compat
210211
else
211212
export SYMFONY_DEPRECATIONS_HELPER=weak &&
212213
cp composer.json composer.json.orig &&

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"monolog/monolog": "^1.25.1",
114114
"nyholm/psr7": "^1.0",
115115
"ocramius/proxy-manager": "^2.1",
116+
"paragonie/sodium_compat": "^1.8",
116117
"php-http/httplug": "^1.0|^2.0",
117118
"predis/predis": "~1.1",
118119
"psr/http-client": "^1.0",

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ CHANGELOG
1717
* Added new `error_controller` configuration to handle system exceptions
1818
* Added sort option for `translation:update` command.
1919
* [BC Break] The `framework.messenger.routing.senders` config key is not deep merged anymore.
20+
* Added `secrets:*` commands and `%env(secret:...)%` processor to deal with secrets seamlessly.
2021

2122
4.3.0
2223
-----
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Command;
13+
14+
use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Input\InputOption;
18+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
22+
/**
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*
25+
* @internal
26+
*/
27+
final class SecretsDecryptToLocalCommand extends Command
28+
{
29+
protected static $defaultName = 'secrets:decrypt-to-local';
30+
31+
private $vault;
32+
private $localVault;
33+
34+
public function __construct(AbstractVault $vault, AbstractVault $localVault = null)
35+
{
36+
$this->vault = $vault;
37+
$this->localVault = $localVault;
38+
39+
parent::__construct();
40+
}
41+
42+
protected function configure()
43+
{
44+
$this
45+
->setDescription('Decrypts all secrets and stores them in the local vault.')
46+
->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces overriding of secrets that already exist in the local vault')
47+
->setHelp(<<<'EOF'
48+
The <info>%command.name%</info> command list decrypts all secrets and stores them in the local vault..
49+
50+
<info>%command.full_name%</info>
51+
52+
When the option <info>--force</info> is provided, secrets that already exist in the local vault are overriden.
53+
54+
<info>%command.full_name% --force</info>
55+
EOF
56+
)
57+
;
58+
}
59+
60+
protected function execute(InputInterface $input, OutputInterface $output): int
61+
{
62+
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
63+
64+
if (null === $this->localVault) {
65+
$io->error('The local vault is disabled.');
66+
67+
return 1;
68+
}
69+
70+
$secrets = $this->vault->list(true);
71+
72+
if (!$input->getOption('force')) {
73+
foreach ($this->localVault->list() as $k => $v) {
74+
unset($secrets[$k]);
75+
}
76+
}
77+
78+
foreach ($secrets as $k => $v) {
79+
if (null === $v) {
80+
$io->error($this->vault->getLastMessage());
81+
82+
return 1;
83+
}
84+
85+
$this->localVault->seal($k, $v);
86+
}
87+
88+
return 0;
89+
}
90+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Command;
13+
14+
use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Input\InputOption;
18+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
22+
/**
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*
25+
* @internal
26+
*/
27+
final class SecretsEncryptFromLocalCommand extends Command
28+
{
29+
protected static $defaultName = 'secrets:encrypt-from-local';
30+
31+
private $vault;
32+
private $localVault;
33+
34+
public function __construct(AbstractVault $vault, AbstractVault $localVault = null)
35+
{
36+
$this->vault = $vault;
37+
$this->localVault = $localVault;
38+
39+
parent::__construct();
40+
}
41+
42+
protected function configure()
43+
{
44+
$this
45+
->setDescription('Encrypts all local secrets to the vault.')
46+
->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces overriding of secrets that already exist in the vault')
47+
->setHelp(<<<'EOF'
48+
The <info>%command.name%</info> command list encrypts all local secrets and stores them in the vault..
49+
50+
<info>%command.full_name%</info>
51+
52+
When the option <info>--force</info> is provided, secrets that already exist in the vault are overriden.
53+
54+
<info>%command.full_name% --force</info>
55+
EOF
56+
)
57+
;
58+
}
59+
60+
protected function execute(InputInterface $input, OutputInterface $output): int
61+
{
62+
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
63+
64+
if (null === $this->localVault) {
65+
$io->error('The local vault is disabled.');
66+
67+
return 1;
68+
}
69+
70+
$secrets = $this->localVault->list(true);
71+
72+
if (!$input->getOption('force')) {
73+
foreach ($this->vault->list() as $k => $v) {
74+
unset($secrets[$k]);
75+
}
76+
}
77+
78+
foreach ($secrets as $k => $v) {
79+
if (null === $v) {
80+
$io->error($this->localVault->getLastMessage());
81+
82+
return 1;
83+
}
84+
85+
$this->vault->seal($k, $v);
86+
}
87+
88+
return 0;
89+
}
90+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Command;
13+
14+
use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Input\InputOption;
18+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
22+
/**
23+
* @author Tobias Schultze <http://tobion.de>
24+
* @author Jérémy Derussé <jeremy@derusse.com>
25+
* @author Nicolas Grekas <p@tchwork.com>
26+
*
27+
* @internal
28+
*/
29+
final class SecretsGenerateKeysCommand extends Command
30+
{
31+
protected static $defaultName = 'secrets:generate-keys';
32+
33+
private $vault;
34+
private $localVault;
35+
36+
public function __construct(AbstractVault $vault, AbstractVault $localVault = null)
37+
{
38+
$this->vault = $vault;
39+
$this->localVault = $localVault;
40+
41+
parent::__construct();
42+
}
43+
44+
protected function configure()
45+
{
46+
$this
47+
->setDescription('Generates new encryption keys.')
48+
->addOption('local', 'l', InputOption::VALUE_NONE, 'Updates the local vault.')
49+
->addOption('rotate', 'r', InputOption::VALUE_NONE, 'Re-encrypts existing secrets with the newly generated keys.')
50+
->setHelp(<<<'EOF'
51+
The <info>%command.name%</info> command generates a new encryption key.
52+
53+
<info>%command.full_name%</info>
54+
55+
If encryption keys already exist, the command must be called with
56+
the <info>--rotate</info> option in order to override those keys and re-encrypt
57+
existing secrets.
58+
59+
<info>%command.full_name% --rotate</info>
60+
EOF
61+
)
62+
;
63+
}
64+
65+
protected function execute(InputInterface $input, OutputInterface $output): int
66+
{
67+
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
68+
$vault = $input->getOption('local') ? $this->localVault : $this->vault;
69+
70+
if (null === $vault) {
71+
$io->success('The local vault is disabled.');
72+
73+
return 1;
74+
}
75+
76+
if (!$input->getOption('rotate')) {
77+
if ($vault->generateKeys()) {
78+
$io->success($vault->getLastMessage());
79+
80+
if ($this->vault === $vault) {
81+
$io->caution('DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️');
82+
}
83+
84+
return 0;
85+
}
86+
87+
$io->warning($vault->getLastMessage());
88+
89+
return 1;
90+
}
91+
92+
$secrets = [];
93+
foreach ($vault->list(true) as $name => $value) {
94+
if (null === $value) {
95+
$io->error($vault->getLastMessage());
96+
97+
return 1;
98+
}
99+
100+
$secrets[$name] = $value;
101+
}
102+
103+
if (!$vault->generateKeys(true)) {
104+
$io->warning($vault->getLastMessage());
105+
106+
return 1;
107+
}
108+
109+
$io->success($vault->getLastMessage());
110+
111+
if ($secrets) {
112+
foreach ($secrets as $name => $value) {
113+
$vault->seal($name, $value);
114+
}
115+
116+
$io->comment('Existing secrets have been rotated to the new keys.');
117+
}
118+
119+
if ($this->vault === $vault) {
120+
$io->caution('DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️');
121+
}
122+
123+
return 0;
124+
}
125+
}

0 commit comments

Comments
 (0)
0