Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/ideploy/app/Jobs/ApplicationDeploymentJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -2410,6 +2410,10 @@ private function generate_compose_file()
$labelValue = trim($labelValue, '"\'');
}

// Escape YAML special chars with quotes
if (str_contains($labelValue, ':')) {
$labelValue = '"' . str_replace('"', '\\"', $labelValue) . '"';
}
$labelsArray[$labelKey] = $labelValue;
}
}
Expand Down
48 changes: 38 additions & 10 deletions apps/ideploy/app/Jobs/Security/ConfigureCrowdSecTraefikLogsJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,29 @@ private function backupDockerCompose(): void
{
ray("Creating backup...");

// Check if directory and file exist first
$dirCheck = instant_remote_process([
'test -d /var/lib/coolify/crowdsec && echo "DIR_EXISTS" || echo "DIR_NOT_FOUND"'
], $this->server);

if (str_contains($dirCheck, 'DIR_NOT_FOUND')) {
throw new \Exception('CrowdSec directory /var/lib/coolify/crowdsec not found. Please install CrowdSec first.');
}

$fileCheck = instant_remote_process([
'test -f /var/lib/coolify/crowdsec/docker-compose.yml && echo "FILE_EXISTS" || echo "FILE_NOT_FOUND"'
], $this->server);

if (str_contains($fileCheck, 'FILE_NOT_FOUND')) {
throw new \Exception('CrowdSec docker-compose.yml not found. Please install CrowdSec first.');
}

instant_remote_process([
'cd /var/lib/coolify/crowdsec',
'cp docker-compose.yml docker-compose.yml.backup-' . date('YmdHis'),
], $this->server);

ray("✅ Backup created");
}

private function updateDockerCompose(): void
Expand All @@ -73,7 +92,7 @@ private function updateDockerCompose(): void
], $this->server);

// Check if already configured
if (str_contains($currentCompose, 'coolify_dev_coolify_data/_data/proxy')) {
if (str_contains($currentCompose, '/data/coolify/proxy:/traefik:ro')) {
ray("✅ Traefik logs already mounted");
return;
}
Expand All @@ -94,7 +113,7 @@ private function updateDockerCompose(): void
- './config:/etc/crowdsec'
- './data:/var/lib/crowdsec/data'
- '/var/log:/var/log:ro'
- '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy:/traefik:ro'
- '/data/coolify/proxy:/traefik:ro'
ports:
- '0.0.0.0:8081:8080'
networks:
Expand Down Expand Up @@ -171,8 +190,8 @@ private function recreateCrowdSec(): void

instant_remote_process([
'cd /var/lib/coolify/crowdsec',
'docker-compose down',
'docker-compose up -d',
'docker compose down',
'docker compose up -d',
], $this->server);

// Wait for CrowdSec to be healthy
Expand All @@ -186,12 +205,21 @@ private function verifyLogReading(): void
ray("Verifying log reading...");

// Check if file is accessible
$result = instant_remote_process([
'docker exec crowdsec-live ls -lh /traefik/access.log 2>&1'
], $this->server);

if (str_contains($result, 'No such file')) {
throw new \Exception('Traefik access.log not accessible in CrowdSec container');
try {
$result = instant_remote_process([
'docker exec crowdsec-live ls -lh /traefik/access.log 2>&1'
], $this->server);

if (str_contains($result, 'No such file')) {
ray("⚠️ Traefik access.log not found, but continuing...");
// Ne pas throw, juste un warning
return;
}

ray("✅ Traefik logs accessible in CrowdSec container");
} catch (\Exception $e) {
ray("⚠️ Could not verify log reading: {$e->getMessage()}");
// Continuer sans échec
}

// Check CrowdSec logs for acquisition
Expand Down
33 changes: 26 additions & 7 deletions apps/ideploy/app/Jobs/Security/DeployTrafficLoggerJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function handle(): void
], $this->server);

// 2. Upload script Python
$loggerScript = base_path('traffic-logger-v2.py');
$loggerScript = base_path('templates/traffic-logger/app/logger.py');

if (!file_exists($loggerScript)) {
ray("❌ Traffic Logger script not found: {$loggerScript}");
Expand All @@ -44,7 +44,18 @@ public function handle(): void
$this->server
);

ray("✅ Script uploaded");
// 2.1. Upload requirements.txt
$requirementsFile = base_path('templates/traffic-logger/app/requirements.txt');
if (file_exists($requirementsFile)) {
instant_scp(
$requirementsFile,
'/opt/traffic-logger/requirements.txt',
$this->server
);
ray("✅ Requirements uploaded");
}

ray("✅ Traffic Logger files uploaded");

// 3. Arrêter ancien container si existe
instant_remote_process([
Expand All @@ -56,10 +67,17 @@ public function handle(): void
// IMPORTANT: iDeploy est sur un serveur différent, utiliser l'IP publique
$iDeployPublicUrl = config('app.url');

// Si URL locale, utiliser l'IP du serveur iDeploy
// Si URL locale, utiliser l'IP accessible depuis le serveur distant
if (str_contains($iDeployPublicUrl, 'localhost') || str_contains($iDeployPublicUrl, '127.0.0.1')) {
// Récupérer l'IP publique du serveur iDeploy depuis les settings
$iDeployPublicUrl = 'http://' . gethostname() . ':8000';
// Utiliser une IP réellement accessible depuis le serveur de production
// Solution temporaire : utiliser l'IP publique ou un tunnel
$iDeployPublicUrl = 'http://142.93.201.15:8000'; // Remplacer par votre IP publique réelle
}

// Generate API key for secure communication
$apiKey = $this->server->traffic_logger_api_key ?? \Str::random(32);
if (!$this->server->traffic_logger_api_key) {
$this->server->update(['traffic_logger_api_key' => $apiKey]);
}

ray("Traffic Logger will connect to: {$iDeployPublicUrl}");
Expand All @@ -68,12 +86,13 @@ public function handle(): void
"docker run -d --name traffic-logger \\
--network coolify \\
--restart unless-stopped \\
-v /var/log/traefik:/var/log/traefik:ro \\
-v /data/coolify/proxy:/var/log/traefik:ro \\
-v /opt/traffic-logger:/app \\
-e IDEPLOY_API_URL={$iDeployPublicUrl}/api/internal/traffic-metrics \\
-e IDEPLOY_API_KEY={$apiKey} \\
-e CROWDSEC_LAPI_URL=http://crowdsec-live:8080 \\
python:3.11-slim \\
sh -c 'pip install requests && python /app/logger.py'"
sh -c 'cd /app && pip install -r requirements.txt && python logger.py'"
], $this->server);

ray("✅ Container started");
Expand Down
110 changes: 93 additions & 17 deletions apps/ideploy/app/Jobs/Security/EnableTraefikHeaderLoggingJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,33 +42,81 @@ public function handle(): void
return;
}

// 3. Add header logging arguments
// 3. Initialize newLines array and ensure access log file creation directive exists
$newLines = [];

if (!str_contains($composeContent, '--accesslog.filepath=')) {
ray("Adding accesslog.filepath directive");
// Add access log file creation before header configs
$accesslogLines = [
" - '--accesslog.filepath=/traefik/access.log'",
" - '--accesslog.format=json'",
" - '--accesslog.bufferingsize=100'",
];

// Insert access log config first
foreach ($accesslogLines as $line) {
if (!str_contains($composeContent, trim(str_replace(" - '", "", str_replace("'", "", $line))))) {
$newLines[] = $line;
}
}
}

// 4. Add header logging arguments
$headersToLog = [
'User-Agent',
'Referer',
'X-Forwarded-For',
'X-Real-Ip',
];

$newLines = [];
foreach ($headersToLog as $header) {
$newLines[] = " - '--accesslog.fields.headers.names.{$header}=keep'";
}

// 4. Insert after defaultmode=keep line
// 4. Insert after existing accesslog configurations or command section
$lines = explode("\n", $composeContent);
$newContent = [];
$inserted = false;

foreach ($lines as $line) {
$newContent[] = $line;
if (str_contains($line, '--accesslog.fields.headers.defaultmode=keep')) {
// Insert after the last accesslog line, or after command section starts
if (!$inserted && (
str_contains($line, '--accesslog.fields.names.ServiceName=keep') ||
str_contains($line, '--accesslog.bufferingsize=') ||
str_contains($line, '--providers.docker=true')
)) {
// Insert new lines after this one
foreach ($newLines as $newLine) {
$newContent[] = $newLine;
}
$inserted = true;
}
}

// If not inserted yet, add at the end of command section
if (!$inserted && !empty($newLines)) {
// Find the end of command section and insert before
$tempContent = [];
$inCommand = false;
foreach ($newContent as $line) {
if (str_contains($line, 'command:')) {
$inCommand = true;
}

if ($inCommand && (str_contains($line, 'labels:') || str_contains($line, 'volumes:'))) {
// End of command section, insert here
foreach ($newLines as $newLine) {
$tempContent[] = $newLine;
}
$inCommand = false;
}
$tempContent[] = $line;
}
$newContent = $tempContent;
}

$finalContent = implode("\n", $newContent);

// 5. Write to temp file and upload
Expand All @@ -87,16 +135,26 @@ public function handle(): void
ray("Recreating Traefik with new config...");
instant_remote_process([
'cd /data/coolify/proxy',
'docker-compose up -d --force-recreate'
'docker compose up -d --force-recreate'
], $this->server);

// 7. Wait for Traefik to be healthy
sleep(10);

// 8. Verify
// 8. Ensure access.log file exists and generate initial traffic
ray("Creating access.log file and generating initial traffic...");
instant_remote_process([
'touch /data/coolify/proxy/access.log',
'chmod 666 /data/coolify/proxy/access.log',
'curl -s http://localhost >/dev/null || true',
'curl -s http://localhost/health >/dev/null || true',
'sleep 2'
], $this->server);

// 9. Verify
$this->verifyHeaderLogging();

// 9. Update metadata
// 10. Update metadata
$this->updateServerMetadata();

ray("✅ Traefik header logging enabled successfully");
Expand All @@ -111,23 +169,41 @@ private function verifyHeaderLogging(): void
{
ray("Verifying header logging...");

// Try multiple possible log paths
$possiblePaths = [
'/data/coolify/proxy/access.log',
'/var/lib/docker/volumes/coolify_coolify_data/_data/proxy/access.log',
'/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/access.log',
];

// Make test request with custom User-Agent
instant_remote_process([
'curl -s -A "iDeploy-Test-Bot" http://localhost > /dev/null',
'sleep 2',
'sleep 3',
], $this->server);

// Check if User-Agent appears in log
$logPath = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/access.log';
$result = instant_remote_process([
"tail -5 {$logPath} | grep -i 'user-agent' || echo 'NOT_FOUND'"
], $this->server);

if (str_contains($result, 'NOT_FOUND')) {
throw new \Exception('User-Agent not found in access logs after configuration');
$found = false;
foreach ($possiblePaths as $logPath) {
try {
$result = instant_remote_process([
"test -f {$logPath} && tail -10 {$logPath} | grep -i 'user-agent' || echo 'NOT_FOUND'"
], $this->server);

if (!str_contains($result, 'NOT_FOUND')) {
ray("✅ Header logging verified at: {$logPath}");
$found = true;
break;
}
} catch (\Exception $e) {
ray("Path {$logPath} not accessible: {$e->getMessage()}");
continue;
}
}

ray("✅ Header logging verified");
if (!$found) {
ray("⚠️ Could not verify header logging, but configuration was applied");
// Ne pas throw une exception, juste un warning
}
}

private function updateServerMetadata(): void
Expand Down
27 changes: 27 additions & 0 deletions apps/ideploy/app/Jobs/Security/ValidateServerInstallationJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public function handle()

if ($isComplete) {
ray("✅ Serveur {$this->server->name} - Installation complète et validée");
// Update server metadata based on successful validations
$this->updateServerMetadata($validationResults);

ray("🎉 All firewall components validated successfully!");
} else {
ray("⚠️ Serveur {$this->server->name} - Installation incomplète: " . json_encode($validationResults));

Expand Down Expand Up @@ -130,4 +134,27 @@ private function retryFailedComponents(array $validationResults): void
\App\Jobs\Security\DeployTrafficLoggerJob::dispatch($this->server)->delay(now()->addMinutes(3));
}
}

private function updateServerMetadata(array $validationResults): void
{
$updateData = [];

if ($validationResults['crowdsec'] ?? false) {
$updateData['crowdsec_installed'] = true;
$updateData['crowdsec_available'] = true;
}

if ($validationResults['traefik_logging'] ?? false) {
$updateData['traefik_logging_enabled'] = true;
}

if ($validationResults['traffic_logger'] ?? false) {
$updateData['traffic_logger_installed'] = true;
}

if (!empty($updateData)) {
$this->server->update($updateData);
ray("✅ Server metadata updated: " . implode(', ', array_keys($updateData)));
}
}
}
2 changes: 2 additions & 0 deletions apps/ideploy/app/Observers/FirewallRuleObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class FirewallRuleObserver
*/
public function saved(FirewallRule $rule): void
{
ray("🔥 FirewallRuleObserver::saved triggered for rule: {$rule->name}");

// Only deploy if firewall is enabled
if (!$rule->config->enabled) {
ray("Rule saved but firewall disabled, skipping deployment");
Expand Down
4 changes: 2 additions & 2 deletions apps/ideploy/app/Services/Security/FirewallConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ private function initializeCrowdSecForApp(Application $application, Server $serv
try {
// Create bouncer directly via SSH (more reliable than LAPI)
$result = instant_remote_process([
"docker exec crowdsec cscli bouncers add {$bouncerName} -o raw"
"docker exec crowdsec-live cscli bouncers add {$bouncerName} -o raw"
], $server);

$apiKey = trim($result);
Expand Down Expand Up @@ -271,7 +271,7 @@ public function reloadCrowdSec(Server $server): void
{
// Send SIGHUP to CrowdSec container to reload config
instant_remote_process([
'docker exec crowdsec kill -SIGHUP 1',
'docker exec crowdsec-live kill -SIGHUP 1',
], $server);

ray('CrowdSec configuration reloaded');
Expand Down
Loading