Skip to content

Commit 5c460dd

Browse files
authored
fix(proxy): validate stored config matches proxy type (#9146)
2 parents 4f1f871 + b8e52c6 commit 5c460dd

File tree

3 files changed

+106
-3
lines changed

3 files changed

+106
-3
lines changed

app/Actions/Proxy/GetProxyConfiguration.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
namespace App\Actions\Proxy;
44

5+
use App\Enums\ProxyTypes;
56
use App\Models\Server;
67
use App\Services\ProxyDashboardCacheService;
78
use Illuminate\Support\Facades\Log;
89
use Lorisleiva\Actions\Concerns\AsAction;
10+
use Symfony\Component\Yaml\Yaml;
911

1012
class GetProxyConfiguration
1113
{
@@ -24,6 +26,17 @@ public function handle(Server $server, bool $forceRegenerate = false): string
2426
// Primary source: database
2527
$proxy_configuration = $server->proxy->get('last_saved_proxy_configuration');
2628

29+
// Validate stored config matches current proxy type
30+
if (! empty(trim($proxy_configuration ?? ''))) {
31+
if (! $this->configMatchesProxyType($proxyType, $proxy_configuration)) {
32+
Log::warning('Stored proxy config does not match current proxy type, will regenerate', [
33+
'server_id' => $server->id,
34+
'proxy_type' => $proxyType,
35+
]);
36+
$proxy_configuration = null;
37+
}
38+
}
39+
2740
// Backfill: existing servers may not have DB config yet — read from disk once
2841
if (empty(trim($proxy_configuration ?? ''))) {
2942
$proxy_configuration = $this->backfillFromDisk($server);
@@ -55,6 +68,29 @@ public function handle(Server $server, bool $forceRegenerate = false): string
5568
return $proxy_configuration;
5669
}
5770

71+
/**
72+
* Check that the stored docker-compose YAML contains the expected service
73+
* for the server's current proxy type. Returns false if the config belongs
74+
* to a different proxy type (e.g. Traefik config on a CADDY server).
75+
*/
76+
private function configMatchesProxyType(string $proxyType, string $configuration): bool
77+
{
78+
try {
79+
$yaml = Yaml::parse($configuration);
80+
$services = data_get($yaml, 'services', []);
81+
82+
return match ($proxyType) {
83+
ProxyTypes::TRAEFIK->value => isset($services['traefik']),
84+
ProxyTypes::CADDY->value => isset($services['caddy']),
85+
ProxyTypes::NGINX->value => isset($services['nginx']),
86+
default => true,
87+
};
88+
} catch (\Throwable $e) {
89+
// If YAML is unparseable, don't block — let the existing flow handle it
90+
return true;
91+
}
92+
}
93+
5894
/**
5995
* Backfill: read config from disk for servers that predate DB storage.
6096
* Stores the result in the database so future reads skip SSH entirely.

app/Models/Server.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,9 @@ public function changeProxy(string $proxyType, bool $async = true)
14711471
if ($validProxyTypes->contains(str($proxyType)->lower())) {
14721472
$this->proxy->set('type', str($proxyType)->upper());
14731473
$this->proxy->set('status', 'exited');
1474+
$this->proxy->set('last_saved_proxy_configuration', null);
1475+
$this->proxy->set('last_saved_settings', null);
1476+
$this->proxy->set('last_applied_settings', null);
14741477
$this->save();
14751478
if ($this->proxySet()) {
14761479
if ($async) {

tests/Unit/ProxyConfigRecoveryTest.php

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,26 @@
1010
Cache::spy();
1111
});
1212

13-
function mockServerWithDbConfig(?string $savedConfig): object
13+
function mockServerWithDbConfig(?string $savedConfig, string $proxyType = 'TRAEFIK'): object
1414
{
1515
$proxyAttributes = Mockery::mock(SchemalessAttributes::class);
1616
$proxyAttributes->shouldReceive('get')
1717
->with('last_saved_proxy_configuration')
1818
->andReturn($savedConfig);
1919

20+
$proxyPath = match ($proxyType) {
21+
'CADDY' => '/data/coolify/proxy/caddy',
22+
'NGINX' => '/data/coolify/proxy/nginx',
23+
default => '/data/coolify/proxy/',
24+
};
25+
2026
$server = Mockery::mock('App\Models\Server');
2127
$server->shouldIgnoreMissing();
2228
$server->shouldReceive('getAttribute')->with('proxy')->andReturn($proxyAttributes);
2329
$server->shouldReceive('getAttribute')->with('id')->andReturn(1);
2430
$server->shouldReceive('getAttribute')->with('name')->andReturn('Test Server');
25-
$server->shouldReceive('proxyType')->andReturn('TRAEFIK');
26-
$server->shouldReceive('proxyPath')->andReturn('/data/coolify/proxy');
31+
$server->shouldReceive('proxyType')->andReturn($proxyType);
32+
$server->shouldReceive('proxyPath')->andReturn($proxyPath);
2733

2834
return $server;
2935
}
@@ -107,3 +113,61 @@ function mockServerWithDbConfig(?string $savedConfig): object
107113

108114
expect($result)->toBe($savedConfig);
109115
});
116+
117+
it('rejects stored Traefik config when proxy type is CADDY', function () {
118+
Log::swap(new \Illuminate\Log\LogManager(app()));
119+
Log::spy();
120+
121+
$traefikConfig = "services:\n traefik:\n image: traefik:v3.6\n";
122+
$server = mockServerWithDbConfig($traefikConfig, 'CADDY');
123+
124+
// Config type mismatch should trigger regeneration, which will try
125+
// backfillFromDisk (instant_remote_process) then generateDefault.
126+
// Both will fail in test env, but the warning log proves mismatch was detected.
127+
try {
128+
GetProxyConfiguration::run($server);
129+
} catch (\Throwable $e) {
130+
// Expected — regeneration requires SSH/full server setup
131+
}
132+
133+
Log::shouldHaveReceived('warning')
134+
->withArgs(fn ($message) => str_contains($message, 'does not match current proxy type'))
135+
->once();
136+
});
137+
138+
it('rejects stored Caddy config when proxy type is TRAEFIK', function () {
139+
Log::swap(new \Illuminate\Log\LogManager(app()));
140+
Log::spy();
141+
142+
$caddyConfig = "services:\n caddy:\n image: lucaslorentz/caddy-docker-proxy:2.8-alpine\n";
143+
$server = mockServerWithDbConfig($caddyConfig, 'TRAEFIK');
144+
145+
try {
146+
GetProxyConfiguration::run($server);
147+
} catch (\Throwable $e) {
148+
// Expected — regeneration requires SSH/full server setup
149+
}
150+
151+
Log::shouldHaveReceived('warning')
152+
->withArgs(fn ($message) => str_contains($message, 'does not match current proxy type'))
153+
->once();
154+
});
155+
156+
it('accepts stored Caddy config when proxy type is CADDY', function () {
157+
$caddyConfig = "services:\n caddy:\n image: lucaslorentz/caddy-docker-proxy:2.8-alpine\n";
158+
$server = mockServerWithDbConfig($caddyConfig, 'CADDY');
159+
160+
$result = GetProxyConfiguration::run($server);
161+
162+
expect($result)->toBe($caddyConfig);
163+
});
164+
165+
it('accepts stored config when YAML parsing fails', function () {
166+
$invalidYaml = "this: is: not: [valid yaml: {{{}}}";
167+
$server = mockServerWithDbConfig($invalidYaml, 'TRAEFIK');
168+
169+
// Invalid YAML should not block — configMatchesProxyType returns true on parse failure
170+
$result = GetProxyConfiguration::run($server);
171+
172+
expect($result)->toBe($invalidYaml);
173+
});

0 commit comments

Comments
 (0)