|
|
@ -37,6 +37,7 @@ description:
|
|
|
|
- "Although the defaults are chosen so that the module can be used with
|
|
|
|
- "Although the defaults are chosen so that the module can be used with
|
|
|
|
the Let's Encrypt CA, the module can be used with any service using the ACME
|
|
|
|
the Let's Encrypt CA, the module can be used with any service using the ACME
|
|
|
|
v1 or v2 protocol."
|
|
|
|
v1 or v2 protocol."
|
|
|
|
|
|
|
|
- "At least one of C(dest) and C(fullchain_dest) must be specified."
|
|
|
|
requirements:
|
|
|
|
requirements:
|
|
|
|
- "python >= 2.6"
|
|
|
|
- "python >= 2.6"
|
|
|
|
- openssl
|
|
|
|
- openssl
|
|
|
@ -119,13 +120,17 @@ options:
|
|
|
|
- "The value that must be used here will be provided by a previous use
|
|
|
|
- "The value that must be used here will be provided by a previous use
|
|
|
|
of this module."
|
|
|
|
of this module."
|
|
|
|
dest:
|
|
|
|
dest:
|
|
|
|
description: The destination file for the certificate.
|
|
|
|
description:
|
|
|
|
required: true
|
|
|
|
- "The destination file for the certificate."
|
|
|
|
|
|
|
|
- "Required if C(fullchain_dest) is not specified."
|
|
|
|
aliases: ['cert']
|
|
|
|
aliases: ['cert']
|
|
|
|
fullchain:
|
|
|
|
fullchain_dest:
|
|
|
|
description: Include the full certificate chain in the destination file.
|
|
|
|
description:
|
|
|
|
default: false
|
|
|
|
- "The destination file for the full chain (i.e. certificate followed
|
|
|
|
|
|
|
|
by chain of intermediate certificates)."
|
|
|
|
|
|
|
|
- "Required if C(dest) is not specified."
|
|
|
|
version_added: 2.5
|
|
|
|
version_added: 2.5
|
|
|
|
|
|
|
|
aliases: ['fullchain']
|
|
|
|
remaining_days:
|
|
|
|
remaining_days:
|
|
|
|
description:
|
|
|
|
description:
|
|
|
|
- "The number of days the certificate must have left being valid.
|
|
|
|
- "The number of days the certificate must have left being valid.
|
|
|
@ -150,7 +155,7 @@ EXAMPLES = '''
|
|
|
|
letsencrypt:
|
|
|
|
letsencrypt:
|
|
|
|
account_key_content: "{{ lookup('hashi_vault', 'secret=secret/account_private_key:value') }}"
|
|
|
|
account_key_content: "{{ lookup('hashi_vault', 'secret=secret/account_private_key:value') }}"
|
|
|
|
csr: /etc/pki/cert/csr/sample.com.csr
|
|
|
|
csr: /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
|
|
|
|
register: sample_com_challenge
|
|
|
|
|
|
|
|
|
|
|
|
# Alternative first step:
|
|
|
|
# Alternative first step:
|
|
|
@ -159,6 +164,7 @@ EXAMPLES = '''
|
|
|
|
account_key_src: /etc/pki/cert/private/account.key
|
|
|
|
account_key_src: /etc/pki/cert/private/account.key
|
|
|
|
csr: /etc/pki/cert/csr/sample.com.csr
|
|
|
|
csr: /etc/pki/cert/csr/sample.com.csr
|
|
|
|
dest: /etc/httpd/ssl/sample.com.crt
|
|
|
|
dest: /etc/httpd/ssl/sample.com.crt
|
|
|
|
|
|
|
|
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
|
|
|
|
register: sample_com_challenge
|
|
|
|
register: sample_com_challenge
|
|
|
|
|
|
|
|
|
|
|
|
# perform the necessary steps to fulfill the challenge
|
|
|
|
# perform the necessary steps to fulfill the challenge
|
|
|
@ -174,6 +180,7 @@ EXAMPLES = '''
|
|
|
|
account_key_src: /etc/pki/cert/private/account.key
|
|
|
|
account_key_src: /etc/pki/cert/private/account.key
|
|
|
|
csr: /etc/pki/cert/csr/sample.com.csr
|
|
|
|
csr: /etc/pki/cert/csr/sample.com.csr
|
|
|
|
dest: /etc/httpd/ssl/sample.com.crt
|
|
|
|
dest: /etc/httpd/ssl/sample.com.crt
|
|
|
|
|
|
|
|
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
|
|
|
|
data: "{{ sample_com_challenge }}"
|
|
|
|
data: "{{ sample_com_challenge }}"
|
|
|
|
|
|
|
|
|
|
|
|
### Example with DNS challenge against production ACME server ###
|
|
|
|
### Example with DNS challenge against production ACME server ###
|
|
|
@ -311,7 +318,8 @@ def simple_get(module, url):
|
|
|
|
def get_cert_days(module, cert_file):
|
|
|
|
def get_cert_days(module, cert_file):
|
|
|
|
'''
|
|
|
|
'''
|
|
|
|
Return the days the certificate in cert_file remains valid and -1
|
|
|
|
Return the days the certificate in cert_file remains valid and -1
|
|
|
|
if the file was not found.
|
|
|
|
if the file was not found. If cert_file contains more than one
|
|
|
|
|
|
|
|
certificate, only the first one will be considered.
|
|
|
|
'''
|
|
|
|
'''
|
|
|
|
if not os.path.exists(cert_file):
|
|
|
|
if not os.path.exists(cert_file):
|
|
|
|
return -1
|
|
|
|
return -1
|
|
|
@ -731,7 +739,8 @@ class ACMEClient(object):
|
|
|
|
self.version = module.params['acme_version']
|
|
|
|
self.version = module.params['acme_version']
|
|
|
|
self.challenge = module.params['challenge']
|
|
|
|
self.challenge = module.params['challenge']
|
|
|
|
self.csr = module.params['csr']
|
|
|
|
self.csr = module.params['csr']
|
|
|
|
self.dest = module.params['dest']
|
|
|
|
self.dest = module.get('dest')
|
|
|
|
|
|
|
|
self.fullchain_dest = module.get('fullchain_dest')
|
|
|
|
self.account = ACMEAccount(module)
|
|
|
|
self.account = ACMEAccount(module)
|
|
|
|
self.directory = self.account.directory
|
|
|
|
self.directory = self.account.directory
|
|
|
|
self.data = module.params['data']
|
|
|
|
self.data = module.params['data']
|
|
|
@ -1066,7 +1075,7 @@ class ACMEClient(object):
|
|
|
|
Only do this if a destination file was provided and if all authorizations
|
|
|
|
Only do this if a destination file was provided and if all authorizations
|
|
|
|
for the domains of the csr are valid. No Return value.
|
|
|
|
for the domains of the csr are valid. No Return value.
|
|
|
|
'''
|
|
|
|
'''
|
|
|
|
if self.dest is None:
|
|
|
|
if self.dest is None and self.fullchain_dest is None:
|
|
|
|
return
|
|
|
|
return
|
|
|
|
if self.finalize_uri is None and self.version != 1:
|
|
|
|
if self.finalize_uri is None and self.version != 1:
|
|
|
|
return
|
|
|
|
return
|
|
|
@ -1085,14 +1094,15 @@ class ACMEClient(object):
|
|
|
|
pem_cert = cert['cert']
|
|
|
|
pem_cert = cert['cert']
|
|
|
|
|
|
|
|
|
|
|
|
chain = [link for link in cert.get('chain', [])]
|
|
|
|
chain = [link for link in cert.get('chain', [])]
|
|
|
|
if chain:
|
|
|
|
|
|
|
|
if self.module.params['fullchain']:
|
|
|
|
|
|
|
|
pem_cert += "\n".join(chain)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if write_file(self.module, self.dest, pem_cert.encode('utf8')):
|
|
|
|
if self.dest and write_file(self.module, self.dest, pem_cert.encode('utf8')):
|
|
|
|
self.cert_days = get_cert_days(self.module, self.dest)
|
|
|
|
self.cert_days = get_cert_days(self.module, self.dest)
|
|
|
|
self.changed = True
|
|
|
|
self.changed = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.fullchain_dest and write_file(self.module, self.fullchain_dest, (pem_cert + "\n".join(chain)).encode('utf8')):
|
|
|
|
|
|
|
|
self.cert_days = get_cert_days(self.module, self.fullchain_dest)
|
|
|
|
|
|
|
|
self.changed = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
def main():
|
|
|
|
module = AnsibleModule(
|
|
|
|
module = AnsibleModule(
|
|
|
@ -1107,12 +1117,13 @@ def main():
|
|
|
|
challenge=dict(required=False, default='http-01', choices=['http-01', 'dns-01', 'tls-sni-02'], type='str'),
|
|
|
|
challenge=dict(required=False, default='http-01', choices=['http-01', 'dns-01', 'tls-sni-02'], type='str'),
|
|
|
|
csr=dict(required=True, aliases=['src'], type='path'),
|
|
|
|
csr=dict(required=True, aliases=['src'], type='path'),
|
|
|
|
data=dict(required=False, no_log=True, default=None, type='dict'),
|
|
|
|
data=dict(required=False, no_log=True, default=None, type='dict'),
|
|
|
|
fullchain=dict(required=False, default=False, type='bool'),
|
|
|
|
dest=dict(aliases=['cert'], type='path'),
|
|
|
|
dest=dict(required=True, aliases=['cert'], type='path'),
|
|
|
|
fullchain_dest=dict(aliases=['fullchain'], type='path'),
|
|
|
|
remaining_days=dict(required=False, default=10, type='int'),
|
|
|
|
remaining_days=dict(required=False, default=10, type='int'),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
required_one_of=(
|
|
|
|
required_one_of=(
|
|
|
|
['account_key_src', 'account_key_content'],
|
|
|
|
['account_key_src', 'account_key_content'],
|
|
|
|
|
|
|
|
['dest', 'fullchain_dest'],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
mutually_exclusive=(
|
|
|
|
mutually_exclusive=(
|
|
|
|
['account_key_src', 'account_key_content'],
|
|
|
|
['account_key_src', 'account_key_content'],
|
|
|
@ -1124,7 +1135,10 @@ def main():
|
|
|
|
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
|
|
|
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
|
|
|
locale.setlocale(locale.LC_ALL, 'C')
|
|
|
|
locale.setlocale(locale.LC_ALL, 'C')
|
|
|
|
|
|
|
|
|
|
|
|
cert_days = get_cert_days(module, module.params['dest'])
|
|
|
|
if module.params.get('dest'):
|
|
|
|
|
|
|
|
cert_days = get_cert_days(module, module.params['dest'])
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
cert_days = get_cert_days(module, module.params['fullchain_dest'])
|
|
|
|
if cert_days < module.params['remaining_days']:
|
|
|
|
if cert_days < module.params['remaining_days']:
|
|
|
|
# If checkmode is active, base the changed state solely on the status
|
|
|
|
# If checkmode is active, base the changed state solely on the status
|
|
|
|
# of the certificate file as all other actions (accessing an account, checking
|
|
|
|
# of the certificate file as all other actions (accessing an account, checking
|
|
|
|