mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
ACME: add dns-persist-01 support (#997)
* Add dns-persist-01 DNS TXT record filters. * Refactor parsing and joining CAA issue-values out. * Add basic tests. * Fix bug and add integration tests for filters. * Add dns-persist-01 support to ACME modules. * Add changelog fragment.
This commit is contained in:
177
plugins/filter/acme_dns_persist_record.py
Normal file
177
plugins/filter/acme_dns_persist_record.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# Copyright (c) 2026, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: acme_dns_persist_record
|
||||
short_description: Craft a DNS record for ACME C(dns-persist-01) challenges
|
||||
author: Felix Fontein (@felixfontein)
|
||||
version_added: 3.2.0
|
||||
description:
|
||||
- Craft the content for a ACME C(dns-persist-01) DNS TXT record V(_validation-persist.<domain>).
|
||||
- This filter conforms to the L(acme-dns-persist draft 01, https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-01.html).
|
||||
Note that the supported draft version can change at any time,
|
||||
and changes will only be considered breaking once the draft reached RFC status.
|
||||
options:
|
||||
_input:
|
||||
description:
|
||||
- The issuer domain name.
|
||||
type: string
|
||||
required: true
|
||||
account_uri:
|
||||
description:
|
||||
- The ACME account URI.
|
||||
type: string
|
||||
required: true
|
||||
policy:
|
||||
description:
|
||||
- The validation scope.
|
||||
type: string
|
||||
choices:
|
||||
wildcard:
|
||||
- If this value is present, the CA MAY consider this validation sufficient for issuing certificates
|
||||
for the validated FQDN, for specific subdomains of the validated FQDN
|
||||
(as covered by wildcard scope or specific subdomain validation rules),
|
||||
and for wildcard certificates (for example V(*.example.com)). See
|
||||
L(Section 5, https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-01.html#wildcard-certificate-validation)
|
||||
and L(Section 6, https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-01.html#subdomain-certificate-validation)
|
||||
of the L(acme-dns-persist draft 01, https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-01.html).
|
||||
persist_until:
|
||||
description:
|
||||
- Until when the record is valid.
|
||||
- Can be specified as a UNIX time stamp (integer), as a Python datetime object,
|
||||
or as a relative time or absolute timestamp specified as a string.
|
||||
- Times specified as strings will always be interpreted as UTC.
|
||||
Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
|
||||
type: any
|
||||
seealso:
|
||||
- module: community.crypto.acme_certificate
|
||||
- module: community.crypto.acme_certificate_order_create
|
||||
- module: community.crypto.acme_certificate_order_validate
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
---
|
||||
- name: Create _validation-persist.<domain> TXT record contents
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{
|
||||
'letsencrypt.org' | community.crypto.acme_dns_persist_record(
|
||||
account_uri='https://acme-v02.api.letsencrypt.org/acme/acct/1234',
|
||||
policy='wildcard',
|
||||
persist_until='+1w',
|
||||
)
|
||||
}}
|
||||
|
||||
- name: Create _validation-persist.<domain> TXT record for example.com
|
||||
community.dns.hetzner_dns_record_set:
|
||||
prefix: _validation-persist
|
||||
zone_name: example.com
|
||||
value:
|
||||
- >-
|
||||
{{
|
||||
'letsencrypt.org' | community.crypto.acme_dns_persist_record(
|
||||
account_uri='https://acme-v02.api.letsencrypt.org/acme/acct/4321',
|
||||
persist_until='20190331202428Z',
|
||||
)
|
||||
}}
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_value:
|
||||
description:
|
||||
- The content for the V(_validation-persist.<domain>) TXT record.
|
||||
type: string
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import typing as t
|
||||
from collections.abc import Callable
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils._caa import (
|
||||
join_issue_value,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._time import (
|
||||
get_epoch_seconds,
|
||||
get_now_datetime,
|
||||
get_relative_time_option,
|
||||
)
|
||||
|
||||
|
||||
def acme_dns_persist_record(
|
||||
domain_issuer_name: t.Any,
|
||||
*,
|
||||
account_uri: t.Any,
|
||||
policy: t.Any | None = None,
|
||||
persist_until: t.Any | None = None,
|
||||
) -> str:
|
||||
if not isinstance(domain_issuer_name, str):
|
||||
raise AnsibleFilterError(
|
||||
"The input for the community.crypto.acme_dns_persist_record filter"
|
||||
f" must be a string; got {type(domain_issuer_name)} instead"
|
||||
)
|
||||
if not isinstance(account_uri, str):
|
||||
raise AnsibleFilterError(
|
||||
"The account_uri parameter for the community.crypto.acme_dns_persist_record filter"
|
||||
f" must be a string; got {type(account_uri)} instead"
|
||||
)
|
||||
valid_policies = ("wildcard",)
|
||||
if policy is not None and policy not in valid_policies:
|
||||
choices = ", ".join(f'"{vp}"' for vp in valid_policies)
|
||||
raise AnsibleFilterError(
|
||||
"The policy parameter for the community.crypto.acme_dns_persist_record filter"
|
||||
f" must be one of {choices}; got {policy!r} instead"
|
||||
)
|
||||
if persist_until is not None:
|
||||
if isinstance(persist_until, str):
|
||||
try:
|
||||
persist_until = get_relative_time_option(
|
||||
persist_until,
|
||||
input_name="persist_until",
|
||||
with_timezone=True,
|
||||
now=get_now_datetime(with_timezone=True),
|
||||
)
|
||||
except OpenSSLObjectError as exc:
|
||||
raise AnsibleFilterError(
|
||||
"Error parsing persist_until parameter for the community.crypto.acme_dns_persist_record filter:"
|
||||
f" {exc}"
|
||||
) from None
|
||||
if isinstance(persist_until, int) and not isinstance(persist_until, bool):
|
||||
pass
|
||||
elif isinstance(persist_until, datetime.datetime):
|
||||
persist_until = int(get_epoch_seconds(persist_until))
|
||||
else:
|
||||
raise AnsibleFilterError(
|
||||
"The persist_until parameter for the community.crypto.acme_dns_persist_record filter"
|
||||
f" must be an integer, a string, or a datetime object; got {type(persist_until)} instead"
|
||||
)
|
||||
|
||||
parts = [("accounturi", account_uri)]
|
||||
if policy is not None:
|
||||
parts.append(("policy", policy))
|
||||
if persist_until is not None:
|
||||
parts.append(("persistUntil", str(persist_until)))
|
||||
try:
|
||||
return join_issue_value(domain_issuer_name, parts)
|
||||
except ValueError as exc:
|
||||
raise AnsibleFilterError(
|
||||
"Error composing result for the community.crypto.acme_dns_persist_record filter:"
|
||||
f" {exc}"
|
||||
) from exc
|
||||
|
||||
|
||||
class FilterModule:
|
||||
"""Ansible jinja2 filters"""
|
||||
|
||||
def filters(self) -> dict[str, Callable]:
|
||||
return {
|
||||
"acme_dns_persist_record": acme_dns_persist_record,
|
||||
}
|
||||
163
plugins/filter/acme_dns_persist_record_parse.py
Normal file
163
plugins/filter/acme_dns_persist_record_parse.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# Copyright (c) 2026, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: acme_dns_persist_record_parse
|
||||
short_description: Parse a DNS record for ACME C(dns-persist-01) challenges
|
||||
author: Felix Fontein (@felixfontein)
|
||||
version_added: 3.2.0
|
||||
description:
|
||||
- Parse the content for a ACME C(dns-persist-01) DNS TXT record V(_validation-persist.<domain>).
|
||||
- This filter conforms to the L(acme-dns-persist draft 01, https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-01.html).
|
||||
Note that the supported draft version can change at any time,
|
||||
and changes will only be considered breaking once the draft reached RFC status.
|
||||
options:
|
||||
_input:
|
||||
description:
|
||||
- The DNS TXT record entry.
|
||||
type: string
|
||||
required: true
|
||||
seealso:
|
||||
- module: community.crypto.acme_certificate
|
||||
- module: community.crypto.acme_certificate_order_create
|
||||
- module: community.crypto.acme_certificate_order_validate
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
---
|
||||
- name: Create _validation-persist.<domain> TXT record contents
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ record | community.crypto.acme_dns_persist_record_parse }}"
|
||||
var:
|
||||
record: >-
|
||||
letsencrypt.org;
|
||||
accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234;
|
||||
policy=wildcard;
|
||||
persistUntil=1774813004
|
||||
|
||||
- name: Create _validation-persist.<domain> TXT record for example.com
|
||||
community.dns.hetzner_dns_record_set:
|
||||
prefix: _validation-persist
|
||||
zone_name: example.com
|
||||
value:
|
||||
- >-
|
||||
{{
|
||||
'letsencrypt.org' | community.crypto.acme_dns_persist_record(
|
||||
account_uri='https://acme-v02.api.letsencrypt.org/acme/acct/4321',
|
||||
persist_until='20190331202428Z',
|
||||
)
|
||||
}}
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_value:
|
||||
description:
|
||||
- The content for the V(_validation-persist.<domain>) TXT record.
|
||||
type: dictionary
|
||||
contains:
|
||||
issuer_domain_name:
|
||||
type: string
|
||||
description:
|
||||
- The issuer domain name.
|
||||
sample: letsencrypt.org
|
||||
account_uri:
|
||||
type: string
|
||||
description:
|
||||
- The ACME account URI.
|
||||
policy:
|
||||
description:
|
||||
- The validation scope.
|
||||
- Is V(null) if not present.
|
||||
type: string
|
||||
persist_until:
|
||||
description:
|
||||
- Until when the record is valid.
|
||||
- This is a UNIX timestamp, that is the number of seconds since January 1st, 1970, in UTC.
|
||||
- Is V(null) if V(persistUntil) is not present.
|
||||
type: string
|
||||
persist_until_str:
|
||||
description:
|
||||
- A ASN.1 string representation of RV(_value.persist_until).
|
||||
- Is V(null) if V(persistUntil) is not present.
|
||||
type: string
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
from collections.abc import Callable
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils._caa import (
|
||||
parse_issue_value,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._time import (
|
||||
from_epoch_seconds,
|
||||
)
|
||||
|
||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
||||
|
||||
|
||||
def acme_dns_persist_record_parse(
|
||||
record_value: t.Any,
|
||||
) -> dict[str, t.Any]:
|
||||
if not isinstance(record_value, str):
|
||||
raise AnsibleFilterError(
|
||||
"The input for the community.crypto.acme_dns_persist_record_parse filter"
|
||||
f" must be a string; got {type(record_value)} instead"
|
||||
)
|
||||
try:
|
||||
domain_name, pairs = parse_issue_value(record_value)
|
||||
except ValueError as exc:
|
||||
raise AnsibleFilterError(
|
||||
"community.crypto.acme_dns_persist_record_parse filter could not parse"
|
||||
f" value: {exc}"
|
||||
) from exc
|
||||
values = dict(pairs)
|
||||
if domain_name is None:
|
||||
raise AnsibleFilterError(
|
||||
"community.crypto.acme_dns_persist_record_parse filter: domain name not present"
|
||||
)
|
||||
try:
|
||||
account_uri = values.pop("accounturi")
|
||||
except KeyError:
|
||||
raise AnsibleFilterError(
|
||||
"community.crypto.acme_dns_persist_record_parse filter: cannot find account URI"
|
||||
) from None
|
||||
policy = values.pop("policy", None)
|
||||
if policy is not None:
|
||||
policy = policy.lower()
|
||||
# TODO unknown policy
|
||||
persist_until_v = values.pop("persistUntil", None)
|
||||
persist_until: int | None = None
|
||||
persist_until_str: str | None = None
|
||||
if persist_until_v is not None:
|
||||
try:
|
||||
persist_until = int(persist_until_v)
|
||||
except ValueError as exc:
|
||||
raise AnsibleFilterError(
|
||||
f"community.crypto.acme_dns_persist_record_parse filter: error when parsing persistUntil: {exc}"
|
||||
) from None
|
||||
persist_until_str = from_epoch_seconds(
|
||||
persist_until, with_timezone=True
|
||||
).strftime(TIMESTAMP_FORMAT)
|
||||
result: dict[str, t.Any] = {
|
||||
"issuer_domain_name": domain_name,
|
||||
"account_uri": account_uri,
|
||||
"policy": policy,
|
||||
"persist_until": persist_until,
|
||||
"persist_until_str": persist_until_str,
|
||||
}
|
||||
# TODO values not empty
|
||||
return result
|
||||
|
||||
|
||||
class FilterModule:
|
||||
"""Ansible jinja2 filters"""
|
||||
|
||||
def filters(self) -> dict[str, Callable]:
|
||||
return {
|
||||
"acme_dns_persist_record_parse": acme_dns_persist_record_parse,
|
||||
}
|
||||
@@ -26,6 +26,9 @@ from ansible_collections.community.crypto.plugins.module_utils._acme.errors impo
|
||||
from ansible_collections.community.crypto.plugins.module_utils._acme.utils import (
|
||||
nopad_b64,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._caa import (
|
||||
_check_domain_name,
|
||||
)
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from ansible.module_utils.basic import AnsibleModule # pragma: no cover
|
||||
@@ -104,6 +107,52 @@ class Challenge:
|
||||
def get_validation_data(
|
||||
self, *, client: ACMEClient, identifier_type: str, identifier: str
|
||||
) -> dict[str, t.Any] | None:
|
||||
if self.type == "dns-persist-01":
|
||||
# https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-01.html#section-3.1
|
||||
account_uri = self.data.get("accounturi")
|
||||
issuer_domain_names = self.data.get("issuer-domain-names")
|
||||
if account_uri is None:
|
||||
# In version 00 of the draft, accounturi isn't present.
|
||||
# Since that's what Pebble currently implements,
|
||||
# let's fake the value if we have it.
|
||||
# (https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-00.html#section-6)
|
||||
account_uri = client.account_uri
|
||||
if (
|
||||
not isinstance(account_uri, str)
|
||||
or not isinstance(issuer_domain_names, list)
|
||||
or not all(isinstance(idn, str) for idn in issuer_domain_names)
|
||||
):
|
||||
return None
|
||||
if client.account_uri is not None and account_uri != client.account_uri:
|
||||
# While the RFC doesn't demand this, I think it's a bad sign if the account URIs disagree.
|
||||
# Better err on the side of caution...
|
||||
client.module.warn(
|
||||
f"The dns-persist-01 challenge for DNS:{identifier} has account URI {account_uri!r},"
|
||||
f" while the client is has account URI {client.account_uri}. Ignoring malformed challenge."
|
||||
)
|
||||
return None
|
||||
if not (1 <= len(issuer_domain_names) <= 10):
|
||||
client.module.warn(
|
||||
f"The dns-persist-01 challenge for DNS:{identifier} has {len(issuer_domain_names)}"
|
||||
" issuer domain names, which is not in [1, 10]. Ignoring malformed challenge."
|
||||
)
|
||||
return None
|
||||
for idn in issuer_domain_names:
|
||||
try:
|
||||
_check_domain_name(idn)
|
||||
if idn != idn.lower() or len(idn) > 253:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
client.module.warn(
|
||||
f"The dns-persist-01 challenge for DNS:{identifier} has an invalid"
|
||||
f" issuer domain name {idn!r}. Ignoring malformed challenge."
|
||||
)
|
||||
return None
|
||||
return {
|
||||
"account_uri": account_uri,
|
||||
"issuer_domain_names": issuer_domain_names,
|
||||
}
|
||||
|
||||
if self.token is None:
|
||||
return None
|
||||
|
||||
|
||||
98
plugins/module_utils/_caa.py
Normal file
98
plugins/module_utils/_caa.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
|
||||
# Do not use this from other collections or standalone plugins/modules!
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_VALUE_RE = re.compile("^[\x21-\x3a\x3c-\x7e]*$")
|
||||
_LABEL_RE = re.compile("^[0-9a-zA-Z][0-9a-zA-Z-]*$")
|
||||
|
||||
|
||||
def _check_value(value: str) -> None:
|
||||
if not _VALUE_RE.match(value):
|
||||
raise ValueError(f"Invalid value {value!r}")
|
||||
|
||||
|
||||
def _check_label(label: str, what: str) -> None:
|
||||
if not _LABEL_RE.match(label):
|
||||
raise ValueError(f"Invalid {what} {label!r}")
|
||||
|
||||
|
||||
def _check_domain_name(value: str) -> None:
|
||||
for p in value.split("."):
|
||||
_check_label(p, "label")
|
||||
|
||||
|
||||
def parse_issue_value(
|
||||
value: str, *, check_for_duplicates: bool = True, strict: bool = True
|
||||
) -> tuple[str | None, list[tuple[str, str]]]:
|
||||
"""
|
||||
Given a CAA issue property, parses it according to the syntax defined in RFC 8659.
|
||||
|
||||
More precisely, see https://www.rfc-editor.org/rfc/rfc8659.html#section-4.2.
|
||||
|
||||
If ``check_for_duplicates == True``, duplicate tags are reported as an error.
|
||||
If ``strict == True``, invalid characters are reported as an error.
|
||||
"""
|
||||
parts = [v.strip(" \t") for v in value.split(";")]
|
||||
if len(parts) > 1 and not parts[-1]:
|
||||
del parts[-1]
|
||||
domain_name = parts[0] or None
|
||||
if domain_name is not None and strict:
|
||||
_check_domain_name(domain_name)
|
||||
pairs = []
|
||||
previous_tags: set[str] = set()
|
||||
for part in parts[1:]:
|
||||
pieces = part.split("=", 1)
|
||||
if len(pieces) != 2:
|
||||
raise ValueError(f"{part!r} is not of the form tag=value")
|
||||
tag, value = pieces[0].rstrip(" \t"), pieces[1].lstrip(" \t")
|
||||
if strict:
|
||||
_check_label(tag, "tag")
|
||||
_check_value(value)
|
||||
pairs.append((tag, value))
|
||||
if check_for_duplicates:
|
||||
if tag in previous_tags:
|
||||
raise ValueError(f"Tag {tag!r} appears multiple times")
|
||||
previous_tags.add(tag)
|
||||
return domain_name, pairs
|
||||
|
||||
|
||||
def join_issue_value(
|
||||
domain_name: str | None,
|
||||
pairs: list[tuple[str, str]],
|
||||
*,
|
||||
check_for_duplicates: bool = True,
|
||||
strict: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Given a domain name and a list of tag-value pairs, joins them according
|
||||
to the syntax defined in RFC 8659.
|
||||
|
||||
More precisely, see https://www.rfc-editor.org/rfc/rfc8659.html#section-4.2.
|
||||
|
||||
If ``check_for_duplicates == True``, duplicate tags are reported as an error.
|
||||
If ``strict == True``, invalid characters are reported as an error.
|
||||
"""
|
||||
if domain_name is not None and strict:
|
||||
_check_domain_name(domain_name)
|
||||
parts = [domain_name or ""]
|
||||
previous_tags: set[str] = set()
|
||||
for tag, value in pairs:
|
||||
if strict:
|
||||
_check_label(tag, "tag")
|
||||
_check_value(value)
|
||||
parts.append(f"{tag}={value}")
|
||||
if check_for_duplicates:
|
||||
if tag in previous_tags:
|
||||
raise ValueError(f"Tag {tag!r} appears multiple times")
|
||||
previous_tags.add(tag)
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
__all__ = ("parse_issue_value",)
|
||||
@@ -12,12 +12,13 @@ short_description: Create SSL/TLS certificates with the ACME protocol
|
||||
description:
|
||||
- Create and renew SSL/TLS certificates with a CA supporting the L(ACME protocol,https://tools.ietf.org/html/rfc8555), such
|
||||
as L(Let's Encrypt,https://letsencrypt.org/).
|
||||
The current implementation supports the V(http-01), V(dns-01), V(dns-account-01) and V(tls-alpn-01) challenges.
|
||||
The current implementation supports the V(http-01), V(dns-01), V(dns-account-01), V(dns-persist-01), and V(tls-alpn-01) challenges.
|
||||
- To use this module, it has to be executed twice. Either as two different tasks in the same run or during two runs. Note
|
||||
that the output of the first run needs to be recorded and passed to the second run as the module argument O(data).
|
||||
- Between these two tasks you have to fulfill the required steps for the chosen challenge by whatever means necessary. For
|
||||
V(http-01) that means creating the necessary challenge file on the destination webserver. For V(dns-01) and V(dns-account-01)
|
||||
the necessary DNS records have to be created. For V(tls-alpn-01) the necessary certificate has to be created and served.
|
||||
- Between these two tasks you have to fulfill the required steps for the chosen challenge by whatever means necessary.
|
||||
For V(http-01) that means creating the necessary challenge file on the destination webserver.
|
||||
For V(dns-01), V(dns-account-01), and V(dns-persist-01) the necessary DNS records have to be created.
|
||||
For V(tls-alpn-01) the necessary certificate has to be created and served.
|
||||
It is I(not) the responsibility of this module to perform these steps.
|
||||
- For details on how to fulfill these challenges, you might have to read through L(the main ACME specification,https://tools.ietf.org/html/rfc8555#section-8)
|
||||
and the L(TLS-ALPN-01 specification,https://www.rfc-editor.org/rfc/rfc8737.html#section-3). Also, consider the examples
|
||||
@@ -27,6 +28,10 @@ description:
|
||||
L(acme-dns-account-label draft 02, https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-account-label-02).
|
||||
Note that the supported draft version can change at any time,
|
||||
and changes will only be considered breaking once the draft reached RFC status.
|
||||
- The module supports the V(dns-persist-01) challenge type according to
|
||||
L(acme-dns-persist draft 01, https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-01.html).
|
||||
Note that the supported draft version can change at any time,
|
||||
and changes will only be considered breaking once the draft reached RFC status.
|
||||
notes:
|
||||
- At least one of O(dest) and O(fullchain_dest) must be specified.
|
||||
- This module includes basic account management functionality. If you want to have more control over your ACME account,
|
||||
@@ -119,13 +124,14 @@ options:
|
||||
- If set to V(no challenge), no challenge will be used. This is necessary for some private CAs which use External Account
|
||||
Binding and other means of validating certificate assurance. For example, an account could be allowed to issue certificates
|
||||
for C(foo.example.com) without any further validation for a certain period of time.
|
||||
- Support for V(dns-account-01) has been added in community.crypto 3.2.0.
|
||||
- Support for V(dns-account-01) and V(dns-persist-01) has been added in community.crypto 3.2.0.
|
||||
type: str
|
||||
default: http-01
|
||||
choices:
|
||||
- http-01
|
||||
- dns-01
|
||||
- dns-account-01
|
||||
- dns-persist-01
|
||||
- tls-alpn-01
|
||||
- no challenge
|
||||
csr:
|
||||
@@ -473,14 +479,14 @@ challenge_data:
|
||||
description:
|
||||
- Data for every challenge type.
|
||||
- The keys in this dictionary are the challenge types. C(challenge-type) is a placeholder used in the documentation.
|
||||
Possible keys are V(http-01), V(dns-01), V(dns-account-01), and V(tls-alpn-01).
|
||||
Possible keys are V(http-01), V(dns-01), V(dns-account-01), V(dns-persist-01), and V(tls-alpn-01).
|
||||
- Note that the keys are not valid Jinja2 identifiers.
|
||||
returned: changed
|
||||
type: dict
|
||||
contains:
|
||||
resource:
|
||||
description: The challenge resource that must be created for validation.
|
||||
returned: changed
|
||||
returned: changed and challenge is not V(dns-persist-01)
|
||||
type: str
|
||||
sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
|
||||
resource_original:
|
||||
@@ -497,7 +503,7 @@ challenge_data:
|
||||
binary blob which has to be put into the acmeValidation x509 extension; see U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3)
|
||||
for details. To do this, you might need the P(ansible.builtin.b64decode#filter) Jinja filter to extract
|
||||
the binary blob from this return value.
|
||||
returned: changed
|
||||
returned: changed and challenge is not V(dns-persist-01)
|
||||
type: str
|
||||
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
|
||||
record:
|
||||
@@ -505,6 +511,20 @@ challenge_data:
|
||||
returned: changed and challenge is V(dns-01) or V(dns-account-01)
|
||||
type: str
|
||||
sample: _acme-challenge.example.com
|
||||
account_uri:
|
||||
description:
|
||||
- The account URI that must be mentioned in the DNS TXT record.
|
||||
returned: changed and challenge is V(dns-persist-01)
|
||||
type: str
|
||||
sample: https://ca.example/acct/123
|
||||
issuer_domain_names:
|
||||
description:
|
||||
- One of the issuer domain names must be mentioned in the DNS TXT record.
|
||||
returned: changed and challenge is V(dns-persist-01)
|
||||
type: list
|
||||
elements: str
|
||||
sample:
|
||||
- letsencrypt.org
|
||||
challenge_data_dns:
|
||||
description:
|
||||
- List of TXT values per DNS record, in case challenge is V(dns-01) or V(dns-account-01).
|
||||
@@ -987,6 +1007,7 @@ def main() -> t.NoReturn:
|
||||
"http-01",
|
||||
"dns-01",
|
||||
"dns-account-01",
|
||||
"dns-persist-01",
|
||||
"tls-alpn-01",
|
||||
NO_CHALLENGE,
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ description:
|
||||
Authority such as L(Let's Encrypt,https://letsencrypt.org/).
|
||||
This module does not support ACME v1, the original version of the ACME protocol
|
||||
before standardization.
|
||||
- The current implementation supports the V(http-01), V(dns-01), V(dns-account-01),
|
||||
- The current implementation supports the V(http-01), V(dns-01), V(dns-account-01), V(dns-persist-01),
|
||||
and V(tls-alpn-01) challenges.
|
||||
- This module needs to be used in conjunction with the
|
||||
M(community.crypto.acme_certificate_order_validate) and.
|
||||
@@ -29,9 +29,9 @@ description:
|
||||
- Between the call of this module and M(community.crypto.acme_certificate_order_finalize),
|
||||
you have to fulfill the required steps for the chosen challenge by whatever means necessary.
|
||||
For V(http-01) that means creating the necessary challenge file on the destination webserver.
|
||||
For V(dns-01) and V(dns-account-01) the necessary DNS records have to be created. For V(tls-alpn-01) the necessary
|
||||
certificate has to be created and served. It is I(not) the responsibility of this module to
|
||||
perform these steps.
|
||||
For V(dns-01), V(dns-account-01), and V(dns-persist-01) the necessary DNS records have to be created.
|
||||
For V(tls-alpn-01) the necessary certificate has to be created and served.
|
||||
It is I(not) the responsibility of this module to perform these steps.
|
||||
- For details on how to fulfill these challenges, you might have to read through
|
||||
L(the main ACME specification,https://tools.ietf.org/html/rfc8555#section-8)
|
||||
and the L(TLS-ALPN-01 specification,https://www.rfc-editor.org/rfc/rfc8737.html#section-3).
|
||||
@@ -42,6 +42,10 @@ description:
|
||||
L(acme-dns-account-label draft 02, https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-account-label-02).
|
||||
Note that the supported draft version can change at any time,
|
||||
and changes will only be considered breaking once the draft reached RFC status.
|
||||
- The module supports the V(dns-persist-01) challenge type according to
|
||||
L(acme-dns-persist draft 01, https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-01.html).
|
||||
Note that the supported draft version can change at any time,
|
||||
and changes will only be considered breaking once the draft reached RFC status.
|
||||
seealso:
|
||||
- module: community.crypto.acme_certificate_order_validate
|
||||
description: Validate pending authorizations of an ACME order.
|
||||
@@ -307,6 +311,7 @@ challenge_data:
|
||||
resource:
|
||||
description:
|
||||
- Always contains the string V(_acme-challenge).
|
||||
returned: success
|
||||
type: str
|
||||
sample: _acme-challenge
|
||||
resource_value:
|
||||
@@ -332,6 +337,7 @@ challenge_data:
|
||||
resource:
|
||||
description:
|
||||
- Always ends with the string V(._acme-challenge).
|
||||
returned: success
|
||||
type: str
|
||||
sample: _ujmmovf2vn55tgye._acme-challenge
|
||||
resource_value:
|
||||
@@ -345,6 +351,29 @@ challenge_data:
|
||||
returned: success
|
||||
type: str
|
||||
sample: _ujmmovf2vn55tgye._acme-challenge.example.com
|
||||
dns-persist-01:
|
||||
description:
|
||||
- Information for V(dns-persist-01) authorization.
|
||||
- A DNS TXT record needs to be created with the record name V(_validation-persist.<domain>).
|
||||
See the P(community.crypto.acme_dns_persist_record#filter) for how to create the record's content.
|
||||
returned: if the identifier supports V(dns-persist-01) authorization
|
||||
version_added: 3.2.0
|
||||
type: dict
|
||||
contains:
|
||||
account_uri:
|
||||
description:
|
||||
- The account URI that must be mentioned in the DNS TXT record.
|
||||
returned: success
|
||||
type: str
|
||||
sample: https://ca.example/acct/123
|
||||
issuer_domain_names:
|
||||
description:
|
||||
- One of the issuer domain names must be mentioned in the DNS TXT record.
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample:
|
||||
- letsencrypt.org
|
||||
tls-alpn-01:
|
||||
description:
|
||||
- Information for V(tls-alpn-01) authorization.
|
||||
|
||||
@@ -24,6 +24,10 @@ description:
|
||||
L(acme-dns-account-label draft 02, https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-account-label-02).
|
||||
Note that the supported draft version can change at any time,
|
||||
and changes will only be considered breaking once the draft reached RFC status.
|
||||
- The module supports the V(dns-persist-01) challenge type according to
|
||||
L(acme-dns-persist draft 01, https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-01.html).
|
||||
Note that the supported draft version can change at any time,
|
||||
and changes will only be considered breaking once the draft reached RFC status.
|
||||
seealso:
|
||||
- module: community.crypto.acme_certificate_order_create
|
||||
description: Create an ACME order.
|
||||
@@ -260,13 +264,14 @@ authorizations_by_identifier:
|
||||
type:
|
||||
description:
|
||||
- The type of challenge encoded in the object.
|
||||
- Support for V(dns-account-01) has been added in community.crypto 3.2.0.
|
||||
- Support for V(dns-account-01) and V(dns-persist-01) has been added in community.crypto 3.2.0.
|
||||
type: str
|
||||
returned: always
|
||||
choices:
|
||||
- http-01
|
||||
- dns-01
|
||||
- dns-account-01
|
||||
- dns-persist-01
|
||||
- tls-alpn-01
|
||||
url:
|
||||
description:
|
||||
|
||||
@@ -65,12 +65,13 @@ options:
|
||||
- In case of authorization reuse, or in case of CAs which use External Account Binding
|
||||
and other means of validating certificate assurance, it might not be necessary
|
||||
to provide this option.
|
||||
- Support for V(dns-account-01) has been added in community.crypto 3.2.0.
|
||||
- Support for V(dns-account-01) and V(dns-persist-01) has been added in community.crypto 3.2.0.
|
||||
type: str
|
||||
choices:
|
||||
- http-01
|
||||
- dns-01
|
||||
- dns-account-01
|
||||
- dns-persist-01
|
||||
- tls-alpn-01
|
||||
order_uri:
|
||||
description:
|
||||
@@ -250,7 +251,13 @@ def main() -> t.NoReturn:
|
||||
order_uri={"type": "str", "required": True},
|
||||
challenge={
|
||||
"type": "str",
|
||||
"choices": ["http-01", "dns-01", "dns-account-01", "tls-alpn-01"],
|
||||
"choices": [
|
||||
"http-01",
|
||||
"dns-01",
|
||||
"dns-account-01",
|
||||
"dns-persist-01",
|
||||
"tls-alpn-01",
|
||||
],
|
||||
},
|
||||
deactivate_authzs={"type": "bool", "default": True},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user