feature #34131 [FrameworkBundle] Remove suffix convention when using env vars to override secrets from the vault (nicolas-grekas)

This PR was merged into the 4.4 branch.

Discussion
----------

[FrameworkBundle] Remove suffix convention when using env vars to override secrets from the vault

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

Right now, env vars that override encrypted secrets must en up with `_SECRET`.
This PR removes this convention. It also enforces that only vars defined in the vault can be overriden locally. This means one cannot set a local-only secret.

Commits
-------

2ec9647e75 [FrameworkBundle] Remove suffix convention when using env vars to override secrets from the vault
This commit is contained in:
Fabien Potencier 2019-10-27 09:05:59 +01:00
commit 22230f7b8e
8 changed files with 54 additions and 47 deletions

View File

@ -45,7 +45,7 @@ final class SecretsDecryptToLocalCommand extends Command
->setDescription('Decrypts all secrets and stores them in the local vault.')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces overriding of secrets that already exist in the local vault')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command list decrypts all secrets and stores them in the local vault..
The <info>%command.name%</info> command decrypts all secrets and copies them in the local vault.
<info>%command.full_name%</info>

View File

@ -14,7 +14,6 @@ namespace Symfony\Bundle\FrameworkBundle\Command;
use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
@ -43,15 +42,10 @@ final class SecretsEncryptFromLocalCommand extends Command
{
$this
->setDescription('Encrypts all local secrets to the vault.')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces overriding of secrets that already exist in the vault')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command list encrypts all local secrets and stores them in the vault..
The <info>%command.name%</info> command encrypts all locally overridden secrets to the vault.
<info>%command.full_name%</info>
When the option <info>--force</info> is provided, secrets that already exist in the vault are overriden.
<info>%command.full_name% --force</info>
EOF
)
;
@ -67,22 +61,16 @@ EOF
return 1;
}
$secrets = $this->localVault->list(true);
foreach ($this->vault->list(true) as $name => $value) {
$localValue = $this->localVault->reveal($name);
if (!$input->getOption('force')) {
foreach ($this->vault->list() as $k => $v) {
unset($secrets[$k]);
}
}
foreach ($secrets as $k => $v) {
if (null === $v) {
$io->error($this->localVault->getLastMessage());
if (null !== $localValue && $value !== $localValue) {
$this->vault->seal($name, $localValue);
} elseif (null !== $message = $this->localVault->getLastMessage()) {
$io->error($message);
return 1;
}
$this->vault->seal($k, $v);
}
return 0;

View File

@ -89,11 +89,11 @@ EOF
}
foreach ($localSecrets ?? [] as $name => $value) {
$rows[$name] = [$name, $rows[$name][1] ?? '', $dump($value)];
if (isset($rows[$name])) {
$rows[$name][] = $dump($value);
}
}
uksort($rows, 'strnatcmp');
if (null !== $this->localVault && null !== $message = $this->localVault->getLastMessage()) {
$io->comment($message);
}

View File

@ -86,6 +86,12 @@ EOF
return 1;
}
if ($this->localVault === $vault && !\array_key_exists($name, $this->vault->list())) {
$io->error(sprintf('Secret "%s" does not exist in the vault, you cannot override it locally.', $name));
return 1;
}
if (0 < $random = $input->getOption('random') ?? 16) {
$value = strtr(substr(base64_encode(random_bytes($random)), 0, $random), '+/', '-_');
} elseif (!$file = $input->getArgument('file')) {

View File

@ -36,14 +36,13 @@ class DotenvVault extends AbstractVault
{
$this->lastMessage = null;
$this->validateName($name);
$k = $name.'_SECRET';
$v = str_replace("'", "'\\''", $value);
$content = file_exists($this->dotenvFile) ? file_get_contents($this->dotenvFile) : '';
$content = preg_replace("/^$k=((\\\\'|'[^']++')++|.*)/m", "$k='$v'", $content, -1, $count);
$content = preg_replace("/^$name=((\\\\'|'[^']++')++|.*)/m", "$name='$v'", $content, -1, $count);
if (!$count) {
$content .= "$k='$v'\n";
$content .= "$name='$v'\n";
}
file_put_contents($this->dotenvFile, $content);
@ -55,8 +54,7 @@ class DotenvVault extends AbstractVault
{
$this->lastMessage = null;
$this->validateName($name);
$k = $name.'_SECRET';
$v = \is_string($_SERVER[$k] ?? null) ? $_SERVER[$k] : ($_ENV[$k] ?? null);
$v = \is_string($_SERVER[$name] ?? null) ? $_SERVER[$name] : ($_ENV[$name] ?? null);
if (null === $v) {
$this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath($this->dotenvFile));
@ -71,10 +69,9 @@ class DotenvVault extends AbstractVault
{
$this->lastMessage = null;
$this->validateName($name);
$k = $name.'_SECRET';
$content = file_exists($this->dotenvFile) ? file_get_contents($this->dotenvFile) : '';
$content = preg_replace("/^$k=((\\\\'|'[^']++')++|.*)\n?/m", '', $content, -1, $count);
$content = preg_replace("/^$name=((\\\\'|'[^']++')++|.*)\n?/m", '', $content, -1, $count);
if ($count) {
file_put_contents($this->dotenvFile, $content);
@ -94,14 +91,14 @@ class DotenvVault extends AbstractVault
$secrets = [];
foreach ($_ENV as $k => $v) {
if (preg_match('/^(\w+)_SECRET$/D', $k, $m)) {
$secrets[$m[1]] = $reveal ? $v : null;
if (preg_match('/^\w+$/D', $k)) {
$secrets[$k] = $reveal ? $v : null;
}
}
foreach ($_SERVER as $k => $v) {
if (\is_string($v) && preg_match('/^(\w+)_SECRET$/D', $k, $m)) {
$secrets[$m[1]] = $reveal ? $v : null;
if (\is_string($v) && preg_match('/^\w+$/D', $k)) {
$secrets[$k] = $reveal ? $v : null;
}
}

View File

@ -44,9 +44,9 @@ class SecretEnvVarProcessor implements EnvVarProcessorInterface
/**
* {@inheritdoc}
*/
public function getEnv($prefix, $name, \Closure $getEnv)
public function getEnv($prefix, $name, \Closure $getEnv): string
{
if (null !== $this->localVault && null !== $secret = $this->localVault->reveal($name)) {
if (null !== $this->localVault && null !== ($secret = $this->localVault->reveal($name)) && \array_key_exists($name, $this->vault->list())) {
return $secret;
}

View File

@ -87,6 +87,12 @@ class SodiumVault extends AbstractVault
$this->validateName($name);
$this->loadKeys();
$this->export($name.'.'.substr_replace(md5($name), '.sodium', -26), sodium_crypto_box_seal($value, $this->encryptionKey ?? sodium_crypto_box_publickey($this->decryptionKey)));
$list = $this->list();
$list[$name] = null;
uksort($list, 'strnatcmp');
file_put_contents($this->pathPrefix.'sodium.list', sprintf("<?php\n\nreturn %s;\n", var_export($list, true), LOCK_EX));
$this->lastMessage = sprintf('Secret "%s" encrypted in "%s"; you can commit it.', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR));
}
@ -123,6 +129,10 @@ class SodiumVault extends AbstractVault
return false;
}
$list = $this->list();
unset($list[$name]);
file_put_contents($this->pathPrefix.'sodium.list', sprintf("<?php\n\nreturn %s;\n", var_export($list, true), LOCK_EX));
$this->lastMessage = sprintf('Secret "%s" removed from "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR));
return @unlink($file) || !file_exists($file);
@ -131,13 +141,19 @@ class SodiumVault extends AbstractVault
public function list(bool $reveal = false): array
{
$this->lastMessage = null;
$secrets = [];
$regexp = sprintf('{^%s(\w++)\.[0-9a-f]{6}\.sodium$}D', preg_quote(basename($this->pathPrefix)));
foreach (scandir(\dirname($this->pathPrefix)) as $name) {
if (preg_match($regexp, $name, $m)) {
$secrets[$m[1]] = $reveal ? $this->reveal($m[1]) : null;
}
if (!file_exists($file = $this->pathPrefix.'sodium.list')) {
return [];
}
$secrets = include $file;
if (!$reveal) {
return $secrets;
}
foreach ($secrets as $name => $value) {
$secrets[$name] = $this->reveal($name);
}
return $secrets;

View File

@ -37,21 +37,21 @@ class DotenvVaultTest extends TestCase
$vault->seal('foo', $plain);
unset($_SERVER['foo_SECRET'], $_ENV['foo_SECRET']);
unset($_SERVER['foo'], $_ENV['foo']);
(new Dotenv(false))->load($this->envFile);
$decrypted = $vault->reveal('foo');
$this->assertSame($plain, $decrypted);
$this->assertSame(['foo' => null], $vault->list());
$this->assertSame(['foo' => $plain], $vault->list(true));
$this->assertSame(['foo' => null], array_intersect_key($vault->list(), ['foo' => 123]));
$this->assertSame(['foo' => $plain], array_intersect_key($vault->list(true), ['foo' => 123]));
$this->assertTrue($vault->remove('foo'));
$this->assertFalse($vault->remove('foo'));
unset($_SERVER['foo_SECRET'], $_ENV['foo_SECRET']);
unset($_SERVER['foo'], $_ENV['foo']);
(new Dotenv(false))->load($this->envFile);
$this->assertSame([], $vault->list());
$this->assertArrayNotHasKey('foo', $vault->list());
}
}