feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -4,17 +4,17 @@ declare(strict_types=1);
namespace App\Framework\Vault\Console;
use App\Framework\Attributes\ConsoleCommand;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleOutputInterface;
use App\Framework\Console\ExitCode;
use App\Framework\Vault\DatabaseVault;
use App\Framework\Vault\Exceptions\VaultKeyNotFoundException;
use App\Framework\Vault\Vault;
use App\Framework\Vault\VaultAuditLogger;
use App\Framework\Vault\ValueObjects\SecretKey;
use App\Framework\Vault\ValueObjects\SecretValue;
use App\Framework\Vault\Vault;
use App\Framework\Vault\VaultAuditLogger;
/**
* CLI Commands für Vault Management
@@ -22,64 +22,25 @@ use App\Framework\Vault\ValueObjects\SecretValue;
final readonly class VaultCommands
{
public function __construct(
private ?Vault $vault = null,
private ?VaultAuditLogger $auditLogger = null
private Vault $vault,
private VaultAuditLogger $auditLogger
) {
}
#[ConsoleCommand('vault:generate-key', 'Generate a new Vault encryption key')]
public function generateKey(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$output->writeLine('🔐 Generating Vault encryption key...', ConsoleColor::CYAN);
$output->newLine();
try {
$key = DatabaseVault::generateEncryptionKey();
$encodedKey = DatabaseVault::encodeKey($key);
$output->writeSuccess('✅ Encryption key generated successfully!');
$output->newLine();
$output->writeLine('VAULT_ENCRYPTION_KEY=' . $encodedKey, ConsoleColor::YELLOW);
$output->newLine();
$output->writeWarning('🚨 SECURITY WARNINGS:');
$output->writeLine('• Store this key securely - it cannot be recovered if lost');
$output->writeLine('• Add this to your .env file (never commit to version control)');
$output->writeLine('• Use different keys for different environments');
$output->newLine();
$output->writeInfo('📋 Next steps:');
$output->writeLine('1. Add the key to your .env file');
$output->writeLine('2. Run migrations: php console.php db:migrate');
$output->writeLine('3. Start using vault: php console.php vault:set my-secret "secret-value"');
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
$output->writeError('❌ Failed to generate encryption key: ' . $e->getMessage());
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand('vault:set', 'Store a secret in the vault')]
public function set(ConsoleInput $input, ConsoleOutput $output): ExitCode
public function set(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
if ($this->vault === null) {
$output->writeError('❌ Vault not available. Make sure VAULT_ENCRYPTION_KEY is set in .env');
return ExitCode::CONFIG_ERROR;
}
$keyArg = $input->getArgument(0);
$valueArg = $input->getArgument(1);
if (!$keyArg) {
if (! $keyArg) {
$output->writeError('❌ Usage: vault:set <key> <value>');
return ExitCode::INVALID_INPUT;
}
if (!$valueArg) {
if (! $valueArg) {
$valueArg = $output->askPassword('Enter secret value:');
}
@@ -106,7 +67,7 @@ final readonly class VaultCommands
}
#[ConsoleCommand('vault:get', 'Retrieve a secret from the vault')]
public function get(ConsoleInput $input, ConsoleOutput $output): ExitCode
public function get(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
if ($this->vault === null) {
$output->writeError('❌ Vault not available. Make sure VAULT_ENCRYPTION_KEY is set in .env');
@@ -116,7 +77,7 @@ final readonly class VaultCommands
$keyArg = $input->getArgument(0);
if (!$keyArg) {
if (! $keyArg) {
$output->writeError('❌ Usage: vault:get <key>');
return ExitCode::INVALID_INPUT;
@@ -145,7 +106,7 @@ final readonly class VaultCommands
}
#[ConsoleCommand('vault:delete', 'Delete a secret from the vault')]
public function delete(ConsoleInput $input, ConsoleOutput $output): ExitCode
public function delete(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
if ($this->vault === null) {
$output->writeError('❌ Vault not available. Make sure VAULT_ENCRYPTION_KEY is set in .env');
@@ -155,13 +116,13 @@ final readonly class VaultCommands
$keyArg = $input->getArgument(0);
if (!$keyArg) {
if (! $keyArg) {
$output->writeError('❌ Usage: vault:delete <key>');
return ExitCode::INVALID_INPUT;
}
if (!$output->confirm("⚠️ Delete secret '{$keyArg}'?", false)) {
if (! $output->confirm("⚠️ Delete secret '{$keyArg}'?", false)) {
$output->writeInfo('Operation cancelled');
return ExitCode::SUCCESS;
@@ -186,7 +147,7 @@ final readonly class VaultCommands
}
#[ConsoleCommand('vault:list', 'List all secrets in the vault')]
public function listSecrets(ConsoleInput $input, ConsoleOutput $output): ExitCode
public function listSecrets(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
if ($this->vault === null) {
$output->writeError('❌ Vault not available. Make sure VAULT_ENCRYPTION_KEY is set in .env');
@@ -233,7 +194,7 @@ final readonly class VaultCommands
}
#[ConsoleCommand('vault:audit', 'Show vault audit log')]
public function audit(ConsoleInput $input, ConsoleOutput $output): ExitCode
public function audit(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
if ($this->auditLogger === null) {
$output->writeError('❌ Audit logger not available');
@@ -278,7 +239,7 @@ final readonly class VaultCommands
$output->writeLine(' ' . implode(', ', $details), ConsoleColor::GRAY);
}
if (!$entry->success && $entry->errorMessage) {
if (! $entry->success && $entry->errorMessage) {
$output->writeLine(" Error: {$entry->errorMessage}", ConsoleColor::RED);
}
}
@@ -295,7 +256,7 @@ final readonly class VaultCommands
}
#[ConsoleCommand('vault:rotate-key', 'Rotate vault encryption key')]
public function rotateKey(ConsoleInput $input, ConsoleOutput $output): ExitCode
public function rotateKey(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
if ($this->vault === null) {
$output->writeError('❌ Vault not available. Make sure VAULT_ENCRYPTION_KEY is set in .env');
@@ -309,7 +270,7 @@ final readonly class VaultCommands
$output->writeLine('Make sure you have a backup before proceeding!');
$output->newLine();
if (!$output->confirm('Continue with key rotation?', false)) {
if (! $output->confirm('Continue with key rotation?', false)) {
$output->writeInfo('Operation cancelled');
return ExitCode::SUCCESS;

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\Vault\Console;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutputInterface;
use App\Framework\Console\ExitCode;
use App\Framework\Vault\DatabaseVault;
/**
* Vault Key Generation Command (standalone - no Vault dependency)
*/
final readonly class VaultKeyCommand
{
#[ConsoleCommand('vault:generate-key', 'Generate a new Vault encryption key')]
public function generateKey(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
$output->writeLine('🔐 Generating Vault encryption key...', ConsoleColor::CYAN);
$output->newLine();
try {
$key = DatabaseVault::generateEncryptionKey();
$encodedKey = DatabaseVault::encodeKey($key);
$output->writeSuccess('✅ Encryption key generated successfully!');
$output->newLine();
$output->writeLine('VAULT_ENCRYPTION_KEY=' . $encodedKey, ConsoleColor::YELLOW);
$output->newLine();
$output->writeWarning('🚨 SECURITY WARNINGS:');
$output->writeLine('• Store this key securely - it cannot be recovered if lost');
$output->writeLine('• Add this to your .env file (never commit to version control)');
$output->writeLine('• Use different keys for different environments');
$output->newLine();
$output->writeInfo('📋 Next steps:');
$output->writeLine('1. Add the key to your .env file');
$output->writeLine('2. Restart your application');
$output->writeLine('3. Start using vault: php console.php vault:set my-secret "secret-value"');
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
$output->writeError('❌ Failed to generate encryption key: ' . $e->getMessage());
return ExitCode::GENERAL_ERROR;
}
}
}

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace App\Framework\Vault;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\DateTime\Clock;
use App\Framework\Http\IpAddress;
use App\Framework\Ulid\Ulid;
use App\Framework\UserAgent\UserAgent;
@@ -35,10 +37,11 @@ final readonly class DatabaseVault implements Vault
private ConnectionInterface $connection,
private string $encryptionKey,
private VaultAuditLogger $auditLogger,
private Clock $clock,
private ?IpAddress $clientIp = null,
private ?UserAgent $userAgent = null
) {
if (!extension_loaded('sodium')) {
if (! extension_loaded('sodium')) {
throw new \RuntimeException('Sodium extension required for DatabaseVault');
}
@@ -51,10 +54,11 @@ final readonly class DatabaseVault implements Vault
public function get(SecretKey $key): SecretValue
{
$row = $this->connection->queryOne(
$query = SqlQuery::create(
'SELECT encrypted_value, encryption_nonce FROM vault_secrets WHERE secret_key = ?',
[$key->value]
);
$row = $this->connection->queryOne($query);
if ($row === null) {
throw VaultKeyNotFoundException::forKey($key);
@@ -100,7 +104,7 @@ final readonly class DatabaseVault implements Vault
$encrypted = $this->encrypt($value->reveal(), $nonce);
$data = [
'id' => Ulid::generate(),
'id' => (string) new Ulid($this->clock),
'secret_key' => $key->value,
'encrypted_value' => base64_encode($encrypted),
'encryption_nonce' => base64_encode($nonce),
@@ -111,17 +115,19 @@ final readonly class DatabaseVault implements Vault
// Upsert: Insert oder Update wenn bereits existiert
if ($this->has($key)) {
$this->connection->update(
$updateQuery = SqlQuery::update(
'vault_secrets',
[
'encrypted_value' => $data['encrypted_value'],
'encryption_nonce' => $data['encryption_nonce'],
'updated_at' => $data['updated_at'],
],
['secret_key' => $key->value]
);
'secret_key = :secret_key'
)->withParameter('secret_key', $key->value);
$this->connection->execute($updateQuery);
} else {
$this->connection->insert('vault_secrets', $data);
$insertQuery = SqlQuery::insert('vault_secrets', $data);
$this->connection->execute($insertQuery);
}
// Audit Log
@@ -147,24 +153,24 @@ final readonly class DatabaseVault implements Vault
public function has(SecretKey $key): bool
{
$result = $this->connection->queryOne(
$query = SqlQuery::create(
'SELECT COUNT(*) as count FROM vault_secrets WHERE secret_key = ?',
[$key->value]
);
$result = $this->connection->queryOne($query);
return ($result['count'] ?? 0) > 0;
}
public function delete(SecretKey $key): void
{
if (!$this->has($key)) {
if (! $this->has($key)) {
throw VaultKeyNotFoundException::forKey($key);
}
$this->connection->delete(
'vault_secrets',
['secret_key' => $key->value]
);
$deleteQuery = SqlQuery::delete('vault_secrets', 'secret_key = :secret_key')
->withParameter('secret_key', $key->value);
$this->connection->execute($deleteQuery);
// Audit Log
$this->auditLogger->log(
@@ -180,22 +186,24 @@ final readonly class DatabaseVault implements Vault
public function all(): array
{
$result = $this->connection->query(
'SELECT secret_key FROM vault_secrets ORDER BY secret_key ASC'
);
$query = SqlQuery::create('SELECT secret_key FROM vault_secrets ORDER BY secret_key ASC');
$result = $this->connection->query($query);
return array_map(
fn (array $row) => $row['secret_key'],
$result
);
$keys = [];
foreach ($result as $row) {
$keys[] = $row['secret_key'];
}
return $keys;
}
public function getMetadata(SecretKey $key): VaultMetadata
{
$row = $this->connection->queryOne(
$query = SqlQuery::create(
'SELECT * FROM vault_secrets WHERE secret_key = ?',
[$key->value]
);
$row = $this->connection->queryOne($query);
if ($row === null) {
throw VaultKeyNotFoundException::forKey($key);
@@ -205,8 +213,8 @@ final readonly class DatabaseVault implements Vault
key: $key,
createdAt: new DateTimeImmutable($row['created_at']),
updatedAt: new DateTimeImmutable($row['updated_at']),
createdBy: $row['created_by'],
updatedBy: $row['updated_by'],
createdBy: $row['created_by'] ?? null,
updatedBy: $row['updated_by'] ?? null,
accessCount: (int) $row['access_count'],
lastAccessedAt: $row['last_accessed_at']
? new DateTimeImmutable($row['last_accessed_at'])
@@ -222,9 +230,8 @@ final readonly class DatabaseVault implements Vault
);
}
$allSecrets = $this->connection->query(
'SELECT secret_key, encrypted_value, encryption_nonce FROM vault_secrets'
);
$query = SqlQuery::create('SELECT secret_key, encrypted_value, encryption_nonce FROM vault_secrets');
$allSecrets = $this->connection->query($query);
$rotatedCount = 0;
@@ -245,15 +252,16 @@ final readonly class DatabaseVault implements Vault
);
// Update in Database
$this->connection->update(
$updateQuery = SqlQuery::update(
'vault_secrets',
[
'encrypted_value' => base64_encode($newEncrypted),
'encryption_nonce' => base64_encode($newNonce),
'updated_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
],
['secret_key' => $row['secret_key']]
);
'secret_key = :secret_key'
)->withParameter('secret_key', $row['secret_key']);
$this->connection->execute($updateQuery);
$rotatedCount++;
@@ -298,13 +306,14 @@ final readonly class DatabaseVault implements Vault
private function updateAccessMetadata(SecretKey $key): void
{
$this->connection->query(
$query = SqlQuery::create(
'UPDATE vault_secrets
SET access_count = access_count + 1,
last_accessed_at = ?
WHERE secret_key = ?',
[(new DateTimeImmutable())->format('Y-m-d H:i:s'), $key->value]
);
$this->connection->execute($query);
}
/**

View File

@@ -28,7 +28,7 @@ final readonly class SecretKey
}
// Nur alphanumerisch, dots, underscores, hyphens
if (!preg_match('/^[a-zA-Z0-9._-]+$/', $value)) {
if (! preg_match('/^[a-zA-Z0-9._-]+$/', $value)) {
throw new InvalidArgumentException(
'Secret key can only contain letters, numbers, dots, underscores and hyphens'
);

View File

@@ -5,11 +5,11 @@ declare(strict_types=1);
namespace App\Framework\Vault;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Vault\ValueObjects\VaultAction;
use App\Framework\Vault\ValueObjects\VaultAuditEntry;
use App\Framework\Vault\ValueObjects\SecretKey;
use App\Framework\Http\IpAddress;
use App\Framework\UserAgent\UserAgent;
use App\Framework\Vault\ValueObjects\SecretKey;
use App\Framework\Vault\ValueObjects\VaultAction;
use App\Framework\Vault\ValueObjects\VaultAuditEntry;
use DateTimeImmutable;
/**
@@ -27,10 +27,24 @@ final readonly class VaultAuditLogger
public function log(VaultAuditEntry $entry): void
{
try {
$this->connection->insert(
'vault_audit_log',
$entry->toArray()
);
$data = $entry->toArray();
$sql = 'INSERT INTO vault_audit_log (secret_key, action, user_id, ip_address, user_agent, success, error_message, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)';
$params = [
$data['secret_key'],
$data['action'],
$data['user_id'] ?? null,
$data['ip_address'] ?? null,
$data['user_agent'] ?? null,
$data['success'] ? 1 : 0,
$data['error_message'] ?? null,
$data['timestamp'] ?? date('Y-m-d H:i:s'),
];
$query = \App\Framework\Database\ValueObjects\SqlQuery::create($sql, $params);
$this->connection->execute($query);
} catch (\Throwable $e) {
// Audit Logging sollte niemals Operations blockieren
// Aber wir loggen den Fehler für Monitoring
@@ -45,13 +59,13 @@ final readonly class VaultAuditLogger
*/
public function getAuditLog(string $secretKey, int $limit = 100): array
{
$result = $this->connection->query(
'SELECT * FROM vault_audit_log
WHERE secret_key = ?
ORDER BY timestamp DESC
LIMIT ?',
[$secretKey, $limit]
);
$sql = 'SELECT * FROM vault_audit_log
WHERE secret_key = ?
ORDER BY timestamp DESC
LIMIT ?';
$query = \App\Framework\Database\ValueObjects\SqlQuery::create($sql, [$secretKey, $limit]);
$result = $this->connection->query($query);
$entries = [];
foreach ($result as $row) {
@@ -68,12 +82,12 @@ final readonly class VaultAuditLogger
*/
public function getRecentAuditLog(int $limit = 100, int $offset = 0): array
{
$result = $this->connection->query(
'SELECT * FROM vault_audit_log
ORDER BY timestamp DESC
LIMIT ? OFFSET ?',
[$limit, $offset]
);
$sql = 'SELECT * FROM vault_audit_log
ORDER BY timestamp DESC
LIMIT ? OFFSET ?';
$query = \App\Framework\Database\ValueObjects\SqlQuery::create($sql, [$limit, $offset]);
$result = $this->connection->query($query);
$entries = [];
foreach ($result as $row) {

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Framework\Vault;
use App\Framework\Attributes\Initializer;
use App\Framework\Config\EnvKey;
use App\Framework\Config\Environment;
use App\Framework\Config\EnvKey;
use App\Framework\Database\ConnectionInterface;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\Http\ServerEnvironment;
use RuntimeException;