mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 17:08:45 +00:00
Merge pull request #10267 from appwrite/feat-check-CAA-in-DNS
Feat: CAA validator
This commit is contained in:
commit
7dd9ade894
13 changed files with 270 additions and 47 deletions
2
.env
2
.env
|
|
@ -21,6 +21,7 @@ _APP_OPTIONS_ROUTER_PROTECTION=disabled
|
||||||
_APP_OPTIONS_FORCE_HTTPS=disabled
|
_APP_OPTIONS_FORCE_HTTPS=disabled
|
||||||
_APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled
|
_APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled
|
||||||
_APP_OPENSSL_KEY_V1=your-secret-key
|
_APP_OPENSSL_KEY_V1=your-secret-key
|
||||||
|
_APP_DNS=8.8.8.8
|
||||||
_APP_DOMAIN=traefik
|
_APP_DOMAIN=traefik
|
||||||
_APP_CONSOLE_DOMAIN=localhost
|
_APP_CONSOLE_DOMAIN=localhost
|
||||||
_APP_DOMAIN_FUNCTIONS=functions.localhost
|
_APP_DOMAIN_FUNCTIONS=functions.localhost
|
||||||
|
|
@ -28,6 +29,7 @@ _APP_DOMAIN_SITES=sites.localhost
|
||||||
_APP_DOMAIN_TARGET_CNAME=test.localhost
|
_APP_DOMAIN_TARGET_CNAME=test.localhost
|
||||||
_APP_DOMAIN_TARGET_A=127.0.0.1
|
_APP_DOMAIN_TARGET_A=127.0.0.1
|
||||||
_APP_DOMAIN_TARGET_AAAA=::1
|
_APP_DOMAIN_TARGET_AAAA=::1
|
||||||
|
_APP_DOMAIN_TARGET_CAA=digicert.com
|
||||||
_APP_RULES_FORMAT=md5
|
_APP_RULES_FORMAT=md5
|
||||||
_APP_REDIS_HOST=redis
|
_APP_REDIS_HOST=redis
|
||||||
_APP_REDIS_PORT=6379
|
_APP_REDIS_PORT=6379
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,24 @@ return [
|
||||||
'question' => '',
|
'question' => '',
|
||||||
'filter' => ''
|
'filter' => ''
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => '_APP_DOMAIN_TARGET_CAA',
|
||||||
|
'description' => 'A CAA record domain that can be used to validate custom domains. Value should be domain\'s hostname.',
|
||||||
|
'introduction' => '',
|
||||||
|
'default' => '',
|
||||||
|
'required' => false,
|
||||||
|
'question' => '',
|
||||||
|
'filter' => ''
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '_APP_DNS',
|
||||||
|
'description' => 'DNS server to use for domain validation. Default: 8.8.8.8',
|
||||||
|
'introduction' => '',
|
||||||
|
'default' => '8.8.8.8',
|
||||||
|
'required' => false,
|
||||||
|
'question' => '',
|
||||||
|
'filter' => ''
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => '_APP_CONSOLE_WHITELIST_ROOT',
|
'name' => '_APP_CONSOLE_WHITELIST_ROOT',
|
||||||
'description' => 'This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by inviting them to your project. By default this option is enabled.',
|
'description' => 'This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by inviting them to your project. By default this option is enabled.',
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ App::get('/v1/console/variables')
|
||||||
'_APP_DOMAIN_TARGET_CNAME' => System::getEnv('_APP_DOMAIN_TARGET_CNAME'),
|
'_APP_DOMAIN_TARGET_CNAME' => System::getEnv('_APP_DOMAIN_TARGET_CNAME'),
|
||||||
'_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'),
|
'_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'),
|
||||||
'_APP_DOMAIN_TARGET_A' => System::getEnv('_APP_DOMAIN_TARGET_A'),
|
'_APP_DOMAIN_TARGET_A' => System::getEnv('_APP_DOMAIN_TARGET_A'),
|
||||||
|
// Combine CAA domain with most common flags and tag (no parameters)
|
||||||
|
'_APP_DOMAIN_TARGET_CAA' => '0 issue "' . System::getEnv('_APP_DOMAIN_TARGET_CAA') . '"',
|
||||||
'_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'),
|
'_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'),
|
||||||
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
|
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
|
||||||
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
|
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,19 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
|
||||||
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
|
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(Exception::RULE_VERIFICATION_FAILED, 'Domain verification failed because CAA records do not allow Appwrite\'s certificate issuer.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
|
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
|
||||||
|
|
||||||
// Issue a TLS certificate when domain is verified
|
// Issue a TLS certificate when domain is verified
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ $image = $this->getParam('image', '');
|
||||||
- _APP_DOMAIN_TARGET_CNAME
|
- _APP_DOMAIN_TARGET_CNAME
|
||||||
- _APP_DOMAIN_TARGET_AAAA
|
- _APP_DOMAIN_TARGET_AAAA
|
||||||
- _APP_DOMAIN_TARGET_A
|
- _APP_DOMAIN_TARGET_A
|
||||||
|
- _APP_DOMAIN_TARGET_CAA
|
||||||
|
- _APP_DNS
|
||||||
- _APP_DOMAIN_FUNCTIONS
|
- _APP_DOMAIN_FUNCTIONS
|
||||||
- _APP_REDIS_HOST
|
- _APP_REDIS_HOST
|
||||||
- _APP_REDIS_PORT
|
- _APP_REDIS_PORT
|
||||||
|
|
@ -472,6 +474,8 @@ $image = $this->getParam('image', '');
|
||||||
- _APP_DOMAIN_TARGET_CNAME
|
- _APP_DOMAIN_TARGET_CNAME
|
||||||
- _APP_DOMAIN_TARGET_AAAA
|
- _APP_DOMAIN_TARGET_AAAA
|
||||||
- _APP_DOMAIN_TARGET_A
|
- _APP_DOMAIN_TARGET_A
|
||||||
|
- _APP_DOMAIN_TARGET_CAA
|
||||||
|
- _APP_DNS
|
||||||
- _APP_DOMAIN_FUNCTIONS
|
- _APP_DOMAIN_FUNCTIONS
|
||||||
- _APP_EMAIL_CERTIFICATES
|
- _APP_EMAIL_CERTIFICATES
|
||||||
- _APP_REDIS_HOST
|
- _APP_REDIS_HOST
|
||||||
|
|
@ -629,6 +633,8 @@ $image = $this->getParam('image', '');
|
||||||
- _APP_DOMAIN_TARGET_CNAME
|
- _APP_DOMAIN_TARGET_CNAME
|
||||||
- _APP_DOMAIN_TARGET_AAAA
|
- _APP_DOMAIN_TARGET_AAAA
|
||||||
- _APP_DOMAIN_TARGET_A
|
- _APP_DOMAIN_TARGET_A
|
||||||
|
- _APP_DOMAIN_TARGET_CAA
|
||||||
|
- _APP_DNS
|
||||||
- _APP_EMAIL_SECURITY
|
- _APP_EMAIL_SECURITY
|
||||||
- _APP_REDIS_HOST
|
- _APP_REDIS_HOST
|
||||||
- _APP_REDIS_PORT
|
- _APP_REDIS_PORT
|
||||||
|
|
@ -660,6 +666,8 @@ $image = $this->getParam('image', '');
|
||||||
- _APP_DOMAIN_TARGET_CNAME
|
- _APP_DOMAIN_TARGET_CNAME
|
||||||
- _APP_DOMAIN_TARGET_AAAA
|
- _APP_DOMAIN_TARGET_AAAA
|
||||||
- _APP_DOMAIN_TARGET_A
|
- _APP_DOMAIN_TARGET_A
|
||||||
|
- _APP_DOMAIN_TARGET_CAA
|
||||||
|
- _APP_DNS
|
||||||
- _APP_DOMAIN_FUNCTIONS
|
- _APP_DOMAIN_FUNCTIONS
|
||||||
- _APP_OPENSSL_KEY_V1
|
- _APP_OPENSSL_KEY_V1
|
||||||
- _APP_REDIS_HOST
|
- _APP_REDIS_HOST
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
"utopia-php/database": "0.71.*",
|
"utopia-php/database": "0.71.*",
|
||||||
"utopia-php/detector": "0.1.*",
|
"utopia-php/detector": "0.1.*",
|
||||||
"utopia-php/domains": "0.8.*",
|
"utopia-php/domains": "0.8.*",
|
||||||
|
"utopia-php/dns": "0.3.*",
|
||||||
"utopia-php/dsn": "0.2.1",
|
"utopia-php/dsn": "0.2.1",
|
||||||
"utopia-php/framework": "0.33.*",
|
"utopia-php/framework": "0.33.*",
|
||||||
"utopia-php/fetch": "0.4.*",
|
"utopia-php/fetch": "0.4.*",
|
||||||
|
|
|
||||||
62
composer.lock
generated
62
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "7b2ef6192403daf5c492219822ce0aa1",
|
"content-hash": "761a7e17b49381e68038c92873888125",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "adhocore/jwt",
|
"name": "adhocore/jwt",
|
||||||
|
|
@ -3641,6 +3641,62 @@
|
||||||
},
|
},
|
||||||
"time": "2025-05-19T11:01:28+00:00"
|
"time": "2025-05-19T11:01:28+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "utopia-php/dns",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/utopia-php/dns.git",
|
||||||
|
"reference": "8fd4161bc3a8021a670c1101b40f6b09a97f1a54"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/utopia-php/dns/zipball/8fd4161bc3a8021a670c1101b40f6b09a97f1a54",
|
||||||
|
"reference": "8fd4161bc3a8021a670c1101b40f6b09a97f1a54",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0",
|
||||||
|
"utopia-php/cli": "0.15.*",
|
||||||
|
"utopia-php/telemetry": "^0.1.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"laravel/pint": "1.2.*",
|
||||||
|
"phpstan/phpstan": "1.8.*",
|
||||||
|
"phpunit/phpunit": "^9.3",
|
||||||
|
"rregeer/phpunit-coverage-check": "^0.3.1",
|
||||||
|
"swoole/ide-helper": "4.6.6"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Utopia\\DNS\\": "src/DNS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Eldad Fux",
|
||||||
|
"email": "eldad@appwrite.io"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Lite & fast micro PHP DNS server abstraction that is **easy to use**.",
|
||||||
|
"keywords": [
|
||||||
|
"dns",
|
||||||
|
"framework",
|
||||||
|
"php",
|
||||||
|
"upf",
|
||||||
|
"utopia"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/utopia-php/dns/issues",
|
||||||
|
"source": "https://github.com/utopia-php/dns/tree/0.3.0"
|
||||||
|
},
|
||||||
|
"time": "2025-08-04T11:05:53+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "utopia-php/domains",
|
"name": "utopia-php/domains",
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
|
|
@ -8308,7 +8364,7 @@
|
||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": {},
|
"stability-flags": [],
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
|
|
@ -8332,5 +8388,5 @@
|
||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "8.3"
|
"php": "8.3"
|
||||||
},
|
},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.3.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@ services:
|
||||||
- _APP_DOMAIN_TARGET_CNAME
|
- _APP_DOMAIN_TARGET_CNAME
|
||||||
- _APP_DOMAIN_TARGET_AAAA
|
- _APP_DOMAIN_TARGET_AAAA
|
||||||
- _APP_DOMAIN_TARGET_A
|
- _APP_DOMAIN_TARGET_A
|
||||||
|
- _APP_DOMAIN_TARGET_CAA
|
||||||
|
- _APP_DNS
|
||||||
- _APP_DOMAIN_FUNCTIONS
|
- _APP_DOMAIN_FUNCTIONS
|
||||||
- _APP_REDIS_HOST
|
- _APP_REDIS_HOST
|
||||||
- _APP_REDIS_PORT
|
- _APP_REDIS_PORT
|
||||||
|
|
@ -535,6 +537,8 @@ services:
|
||||||
- _APP_DOMAIN_TARGET_CNAME
|
- _APP_DOMAIN_TARGET_CNAME
|
||||||
- _APP_DOMAIN_TARGET_AAAA
|
- _APP_DOMAIN_TARGET_AAAA
|
||||||
- _APP_DOMAIN_TARGET_A
|
- _APP_DOMAIN_TARGET_A
|
||||||
|
- _APP_DOMAIN_TARGET_CAA
|
||||||
|
- _APP_DNS
|
||||||
- _APP_DOMAIN_FUNCTIONS
|
- _APP_DOMAIN_FUNCTIONS
|
||||||
- _APP_EMAIL_CERTIFICATES
|
- _APP_EMAIL_CERTIFICATES
|
||||||
- _APP_REDIS_HOST
|
- _APP_REDIS_HOST
|
||||||
|
|
@ -704,6 +708,8 @@ services:
|
||||||
- _APP_DOMAIN_TARGET_CNAME
|
- _APP_DOMAIN_TARGET_CNAME
|
||||||
- _APP_DOMAIN_TARGET_AAAA
|
- _APP_DOMAIN_TARGET_AAAA
|
||||||
- _APP_DOMAIN_TARGET_A
|
- _APP_DOMAIN_TARGET_A
|
||||||
|
- _APP_DOMAIN_TARGET_CAA
|
||||||
|
- _APP_DNS
|
||||||
- _APP_EMAIL_SECURITY
|
- _APP_EMAIL_SECURITY
|
||||||
- _APP_REDIS_HOST
|
- _APP_REDIS_HOST
|
||||||
- _APP_REDIS_PORT
|
- _APP_REDIS_PORT
|
||||||
|
|
@ -738,6 +744,8 @@ services:
|
||||||
- _APP_DOMAIN_TARGET_CNAME
|
- _APP_DOMAIN_TARGET_CNAME
|
||||||
- _APP_DOMAIN_TARGET_AAAA
|
- _APP_DOMAIN_TARGET_AAAA
|
||||||
- _APP_DOMAIN_TARGET_A
|
- _APP_DOMAIN_TARGET_A
|
||||||
|
- _APP_DOMAIN_TARGET_CAA
|
||||||
|
- _APP_DNS
|
||||||
- _APP_DOMAIN_FUNCTIONS
|
- _APP_DOMAIN_FUNCTIONS
|
||||||
- _APP_OPENSSL_KEY_V1
|
- _APP_OPENSSL_KEY_V1
|
||||||
- _APP_REDIS_HOST
|
- _APP_REDIS_HOST
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,38 @@
|
||||||
|
|
||||||
namespace Appwrite\Network\Validator;
|
namespace Appwrite\Network\Validator;
|
||||||
|
|
||||||
|
use Utopia\DNS\Client;
|
||||||
|
use Utopia\Domains\Domain;
|
||||||
|
use Utopia\System\System;
|
||||||
use Utopia\Validator;
|
use Utopia\Validator;
|
||||||
|
|
||||||
class DNS extends Validator
|
class DNS extends Validator
|
||||||
{
|
{
|
||||||
public const RECORD_A = 'a';
|
public const RECORD_A = 'A';
|
||||||
public const RECORD_AAAA = 'aaaa';
|
public const RECORD_AAAA = 'AAAA';
|
||||||
public const RECORD_CNAME = 'cname';
|
public const RECORD_CNAME = 'CNAME';
|
||||||
|
public const RECORD_CAA = 'CAA'; // You can provide domain only (as $target) for CAA validation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var mixed
|
* @var mixed
|
||||||
*/
|
*/
|
||||||
protected mixed $logs;
|
protected mixed $logs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected string $dnsServer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $target
|
* @param string $target
|
||||||
*/
|
*/
|
||||||
public function __construct(protected string $target, protected string $type = self::RECORD_CNAME)
|
public function __construct(protected string $target, protected string $type = self::RECORD_CNAME, string $dnsServer = '')
|
||||||
{
|
{
|
||||||
|
if (empty($dnsServer)) {
|
||||||
|
$dnsServer = System::getEnv('_APP_DNS', '8.8.8.8');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dnsServer = $dnsServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -42,42 +56,65 @@ class DNS extends Validator
|
||||||
* Check if DNS record value matches specific value
|
* Check if DNS record value matches specific value
|
||||||
*
|
*
|
||||||
* @param mixed $domain
|
* @param mixed $domain
|
||||||
*
|
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isValid($value): 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)) {
|
if (!is_string($value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dns = new Client($this->dnsServer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$records = \dns_get_record($value, $typeNative);
|
$rawQuery = $dns->query($value, $this->type);
|
||||||
$this->logs = $records;
|
|
||||||
} catch (\Throwable $th) {
|
// Some DNS servers return all records, not only type that's asked for
|
||||||
|
// Likely occurs when no records of specific type are found
|
||||||
|
$query = array_filter($rawQuery, function ($record) {
|
||||||
|
return $record->getTypeName() === $this->type;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->logs = $query;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logs = ['error' => $e->getMessage()];
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$records) {
|
if (empty($query)) {
|
||||||
|
// CAA records inherit from parent (custom CAA behaviour)
|
||||||
|
if ($this->type === self::RECORD_CAA) {
|
||||||
|
$domain = new Domain($value);
|
||||||
|
if ($domain->get() === $domain->getApex()) {
|
||||||
|
return true; // No CAA on apex domain means anyone can issue certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive validation by parent domain
|
||||||
|
$parts = \explode('.', $value);
|
||||||
|
\array_shift($parts);
|
||||||
|
$parentDomain = \implode('.', $parts);
|
||||||
|
$validator = new DNS($this->target, DNS::RECORD_CAA, $this->dnsServer);
|
||||||
|
return $validator->isValid($parentDomain);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($records as $record) {
|
foreach ($query as $record) {
|
||||||
if (isset($record[$dnsKey]) && $record[$dnsKey] === $this->target) {
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,19 @@ class Certificates extends Action
|
||||||
|
|
||||||
throw new Exception('Failed to verify domain DNS records.');
|
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 Appwrite\'s certificate issuer.');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Main domain validation
|
// Main domain validation
|
||||||
// TODO: Would be awesome to check A/AAAA record here. Maybe dry run?
|
// TODO: Would be awesome to check A/AAAA record here. Maybe dry run?
|
||||||
|
|
|
||||||
|
|
@ -10,24 +10,30 @@ class ConsoleVariables extends Model
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->addRule('_APP_DOMAIN_TARGET_CNAME', [
|
->addRule('_APP_DOMAIN_TARGET_CNAME', [
|
||||||
'type' => self::TYPE_STRING,
|
'type' => self::TYPE_STRING,
|
||||||
'description' => 'CNAME target for your Appwrite custom domains.',
|
'description' => 'CNAME target for your Appwrite custom domains.',
|
||||||
'default' => '',
|
'default' => '',
|
||||||
'example' => 'appwrite.io',
|
'example' => 'appwrite.io',
|
||||||
])
|
])
|
||||||
->addRule('_APP_DOMAIN_TARGET_A', [
|
->addRule('_APP_DOMAIN_TARGET_A', [
|
||||||
'type' => self::TYPE_STRING,
|
'type' => self::TYPE_STRING,
|
||||||
'description' => 'A target for your Appwrite custom domains.',
|
'description' => 'A target for your Appwrite custom domains.',
|
||||||
'default' => '',
|
'default' => '',
|
||||||
'example' => '127.0.0.1',
|
'example' => '127.0.0.1',
|
||||||
])
|
])
|
||||||
->addRule('_APP_DOMAIN_TARGET_AAAA', [
|
->addRule('_APP_DOMAIN_TARGET_AAAA', [
|
||||||
'type' => self::TYPE_STRING,
|
'type' => self::TYPE_STRING,
|
||||||
'description' => 'AAAA target for your Appwrite custom domains.',
|
'description' => 'AAAA target for your Appwrite custom domains.',
|
||||||
'default' => '',
|
'default' => '',
|
||||||
'example' => '::1',
|
'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', [
|
->addRule('_APP_STORAGE_LIMIT', [
|
||||||
'type' => self::TYPE_INTEGER,
|
'type' => self::TYPE_INTEGER,
|
||||||
'description' => 'Maximum file size allowed for file upload in bytes.',
|
'description' => 'Maximum file size allowed for file upload in bytes.',
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@ class ConsoleConsoleClientTest extends Scope
|
||||||
], $this->getHeaders()));
|
], $this->getHeaders()));
|
||||||
|
|
||||||
$this->assertEquals(200, $response['headers']['status-code']);
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
$this->assertCount(13, $response['body']);
|
$this->assertCount(14, $response['body']);
|
||||||
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_CNAME']);
|
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_CNAME']);
|
||||||
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_A']);
|
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_A']);
|
||||||
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_AAAA']);
|
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_AAAA']);
|
||||||
|
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_CAA']);
|
||||||
$this->assertIsInt($response['body']['_APP_STORAGE_LIMIT']);
|
$this->assertIsInt($response['body']['_APP_STORAGE_LIMIT']);
|
||||||
$this->assertIsInt($response['body']['_APP_COMPUTE_SIZE_LIMIT']);
|
$this->assertIsInt($response['body']['_APP_COMPUTE_SIZE_LIMIT']);
|
||||||
$this->assertIsBool($response['body']['_APP_DOMAIN_ENABLED']);
|
$this->assertIsBool($response['body']['_APP_DOMAIN_ENABLED']);
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,18 @@
|
||||||
namespace Tests\Unit\Network\Validators;
|
namespace Tests\Unit\Network\Validators;
|
||||||
|
|
||||||
use Appwrite\Network\Validator\DNS;
|
use Appwrite\Network\Validator\DNS;
|
||||||
|
use Appwrite\Tests\Retry;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/*
|
||||||
|
DNS Setup (on Appwrite Labs digital ocean team, network tab):
|
||||||
|
|
||||||
|
certainly.caa.appwrite.org: CAA 0 issue "certainly.com"
|
||||||
|
certainly-full.caa.appwrite.org: CAA 128 issuewild "certainly.com;account=123456;validationmethods=dns-01"
|
||||||
|
letsencrypt.certainly.caa.appwrite.org: CAA 0 issue "letsencrypt.org"
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
class DNSTest extends TestCase
|
class DNSTest extends TestCase
|
||||||
{
|
{
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
|
|
@ -47,4 +57,52 @@ class DNSTest extends TestCase
|
||||||
$this->assertEquals($validator->isValid('aaaa-unit-test.appwrite.org'), true);
|
$this->assertEquals($validator->isValid('aaaa-unit-test.appwrite.org'), true);
|
||||||
$this->assertEquals($validator->isValid('test1.appwrite.org'), false);
|
$this->assertEquals($validator->isValid('test1.appwrite.org'), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Retry(count: 5)]
|
||||||
|
public function testCAA(): void
|
||||||
|
{
|
||||||
|
$certainly = new DNS('certainly.com', DNS::RECORD_CAA, 'ns1.digitalocean.com');
|
||||||
|
$letsencrypt = new DNS('letsencrypt.org', DNS::RECORD_CAA, 'ns1.digitalocean.com');
|
||||||
|
|
||||||
|
// No CAA record succeeds on main domain & subdomains for any issuer
|
||||||
|
$this->assertEquals($certainly->isValid('caa.appwrite.org'), true);
|
||||||
|
$this->assertEquals($certainly->isValid('sub.caa.appwrite.org'), true);
|
||||||
|
$this->assertEquals($certainly->isValid('sub.sub.caa.appwrite.org'), true);
|
||||||
|
|
||||||
|
$this->assertEquals($letsencrypt->isValid('caa.appwrite.org'), true);
|
||||||
|
$this->assertEquals($letsencrypt->isValid('sub.caa.appwrite.org'), true);
|
||||||
|
$this->assertEquals($letsencrypt->isValid('sub.sub.caa.appwrite.org'), true);
|
||||||
|
|
||||||
|
// Custom flags and tag is allowed, but only for Certainly
|
||||||
|
$this->assertEquals($certainly->isValid('certainly-full.caa.appwrite.org'), true);
|
||||||
|
$this->assertEquals($letsencrypt->isValid('certainly-full.caa.appwrite.org'), false);
|
||||||
|
|
||||||
|
// Custom flags&tag are not allowed if validator includes specific flags&tag
|
||||||
|
$certainlyFull = new DNS('0 issue "certainly.com"', DNS::RECORD_CAA);
|
||||||
|
$this->assertEquals($certainlyFull->isValid('certainly-full.caa.appwrite.org'), false);
|
||||||
|
|
||||||
|
// Custom flags&tag still allows if they match exactly
|
||||||
|
$certainlyFull = new DNS('128 issuewild "certainly.com;account=123456;validationmethods=dns-01"', DNS::RECORD_CAA);
|
||||||
|
$this->assertEquals($certainlyFull->isValid('certainly-full.caa.appwrite.org'), true);
|
||||||
|
|
||||||
|
// Certainly CAA allows Certainly, but not LetsEncrypt; Same for subdomains
|
||||||
|
$this->assertEquals($certainly->isValid('certainly.caa.appwrite.org'), true);
|
||||||
|
$this->assertEquals($letsencrypt->isValid('certainly.caa.appwrite.org'), false);
|
||||||
|
|
||||||
|
$this->assertEquals($certainly->isValid('sub.certainly.caa.appwrite.org'), true);
|
||||||
|
$this->assertEquals($letsencrypt->isValid('sub.certainly.caa.appwrite.org'), false);
|
||||||
|
|
||||||
|
$this->assertEquals($certainly->isValid('sub.sub.certainly.caa.appwrite.org'), true);
|
||||||
|
$this->assertEquals($letsencrypt->isValid('sub.sub.certainly.caa.appwrite.org'), false);
|
||||||
|
|
||||||
|
// LetsEncrypt CAA on subdomain with parent allowing Certainly. Only LetsEncrypt is allowed; Same for subdomains
|
||||||
|
$this->assertEquals($certainly->isValid('letsencrypt.certainly.caa.appwrite.org'), false);
|
||||||
|
$this->assertEquals($letsencrypt->isValid('letsencrypt.certainly.caa.appwrite.org'), true);
|
||||||
|
|
||||||
|
$this->assertEquals($certainly->isValid('sub.letsencrypt.certainly.caa.appwrite.org'), false);
|
||||||
|
$this->assertEquals($letsencrypt->isValid('sub.letsencrypt.certainly.caa.appwrite.org'), true);
|
||||||
|
|
||||||
|
$this->assertEquals($certainly->isValid('sub.sub.letsencrypt.certainly.caa.appwrite.org'), false);
|
||||||
|
$this->assertEquals($letsencrypt->isValid('sub.sub.letsencrypt.certainly.caa.appwrite.org'), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue