community.crypto.acme_certificate (2.18.0) — module

Create SSL/TLS certificates with the ACME protocol

Authors: Michael Gruener (@mgruener)

Install collection

Install with ansible-galaxy collection install community.crypto:==2.18.0


Add to requirements.yml

  collections:
    - name: community.crypto
      version: 2.18.0

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/) or L(Buypass,https://www.buypass.com/). The current implementation supports the V(http-01), V(dns-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) the necessary dns record has 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 provided for this module.

The module includes experimental support for IP identifiers according to the L(RFC 8738,https://www.rfc-editor.org/rfc/rfc8738.html).


Requirements

Usage examples

  • Success
    Steampunk Spotter scan finished with no errors, warnings or hints.
### Example with HTTP challenge ###

- name: Create a challenge for sample.com using a account key from a variable.
  community.crypto.acme_certificate:
    account_key_content: "{{ account_private_key }}"
    csr: /etc/pki/cert/csr/sample.com.csr
    dest: /etc/httpd/ssl/sample.com.crt
  register: sample_com_challenge
  • Success
    Steampunk Spotter scan finished with no errors, warnings or hints.
# Alternative first step:
- name: Create a challenge for sample.com using a account key from Hashi Vault.
  community.crypto.acme_certificate:
    account_key_content: >-
      {{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/account_private_key:value') }}
    csr: /etc/pki/cert/csr/sample.com.csr
    fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
  register: sample_com_challenge
  • Success
    Steampunk Spotter scan finished with no errors, warnings or hints.
# Alternative first step:
- name: Create a challenge for sample.com using a account key file.
  community.crypto.acme_certificate:
    account_key_src: /etc/pki/cert/private/account.key
    csr_content: "{{ lookup('file', '/etc/pki/cert/csr/sample.com.csr') }}"
    dest: /etc/httpd/ssl/sample.com.crt
    fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
  register: sample_com_challenge
  • Success
    Steampunk Spotter scan finished with no errors, warnings or hints.
# perform the necessary steps to fulfill the challenge
# for example:
#
# - name: Copy http-01 challenge for sample.com
#   ansible.builtin.copy:
#     dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }}
#     content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}"
#   when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge['challenge_data']
#
# Alternative way:
#
# - name: Copy http-01 challenges
#   ansible.builtin.copy:
#     dest: /var/www/{{ item.key }}/{{ item.value['http-01']['resource'] }}
#     content: "{{ item.value['http-01']['resource_value'] }}"
#   loop: "{{ sample_com_challenge.challenge_data | dict2items }}"
#   when: sample_com_challenge is changed

- name: Let the challenge be validated and retrieve the cert and intermediate certificate
  community.crypto.acme_certificate:
    account_key_src: /etc/pki/cert/private/account.key
    csr: /etc/pki/cert/csr/sample.com.csr
    dest: /etc/httpd/ssl/sample.com.crt
    fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
    chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
    data: "{{ sample_com_challenge }}"
  • Success
    Steampunk Spotter scan finished with no errors, warnings or hints.
### Example with DNS challenge against production ACME server ###

- name: Create a challenge for sample.com using a account key file.
  community.crypto.acme_certificate:
    account_key_src: /etc/pki/cert/private/account.key
    account_email: myself@sample.com
    src: /etc/pki/cert/csr/sample.com.csr
    cert: /etc/httpd/ssl/sample.com.crt
    challenge: dns-01
    acme_directory: https://acme-v01.api.letsencrypt.org/directory
    # Renew if the certificate is at least 30 days old
    remaining_days: 60
  register: sample_com_challenge
  • Success
    Steampunk Spotter scan finished with no errors, warnings or hints.
# perform the necessary steps to fulfill the challenge
# for example:
#
# - name: Create DNS record for sample.com dns-01 challenge
#   community.aws.route53:
#     zone: sample.com
#     record: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].record }}"
#     type: TXT
#     ttl: 60
#     state: present
#     wait: true
#     # Note: route53 requires TXT entries to be enclosed in quotes
#     value: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].resource_value | regex_replace('^(.*)$', '\"\\1\"') }}"
#   when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge.challenge_data
#
# Alternative way:
#
# - name: Create DNS records for dns-01 challenges
#   community.aws.route53:
#     zone: sample.com
#     record: "{{ item.key }}"
#     type: TXT
#     ttl: 60
#     state: present
#     wait: true
#     # Note: item.value is a list of TXT entries, and route53
#     # requires every entry to be enclosed in quotes
#     value: "{{ item.value | map('regex_replace', '^(.*)$', '\"\\1\"' ) | list }}"
#   loop: "{{ sample_com_challenge.challenge_data_dns | dict2items }}"
#   when: sample_com_challenge is changed

- name: Let the challenge be validated and retrieve the cert and intermediate certificate
  community.crypto.acme_certificate:
    account_key_src: /etc/pki/cert/private/account.key
    account_email: myself@sample.com
    src: /etc/pki/cert/csr/sample.com.csr
    cert: /etc/httpd/ssl/sample.com.crt
    fullchain: /etc/httpd/ssl/sample.com-fullchain.crt
    chain: /etc/httpd/ssl/sample.com-intermediate.crt
    challenge: dns-01
    acme_directory: https://acme-v01.api.letsencrypt.org/directory
    remaining_days: 60
    data: "{{ sample_com_challenge }}"
  when: sample_com_challenge is changed
  • Success
    Steampunk Spotter scan finished with no errors, warnings or hints.
# Alternative second step:
- name: Let the challenge be validated and retrieve the cert and intermediate certificate
  community.crypto.acme_certificate:
    account_key_src: /etc/pki/cert/private/account.key
    account_email: myself@sample.com
    src: /etc/pki/cert/csr/sample.com.csr
    cert: /etc/httpd/ssl/sample.com.crt
    fullchain: /etc/httpd/ssl/sample.com-fullchain.crt
    chain: /etc/httpd/ssl/sample.com-intermediate.crt
    challenge: tls-alpn-01
    remaining_days: 60
    data: "{{ sample_com_challenge }}"
    # We use Let's Encrypt's ACME v2 endpoint
    acme_directory: https://acme-v02.api.letsencrypt.org/directory
    acme_version: 2
    # The following makes sure that if a chain with /CN=DST Root CA X3 in its issuer is provided
    # as an alternative, it will be selected. These are the roots cross-signed by IdenTrust.
    # As long as Let's Encrypt provides alternate chains with the cross-signed root(s) when
    # switching to their own ISRG Root X1 root, this will use the chain ending with a cross-signed
    # root. This chain is more compatible with older TLS clients.
    select_chain:
      - test_certificates: last
        issuer:
          CN: DST Root CA X3
          O: Digital Signature Trust Co.
  when: sample_com_challenge is changed

Inputs

    
csr:
    aliases:
    - src
    description:
    - File containing the CSR for the new certificate.
    - Can be created with M(community.crypto.openssl_csr) or C(openssl req ...).
    - The CSR may contain multiple Subject Alternate Names, but each one will lead to
      an individual challenge that must be fulfilled for the CSR to be signed.
    - 'I(Note): the private key used to create the CSR I(must not) be the account key.
      This is a bad idea from a security point of view, and the CA should not accept the
      CSR. The ACME server should return an error in this case.'
    - Precisely one of O(csr) or O(csr_content) must be specified.
    type: path

data:
    description:
    - The data to validate ongoing challenges. This must be specified for the second run
      of the module only.
    - The value that must be used here will be provided by a previous use of this module.
      See the examples for more details.
    - Note that for ACME v2, only the C(order_uri) entry of O(data) will be used. For
      ACME v1, O(data) must be non-empty to indicate the second stage is active; all needed
      data will be taken from the CSR.
    - 'I(Note): the O(data) option was marked as C(no_log) up to Ansible 2.5. From Ansible
      2.6 on, it is no longer marked this way as it causes error messages to be come unusable,
      and O(data) does not contain any information which can be used without having access
      to the account key or which are not public anyway.'
    type: dict

dest:
    aliases:
    - cert
    description:
    - The destination file for the certificate.
    - Required if O(fullchain_dest) is not specified.
    type: path

force:
    default: false
    description:
    - Enforces the execution of the challenge and validation, even if an existing certificate
      is still valid for more than O(remaining_days).
    - This is especially helpful when having an updated CSR, for example with additional
      domains for which a new certificate is desired.
    type: bool

agreement:
    description:
    - URI to a terms of service document you agree to when using the ACME v1 service at
      O(acme_directory).
    - Default is latest gathered from O(acme_directory) URL.
    - This option will only be used when O(acme_version) is 1.
    type: str

challenge:
    choices:
    - http-01
    - dns-01
    - tls-alpn-01
    - no challenge
    default: http-01
    description:
    - The challenge to be performed.
    - 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.
    type: str

chain_dest:
    aliases:
    - chain
    description:
    - If specified, the intermediate certificate will be written to this file.
    type: path

account_uri:
    description:
    - If specified, assumes that the account URI is as given. If the account key does
      not match this account, or an account with this URI does not exist, the module fails.
    type: str

csr_content:
    description:
    - Content of the CSR for the new certificate.
    - Can be created with M(community.crypto.openssl_csr_pipe) or C(openssl req ...).
    - The CSR may contain multiple Subject Alternate Names, but each one will lead to
      an individual challenge that must be fulfilled for the CSR to be signed.
    - 'I(Note): the private key used to create the CSR I(must not) be the account key.
      This is a bad idea from a security point of view, and the CA should not accept the
      CSR. The ACME server should return an error in this case.'
    - Precisely one of O(csr) or O(csr_content) must be specified.
    type: str
    version_added: 1.2.0
    version_added_collection: community.crypto

acme_version:
    choices:
    - 1
    - 2
    description:
    - The ACME version of the endpoint.
    - Must be V(1) for the classic Let's Encrypt and Buypass ACME endpoints, or V(2) for
      standardized ACME v2 endpoints.
    - The value V(1) is deprecated since community.crypto 2.0.0 and will be removed from
      community.crypto 3.0.0.
    required: true
    type: int

select_chain:
    description:
    - Allows to specify criteria by which an (alternate) trust chain can be selected.
    - The list of criteria will be processed one by one until a chain is found matching
      a criterium. If such a chain is found, it will be used by the module instead of
      the default chain.
    - If a criterium matches multiple chains, the first one matching will be returned.
      The order is determined by the ordering of the C(Link) headers returned by the ACME
      server and might not be deterministic.
    - Every criterium can consist of multiple different conditions, like O(select_chain[].issuer)
      and O(select_chain[].subject). For the criterium to match a chain, all conditions
      must apply to the same certificate in the chain.
    - This option can only be used with the C(cryptography) backend.
    elements: dict
    suboptions:
      authority_key_identifier:
        description:
        - Checks for the AuthorityKeyIdentifier extension. This is an identifier based
          on the private key of the issuer of the intermediate certificate.
        - The identifier must be of the form V(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10).
        type: str
      issuer:
        description:
        - Allows to specify parts of the issuer of a certificate in the chain must have
          to be selected.
        - If O(select_chain[].issuer) is empty, any certificate will match.
        - 'An example value would be V({"commonName": "My Preferred CA Root"}).'
        type: dict
      subject:
        description:
        - Allows to specify parts of the subject of a certificate in the chain must have
          to be selected.
        - If O(select_chain[].subject) is empty, any certificate will match.
        - 'An example value would be V({"CN": "My Preferred CA Intermediate"})'
        type: dict
      subject_key_identifier:
        description:
        - Checks for the SubjectKeyIdentifier extension. This is an identifier based on
          the private key of the intermediate certificate.
        - The identifier must be of the form V(A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1).
        type: str
      test_certificates:
        choices:
        - first
        - last
        - all
        default: all
        description:
        - Determines which certificates in the chain will be tested.
        - V(all) tests all certificates in the chain (excluding the leaf, which is identical
          in all chains).
        - V(first) only tests the first certificate in the chain, that is the one which
          signed the leaf.
        - V(last) only tests the last certificate in the chain, that is the one furthest
          away from the leaf. Its issuer is the root certificate of this chain.
        type: str
    type: list
    version_added: 1.0.0
    version_added_collection: community.crypto

terms_agreed:
    default: false
    description:
    - Boolean indicating whether you agree to the terms of service document.
    - ACME servers can require this to be true.
    - This option will only be used when O(acme_version) is not 1.
    type: bool

account_email:
    description:
    - The email address associated with this account.
    - It will be used for certificate expiration warnings.
    - Note that when O(modify_account) is not set to V(false) and you also used the M(community.crypto.acme_account)
      module to specify more than one contact for your account, this module will update
      your account and restrict it to the (at most one) contact email address specified
      here.
    type: str

acme_directory:
    description:
    - The ACME directory to use. This is the entry point URL to access the ACME CA server
      API.
    - For safety reasons the default is set to the Let's Encrypt staging server (for the
      ACME v1 protocol). This will create technically correct, but untrusted certificates.
    - 'For Let''s Encrypt, all staging endpoints can be found here: U(https://letsencrypt.org/docs/staging-environment/).
      For Buypass, all endpoints can be found here: U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)'
    - For B(Let's Encrypt), the production directory URL for ACME v2 is U(https://acme-v02.api.letsencrypt.org/directory).
    - For B(Buypass), the production directory URL for ACME v2 and v1 is U(https://api.buypass.com/acme/directory).
    - For B(ZeroSSL), the production directory URL for ACME v2 is U(https://acme.zerossl.com/v2/DV90).
    - For B(Sectigo), the production directory URL for ACME v2 is U(https://acme-qa.secure.trust-provider.com/v2/DV).
    - The notes for this module contain a list of ACME services this module has been tested
      against.
    required: true
    type: str

fullchain_dest:
    aliases:
    - fullchain
    description:
    - The destination file for the full chain (that is, a certificate followed by chain
      of intermediate certificates).
    - Required if O(dest) is not specified.
    type: path

modify_account:
    default: true
    description:
    - Boolean indicating whether the module should create the account if necessary, and
      update its contact data.
    - Set to V(false) if you want to use the M(community.crypto.acme_account) module to
      manage your account instead, and to avoid accidental creation of a new account using
      an old key if you changed the account key with M(community.crypto.acme_account).
    - If set to V(false), O(terms_agreed) and O(account_email) are ignored.
    type: bool

remaining_days:
    default: 10
    description:
    - The number of days the certificate must have left being valid. If RV(cert_days)
      < O(remaining_days), then it will be renewed. If the certificate is not renewed,
      module return values will not include RV(challenge_data).
    - To make sure that the certificate is renewed in any case, you can use the O(force)
      option.
    type: int

validate_certs:
    default: true
    description:
    - Whether calls to the ACME directory will validate TLS certificates.
    - B(Warning:) Should B(only ever) be set to V(false) for testing purposes, for example
      when testing against a local Pebble server.
    type: bool

account_key_src:
    aliases:
    - account_key
    description:
    - Path to a file containing the ACME account RSA or Elliptic Curve key.
    - 'Private keys can be created with the M(community.crypto.openssl_privatekey) or
      M(community.crypto.openssl_privatekey_pipe) modules. If the requisite (cryptography)
      is not available, keys can also be created directly with the C(openssl) command
      line tool: RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys
      can be created with C(openssl ecparam -genkey ...). Any other tool creating private
      keys in PEM format can be used as well.'
    - Mutually exclusive with O(account_key_content).
    - Required if O(account_key_content) is not used.
    type: path

request_timeout:
    default: 10
    description:
    - The time Ansible should wait for a response from the ACME API.
    - This timeout is applied to all HTTP(S) requests (HEAD, GET, POST).
    type: int
    version_added: 2.3.0
    version_added_collection: community.crypto

deactivate_authzs:
    default: false
    description:
    - Deactivate authentication objects (authz) after issuing a certificate, or when issuing
      the certificate failed.
    - Authentication objects are bound to an account key and remain valid for a certain
      amount of time, and can be used to issue certificates without having to re-authenticate
      the domain. This can be a security concern.
    type: bool

account_key_content:
    description:
    - Content of the ACME account RSA or Elliptic Curve key.
    - Mutually exclusive with O(account_key_src).
    - Required if O(account_key_src) is not used.
    - "B(Warning:) the content will be written into a temporary file, which will be deleted\
      \ by Ansible when the module completes. Since this is an important private key \u2014\
      \ it can be used to change the account key, or to revoke your certificates without\
      \ knowing their private keys \u2014, this might not be acceptable."
    - In case C(cryptography) is used, the content is not written into a temporary file.
      It can still happen that it is written to disk by Ansible in the process of moving
      the module with its argument to the node where it is executed.
    type: str

select_crypto_backend:
    choices:
    - auto
    - cryptography
    - openssl
    default: auto
    description:
    - Determines which crypto backend to use.
    - The default choice is V(auto), which tries to use C(cryptography) if available,
      and falls back to C(openssl).
    - If set to V(openssl), will try to use the C(openssl) binary.
    - If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/)
      library.
    type: str

account_key_passphrase:
    description:
    - Phassphrase to use to decode the account key.
    - B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography)
      backend.
    type: str
    version_added: 1.6.0
    version_added_collection: community.crypto

retrieve_all_alternates:
    default: false
    description:
    - When set to V(true), will retrieve all alternate trust chains offered by the ACME
      CA. These will not be written to disk, but will be returned together with the main
      chain as RV(all_chains). See the documentation for the RV(all_chains) return value
      for details.
    type: bool

Outputs

account_uri:
  description: ACME account URI.
  returned: changed
  type: str
all_chains:
  contains:
    cert:
      description:
      - The leaf certificate itself, in PEM format.
      returned: always
      type: str
    chain:
      description:
      - The certificate chain, excluding the root, as concatenated PEM certificates.
      returned: always
      type: str
    full_chain:
      description:
      - The certificate chain, excluding the root, but including the leaf certificate,
        as concatenated PEM certificates.
      returned: always
      type: str
  description:
  - When O(retrieve_all_alternates) is set to V(true), the module will query the ACME
    server for alternate chains. This return value will contain a list of all chains
    returned, the first entry being the main chain returned by the server.
  - See L(Section 7.4.2 of RFC8555,https://tools.ietf.org/html/rfc8555#section-7.4.2)
    for details.
  elements: dict
  returned: when certificate was retrieved and O(retrieve_all_alternates) is set to
    V(true)
  type: list
authorizations:
  description:
  - ACME authorization data.
  - Maps an identifier to ACME authorization objects. See U(https://tools.ietf.org/html/rfc8555#section-7.1.4).
  returned: changed
  sample:
    example.com:
      challenges:
      - status: valid
        token: A5b1C3d2E9f8G7h6
        type: http-01
        url: https://example.org/acme/challenge/12345
        validated: '2022-08-01T01:01:02.34Z'
      expires: '2022-08-04T01:02:03.45Z'
      identifier:
        type: dns
        value: example.com
      status: valid
      wildcard: false
  type: dict
cert_days:
  description: The number of days the certificate remains valid.
  returned: success
  type: int
challenge_data:
  contains:
    record:
      description: The full DNS record's name for the challenge.
      returned: changed and challenge is V(dns-01)
      sample: _acme-challenge.example.com
      type: str
    resource:
      description: The challenge resource that must be created for validation.
      returned: changed
      sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
      type: str
    resource_original:
      description:
      - The original challenge resource including type identifier for V(tls-alpn-01)
        challenges.
      returned: changed and O(challenge) is V(tls-alpn-01)
      sample: DNS:example.com
      type: str
    resource_value:
      description:
      - The value the resource has to produce for the validation.
      - For V(http-01) and V(dns-01) challenges, the value can be used as-is.
      - For V(tls-alpn-01) challenges, note that this return value contains a Base64
        encoded version of the correct 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
      sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
      type: str
  description:
  - Per identifier / challenge type challenge data.
  - Since Ansible 2.8.5, only challenges which are not yet valid are returned.
  elements: dict
  returned: changed
  type: list
challenge_data_dns:
  description:
  - List of TXT values per DNS record, in case challenge is V(dns-01).
  - Since Ansible 2.8.5, only challenges which are not yet valid are returned.
  returned: changed
  type: dict
finalization_uri:
  description: ACME finalization URI.
  returned: changed
  type: str
order_uri:
  description: ACME order URI.
  returned: changed
  type: str

See also