8000 Add secrets management · symfony/symfony@02b5d74 · GitHub
[go: up one dir, main page]

Skip to content

Commit 02b5d74

Browse files
jderussenicolas-grekas
authored andcommitted
Add secrets management
1 parent 8c8f623 commit 02b5d74

27 files changed

+951
-272
lines changed

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 management.
2021

2122
4.3.0
2223
-----
Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,70 @@
11
<?php
22

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+
312
namespace Symfony\Bundle\FrameworkBundle\Command;
413

5-
use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
14+
use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
15+
use Symfony\Bundle\FrameworkBundle\Secret\Storage\MutableSecretStorageInterface;
616
use Symfony\Component\Console\Command\Command;
7-
use Symfony\Component\Console\Helper\QuestionHelper;
817
use Symfony\Component\Console\Input\InputArgument;
918
use Symfony\Component\Console\Input\InputInterface;
1019
use Symfony\Component\Console\Output\OutputInterface;
11-
use Symfony\Component\Console\Question\Question;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
1221

22+
/**
23+
* @author Tobias Schultze <http://tobion.de>
24+
* @author Jérémy Derussé <jeremy@derusse.com>
25+
*/
1326
final class SecretsAddCommand extends Command
1427
{
1528
protected static $defaultName = 'secrets:add';
1629

17-
/**
18-
* @var SecretStorageInterface
19-
*/
20-
private $secretStorage;
30+
private $secretsStorage;
2131

22-
public function __construct(SecretStorageInterface $secretStorage)
32+
public function __construct(MutableSecretStorageInterface $secretsStorage)
2333
{
24-
$this->secretStorage = $secretStorage;
34+
$this->secretsStorage = $secretsStorage;
2535

2636
parent::__construct();
2737
}
2838

2939
protected function configure()
3040
{
3141
$this
32-
->setDescription('Adds a secret with the key.')
33-
->addArgument(
34-
'key',
35-
InputArgument::REQUIRED
36-
)
37-
->addArgument(
38-
10000 'secret',
39-
InputArgument::REQUIRED
42+
->setDefinition([
43+
new InputArgument('name', InputArgument::REQUIRED, 'The name of the secret'),
44+
])
45+
->setDescription('Adds a secret in the storage.')
46+
->setHelp(<<<'EOF'
47+
The <info>%command.name%</info> command stores a secret.
48+
49+
%command.full_name% <name>
50+
EOF
4051
)
4152
;
4253
}
4354

4455
protected function execute(InputInterface $input, OutputInterface $output)
4556
{
46-
$key = $input->getArgument('key');
47-
$secret = $input->getArgument('secret');
48-
49-
$this->secretStorage->putSecret($key, $secret);
50-
}
51-
52-
protected function interact(InputInterface $input, OutputInterface $output)
53-
{
54-
/** @var QuestionHelper $helper */
55-
$helper = $this->getHelper('question');
56-
57-
$question = new Question('Key of the secret: ', $input->getArgument('key'));
57+
$io = new SymfonyStyle($input, $output);
5858

59-
$key = $helper->ask($input, $output, $question);
60-
$input->setArgument('key', $key);
59+
$name = $input->getArgument('name');
60+
$secret = $io->askHidden('Value of the secret');
6161

62-
$question = new Question('Plaintext secret value: ', $input->getArgument('secret'));
63-
$question->setHidden(true);
62+
try {
63+
$this->secretsStorage->setSecret($name, $secret);
64+
} catch (EncryptionKeyNotFoundException $e) {
65+
throw new \LogicException(sprintf('No encryption keys found. You should call the "%s" command.', SecretsGenerateKeyCommand::getDefaultName()));
66+
}
6467

65-
$secret = $helper->ask($input, $output, $question);
66-
$input->setArgument('secret', $secret);
68+
$io->success('Secret was successfully stored.');
6769
}
6870
}
Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,97 @@
11
<?php
22

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+
312
namespace Symfony\Bundle\FrameworkBundle\Command;
413

14+
use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
15+
use Symfony\Bundle\FrameworkBundle\Secret\Encoder\EncoderInterface;
16+
use Symfony\Bundle\FrameworkBundle\Secret\Storage\MutableSecretStorageInterface;
517
use Symfony\Component\Console\Command\Command;
618
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Input\InputOption;
720
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
822

23+
/**
24+
* @author Tobias Schultze <http://tobion.de>
25+
* @author Jérémy Derussé <jeremy@derusse.com>
26+
*/
927
final class SecretsGenerateKeyCommand extends Command
1028
{
1129
protected static $defaultName = 'secrets:generate-key';
30+
private $secretsStorage;
31+
private $encoder;
32+
33+
public function __construct(EncoderInterface $encoder, MutableSecretStorageInterface $secretsStorage)
34+
{
35+
$this->secretsStorage = $secretsStorage;
36+
$this->encoder = $encoder;
37+
parent::__construct();
38+
}
1239

1340
protected function configure()
1441
{
1542
$this
16-
->setDescription('Prints a randomly generated encryption key.')
43+
->setDefinition([
44+
new InputOption('rekey', 'r', InputOption::VALUE_NONE, 'Re-encrypt previous secret with the new key.'),
45+
])
46+
->setDescription('Generates a new encryption key.')
47+
->setHelp(<<<'EOF'
48+
The <info>%command.name%</info> command generates a new encryption key.
49+
50+
%command.full_name%
51+
52+
If a previous encryption key already exists, the command must be called with
53+
the <info>--rekey</info> option in order to override that key and re-encrypt
54+
previous secrets.
55+
56+
%command.full_name% --rekey
57+
EOF
58+
)
1759
;
1860
}
1961

2062
protected function execute(InputInterface $input, OutputInterface $output)
2163
{
22-
$encryptionKey = sodium_crypto_stream_keygen();
64+
$rekey = $input->getOption('rekey');
65+
66+
$previousSecrets = [];
67+
try {
68+
foreach ($this->secretsStorage->listSecrets(true) as $name => $decryptedSecret) {
69+
$previousSecrets[$name] = $decryptedSecret;
70+
}
71+
} catch (EncryptionKeyNotFoundException $e) {
72+
if (!$rekey) {
73+
throw $e;
74+
}
75+
}
2376

24-
$output->write($encryptionKey, false, OutputInterface::OUTPUT_RAW);
77+
$keys = $this->encoder->generateKeys($rekey);
78+
foreach ($previousSecrets as $name => $decryptedSecret) {
79+
$this->secretsStorage->setSecret($name, $decryptedSecret);
80+
}
2581

26-
sodium_memzero($encryptionKey);
82+
$io = new SymfonyStyle($input, $output);
83+
switch (\count($keys)) {
84+
case 0:
85+
$io->success('Keys have been generated.');
86+
break;
87+
case 1:
88+
$io->success(sprintf('A key has been generated in "%s".', $keys[0]));
89+
$io->caution('DO NOT COMMIT that file!');
90+
break;
91+
default:
92+
$io->success(sprintf("Keys have been generated in :\n -%s", implode("\n -", $keys)));
93+
$io->caution('DO NOT COMMIT those files!');
94+
break;
95+
}
2796
}
2897
}
Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
11
<?php
22

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+
312
namespace Symfony\Bundle\FrameworkBundle\Command;
413

5-
use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
14+
use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
15+
use Symfony\Bundle\FrameworkBundle\Secret\Storage\SecretStorageInterface;
616
use Symfony\Component\Console\Command\Command;
7-
use Symfony\Component\Console\Helper\Table;
817
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
919
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
1021

22+
/**
23+
* @author Tobias Schultze <http://tobion.de>
24+
* @author Jérémy Derussé <jeremy@derusse.com>
25+
*/
1126
final class SecretsListCommand extends Command
1227
{
13-
protected static $defaultName = 'secrets:list';
28+
protected static $defaultName = 'debug:secrets';
1429

15-
/**
16-
* @var SecretStorageInterface
17-
*/
1830
private $secretStorage;
1931

2032
public function __construct(SecretStorageInterface $secretStorage)
@@ -27,19 +39,54 @@ public function __construct(SecretStorageInterface $secretStorage)
2739
protected function configure()
2840
{
2941
$this
42+
->setDefinition([
43+
new InputOption('reveal', 'r', InputOption::VALUE_NONE, 'Display decrypted values alongside names'),
44+
])
3045
->setDescription('Lists all secrets.')
46+
->setHelp(<<<'EOF'
47+
The <info>%command.name%</info> command list all stored secrets.
48+
49+
%command.full_name%
50+
51+
When the the option <info>--reveal</info> is provided, the decrypted secrets are also displayed.
52+
53+
%command.full_name% --reveal
54+
EOF
55+
)
3156
;
57+
58+
$this
59+
->setDescription('Lists all secrets.')
60+
->addOption('reveal', 'r', InputOption::VALUE_NONE, 'Display decrypted values alongside names');
3261
}
3362

3463
protected function execute(InputInterface $input, OutputInterface $output)
3564
{
36-
$table = new Table($output);
37-
$table->setHeaders(['key', 'plaintext secret']);
65+
$reveal = $input->getOption('reveal');
66+
$io = new SymfonyStyle($input, $output);
67+
68+
try {
69+
$secrets = $this->secretStorage->listSecrets($reveal);
70+
} catch (EncryptionKeyNotFoundException $e) {
71+
throw new \LogicException(sprintf('Unable to decrypt secrets, the encryption key "%s" is missing.', $e->getKeyLocation()));
72+
}
73+
74+
if ($reveal) {
75+
$rows = [];
76+
foreach ($secrets as $name => $value) {
77+
$rows[] = [$name, $value];
78+
}
79+
$io->table(['name', 'secret'], $rows);
80+
81+
return;
82+
}
3883

39-
foreach ($this->secretStorage->listSecrets() as $key => $secret) {
40-
$table->addRow([$key, $secret]);
84+
$rows = [];
85+
foreach ($secrets as $name => $_) {
86+
$rows[] = [$name];
4187
}
4288

43-
$table->render();
89+
$io->comment(sprintf('To reveal the values of the secrets use <info>php %s %s --reveal</info>', $_SERVER['PHP_SELF'], $this->getName()));
90+
$io->table(['name'], $rows);
4491
}
4592
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Secret\Storage\MutableSecretStorageInterface;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputArgument;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
use Symfony\Component\Console\Style\SymfonyStyle;
20+
21+
/**
22+
* @author Jérémy Derussé <jeremy@derusse.com>
23+
*/
24+
final class SecretsRemoveCommand extends Command
25+
{
26+
protected static $defaultName = 'secrets:remove';
27+
28+
private $secretsStorage;
29+
30+
public function __construct(MutableSecretStorageInterface $secretsStorage)
31+
{
32+
$this->secretsStorage = $secretsStorage;
33+
34+
parent::__construct();
35+
}
36+
37+
protected function configure()
38+
{
39+
$this
40+
->setDefinition([
41+
new InputArgument('name', InputArgument::REQUIRED, 'The name of the secret'),
42+
])
43+
->setDescription('Removes a secret from the storage.')
44+
->setHelp(<<<'EOF'
45+
The <info>%command.name%</info> command remove a secret.
46+
47+
%command.full_name% <name>
48+
EOF
49+
)
50+
;
51+
}
52+
53+
protected function execute(InputInterface $input, OutputInterface $output)
54+
{
55+
$io = new SymfonyStyle($input, $output);
56+
57+
$this->secretsStorage->removeSecret($input->getArgument('name'));
58+
59+
$io->success('Secret was successfully removed.');
60+
}
61+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,8 @@ private function addSecretsSection(ArrayNodeDefinition $rootNode)
127127
->arrayNode('secrets')
128128
->canBeEnabled()
129129
->children()
130-
->scalarNode('encrypted_secrets_dir')->end()
131-
->scalarNode('encryption_key')->end()
132-
//->scalarNode('public_key')->end()
133-
//->scalarNode('private_key')->end()
134-
->scalarNode('decrypted_secrets_cache')->end()
130+
->scalarNode('encrypted_secrets_dir')->defaultValue('%kernel.project_dir%/config/secrets/%kernel.environment%')->cannotBeEmpty()->end()
131+
->scalarNode('encryption_key')->defaultValue('%kernel.project_dir%/config/secrets/encryption_%kernel.environment%.key')->cannotBeEmpty()->end()
135132
->end()
136133
->end()
137134
->end()

0 commit comments

Comments
 (0)
0