Skip to content

Commit

Permalink
Add secrets management
Browse files Browse the repository at this point in the history
  • Loading branch information
jderusse authored and nicolas-grekas committed Oct 18, 2019
1 parent 8c8f623 commit 02b5d74
Show file tree
Hide file tree
Showing 27 changed files with 951 additions and 272 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ CHANGELOG
* Added new `error_controller` configuration to handle system exceptions
* Added sort option for `translation:update` command.
* [BC Break] The `framework.messenger.routing.senders` config key is not deep merged anymore.
* Added secrets management.

4.3.0
-----
Expand Down
72 changes: 37 additions & 35 deletions src/Symfony/Bundle/FrameworkBundle/Command/SecretsAddCommand.php
Original file line number Diff line number Diff line change
@@ -1,68 +1,70 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\Command;

use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
use Symfony\Bundle\FrameworkBundle\Secret\Storage\MutableSecretStorageInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;

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

/**
* @var SecretStorageInterface
*/
private $secretStorage;
private $secretsStorage;

public function __construct(SecretStorageInterface $secretStorage)
public function __construct(MutableSecretStorageInterface $secretsStorage)
{
$this->secretStorage = $secretStorage;
$this->secretsStorage = $secretsStorage;

parent::__construct();
}

protected function configure()
{
$this
->setDescription('Adds a secret with the key.')
->addArgument(
'key',
InputArgument::REQUIRED
)
->addArgument(
'secret',
InputArgument::REQUIRED
->setDefinition([
new InputArgument('name', InputArgument::REQUIRED, 'The name of the secret'),
])
->setDescription('Adds a secret in the storage.')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command stores a secret.
%command.full_name% <name>
EOF
)
;
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$key = $input->getArgument('key');
$secret = $input->getArgument('secret');

$this->secretStorage->putSecret($key, $secret);
}

protected function interact(InputInterface $input, OutputInterface $output)
{
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');

$question = new Question('Key of the secret: ', $input->getArgument('key'));
$io = new SymfonyStyle($input, $output);

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

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

$secret = $helper->ask($input, $output, $question);
$input->setArgument('secret', $secret);
$io->success('Secret was successfully stored.');
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,97 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\Command;

use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
use Symfony\Bundle\FrameworkBundle\Secret\Encoder\EncoderInterface;
use Symfony\Bundle\FrameworkBundle\Secret\Storage\MutableSecretStorageInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* @author Tobias Schultze <http://tobion.de>
* @author Jérémy Derussé <jeremy@derusse.com>
*/
final class SecretsGenerateKeyCommand extends Command
{
protected static $defaultName = 'secrets:generate-key';
private $secretsStorage;
private $encoder;

public function __construct(EncoderInterface $encoder, MutableSecretStorageInterface $secretsStorage)
{
$this->secretsStorage = $secretsStorage;
$this->encoder = $encoder;
parent::__construct();
}

protected function configure()
{
$this
->setDescription('Prints a randomly generated encryption key.')
->setDefinition([
new InputOption('rekey', 'r', InputOption::VALUE_NONE, 'Re-encrypt previous secret with the new key.'),
])
->setDescription('Generates a new encryption key.')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command generates a new encryption key.
%command.full_name%
If a previous encryption key already exists, the command must be called with
the <info>--rekey</info> option in order to override that key and re-encrypt
previous secrets.
%command.full_name% --rekey
EOF
)
;
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$encryptionKey = sodium_crypto_stream_keygen();
$rekey = $input->getOption('rekey');

$previousSecrets = [];
try {
foreach ($this->secretsStorage->listSecrets(true) as $name => $decryptedSecret) {
$previousSecrets[$name] = $decryptedSecret;
}
} catch (EncryptionKeyNotFoundException $e) {
if (!$rekey) {
throw $e;
}
}

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

sodium_memzero($encryptionKey);
$io = new SymfonyStyle($input, $output);
switch (\count($keys)) {
case 0:
$io->success('Keys have been generated.');
break;
case 1:
$io->success(sprintf('A key has been generated in "%s".', $keys[0]));
$io->caution('DO NOT COMMIT that file!');
break;
default:
$io->success(sprintf("Keys have been generated in :\n -%s", implode("\n -", $keys)));
$io->caution('DO NOT COMMIT those files!');
break;
}
}
}
69 changes: 58 additions & 11 deletions src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\Command;

use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
use Symfony\Bundle\FrameworkBundle\Secret\Storage\SecretStorageInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* @author Tobias Schultze <http://tobion.de>
* @author Jérémy Derussé <jeremy@derusse.com>
*/
final class SecretsListCommand extends Command
{
protected static $defaultName = 'secrets:list';
protected static $defaultName = 'debug:secrets';

/**
* @var SecretStorageInterface
*/
private $secretStorage;

public function __construct(SecretStorageInterface $secretStorage)
Expand All @@ -27,19 +39,54 @@ public function __construct(SecretStorageInterface $secretStorage)
protected function configure()
{
$this
->setDefinition([
new InputOption('reveal', 'r', InputOption::VALUE_NONE, 'Display decrypted values alongside names'),
])
->setDescription('Lists all secrets.')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command list all stored secrets.
%command.full_name%
When the the option <info>--reveal</info> is provided, the decrypted secrets are also displayed.
%command.full_name% --reveal
EOF
)
;

$this
->setDescription('Lists all secrets.')
->addOption('reveal', 'r', InputOption::VALUE_NONE, 'Display decrypted values alongside names');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$table = new Table($output);
$table->setHeaders(['key', 'plaintext secret']);
$reveal = $input->getOption('reveal');
$io = new SymfonyStyle($input, $output);

try {
$secrets = $this->secretStorage->listSecrets($reveal);
} catch (EncryptionKeyNotFoundException $e) {
throw new \LogicException(sprintf('Unable to decrypt secrets, the encryption key "%s" is missing.', $e->getKeyLocation()));
}

if ($reveal) {
$rows = [];
foreach ($secrets as $name => $value) {
$rows[] = [$name, $value];
}
$io->table(['name', 'secret'], $rows);

return;
}

foreach ($this->secretStorage->listSecrets() as $key => $secret) {
$table->addRow([$key, $secret]);
$rows = [];
foreach ($secrets as $name => $_) {
$rows[] = [$name];
}

$table->render();
$io->comment(sprintf('To reveal the values of the secrets use <info>php %s %s --reveal</info>', $_SERVER['PHP_SELF'], $this->getName()));
$io->table(['name'], $rows);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\Command;

use Symfony\Bundle\FrameworkBundle\Secret\Storage\MutableSecretStorageInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
final class SecretsRemoveCommand extends Command
{
protected static $defaultName = 'secrets:remove';

private $secretsStorage;

public function __construct(MutableSecretStorageInterface $secretsStorage)
{
$this->secretsStorage = $secretsStorage;

parent::__construct();
}

protected function configure()
{
$this
->setDefinition([
new InputArgument('name', InputArgument::REQUIRED, 'The name of the secret'),
])
->setDescription('Removes a secret from the storage.')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command remove a secret.
%command.full_name% <name>
EOF
)
;
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);

$this->secretsStorage->removeSecret($input->getArgument('name'));

$io->success('Secret was successfully removed.');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,8 @@ private function addSecretsSection(ArrayNodeDefinition $rootNode)
->arrayNode('secrets')
->canBeEnabled()
->children()
->scalarNode('encrypted_secrets_dir')->end()
->scalarNode('encryption_key')->end()
//->scalarNode('public_key')->end()
//->scalarNode('private_key')->end()
->scalarNode('decrypted_secrets_cache')->end()
->scalarNode('encrypted_secrets_dir')->defaultValue('%kernel.project_dir%/config/secrets/%kernel.environment%')->cannotBeEmpty()->end()
->scalarNode('encryption_key')->defaultValue('%kernel.project_dir%/config/secrets/encryption_%kernel.environment%.key')->cannotBeEmpty()->end()
->end()
->end()
->end()
Expand Down

0 comments on commit 02b5d74

Please sign in to comment.