From 484ebb50591831ca71a5fb4304903e0f732e2de4 Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 4 Aug 2025 00:42:00 +0530 Subject: [PATCH 01/13] Check CAA record in DNSTest --- .env | 4 +- app/config/variables.php | 18 +++ app/controllers/api/console.php | 2 + app/controllers/api/proxy.php | 6 + composer.json | 1 + composer.lock | 107 ++++++++++++++---- docker-compose.yml | 8 ++ src/Appwrite/Network/Validator/DNS.php | 34 +++--- .../Platform/Workers/Certificates.php | 6 + .../Response/Model/ConsoleVariables.php | 12 ++ tests/unit/Network/Validators/DNSTest.php | 23 ++++ 11 files changed, 178 insertions(+), 43 deletions(-) diff --git a/.env b/.env index 76af83a946..7fe837ea7e 100644 --- a/.env +++ b/.env @@ -121,4 +121,6 @@ _APP_MESSAGE_PUSH_TEST_DSN= _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10 _APP_PROJECT_REGIONS=default _APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000 -_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main \ No newline at end of file +_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main +_APP_DOMAINS_DNS=8.8.8.8 +_APP_DOMAIN_TARGET_CAA='0 issue "digicert.com"' \ No newline at end of file diff --git a/app/config/variables.php b/app/config/variables.php index 71ed13a483..61a0cd563f 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -151,6 +151,24 @@ return [ 'question' => '', 'filter' => '' ], + [ + 'name' => '_APP_DOMAIN_TARGET_CAA', + 'description' => 'A CAA record value that can be used to validate custom domains. Format: "0 issue \"certainly.com\""', + 'introduction' => '', + 'default' => '', + 'required' => false, + 'question' => '', + 'filter' => '' + ], + [ + 'name' => '_APP_DOMAINS_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', '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.', diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index 558dc0e4ef..a564031e7c 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -71,6 +71,8 @@ App::get('/v1/console/variables') '_APP_DOMAIN_TARGET_CNAME' => System::getEnv('_APP_DOMAIN_TARGET_CNAME'), '_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'), '_APP_DOMAIN_TARGET_A' => System::getEnv('_APP_DOMAIN_TARGET_A'), + '_APP_DOMAIN_TARGET_CAA' => System::getEnv('_APP_DOMAIN_TARGET_CAA'), + '_APP_DOMAINS_DNS' => System::getEnv('_APP_DOMAINS_DNS'), '_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'), '_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'), '_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'), diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index 417ea602ba..5596124a3a 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -257,6 +257,12 @@ App::patch('/v1/proxy/rules/:ruleId/verification') $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } + // Add CAA validation if configured + $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); + if (!empty($caaTarget)) { + $validators[] = new DNS($caaTarget, DNS::RECORD_CAA); + } + if (empty($validators)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); } diff --git a/composer.json b/composer.json index 31a31af9f2..10b8b7cc88 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,7 @@ "utopia-php/database": "0.71.*", "utopia-php/detector": "0.1.*", "utopia-php/domains": "0.8.*", + "utopia-php/dns": "dev-feat-add-CAA-to-client as 0.2.99", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.4.*", diff --git a/composer.lock b/composer.lock index da084c8fcd..b2c9ba0b0d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "edbe5912c45e1f467f398541a75a77de", + "content-hash": "d36ad770ee5e4ea6e3bb0206c2c584ff", "packages": [ { "name": "adhocore/jwt", @@ -69,16 +69,16 @@ }, { "name": "appwrite/appwrite", - "version": "15.0.0", + "version": "15.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf" + "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/deb97b62e0abed8a4fd5c5d48e77365cf89867cf", - "reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/c438b3885071ac7c0329199dce5e6f6a24dd215b", + "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b", "shasum": "" }, "require": { @@ -104,10 +104,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/15.0.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/15.1.0", "url": "https://appwrite.io/support" }, - "time": "2025-05-18T09:47:10+00:00" + "time": "2025-08-01T04:50:51+00:00" }, { "name": "appwrite/php-clamav", @@ -3596,6 +3596,62 @@ }, "time": "2025-05-19T11:01:28+00:00" }, + { + "name": "utopia-php/dns", + "version": "dev-feat-add-CAA-to-client", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/dns.git", + "reference": "a7d45e4c5dfc7020c0467de9587ccd7e15488206" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/dns/zipball/a7d45e4c5dfc7020c0467de9587ccd7e15488206", + "reference": "a7d45e4c5dfc7020c0467de9587ccd7e15488206", + "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/feat-add-CAA-to-client" + }, + "time": "2025-08-03T18:46:13+00:00" + }, { "name": "utopia-php/domains", "version": "0.8.0", @@ -4814,16 +4870,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.41.27", + "version": "0.41.28", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "083fd2e8163d6a4e59ee971ac6cb97277d831dd5" + "reference": "8eace11070264c62c8da3c69498fb8dc98fcfaf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/083fd2e8163d6a4e59ee971ac6cb97277d831dd5", - "reference": "083fd2e8163d6a4e59ee971ac6cb97277d831dd5", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/8eace11070264c62c8da3c69498fb8dc98fcfaf7", + "reference": "8eace11070264c62c8da3c69498fb8dc98fcfaf7", "shasum": "" }, "require": { @@ -4859,9 +4915,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.41.27" + "source": "https://github.com/appwrite/sdk-generator/tree/0.41.28" }, - "time": "2025-07-31T10:20:46+00:00" + "time": "2025-08-01T11:06:30+00:00" }, { "name": "doctrine/annotations", @@ -5280,16 +5336,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.3", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -5328,7 +5384,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -5336,7 +5392,7 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", @@ -8261,9 +8317,18 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/dns", + "version": "dev-feat-add-CAA-to-client", + "alias": "0.2.99", + "alias_normalized": "0.2.99.0" + } + ], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "utopia-php/dns": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/docker-compose.yml b/docker-compose.yml index 58b78fcd8e..830e0905fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -120,6 +120,8 @@ services: - _APP_DOMAIN_TARGET_CNAME - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A + - _APP_DOMAIN_TARGET_CAA + - _APP_DOMAINS_DNS - _APP_DOMAIN_FUNCTIONS - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -535,6 +537,8 @@ services: - _APP_DOMAIN_TARGET_CNAME - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A + - _APP_DOMAIN_TARGET_CAA + - _APP_DOMAINS_DNS - _APP_DOMAIN_FUNCTIONS - _APP_EMAIL_CERTIFICATES - _APP_REDIS_HOST @@ -704,6 +708,8 @@ services: - _APP_DOMAIN_TARGET_CNAME - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A + - _APP_DOMAIN_TARGET_CAA + - _APP_DOMAINS_DNS - _APP_EMAIL_SECURITY - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -738,6 +744,8 @@ services: - _APP_DOMAIN_TARGET_CNAME - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A + - _APP_DOMAIN_TARGET_CAA + - _APP_DOMAINS_DNS - _APP_DOMAIN_FUNCTIONS - _APP_OPENSSL_KEY_V1 - _APP_REDIS_HOST diff --git a/src/Appwrite/Network/Validator/DNS.php b/src/Appwrite/Network/Validator/DNS.php index 73494ddc3e..eff19c90f1 100644 --- a/src/Appwrite/Network/Validator/DNS.php +++ b/src/Appwrite/Network/Validator/DNS.php @@ -2,6 +2,8 @@ namespace Appwrite\Network\Validator; +use Utopia\DNS\Client; +use Utopia\System\System; use Utopia\Validator; class DNS extends Validator @@ -9,6 +11,7 @@ class DNS extends Validator public const RECORD_A = 'a'; public const RECORD_AAAA = 'aaaa'; public const RECORD_CNAME = 'cname'; + public const RECORD_CAA = 'caa'; /** * @var mixed @@ -42,42 +45,31 @@ 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_DOMAINS_DNS', ''); + $dns = new Client($dnsServer); + try { - $records = \dns_get_record($value, $typeNative); - $this->logs = $records; - } catch (\Throwable $th) { + $query = $dns->query($value, strtoupper($this->type)); + $this->logs = $query; + } catch (\Exception $e) { + $this->logs = ['error' => $e->getMessage()]; return false; } - if (!$records) { + if (empty($query)) { return false; } - foreach ($records as $record) { - if (isset($record[$dnsKey]) && $record[$dnsKey] === $this->target) { + foreach ($query as $record) { + if ($record->getRdata() === $this->target) { return true; } } diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 207f95ff7d..dd927aae84 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -314,6 +314,12 @@ class Certificates extends Action $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } + // Add CAA validation if configured + $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); + if (!empty($caaTarget)) { + $validators[] = new DNS($caaTarget, DNS::RECORD_CAA); + } + // Validate if domain target is properly configured if (empty($validators)) { throw new Exception('At least one of domain targets environment variable must be configured.'); diff --git a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php index 97dae2efcd..21141254ab 100644 --- a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php +++ b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php @@ -27,6 +27,18 @@ class ConsoleVariables extends Model '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' => '0 issue "certainly.com"', + ]) + ->addRule('_APP_DOMAINS_DNS', [ + 'type' => self::TYPE_STRING, + 'description' => 'DNS server to use for domain validation.', + 'default' => '8.8.8.8', + 'example' => '8.8.8.8', ]) ->addRule('_APP_STORAGE_LIMIT', [ 'type' => self::TYPE_INTEGER, diff --git a/tests/unit/Network/Validators/DNSTest.php b/tests/unit/Network/Validators/DNSTest.php index 5e8652381a..19f42892cb 100644 --- a/tests/unit/Network/Validators/DNSTest.php +++ b/tests/unit/Network/Validators/DNSTest.php @@ -47,4 +47,27 @@ class DNSTest extends TestCase $this->assertEquals($validator->isValid('aaaa-unit-test.appwrite.org'), true); $this->assertEquals($validator->isValid('test1.appwrite.org'), false); } + + public function testCAA(): void + { + $validator = new DNS('0 issue "pki.goog"', DNS::RECORD_CAA); + + $this->assertEquals($validator->isValid(''), false); + $this->assertEquals($validator->isValid(null), false); + $this->assertEquals($validator->isValid(false), false); + + $result = $validator->isValid('google.com'); + if ($result === false) { + $logs = $validator->getLogs(); + if (isset($logs['error']) && strpos($logs['error'], 'Failed to receive data') !== false) { + $this->markTestSkipped('DNS resolution not available in test environment: ' . $logs['error']); + } + } + $this->assertEquals($result, true); + + $this->assertEquals($validator->isValid('test1.appwrite.org'), false); + + $validator2 = new DNS('0 issue "letsencrypt.org"', DNS::RECORD_CAA); + $this->assertEquals($validator2->isValid('test2.appwrite.org'), false); + } } From 9f8fabe58a42f33c853a6ad24093b18e25483b9c Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 4 Aug 2025 01:12:58 +0530 Subject: [PATCH 02/13] Verify domain CAA after certificate issuance --- app/controllers/api/proxy.php | 6 ---- .../Platform/Workers/Certificates.php | 34 +++++++++++++++---- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index 5596124a3a..417ea602ba 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -257,12 +257,6 @@ App::patch('/v1/proxy/rules/:ruleId/verification') $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } - // Add CAA validation if configured - $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); - if (!empty($caaTarget)) { - $validators[] = new DNS($caaTarget, DNS::RECORD_CAA); - } - if (empty($validators)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); } diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index dd927aae84..287abfd1a2 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -188,8 +188,11 @@ class Certificates extends Action $certName = ID::unique(); $renewDate = $certificates->issueCertificate($certName, $domain->get(), $domainType); + // Validate CAA records after certificate generation to ensure the CA was authorized + $this->validateCAARecords($domain->get(), $log); + // Command succeeded, store all data into document - $certificate->setAttribute('logs', 'Certificate successfully generated.'); + $certificate->setAttribute('logs', 'Certificate successfully generated and CAA validated.'); // Update certificate info stored in database $certificate->setAttribute('renewDate', $renewDate); @@ -314,12 +317,6 @@ class Certificates extends Action $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } - // Add CAA validation if configured - $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); - if (!empty($caaTarget)) { - $validators[] = new DNS($caaTarget, DNS::RECORD_CAA); - } - // Validate if domain target is properly configured if (empty($validators)) { throw new Exception('At least one of domain targets environment variable must be configured.'); @@ -349,6 +346,29 @@ class Certificates extends Action } } + /** + * Validate CAA records to ensure the certificate authority (CA) was authorized by the domain owner. + * + * @param string $domain The domain to validate CAA records for. + * @param Log $log The logger instance. + * @return void + * @throws Exception + */ + private function validateCAARecords(string $domain, Log $log): void + { + $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); + if (empty($caaTarget)) { + return; + } + + $validator = new DNS($caaTarget, DNS::RECORD_CAA); + if (!$validator->isValid($domain)) { + $log->addTag('caaDomain', $domain); + $log->addExtra('caaResponse', $validator->getLogs()); + throw new Exception('Failed to verify CAA records for domain: ' . $domain); + } + } + /** * Method to make sure information about error is delivered to admnistrator. * From d48aa6060979a43e7757374d95b83b527a495d1a Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 4 Aug 2025 01:22:10 +0530 Subject: [PATCH 03/13] Update test --- tests/unit/Network/Validators/DNSTest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/unit/Network/Validators/DNSTest.php b/tests/unit/Network/Validators/DNSTest.php index 19f42892cb..f5c5e093dd 100644 --- a/tests/unit/Network/Validators/DNSTest.php +++ b/tests/unit/Network/Validators/DNSTest.php @@ -57,12 +57,6 @@ class DNSTest extends TestCase $this->assertEquals($validator->isValid(false), false); $result = $validator->isValid('google.com'); - if ($result === false) { - $logs = $validator->getLogs(); - if (isset($logs['error']) && strpos($logs['error'], 'Failed to receive data') !== false) { - $this->markTestSkipped('DNS resolution not available in test environment: ' . $logs['error']); - } - } $this->assertEquals($result, true); $this->assertEquals($validator->isValid('test1.appwrite.org'), false); From 69dbb3222fd21f167f5417707245f057d5b8ca89 Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 4 Aug 2025 01:33:47 +0530 Subject: [PATCH 04/13] Add check in proxy --- app/controllers/api/proxy.php | 5 +++ .../Platform/Workers/Certificates.php | 34 ++++--------------- tests/unit/Network/Validators/DNSTest.php | 4 +-- 3 files changed, 14 insertions(+), 29 deletions(-) diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index 417ea602ba..eb7dcf625a 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -257,6 +257,11 @@ App::patch('/v1/proxy/rules/:ruleId/verification') $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } + $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); + if (!empty($caaTarget)) { + $validators[] = new DNS($caaTarget, DNS::RECORD_CAA); + } + if (empty($validators)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); } diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 287abfd1a2..dd927aae84 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -188,11 +188,8 @@ class Certificates extends Action $certName = ID::unique(); $renewDate = $certificates->issueCertificate($certName, $domain->get(), $domainType); - // Validate CAA records after certificate generation to ensure the CA was authorized - $this->validateCAARecords($domain->get(), $log); - // Command succeeded, store all data into document - $certificate->setAttribute('logs', 'Certificate successfully generated and CAA validated.'); + $certificate->setAttribute('logs', 'Certificate successfully generated.'); // Update certificate info stored in database $certificate->setAttribute('renewDate', $renewDate); @@ -317,6 +314,12 @@ class Certificates extends Action $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } + // Add CAA validation if configured + $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); + if (!empty($caaTarget)) { + $validators[] = new DNS($caaTarget, DNS::RECORD_CAA); + } + // Validate if domain target is properly configured if (empty($validators)) { throw new Exception('At least one of domain targets environment variable must be configured.'); @@ -346,29 +349,6 @@ class Certificates extends Action } } - /** - * Validate CAA records to ensure the certificate authority (CA) was authorized by the domain owner. - * - * @param string $domain The domain to validate CAA records for. - * @param Log $log The logger instance. - * @return void - * @throws Exception - */ - private function validateCAARecords(string $domain, Log $log): void - { - $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); - if (empty($caaTarget)) { - return; - } - - $validator = new DNS($caaTarget, DNS::RECORD_CAA); - if (!$validator->isValid($domain)) { - $log->addTag('caaDomain', $domain); - $log->addExtra('caaResponse', $validator->getLogs()); - throw new Exception('Failed to verify CAA records for domain: ' . $domain); - } - } - /** * Method to make sure information about error is delivered to admnistrator. * diff --git a/tests/unit/Network/Validators/DNSTest.php b/tests/unit/Network/Validators/DNSTest.php index f5c5e093dd..64293258a4 100644 --- a/tests/unit/Network/Validators/DNSTest.php +++ b/tests/unit/Network/Validators/DNSTest.php @@ -50,13 +50,13 @@ class DNSTest extends TestCase public function testCAA(): void { - $validator = new DNS('0 issue "pki.goog"', DNS::RECORD_CAA); + $validator = new DNS('0 issue "digicert.com"', DNS::RECORD_CAA); $this->assertEquals($validator->isValid(''), false); $this->assertEquals($validator->isValid(null), false); $this->assertEquals($validator->isValid(false), false); - $result = $validator->isValid('google.com'); + $result = $validator->isValid('github.com'); $this->assertEquals($result, true); $this->assertEquals($validator->isValid('test1.appwrite.org'), false); From 783894a82b5b2a0ad696dfa75e9ce88db6ee960a Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 4 Aug 2025 01:40:39 +0530 Subject: [PATCH 05/13] Add default value --- src/Appwrite/Network/Validator/DNS.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Network/Validator/DNS.php b/src/Appwrite/Network/Validator/DNS.php index eff19c90f1..7557a0d04b 100644 --- a/src/Appwrite/Network/Validator/DNS.php +++ b/src/Appwrite/Network/Validator/DNS.php @@ -53,7 +53,7 @@ class DNS extends Validator return false; } - $dnsServer = System::getEnv('_APP_DOMAINS_DNS', ''); + $dnsServer = System::getEnv('_APP_DOMAINS_DNS', '8.8.8.8'); $dns = new Client($dnsServer); try { From 16685dad4e20a0ba4ce3c9126ea2787cf2dac9eb Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 4 Aug 2025 01:49:08 +0530 Subject: [PATCH 06/13] Update console tests --- tests/e2e/Services/Console/ConsoleConsoleClientTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php index 6059cb2000..bfad5b480b 100644 --- a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php +++ b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php @@ -24,10 +24,11 @@ class ConsoleConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(13, $response['body']); + $this->assertCount(15, $response['body']); $this->assertIsString($response['body']['_APP_DOMAIN_TARGET_CNAME']); $this->assertIsString($response['body']['_APP_DOMAIN_TARGET_A']); $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_COMPUTE_SIZE_LIMIT']); $this->assertIsBool($response['body']['_APP_DOMAIN_ENABLED']); @@ -37,6 +38,7 @@ class ConsoleConsoleClientTest extends Scope $this->assertIsString($response['body']['_APP_DOMAIN_FUNCTIONS']); $this->assertIsString($response['body']['_APP_OPTIONS_FORCE_HTTPS']); $this->assertIsString($response['body']['_APP_DOMAINS_NAMESERVERS']); + $this->assertIsString($response['body']['_APP_DOMAINS_DNS']); // When adding new keys, dont forget to update count a few lines above } } From 4cafd2e07d2c775d6a19ac1bf375f5e915d7fadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 Aug 2025 14:28:50 +0200 Subject: [PATCH 07/13] Improve PR quality --- .env | 6 +-- app/config/variables.php | 4 +- app/controllers/api/console.php | 4 +- app/controllers/api/proxy.php | 6 +-- app/views/install/compose.phtml | 8 +++ composer.json | 2 +- composer.lock | 29 ++++------ docker-compose.yml | 8 +-- src/Appwrite/Network/Validator/DNS.php | 18 ++++++- .../Platform/Workers/Certificates.php | 7 +-- .../Response/Model/ConsoleVariables.php | 54 +++++++++---------- .../Console/ConsoleConsoleClientTest.php | 3 +- tests/unit/Network/Validators/DNSTest.php | 15 +++--- 13 files changed, 83 insertions(+), 81 deletions(-) diff --git a/.env b/.env index 7fe837ea7e..4d7c038a6b 100644 --- a/.env +++ b/.env @@ -21,6 +21,7 @@ _APP_OPTIONS_ROUTER_PROTECTION=disabled _APP_OPTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled _APP_OPENSSL_KEY_V1=your-secret-key +_APP_DNS=8.8.8.8 _APP_DOMAIN=traefik _APP_CONSOLE_DOMAIN=localhost _APP_DOMAIN_FUNCTIONS=functions.localhost @@ -28,6 +29,7 @@ _APP_DOMAIN_SITES=sites.localhost _APP_DOMAIN_TARGET_CNAME=test.localhost _APP_DOMAIN_TARGET_A=127.0.0.1 _APP_DOMAIN_TARGET_AAAA=::1 +_APP_DOMAIN_TARGET_CAA=digicert.com _APP_RULES_FORMAT=md5 _APP_REDIS_HOST=redis _APP_REDIS_PORT=6379 @@ -121,6 +123,4 @@ _APP_MESSAGE_PUSH_TEST_DSN= _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10 _APP_PROJECT_REGIONS=default _APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000 -_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main -_APP_DOMAINS_DNS=8.8.8.8 -_APP_DOMAIN_TARGET_CAA='0 issue "digicert.com"' \ No newline at end of file +_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main \ No newline at end of file diff --git a/app/config/variables.php b/app/config/variables.php index 61a0cd563f..8fd00557b3 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -153,7 +153,7 @@ return [ ], [ 'name' => '_APP_DOMAIN_TARGET_CAA', - 'description' => 'A CAA record value that can be used to validate custom domains. Format: "0 issue \"certainly.com\""', + 'description' => 'A CAA record domain that can be used to validate custom domains. Value should be domain\'s hostname.', 'introduction' => '', 'default' => '', 'required' => false, @@ -161,7 +161,7 @@ return [ 'filter' => '' ], [ - 'name' => '_APP_DOMAINS_DNS', + 'name' => '_APP_DNS', 'description' => 'DNS server to use for domain validation. Default: 8.8.8.8', 'introduction' => '', 'default' => '8.8.8.8', diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index a564031e7c..fc9849cc17 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -71,8 +71,8 @@ App::get('/v1/console/variables') '_APP_DOMAIN_TARGET_CNAME' => System::getEnv('_APP_DOMAIN_TARGET_CNAME'), '_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'), '_APP_DOMAIN_TARGET_A' => System::getEnv('_APP_DOMAIN_TARGET_A'), - '_APP_DOMAIN_TARGET_CAA' => System::getEnv('_APP_DOMAIN_TARGET_CAA'), - '_APP_DOMAINS_DNS' => System::getEnv('_APP_DOMAINS_DNS'), + // 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_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'), '_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'), diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index eb7dcf625a..c2be924c40 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -256,10 +256,8 @@ App::patch('/v1/proxy/rules/:ruleId/verification') if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } - - $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); - if (!empty($caaTarget)) { - $validators[] = new DNS($caaTarget, DNS::RECORD_CAA); + if (!empty(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), DNS::RECORD_CAA); } if (empty($validators)) { diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 89facfe0f1..34940e7c6e 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -95,6 +95,8 @@ $image = $this->getParam('image', ''); - _APP_DOMAIN_TARGET_CNAME - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A + - _APP_DOMAIN_TARGET_CAA + - _APP_DOMAINS_DNS - _APP_DOMAIN_FUNCTIONS - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -472,6 +474,8 @@ $image = $this->getParam('image', ''); - _APP_DOMAIN_TARGET_CNAME - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A + - _APP_DOMAIN_TARGET_CAA + - _APP_DOMAINS_DNS - _APP_DOMAIN_FUNCTIONS - _APP_EMAIL_CERTIFICATES - _APP_REDIS_HOST @@ -629,6 +633,8 @@ $image = $this->getParam('image', ''); - _APP_DOMAIN_TARGET_CNAME - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A + - _APP_DOMAIN_TARGET_CAA + - _APP_DOMAINS_DNS - _APP_EMAIL_SECURITY - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -660,6 +666,8 @@ $image = $this->getParam('image', ''); - _APP_DOMAIN_TARGET_CNAME - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A + - _APP_DOMAIN_TARGET_CAA + - _APP_DOMAINS_DNS - _APP_DOMAIN_FUNCTIONS - _APP_OPENSSL_KEY_V1 - _APP_REDIS_HOST diff --git a/composer.json b/composer.json index 10b8b7cc88..1f42130168 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "utopia-php/database": "0.71.*", "utopia-php/detector": "0.1.*", "utopia-php/domains": "0.8.*", - "utopia-php/dns": "dev-feat-add-CAA-to-client as 0.2.99", + "utopia-php/dns": "0.3.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.4.*", diff --git a/composer.lock b/composer.lock index b2c9ba0b0d..6ff2cc6fd3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d36ad770ee5e4ea6e3bb0206c2c584ff", + "content-hash": "65958cc5b58b8f32a044bb8cc33cbeef", "packages": [ { "name": "adhocore/jwt", @@ -3598,16 +3598,16 @@ }, { "name": "utopia-php/dns", - "version": "dev-feat-add-CAA-to-client", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/dns.git", - "reference": "a7d45e4c5dfc7020c0467de9587ccd7e15488206" + "reference": "8fd4161bc3a8021a670c1101b40f6b09a97f1a54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/dns/zipball/a7d45e4c5dfc7020c0467de9587ccd7e15488206", - "reference": "a7d45e4c5dfc7020c0467de9587ccd7e15488206", + "url": "https://api.github.com/repos/utopia-php/dns/zipball/8fd4161bc3a8021a670c1101b40f6b09a97f1a54", + "reference": "8fd4161bc3a8021a670c1101b40f6b09a97f1a54", "shasum": "" }, "require": { @@ -3648,9 +3648,9 @@ ], "support": { "issues": "https://github.com/utopia-php/dns/issues", - "source": "https://github.com/utopia-php/dns/tree/feat-add-CAA-to-client" + "source": "https://github.com/utopia-php/dns/tree/0.3.0" }, - "time": "2025-08-03T18:46:13+00:00" + "time": "2025-08-04T11:05:53+00:00" }, { "name": "utopia-php/domains", @@ -8317,18 +8317,9 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [ - { - "package": "utopia-php/dns", - "version": "dev-feat-add-CAA-to-client", - "alias": "0.2.99", - "alias_normalized": "0.2.99.0" - } - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/dns": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -8352,5 +8343,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 830e0905fa..0e299c8a2c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -121,7 +121,7 @@ services: - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_TARGET_CAA - - _APP_DOMAINS_DNS + - _APP_DNS - _APP_DOMAIN_FUNCTIONS - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -538,7 +538,7 @@ services: - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_TARGET_CAA - - _APP_DOMAINS_DNS + - _APP_DNS - _APP_DOMAIN_FUNCTIONS - _APP_EMAIL_CERTIFICATES - _APP_REDIS_HOST @@ -709,7 +709,7 @@ services: - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_TARGET_CAA - - _APP_DOMAINS_DNS + - _APP_DNS - _APP_EMAIL_SECURITY - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -745,7 +745,7 @@ services: - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_TARGET_CAA - - _APP_DOMAINS_DNS + - _APP_DNS - _APP_DOMAIN_FUNCTIONS - _APP_OPENSSL_KEY_V1 - _APP_REDIS_HOST diff --git a/src/Appwrite/Network/Validator/DNS.php b/src/Appwrite/Network/Validator/DNS.php index 7557a0d04b..b28ef2f800 100644 --- a/src/Appwrite/Network/Validator/DNS.php +++ b/src/Appwrite/Network/Validator/DNS.php @@ -11,7 +11,7 @@ class DNS extends Validator public const RECORD_A = 'a'; public const RECORD_AAAA = 'aaaa'; public const RECORD_CNAME = 'cname'; - public const RECORD_CAA = 'caa'; + public const RECORD_CAA = 'caa'; // Only provide domain as $target for CAA validation /** * @var mixed @@ -53,7 +53,7 @@ class DNS extends Validator return false; } - $dnsServer = System::getEnv('_APP_DOMAINS_DNS', '8.8.8.8'); + $dnsServer = System::getEnv('_APP_DNS', '8.8.8.8'); $dns = new Client($dnsServer); try { @@ -69,6 +69,20 @@ class DNS extends Validator } foreach ($query as $record) { + // CAA validation only needs to ensure domain + if ($this->type === self::RECORD_CAA) { + // Original: 255 issuewild "certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600" + // Extracted: certainly.com + $rdata = $record->getRdata(); + $rdata = \explode(' ', $rdata, 3)[2] ?? ''; + $rdata = \trim('"'); + $rdata = \explode(';', $rdata, 2)[0] ?? ''; + + if ($rdata === $this->target) { + return true; + } + } + if ($record->getRdata() === $this->target) { return true; } diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index dd927aae84..fc2878a1d0 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -313,11 +313,8 @@ class Certificates extends Action if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } - - // Add CAA validation if configured - $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); - if (!empty($caaTarget)) { - $validators[] = new DNS($caaTarget, DNS::RECORD_CAA); + if (!empty(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), DNS::RECORD_CAA); } // Validate if domain target is properly configured diff --git a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php index 21141254ab..b81502f0a1 100644 --- a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php +++ b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php @@ -10,36 +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_CAA', [ - 'type' => self::TYPE_STRING, - 'description' => 'CAA target for your Appwrite custom domains.', - 'default' => '', - 'example' => '0 issue "certainly.com"', - ]) - ->addRule('_APP_DOMAINS_DNS', [ - 'type' => self::TYPE_STRING, - 'description' => 'DNS server to use for domain validation.', - 'default' => '8.8.8.8', - 'example' => '8.8.8.8', - ]) + ->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.', diff --git a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php index bfad5b480b..340cabc8c0 100644 --- a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php +++ b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php @@ -24,7 +24,7 @@ class ConsoleConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(15, $response['body']); + $this->assertCount(14, $response['body']); $this->assertIsString($response['body']['_APP_DOMAIN_TARGET_CNAME']); $this->assertIsString($response['body']['_APP_DOMAIN_TARGET_A']); $this->assertIsString($response['body']['_APP_DOMAIN_TARGET_AAAA']); @@ -38,7 +38,6 @@ class ConsoleConsoleClientTest extends Scope $this->assertIsString($response['body']['_APP_DOMAIN_FUNCTIONS']); $this->assertIsString($response['body']['_APP_OPTIONS_FORCE_HTTPS']); $this->assertIsString($response['body']['_APP_DOMAINS_NAMESERVERS']); - $this->assertIsString($response['body']['_APP_DOMAINS_DNS']); // When adding new keys, dont forget to update count a few lines above } } diff --git a/tests/unit/Network/Validators/DNSTest.php b/tests/unit/Network/Validators/DNSTest.php index 64293258a4..42417cce05 100644 --- a/tests/unit/Network/Validators/DNSTest.php +++ b/tests/unit/Network/Validators/DNSTest.php @@ -50,18 +50,19 @@ class DNSTest extends TestCase public function testCAA(): void { - $validator = new DNS('0 issue "digicert.com"', DNS::RECORD_CAA); + $validator = new DNS('digicert.com', DNS::RECORD_CAA); + $this->assertEquals($validator->isValid('github.com'), true); $this->assertEquals($validator->isValid(''), false); $this->assertEquals($validator->isValid(null), false); $this->assertEquals($validator->isValid(false), false); - - $result = $validator->isValid('github.com'); - $this->assertEquals($result, true); - $this->assertEquals($validator->isValid('test1.appwrite.org'), false); - $validator2 = new DNS('0 issue "letsencrypt.org"', DNS::RECORD_CAA); - $this->assertEquals($validator2->isValid('test2.appwrite.org'), false); + $validator = new DNS('0 issue "digicert.com"', DNS::RECORD_CAA); + + $this->assertEquals($validator->isValid('github.com'), false); + + $validator = new DNS('letsencrypt.org', DNS::RECORD_CAA); + $this->assertEquals($validator->isValid('test2.appwrite.org'), false); } } From 75bf37a074ce5972149b6e7757e19e0ae6c27f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 Aug 2025 14:32:38 +0200 Subject: [PATCH 08/13] Update composer.lock --- composer.lock | 62 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/composer.lock b/composer.lock index aafe1d216c..88356d5349 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7b2ef6192403daf5c492219822ce0aa1", + "content-hash": "761a7e17b49381e68038c92873888125", "packages": [ { "name": "adhocore/jwt", @@ -3641,6 +3641,62 @@ }, "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", "version": "0.8.0", @@ -8308,7 +8364,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -8332,5 +8388,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } From 48a977526e1898354fbbe40a0ab0f37440a41f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 Aug 2025 14:53:13 +0200 Subject: [PATCH 09/13] Fix tests --- app/controllers/api/console.php | 2 +- src/Appwrite/Network/Validator/DNS.php | 4 ++-- tests/unit/Network/Validators/DNSTest.php | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index fc9849cc17..b0619df3b3 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -72,7 +72,7 @@ App::get('/v1/console/variables') '_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'), '_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_DOMAIN_TARGET_CAA' => '0 issue "' . System::getEnv('_APP_DOMAIN_TARGET_CAA') . '"', '_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'), '_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'), '_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'), diff --git a/src/Appwrite/Network/Validator/DNS.php b/src/Appwrite/Network/Validator/DNS.php index b28ef2f800..67c0ca53d5 100644 --- a/src/Appwrite/Network/Validator/DNS.php +++ b/src/Appwrite/Network/Validator/DNS.php @@ -11,7 +11,7 @@ class DNS extends Validator public const RECORD_A = 'a'; public const RECORD_AAAA = 'aaaa'; public const RECORD_CNAME = 'cname'; - public const RECORD_CAA = 'caa'; // Only provide domain as $target for CAA validation + public const RECORD_CAA = 'caa'; // You can provide domain only (as $target) for CAA validation /** * @var mixed @@ -75,7 +75,7 @@ class DNS extends Validator // Extracted: certainly.com $rdata = $record->getRdata(); $rdata = \explode(' ', $rdata, 3)[2] ?? ''; - $rdata = \trim('"'); + $rdata = \trim($rdata, '"'); $rdata = \explode(';', $rdata, 2)[0] ?? ''; if ($rdata === $this->target) { diff --git a/tests/unit/Network/Validators/DNSTest.php b/tests/unit/Network/Validators/DNSTest.php index 42417cce05..377018fe7b 100644 --- a/tests/unit/Network/Validators/DNSTest.php +++ b/tests/unit/Network/Validators/DNSTest.php @@ -51,7 +51,6 @@ class DNSTest extends TestCase public function testCAA(): void { $validator = new DNS('digicert.com', DNS::RECORD_CAA); - $this->assertEquals($validator->isValid('github.com'), true); $this->assertEquals($validator->isValid(''), false); $this->assertEquals($validator->isValid(null), false); @@ -59,7 +58,12 @@ class DNSTest extends TestCase $this->assertEquals($validator->isValid('test1.appwrite.org'), false); $validator = new DNS('0 issue "digicert.com"', DNS::RECORD_CAA); + $this->assertEquals($validator->isValid('github.com'), true); + $validator = new DNS('0 issuewild "digicert.com"', DNS::RECORD_CAA); + $this->assertEquals($validator->isValid('github.com'), true); + + $validator = new DNS('128 issue "digicert.com"', DNS::RECORD_CAA); $this->assertEquals($validator->isValid('github.com'), false); $validator = new DNS('letsencrypt.org', DNS::RECORD_CAA); From 2a63de0c467c810d311f8ddc3a86dc7fbf35076e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 Aug 2025 14:57:35 +0200 Subject: [PATCH 10/13] Improve docs --- src/Appwrite/Network/Validator/DNS.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Network/Validator/DNS.php b/src/Appwrite/Network/Validator/DNS.php index 67c0ca53d5..2807156a2d 100644 --- a/src/Appwrite/Network/Validator/DNS.php +++ b/src/Appwrite/Network/Validator/DNS.php @@ -71,12 +71,11 @@ class DNS extends Validator foreach ($query as $record) { // CAA validation only needs to ensure domain if ($this->type === self::RECORD_CAA) { - // Original: 255 issuewild "certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600" - // Extracted: certainly.com - $rdata = $record->getRdata(); - $rdata = \explode(' ', $rdata, 3)[2] ?? ''; - $rdata = \trim($rdata, '"'); - $rdata = \explode(';', $rdata, 2)[0] ?? ''; + // 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; From 5e294d084985d808c9fc4470cdf724cd1810b4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 Aug 2025 15:35:40 +0200 Subject: [PATCH 11/13] Support empty CAA records --- src/Appwrite/Network/Validator/DNS.php | 26 ++++++++++++++++++----- tests/unit/Network/Validators/DNSTest.php | 15 ++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Network/Validator/DNS.php b/src/Appwrite/Network/Validator/DNS.php index 2807156a2d..95d8436eda 100644 --- a/src/Appwrite/Network/Validator/DNS.php +++ b/src/Appwrite/Network/Validator/DNS.php @@ -8,10 +8,10 @@ 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_CAA = 'caa'; // You can provide domain only (as $target) for CAA validation + 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 @@ -57,7 +57,7 @@ class DNS extends Validator $dns = new Client($dnsServer); try { - $query = $dns->query($value, strtoupper($this->type)); + $query = $dns->query($value, $this->type); $this->logs = $query; } catch (\Exception $e) { $this->logs = ['error' => $e->getMessage()]; @@ -65,10 +65,21 @@ class DNS extends Validator } if (empty($query)) { + // No CAA records means anyone can issue certificate + if ($this->type === self::RECORD_CAA) { + return true; + } + return false; } + $caaCount = 0; + foreach ($query as $record) { + if ($record->getTypeName() === self::RECORD_CAA) { + $caaCount++; + } + // CAA validation only needs to ensure domain if ($this->type === self::RECORD_CAA) { // Extract domain; comments showcase extraction steps in most complex scenario @@ -87,6 +98,11 @@ class DNS extends Validator } } + if ($this->type === self::RECORD_CAA && $caaCount === 0) { + // No CAA records, means anyone can issue certificate + return true; + } + return false; } diff --git a/tests/unit/Network/Validators/DNSTest.php b/tests/unit/Network/Validators/DNSTest.php index 377018fe7b..6609a0838a 100644 --- a/tests/unit/Network/Validators/DNSTest.php +++ b/tests/unit/Network/Validators/DNSTest.php @@ -52,10 +52,7 @@ class DNSTest extends TestCase { $validator = new DNS('digicert.com', DNS::RECORD_CAA); $this->assertEquals($validator->isValid('github.com'), true); - $this->assertEquals($validator->isValid(''), false); - $this->assertEquals($validator->isValid(null), false); - $this->assertEquals($validator->isValid(false), false); - $this->assertEquals($validator->isValid('test1.appwrite.org'), false); + $this->assertEquals($validator->isValid('test1.appwrite.org'), true); $validator = new DNS('0 issue "digicert.com"', DNS::RECORD_CAA); $this->assertEquals($validator->isValid('github.com'), true); @@ -67,6 +64,14 @@ class DNSTest extends TestCase $this->assertEquals($validator->isValid('github.com'), false); $validator = new DNS('letsencrypt.org', DNS::RECORD_CAA); - $this->assertEquals($validator->isValid('test2.appwrite.org'), false); + $this->assertEquals($validator->isValid('github.com'), false); + + // Valid becasue no CAA record configured + $validator = new DNS('anything.com', DNS::RECORD_CAA); + $this->assertEquals($validator->isValid('cloud.appwrite.io'), true); + + // Valid becasue no CAA record configured + $validator = new DNS('something.org', DNS::RECORD_CAA); + $this->assertEquals($validator->isValid('cloud.appwrite.io'), true); } } From 8c3530ee60cd89ad5f0bf7975727756f8dd9d9a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 Aug 2025 16:18:06 +0200 Subject: [PATCH 12/13] Fix caa validation breaking existing validations --- app/controllers/api/proxy.php | 16 +++++++++++++--- src/Appwrite/Platform/Workers/Certificates.php | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index c2be924c40..6b76b0c34c 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -256,9 +256,6 @@ App::patch('/v1/proxy/rules/:ruleId/verification') if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } - if (!empty(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''))) { - $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), DNS::RECORD_CAA); - } if (empty($validators)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); @@ -289,6 +286,19 @@ App::patch('/v1/proxy/rules/:ruleId/verification') 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 certainly.com to issue certificates.'); + } + } + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying')); // Issue a TLS certificate when domain is verified diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index fc2878a1d0..d14bf0428d 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -313,9 +313,6 @@ class Certificates extends Action if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); } - if (!empty(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''))) { - $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), DNS::RECORD_CAA); - } // Validate if domain target is properly configured if (empty($validators)) { @@ -340,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? From 7f690a1048444e5189c7983a124cc0e031c33278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 Aug 2025 16:51:49 +0200 Subject: [PATCH 13/13] Simplify implementation --- src/Appwrite/Network/Validator/DNS.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Appwrite/Network/Validator/DNS.php b/src/Appwrite/Network/Validator/DNS.php index 95d8436eda..7549d18f54 100644 --- a/src/Appwrite/Network/Validator/DNS.php +++ b/src/Appwrite/Network/Validator/DNS.php @@ -73,13 +73,7 @@ class DNS extends Validator return false; } - $caaCount = 0; - foreach ($query as $record) { - if ($record->getTypeName() === self::RECORD_CAA) { - $caaCount++; - } - // CAA validation only needs to ensure domain if ($this->type === self::RECORD_CAA) { // Extract domain; comments showcase extraction steps in most complex scenario @@ -98,11 +92,6 @@ class DNS extends Validator } } - if ($this->type === self::RECORD_CAA && $caaCount === 0) { - // No CAA records, means anyone can issue certificate - return true; - } - return false; }