Back to Checks
cloudflare

CAA record exists with issue or issuewild tag

zone_record_caa_exists
Service: zoneSeverity: low
OfficialBuilt-inVerified
Categories:encryption
Resource Type:Zone

Cloudflare zones are assessed for CAA (Certificate Authority Authorization) DNS records by checking if they exist with issue or issuewild tags that specify which certificate authorities are permitted to issue SSL/TLS certificates for the domain.

Risk

Without CAA records or issue/issuewild tags, any certificate authority can issue certificates for your domain. Unauthorized certificates enable man-in-the-middle attacks and domain impersonation. CAA records with only iodef tags do not restrict certificate issuance.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler cloudflare --checks zone_record_caa_exists

Recommendation

Add CAA records with issue or issuewild tags specifying authorized certificate authorities.

  • issue tag: authorizes CAs for regular (non-wildcard) certificates
  • issuewild tag: authorizes CAs for wildcard certificates specifically
  • Use issue ";" to block all certificate issuance (for domains that should never have certificates)
  • Use iodef tag to receive reports of policy violations (optional, for monitoring)
  • All CAs are required to check CAA records before issuing certificates

Remediation

Terraform

# Create CAA record to restrict certificate issuance
resource "cloudflare_record" "caa_issue" {
  zone_id = "<ZONE_ID>"
  name    = "@"
  type    = "CAA"
  data {
    flags = "0"
    tag   = "issue"                # Restricts which CAs can issue regular certificates
    value = "letsencrypt.org"      # Specify your authorized certificate authority
  }
}

# Create CAA record to restrict wildcard certificate issuance
resource "cloudflare_record" "caa_issuewild" {
  zone_id = "<ZONE_ID>"
  name    = "@"
  type    = "CAA"
  data {
    flags = "0"
    tag   = "issuewild"            # Restricts which CAs can issue wildcard certificates
    value = "letsencrypt.org"
  }
}

Other

1Log in to the Cloudflare dashboard and select your account and domain
2Go to DNS > Records
3Click Add record
4Select CAA as the record type
5Enter @ for the Name field
6Set flags to 0, tag to issue, and value to your authorized CA (e.g., letsencrypt.org)
7Click Save
8Optionally add issuewild records to control wildcard certificate issuance

Check Code

import re

from prowler.lib.check.models import Check, CheckReportCloudflare
from prowler.providers.cloudflare.services.dns.dns_client import dns_client
from prowler.providers.cloudflare.services.zone.zone_client import zone_client


class zone_record_caa_exists(Check):
    """Ensure that CAA record exists with certificate issuance restrictions.

    CAA (Certificate Authority Authorization) is a DNS record type that allows
    domain owners to specify which certificate authorities (CAs) are permitted
    to issue SSL/TLS certificates for their domain. This check verifies that CAA
    records exist with "issue" or "issuewild" tags that explicitly authorize
    specific CAs, preventing unauthorized certificate issuance and reducing the
    risk of man-in-the-middle attacks from fraudulent certificates.
    """

    def execute(self) -> list[CheckReportCloudflare]:
        """Execute the CAA record exists check.

        Iterates through all Cloudflare zones and verifies CAA configuration.
        The check validates two conditions:
        1. CAA records exist for the zone
        2. At least one record has an "issue" or "issuewild" tag specifying authorized CAs

        Returns:
            A list of CheckReportCloudflare objects with PASS status if CAA
            records with issuance restrictions exist, or FAIL status if no CAA
            records are found or they lack proper issue/issuewild tags.
        """
        findings = []

        for zone in zone_client.zones.values():
            report = CheckReportCloudflare(
                metadata=self.metadata(),
                resource=zone,
            )

            # CAA records restrict which CAs can issue certificates
            caa_records = [
                record
                for record in dns_client.records
                if record.zone_id == zone.id and record.type == "CAA"
            ]

            if not caa_records:
                report.status = "FAIL"
                report.status_extended = f"No CAA record found for zone {zone.name}."
            else:
                # Check if CAA records have issue or issuewild tags with CA specified
                issue_records = [
                    record
                    for record in caa_records
                    if self._has_issue_tag_with_ca(record.content)
                ]

                records_str = ", ".join(record.name for record in caa_records)

                if issue_records:
                    report.status = "PASS"
                    report.status_extended = f"CAA record with certificate issuance restrictions exists for zone {zone.name}: {records_str}."
                else:
                    report.status = "FAIL"
                    report.status_extended = f"CAA record exists for zone {zone.name} but does not specify authorized CAs with issue or issuewild tags: {records_str}."

            findings.append(report)

        return findings

    def _has_issue_tag_with_ca(self, content: str) -> bool:
        """Check if CAA record has issue or issuewild tag with a CA specified.

        CAA content format: "flags tag value" e.g., "0 issue letsencrypt.org"
        """
        # Strip quotes that may be present from Cloudflare API
        content_lower = content.strip('"').lower()
        # Match issue or issuewild tag followed by a value (CA name or ";" to block all)
        # Format: "0 issue letsencrypt.org" or "0 issuewild ;" or "0 issue \"digicert.com\""
        issue_match = re.search(r"\bissue\b", content_lower)
        issuewild_match = re.search(r"\bissuewild\b", content_lower)
        return bool(issue_match or issuewild_match)