Acme_certificate: authz.raise_error 'Status is "{status}" and not "valid"'

Summary

When trying to validate a TLS wildcard certificate with Let’s Encrypt in phase II, I get this strange error despite the fact that the challenge is available in the DNS server.

Issue Type

Bug Report

Component Name

acme_certificate

Ansible Version

$ ansible --version
ansible [core 2.18.4]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/ansible/venv/lib/python3.12/site-packages/ansible
  ansible collection location = /opt
  executable location = /opt/ansible/venv/bin/ansible
  python version = 3.12.8 (main, Dec  4 2024, 12:15:27) [GCC 14.2.0] (/opt/ansible/venv/bin/python3.12)
  jinja version = 3.1.6
  libyaml = True

Community.general Version

$ ansible-galaxy collection list community.general
# /opt/ansible/venv/lib/python3.12/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 10.5.0 

# /opt/ansible_collections
Collection        Version
----------------- -------
community.general 10.5.0 

Configuration

$ ansible-config dump --only-changed
CACHE_PLUGIN(/etc/ansible/ansible.cfg) = redis
CACHE_PLUGIN_CONNECTION(/etc/ansible/ansible.cfg) = tls://localhost:<port>:0:<password>
CACHE_PLUGIN_TIMEOUT(/etc/ansible/ansible.cfg) = 259200
COLLECTIONS_PATHS(/etc/ansible/ansible.cfg) = ['/opt']
CONFIG_FILE() = /etc/ansible/ansible.cfg
DEFAULT_EXECUTABLE(/etc/ansible/ansible.cfg) = /bin/bash
DEFAULT_FORKS(/etc/ansible/ansible.cfg) = 1000
DEFAULT_GATHERING(/etc/ansible/ansible.cfg) = explicit
DEFAULT_HASH_BEHAVIOUR(/etc/ansible/ansible.cfg) = replace
DEFAULT_HOST_LIST(/etc/ansible/ansible.cfg) = ['/etc/ansible/hosts']
DEFAULT_LOAD_CALLBACK_PLUGINS(/etc/ansible/ansible.cfg) = True
DEFAULT_LOG_PATH(/etc/ansible/ansible.cfg) = /var/log/ansible.log
DEFAULT_PRIVATE_ROLE_VARS(/etc/ansible/ansible.cfg) = False
DEFAULT_ROLES_PATH(/etc/ansible/ansible.cfg) = ['/etc/ansible/roles'>
DEFAULT_TIMEOUT(/etc/ansible/ansible.cfg) = 240
DEFAULT_TRANSPORT(/etc/ansible/ansible.cfg) = ssh
EDITOR(env: EDITOR) = /usr/bin/gedit -s
ENABLE_TASK_DEBUGGER(/etc/ansible/ansible.cfg) = True
GALAXY_SERVER_LIST(/etc/ansible/ansible.cfg) = ['release_galaxy', 'test_galaxy', 'local_https', 'local_ssh', 'local_file', 'automation_hub']
HOST_KEY_CHECKING(/etc/ansible/ansible.cfg) = True
INJECT_FACTS_AS_VARS(/etc/ansible/ansible.cfg) = True
INTERPRETER_PYTHON(/etc/ansible/ansible.cfg) = /usr/bin/python3
INVENTORY_ENABLED(/etc/ansible/ansible.cfg) = ['ini', 'script', 'auto', 'yaml']
PERSISTENT_COMMAND_TIMEOUT(/etc/ansible/ansible.cfg) = 3599
PERSISTENT_CONNECT_RETRY_TIMEOUT(/etc/ansible/ansible.cfg) = 300
PERSISTENT_CONNECT_TIMEOUT(/etc/ansible/ansible.cfg) = 3600
RETRY_FILES_ENABLED(/etc/ansible/ansible.cfg) = False
SHOW_CUSTOM_STATS(/etc/ansible/ansible.cfg) = True

OS / Environment

Ubuntu 24.10
Python 3.12.8

Steps to Reproduce

- name: Validating a TLS Wildcard domain server Certificate from Let’s Encrypt CA - Phase II
  acme_certificate:
        account_email: "<user>@sdxlive.com"
        account_key_content: null
        account_key_passphrase: null
        account_key_src: "lets_encrypt_rsakey.pem"
        account_uri: null
        acme_directory: "https://acme-v02.api.letsencrypt.org/directory"
        acme_version: 2
        agreement: null
        chain_dest: "sdxlive.com.intermediate.crt"
        challenge: "dns-01"
        csr: "sdxlive.com.csr"
        csr_content: null
        data: {
            account_uri: "https://acme-v02.api.letsencrypt.org/acme/acct/<account_value>"
            authorizations: {
                *.sdxlive.com: {
                    challenges: [
                            {
                            status: "pending"
                            token: "<token>"
                            type: "dns-01"
                            url: "https://acme-v02.api.letsencrypt.org/acme/chall/<account_value>/<value1>/<value2>"
                            }
                        ]
                    expires: "2025-04-08T13:17:05Z"
                    identifier: {
                        type: "dns"
                        value: "sdxlive.com"
                        }
                    status: "pending"
                    uri: "https://acme-v02.api.letsencrypt.org/acme/authz/<account_value>/<value1>"
                    wildcard: true
                    }
                sdxlive.com: {
                    challenges: [
                            {
                            status: "pending"
                            token: "<token>"
                            type: "dns-01"
                            url: "https://acme-v02.api.letsencrypt.org/acme/chall/<account_value>/<value3>/<value4>"
                            }
                            {
                            status: "pending"
                            token: "<token>"
                            type: "http-01"
                            url: "https://acme-v02.api.letsencrypt.org/acme/chall/<account_value>/<value3>/<value5>"
                            }
                            {
                            status: "pending"
                            token: "<token>"
                            type: "tls-alpn-01"
                            url: "https://acme-v02.api.letsencrypt.org/acme/chall/<account_value>/<value3>/7TgUhg"
                            }
                        ]
                    expires: "2025-04-08T13:17:05Z"
                    identifier: {
                        type: "dns"
                        value: "sdxlive.com"
                        }
                    status: "pending"
                    uri: "https://acme-v02.api.letsencrypt.org/acme/authz/<account_value>/<value3>"
                    }
                }
            cert_days: 31
            challenge_data: {
                *.sdxlive.com: {
                    dns-01: {
                        record: "_acme-challenge.sdxlive.com"
                        resource: "_acme-challenge"
                        resource_value: "CdBzTL-h79q107qbJ_FkpRCt5kOvhsbyOlKR4E5NIF0"
                        }
                    }
                sdxlive.com: {
                    dns-01: {
                        record: "_acme-challenge.sdxlive.com"
                        resource: "_acme-challenge"
                        resource_value: "<resource_value>-I"
                        }
                    http-01: {
                        resource: ".well-known/acme-challenge/<token>"
                        resource_value: "<token>.<token2>"
                        }
                    tls-alpn-01: {
                        resource: "sdxlive.com"
                        resource_original: "dns:sdxlive.com"
                        resource_value: "<resource_value>+I="
                        }
                    }
                }
            challenge_data_dns: {
                _acme-challenge.sdxlive.com: [
                    CdBzTL-h79q107qbJ_FkpRCt5kOvhsbyOlKR4E5NIF0"
                    <resource_value>-I"
                    ]
                }
            changed: true
            failed: false
            finalize_uri: "https://acme-v02.api.letsencrypt.org/acme/finalize/<account_value>/<account_value2>"
            order_uri: "https://acme-v02.api.letsencrypt.org/acme/order/<account_value>/<account_value2>"
            }
        deactivate_authzs: false
        dest: "sdxlive.com.crt"
        force: true
        fullchain_dest: "sdxlive.com.fullchain.crt"
        include_renewal_cert_id: "never"
        modify_account: true
        order_creation_error_strategy: "auto"
        order_creation_max_retries: 3
        profile: null
        remaining_days: 91
        request_timeout: 10
        retrieve_all_alternates: false
        select_chain: null
        select_crypto_backend: "cryptography"
        terms_agreed: true
        validate_certs: true

Expected Results

Validated certificate from Let’s Encrypt

Actual Results

The full traceback is:
  File "/tmp/ansible_acme_certificate_payload_pequ5udn/ansible_acme_certificate_payload.zip/ansible_collections/community/crypto/plugins/modules/acme_certificate.py", line 976, in main
    client.get_certificate()
  File "/tmp/ansible_acme_certificate_payload_pequ5udn/ansible_acme_certificate_payload.zip/ansible_collections/community/crypto/plugins/modules/acme_certificate.py", line 858, in get_certificate
    authz.raise_error('Status is "{status}" and not "valid"'.format(status=authz.status), module=self.module)
  File "/tmp/ansible_acme_certificate_payload_pequ5udn/ansible_acme_certificate_payload.zip/ansible_collections/community/crypto/plugins/module_utils/acme/challenges.py", line 249, in raise_error
    raise ACMEProtocolException(
fatal: ...: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
                Cf. above
        }
    },
    "msg": "Failed to validate challenge for dns:*.sdxlive.com: Status is \"invalid\" and not \"valid\". Challenge dns-01: Error urn:ietf:params:acme:error:unauthorized: \"No TXT record found at _acme-challenge.sdxlive.com\".",
    "other": {
        "authorization": {
            "challenges": [
                {
                    "error": {
                        "detail": "No TXT record found at _acme-challenge.sdxlive.com",
                        "status": 403,
                        "type": "urn:ietf:params:acme:error:unauthorized"
                    },
                    "status": "invalid",
                    "token": "<token>",
                    "type": "dns-01",
                    "url": "https://acme-v02.api.letsencrypt.org/acme/chall/<value1>/<value2><value3>",
                    "validated": "2025-04-01T13:21:42Z"
                }
            ],
            "expires": "2025-04-08T13:17:05Z",
            "identifier": {
                "type": "dns",
                "value": "sdxlive.com"
            },
            "status": "invalid",
            "uri": "https://acme-v02.api.letsencrypt.org/acme/authz/<value1>/<value2>",
            "wildcard": true
        },
        "identifier": "dns:*.sdxlive.com"
    }
}

Proof

Everyone can check that the challenge value is available at dns1.sdxlive.com using the public Unbound DNS checker which closely replicates the ISRG setup:

Query results for TXT _acme-challenge.sdxlive.com

Response:
;; opcode: QUERY, status: NOERROR, id: 929
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version 0; flags: do; udp: 1232

;; QUESTION SECTION:
;_acme-challenge.sdxlive.com.	IN	 TXT

;; ANSWER SECTION:
_acme-challenge.sdxlive.com.	0	IN	TXT	"mUNgiFtYbqntKlRadroTILE5KW9iH98hmTEt1RSV2-I"
_acme-challenge.sdxlive.com.	0	IN	TXT	"CdBzTL-h79q107qbJ_FkpRCt5kOvhsbyOlKR4E5NIF0"
_acme-challenge.sdxlive.com.	0	IN	RRSIG	TXT 13 3 60 20250417083537 20250403121937 49706 sdxlive.com. YG5OKU7Kne5KJM6hKYxVB6/HsP2wWBY5v6IqCWXGCBNsdEWV92Sq2qqQBnH85Bzvyr+PPiBD3xsIhIVIy/GUkA==

Code of Conduct

  • I agree to follow the Ansible Code of Conduct

community-general acme

I’ve written an answer in the issue in the community.crypto repository.

Solved here.