From 2e262ef07a01fd019280c222e5c32f39df56183e Mon Sep 17 00:00:00 2001 From: Jonathon Byrdziak Date: Mon, 23 Mar 2026 17:30:14 -0700 Subject: [PATCH] Fix secrets injection: keep secrets in RAM, re-encrypt on config save - ConfigSave now re-encrypts .env files before commit when deployment.secrets is set to 'encrypted', so config save actually pushes updated .env.enc - ProtocolStart no longer copies secrets to disk; passes /dev/shm path directly to Docker Compose override, keeping secrets RAM-only - SecretsProvider uses absolute paths in env_file override so Docker can read from /dev/shm - Added debug logging to SecretsProvider for troubleshooting secrets resolution Co-Authored-By: Claude Opus 4.6 --- src/Commands/ConfigSave.php | 21 +++++++++++++++++++++ src/Commands/ProtocolStart.php | 31 ++++++++++++++----------------- src/Helpers/SecretsProvider.php | 27 +++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/Commands/ConfigSave.php b/src/Commands/ConfigSave.php index a646be9..56a51ab 100644 --- a/src/Commands/ConfigSave.php +++ b/src/Commands/ConfigSave.php @@ -46,6 +46,8 @@ use Gitcd\Helpers\Str; use Gitcd\Helpers\Dir; use Gitcd\Helpers\Git; +use Gitcd\Helpers\FileEncryption; +use Gitcd\Helpers\Secrets; use Gitcd\Utils\Json; Class ConfigSave extends Command { @@ -96,6 +98,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + // Re-encrypt .env files if encryption is configured + $secretsMode = Json::read('deployment.secrets', 'file', $repo_dir); + if ($secretsMode === 'encrypted' && Secrets::hasKey()) { + $unencrypted = FileEncryption::findUnencryptedEnvFiles($configrepo); + if (!empty($unencrypted)) { + $output->writeln("Encrypting .env files..."); + $dir = rtrim($configrepo, '/') . '/'; + foreach ($unencrypted as $envName) { + $envPath = $dir . $envName; + $encPath = $envPath . '.enc'; + if (Secrets::encryptFile($envPath, $encPath)) { + $output->writeln(" ✓ Encrypted: {$envName} → {$envName}.enc"); + } else { + $output->writeln(" ✗ Failed to encrypt: {$envName}"); + } + } + } + } + Git::commit( 'Saving untracked changes', $configrepo ); Git::push( $configrepo ); $output->writeln("Your config repo was saved"); diff --git a/src/Commands/ProtocolStart.php b/src/Commands/ProtocolStart.php index ba46c34..5d732bd 100644 --- a/src/Commands/ProtocolStart.php +++ b/src/Commands/ProtocolStart.php @@ -565,25 +565,26 @@ private function startContainers(string $dir, array $ctx): void } if ($tmpEnv) { - $secretsFile = rtrim($dir, '/') . '/.env.protocol-secrets'; - copy($tmpEnv, $secretsFile); - chmod($secretsFile, 0600); - unlink($tmpEnv); - - $overrideFile = SecretsProvider::generateComposeOverride($composePath, $secretsFile); + // Keep secrets in RAM — use the tmpEnv path directly (already on /dev/shm) + $overrideFile = SecretsProvider::generateComposeOverride($composePath, $tmpEnv); $runner->log("{$dockerCommand} up --build -d (with secrets)"); - Shell::run("cd " . escapeshellarg($dir) + $runner->log("tmpEnv={$tmpEnv} overrideFile={$overrideFile}"); + $runner->log("override contents: " . file_get_contents($overrideFile)); + + $cmd = "cd " . escapeshellarg($dir) . " && {$dockerCommand} -f " . escapeshellarg($composePath) . " -f " . escapeshellarg($overrideFile) . $portOverrideFlag - . " up --build -d 2>&1"); + . " up --build -d 2>&1"; + $runner->log("docker cmd: {$cmd}"); + Shell::run($cmd); - unlink($secretsFile); + unlink($tmpEnv); unlink($overrideFile); $runner->log("Secrets temp files cleaned up"); } else { - $runner->log("{$dockerCommand} up --build -d"); + $runner->log("{$dockerCommand} up --build -d (NO secrets - resolveToTempFile returned null)"); Shell::run("cd " . escapeshellarg($dir) . " && {$dockerCommand}" . " -f " . escapeshellarg($composePath) @@ -602,13 +603,9 @@ private function dockerUpWithSecrets(string $dir, StageRunner $runner): bool { $tmpEnv = SecretsProvider::resolveToTempFile($dir); if ($tmpEnv) { - $secretsFile = rtrim($dir, '/') . '/.env.protocol-secrets'; - copy($tmpEnv, $secretsFile); - chmod($secretsFile, 0600); - unlink($tmpEnv); - + // Keep secrets in RAM — use the tmpEnv path directly (already on /dev/shm) $composePath = rtrim($dir, '/') . '/docker-compose.yml'; - $overrideFile = SecretsProvider::generateComposeOverride($composePath, $secretsFile); + $overrideFile = SecretsProvider::generateComposeOverride($composePath, $tmpEnv); $envFile = rtrim($dir, '/') . '/.env.deployment'; $dockerCommand = Docker::getDockerCommand(); @@ -621,7 +618,7 @@ private function dockerUpWithSecrets(string $dir, StageRunner $runner): bool . " up -d 2>&1", $returnVar); $started = $returnVar === 0; - unlink($secretsFile); + unlink($tmpEnv); unlink($overrideFile); $runner->log("Secrets temp files cleaned up"); return $started; diff --git a/src/Helpers/SecretsProvider.php b/src/Helpers/SecretsProvider.php index cba909a..d6d23ba 100644 --- a/src/Helpers/SecretsProvider.php +++ b/src/Helpers/SecretsProvider.php @@ -54,14 +54,17 @@ class SecretsProvider public static function resolveToTempFile(string $repoDir): ?string { $mode = Json::read('deployment.secrets', 'file', $repoDir); + self::log("resolveToTempFile: mode={$mode} repoDir={$repoDir}"); if ($mode === 'encrypted') { $configRepo = Config::repo($repoDir); if (!$configRepo) { + self::log("resolveToTempFile: no config repo found"); return null; } $encFile = $configRepo . '.env.enc'; if (!is_file($encFile) || !Secrets::hasKey()) { + self::log("resolveToTempFile: enc file missing or no key"); return null; } return Secrets::decryptToTempFile($encFile); @@ -70,15 +73,19 @@ public static function resolveToTempFile(string $repoDir): ?string if ($mode === 'aws') { // Dynamic load to avoid hard dependency on the plugin if (!class_exists('\\Gitcd\\Plugins\\awssecrets\\AwsSecretsHelper')) { + self::log("resolveToTempFile: AwsSecretsHelper class not found"); return null; } // Pull secrets from AWS $awsEnv = \Gitcd\Plugins\awssecrets\AwsSecretsHelper::pullSecret($repoDir); if ($awsEnv === null) { + self::log("resolveToTempFile: AWS pullSecret returned null"); return null; } + self::log("resolveToTempFile: AWS pull OK, got " . strlen($awsEnv) . " bytes"); + // Read the existing .env (non-secret config like LOG_LEVEL, APP_ENV) $baseEnv = self::readBaseEnv($repoDir); @@ -91,12 +98,28 @@ public static function resolveToTempFile(string $repoDir): ?string file_put_contents($tmpFile, $merged); chmod($tmpFile, 0600); + self::log("resolveToTempFile: wrote {$tmpFile}"); return $tmpFile; } + self::log("resolveToTempFile: mode '{$mode}' not handled, returning null"); return null; } + /** + * Write a line to the secrets debug log. + */ + private static function log(string $message): void + { + $logDir = '/var/log/protocol'; + if (!is_dir($logDir)) { + @mkdir($logDir, 0755, true); + } + $logFile = $logDir . '/secrets-provider.log'; + $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . "\n"; + file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX); + } + /** * Generate a docker-compose override file that adds env_file to every service. * @@ -113,14 +136,14 @@ public static function generateComposeOverride(string $composePath, string $envF // Parse the original compose file to get service names $parsed = Yaml::parseFile($composePath); $services = $parsed['services'] ?? []; - $envFileName = basename($envFilePath); + $envFileRef = realpath($envFilePath) ?: $envFilePath; // Build override YAML $override = "services:\n"; foreach (array_keys($services) as $serviceName) { $override .= " {$serviceName}:\n"; $override .= " env_file:\n"; - $override .= " - {$envFileName}\n"; + $override .= " - {$envFileRef}\n"; } $overridePath = dirname($composePath) . '/.docker-compose.secrets-override.yml';