Revert revert of CAA validation

This commit is contained in:
Matej Bačo
2025-08-05 13:44:06 +02:00
parent 8fe999d6d7
commit 2d4e99cb1a
13 changed files with 205 additions and 45 deletions
+28 -23
View File
@@ -2,13 +2,16 @@
namespace Appwrite\Network\Validator;
use Utopia\DNS\Client;
use Utopia\System\System;
use Utopia\Validator;
class DNS extends Validator
{
public const RECORD_A = 'a';
public const RECORD_AAAA = 'aaaa';
public const RECORD_CNAME = 'cname';
public const RECORD_A = 'A';
public const RECORD_AAAA = 'AAAA';
public const RECORD_CNAME = 'CNAME';
public const RECORD_CAA = 'CAA'; // You can provide domain only (as $target) for CAA validation
/**
* @var mixed
@@ -42,33 +45,22 @@ class DNS extends Validator
* Check if DNS record value matches specific value
*
* @param mixed $domain
*
* @return bool
*/
public function isValid($value): bool
{
$typeNative = match ($this->type) {
self::RECORD_A => DNS_A,
self::RECORD_AAAA => DNS_AAAA,
self::RECORD_CNAME => DNS_CNAME,
default => throw new \Exception('Record type not supported.')
};
$dnsKey = match ($this->type) {
self::RECORD_A => 'ip',
self::RECORD_AAAA => 'ipv6',
self::RECORD_CNAME => 'target',
default => throw new \Exception('Record type not supported.')
};
if (!is_string($value)) {
return false;
}
$dnsServer = System::getEnv('_APP_DNS', '8.8.8.8');
$dns = new Client($dnsServer);
try {
$records = \dns_get_record($value, $typeNative);
$this->logs = $records;
} catch (\Throwable $th) {
$query = $dns->query($value, $this->type);
$this->logs = $query;
} catch (\Exception $e) {
$this->logs = ['error' => $e->getMessage()];
return false;
}
@@ -90,8 +82,21 @@ class DNS extends Validator
return false;
}
foreach ($records as $record) {
if (isset($record[$dnsKey]) && $record[$dnsKey] === $this->target) {
foreach ($query as $record) {
// CAA validation only needs to ensure domain
if ($this->type === self::RECORD_CAA) {
// Extract domain; comments showcase extraction steps in most complex scenario
$rdata = $record->getRdata(); // 255 issuewild "certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600"
$rdata = \explode(' ', $rdata, 3)[2] ?? ''; // "certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600"
$rdata = \trim($rdata, '"'); // certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600
$rdata = \explode(';', $rdata, 2)[0] ?? ''; // certainly.com
if ($rdata === $this->target) {
return true;
}
}
if ($record->getRdata() === $this->target) {
return true;
}
}
@@ -337,6 +337,19 @@ class Certificates extends Action
throw new Exception('Failed to verify domain DNS records.');
}
// Ensure CAA won't block certificate issuance
if (!empty(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''))) {
$validationStart = \microtime(true);
$validator = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), DNS::RECORD_CAA);
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTimingCaa', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
$error = $validator->getDescription();
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
throw new Exception('Failed to verify domain DNS records. CAA records do not allow certificates from certainly.com to issue certificates.');
}
}
} else {
// Main domain validation
// TODO: Would be awesome to check A/AAAA record here. Maybe dry run?
@@ -10,24 +10,30 @@ class ConsoleVariables extends Model
public function __construct()
{
$this
->addRule('_APP_DOMAIN_TARGET_CNAME', [
'type' => self::TYPE_STRING,
'description' => 'CNAME target for your Appwrite custom domains.',
'default' => '',
'example' => 'appwrite.io',
])
->addRule('_APP_DOMAIN_TARGET_A', [
'type' => self::TYPE_STRING,
'description' => 'A target for your Appwrite custom domains.',
'default' => '',
'example' => '127.0.0.1',
])
->addRule('_APP_DOMAIN_TARGET_AAAA', [
'type' => self::TYPE_STRING,
'description' => 'AAAA target for your Appwrite custom domains.',
'default' => '',
'example' => '::1',
])
->addRule('_APP_DOMAIN_TARGET_CNAME', [
'type' => self::TYPE_STRING,
'description' => 'CNAME target for your Appwrite custom domains.',
'default' => '',
'example' => 'appwrite.io',
])
->addRule('_APP_DOMAIN_TARGET_A', [
'type' => self::TYPE_STRING,
'description' => 'A target for your Appwrite custom domains.',
'default' => '',
'example' => '127.0.0.1',
])
->addRule('_APP_DOMAIN_TARGET_AAAA', [
'type' => self::TYPE_STRING,
'description' => 'AAAA target for your Appwrite custom domains.',
'default' => '',
'example' => '::1',
])
->addRule('_APP_DOMAIN_TARGET_CAA', [
'type' => self::TYPE_STRING,
'description' => 'CAA target for your Appwrite custom domains.',
'default' => '',
'example' => 'digicert.com',
])
->addRule('_APP_STORAGE_LIMIT', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum file size allowed for file upload in bytes.',