feat(dns): add opt-in Cloudflare proxy mode for new DNS records

This commit is contained in:
Andras Bacsai
2026-10-09 13:52:20 +02:00
parent f39c884cc7
commit f87f80ae96
8 changed files with 87 additions and 1 deletions
@@ -30,6 +30,8 @@ class IntegrationTokenEditor extends Component
public bool $automaticDns = true;
public bool $proxiedDns = false;
public function mount(string $integration_token_uuid): void
{
$this->integrationToken = IntegrationToken::ownedByCurrentTeam()
@@ -43,6 +45,7 @@ class IntegrationTokenEditor extends Component
$this->metadata = $this->integrationToken->metadata ?? [];
$this->loadZones();
$this->automaticDns = $this->integrationToken->automaticDnsEnabled();
$this->proxiedDns = $this->integrationToken->proxiedDnsEnabled();
}
protected function rules(): array
@@ -55,6 +58,7 @@ class IntegrationTokenEditor extends Component
'capabilities' => ['required', 'array', 'min:1'],
'capabilities.*' => ['required', 'in:'.$allowedCapability],
'automaticDns' => ['boolean'],
'proxiedDns' => ['boolean'],
];
if ($this->integrationToken->provider === 'infisical') {
@@ -91,6 +95,11 @@ class IntegrationTokenEditor extends Component
} else {
$metadata['automatic_dns'] = false;
}
if ($validated['proxiedDns']) {
$metadata['proxied_dns'] = true;
} else {
unset($metadata['proxied_dns']);
}
}
$storedMetadata = $this->integrationToken->metadata ?? [];
$changedConnectionFields = $this->changedConnectionFields($provider, $storedMetadata, $metadata);
@@ -27,6 +27,8 @@ class IntegrationTokenForm extends Component
public bool $automaticDns = true;
public bool $proxiedDns = false;
public function mount(): void
{
$this->authorize('create', IntegrationToken::class);
@@ -38,6 +40,7 @@ class IntegrationTokenForm extends Component
$this->capabilities = ['dns'];
$this->metadata = [];
$this->automaticDns = true;
$this->proxiedDns = false;
} else {
$this->capabilities = ['secrets'];
$this->metadata = $this->provider === 'infisical'
@@ -57,6 +60,7 @@ class IntegrationTokenForm extends Component
'capabilities' => ['required', 'array', 'min:1'],
'capabilities.*' => ['required', 'in:'.$allowedCapability],
'automaticDns' => ['boolean'],
'proxiedDns' => ['boolean'],
];
if ($this->provider === 'infisical') {
@@ -94,6 +98,9 @@ class IntegrationTokenForm extends Component
if ($validated['provider'] === 'cloudflare' && ! $validated['automaticDns']) {
$metadata['automatic_dns'] = false;
}
if ($validated['provider'] === 'cloudflare' && $validated['proxiedDns']) {
$metadata['proxied_dns'] = true;
}
try {
if (! $validator->validate($validated['provider'], $validated['token'], $validated['capabilities'], $metadata)) {
+6
View File
@@ -76,6 +76,12 @@ class IntegrationToken extends BaseModel
return $this->provider === 'cloudflare' && data_get($this->metadata, 'automatic_dns', true) !== false;
}
/** New Cloudflare records are created proxied (orange cloud) when enabled, so the server IP is not exposed. */
public function proxiedDnsEnabled(): bool
{
return $this->provider === 'cloudflare' && data_get($this->metadata, 'proxied_dns') === true;
}
public function dopplerTokenType(): ?string
{
if ($this->provider !== 'doppler') {
+2 -1
View File
@@ -153,7 +153,8 @@ class CloudflareDnsProvider
$uuid = new_public_id();
$response = $this->client($zone->integrationToken)->post("https://api.cloudflare.com/client/v4/zones/{$zone->provider_zone_id}/dns_records", [
'type' => $type, 'name' => $hostname, 'content' => $content, 'ttl' => 1, 'proxied' => false,
'type' => $type, 'name' => $hostname, 'content' => $content, 'ttl' => 1,
'proxied' => $zone->integrationToken->proxiedDnsEnabled(),
'comment' => ManagedDnsRecord::ownershipCommentFor($uuid),
]);
if (! $response->successful() || ! is_string($response->json('result.id'))) {
@@ -50,6 +50,13 @@
Create DNS records automatically when a new domain has one unambiguous matching credential.
</p>
</div>
<div class="rounded-lg border border-neutral-200 p-1 dark:border-white/[0.08]">
<x-forms.checkbox id="edit-proxied-dns" label="Proxy DNS records through Cloudflare" fullWidth
wire:model.live="proxiedDns" canGate="update" :canResource="$integrationToken" />
<p class="px-2.5 pb-2 text-[11px] text-neutral-500 dark:text-fg-dim">
Create new DNS records with the Cloudflare proxy on (orange cloud), so the server IP is not public. Existing records keep their proxy status.
</p>
</div>
@else
<div class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[11px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
<div class="font-medium text-black dark:text-fg">Capability: Secrets (read-only)</div>
@@ -58,6 +58,13 @@
Create DNS records automatically when a new domain has one unambiguous matching credential.
</p>
</div>
<div class="rounded-lg border border-neutral-200 p-1 dark:border-white/[0.08]">
<x-forms.checkbox id="proxied-dns" label="Proxy DNS records through Cloudflare" fullWidth
wire:model.live="proxiedDns" canGate="create" :canResource="\App\Models\IntegrationToken::class" />
<p class="px-2.5 pb-2 text-[11px] text-neutral-500 dark:text-fg-dim">
Create new DNS records with the Cloudflare proxy on (orange cloud), so the server IP is not public. Existing records keep their proxy status.
</p>
</div>
@else
<div class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[11px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
<div class="font-medium text-black dark:text-fg">Capability: Secrets (read-only)</div>
@@ -120,6 +120,23 @@ test('a cloudflare record is created and tracked as managed by coolify', functio
&& $request->data()['comment'] === $record->ownershipComment());
});
test('cloudflare records are created proxied only when the credential enables proxy mode', function (?array $metadata, bool $proxied) {
$token = IntegrationToken::factory()->create(['provider' => 'cloudflare', 'token' => 'secret', 'metadata' => $metadata]);
$zone = DnsProviderZone::factory()->for($token)->create(['provider_zone_id' => 'zone-1', 'name' => 'example.com']);
Http::fake([
'https://api.cloudflare.com/client/v4/zones/zone-1/dns_records?*' => Http::response(['success' => true, 'result' => []]),
'https://api.cloudflare.com/client/v4/zones/zone-1/dns_records' => Http::response(['success' => true, 'result' => ['id' => 'record-1']]),
]);
app(CloudflareDnsProvider::class)->createRecord($zone, 'app.example.com', '203.0.113.10');
Http::assertSent(fn ($request) => $request->method() === 'POST' && $request->data()['proxied'] === $proxied);
})->with([
'default' => [null, false],
'enabled' => [['proxied_dns' => true], true],
]);
test('queued dns configuration creates the record and broadcasts completion', function () {
Event::fake([DnsRecordConfigurationFinished::class]);
$token = IntegrationToken::factory()->create(['provider' => 'cloudflare', 'token' => 'secret']);
@@ -102,6 +102,38 @@ test('automatic dns is enabled by default and can be disabled when saving a clou
expect(IntegrationToken::query()->sole()->automaticDnsEnabled())->toBeFalse();
});
test('cloudflare proxy mode is off by default and can be enabled when adding and editing a token', function () {
Http::fake([
'https://api.cloudflare.com/client/v4/user/tokens/verify' => Http::response(['success' => true, 'result' => ['status' => 'active']]),
'https://api.cloudflare.com/client/v4/zones?per_page=1' => Http::response(['success' => true, 'result' => [['id' => 'zone-id']]]),
'https://api.cloudflare.com/client/v4/zones/zone-id/dns_records?per_page=1' => Http::response(['success' => true, 'result' => []]),
'https://api.cloudflare.com/client/v4/zones?page=1&per_page=50' => Http::response([
'success' => true,
'result' => [['id' => 'zone-id', 'name' => 'example.com', 'account' => ['id' => 'account-id', 'name' => 'Production']]],
'result_info' => ['total_pages' => 1],
]),
]);
Livewire::test(IntegrationTokenForm::class)
->assertSet('proxiedDns', false)
->set('name', 'Proxied DNS')
->set('token', 'cloudflare-token')
->set('proxiedDns', true)
->call('addToken')
->assertHasNoErrors();
$token = IntegrationToken::query()->sole();
expect($token->proxiedDnsEnabled())->toBeTrue();
Livewire::test(IntegrationTokenEditor::class, ['integration_token_uuid' => $token->uuid])
->assertSet('proxiedDns', true)
->set('proxiedDns', false)
->call('save')
->assertHasNoErrors();
expect($token->refresh()->proxiedDnsEnabled())->toBeFalse();
});
test('deleting an integration token is audited without storing its value', function () {
$token = IntegrationToken::query()->create([
'team_id' => $this->team->id,