Skip to content

Subtle domain verification bug that prevents MTA-STS records population #2526

@Xombran

Description

@Xombran

I stopped using github 5 years ago but I registered a new account just to share this bug that I have been fighting for the past 2 days. Basically I am in a situation where I can't let MiaB provision it's own certificates and instead, I get my wildcard certificate pushed to the box, everything is fine and green on that front including both, the TLS (SSL) Certificates section and the Status Checks in the Admin Panel.

The only issue is that my MTA-STS records are missing from the External DNS section in the admin panel, after quickly inspecting dns_update.py, I found that these records are only added if specific conditions were met:

# ... inside the build_zone function ...

mta_sts_records = [ ]
if domain_properties[domain]["mail"] \
  and domain_properties[env["PRIMARY_HOSTNAME"]]["certificate-is-valid"] \
  and is_domain_cert_signed_and_valid("mta-sts." + domain, env):
    # ... (code to generate a policy ID) ...
    mta_sts_records.extend([
        ("_mta-sts", "TXT", "v=STSv1; id=" + mta_sts_policy_id, "Optional. Part of the MTA-STS policy for incoming mail. If set, a MTA-STS policy must also be published.")
    ])
# ...
  1. domain_properties[domain]["mail"]: The domain is configured to receive mail on my box. (Which is True).
  2. domain_properties[env["PRIMARY_HOSTNAME"]]["certificate-is-valid"]: The primary hostname of my box (box.mydomain.com) has a valid, signed SSL certificate. (Which is True as per the TLS (SSL) Certificates section)
  3. is_domain_cert_signed_and_valid("mta-sts." + domain, env): The subdomain mta-sts.mydomain.com also has its own valid, signed SSL certificate. (Which is also True as per the TLS (SSL) Certificates section)

And yet it doesn't matter how many times I manually invoke dns_update.py --update, these records are never created! Now I had a doubt that there's a discrepancy going on, and the following is my observations:

  1. The admin panel is using a smart function which is wildcard-aware. It first looks for a literal match (mta-sts.mydomain.com). When that fails, it then looks for a wildcard match (*.mydomain.com). It finds my wildcard certificate and proceeds.
# ssl_certificates.py
def get_domain_ssl_files(domain, ssl_certificates, env, ...):
    # ...
    wildcard_domain = re.sub(r"^[^\.]+", "*", domain) # Creates "*.mydomain.com"
    if domain in ssl_certificates:
        return ssl_certificates[domain]
    if wildcard_domain in ssl_certificates: # <--- BINGO!
        return ssl_certificates[wildcard_domain]
    # ... 
  1. The dns_update.py is using a dumb function that never calls the smart get_domain_ssl_files function. It directly queries the dictionary returned by get_ssl_certificates using a literal key. Since the key 'mta-sts.mydomain.com' doesn't exist in that dictionary (only '*.mydomain.com' does), the check fails immediately and I don't get my MTA-STS records populated.
# A simplified view of the logic in dns_update.py
def is_cert_valid(domain, env):
    all_certs = get_ssl_certificates(env) # This is the "dumb" function
    
    # It looks for a LITERAL key. It does NOT try a wildcard.
    if domain not in all_certs:
        return False # It fails here!
        
    cert_info = all_certs[domain]
    status, _ = check_certificate(domain, cert_info['certificate'], ...)
    return status == 'OK'

My patch was modifying dns_update.py replacing the include from ssl_certificates import get_ssl_certificates, check_certificate to from ssl_certificates import get_ssl_certificates, check_certificate, get_domain_ssl_files and minimally modifying the function is_domain_cert_signed_and_valid as follows:

def is_domain_cert_signed_and_valid(domain, env):
	# Get all certificates known to the system.
	all_certs = get_ssl_certificates(env)

	# Use the SMART, wildcard-aware function to find the correct certificate
	# for this domain, just like the admin panel does.
	# We set allow_missing_cert=True so it returns None instead of the default
	# cert if no match is found.
	cert = get_domain_ssl_files(domain, all_certs, env, allow_missing_cert=True, use_main_cert=False)

	# The rest of the logic remains the same.
	if not cert: return False # no certificate provisioned
	cert_status = check_certificate(domain, cert['certificate'], cert['private-key'])
	return cert_status[0] == 'OK'

Edit: I can confirm that the current patch is tested and working, and would appreciate if someone had a quick look at this bug and hopefully integrate a permanent fix for it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions