Repository: ansible/ansible

Issue:
letsencrypt: account_key from vault
##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
letsencrypt

##### ANSIBLE VERSION
```
ansible 2.4.0.0
```

##### CONFIGURATION

##### OS / ENVIRONMENT

##### SUMMARY
letsencrypt module accept Let's Encrypt `account_key` parameter to be a regular file.
That keeps a team from using Ansible vault for sharing the private key / and ability to renew a SSL certificate.

##### STEPS TO REPRODUCE
Give role and vault key to a trustable team member.

##### EXPECTED RESULTS
Running the playbook would renew the certificate

##### ACTUAL RESULTS
The absence of the account_key file keeps the renewal from working.



Discussion:
cc @mgruener
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I kind of like this feature request. @drzraf do you like to work on that?
I'm sorry, I fear I don't have to time to do that.
(I guess there are also maintainer decisions like "use a new parameter
or not" or "change parameter semantic", ... ...)


Resolving patch:
diff --git a/lib/ansible/modules/web_infrastructure/letsencrypt.py b/lib/ansible/modules/web_infrastructure/letsencrypt.py
--- a/lib/ansible/modules/web_infrastructure/letsencrypt.py
+++ b/lib/ansible/modules/web_infrastructure/letsencrypt.py
@@ -41,11 +41,19 @@
   - "python >= 2.6"
   - openssl
 options:
-  account_key:
+  account_key_src:
     description:
-      - "File containing the Let's Encrypt account RSA key."
+      - "Path to a file containing the Let's Encrypt account RSA key."
       - "Can be created with C(openssl rsa ...)."
-    required: true
+      - "Mutually exclusive with C(account_key_content)."
+      - "Required if C(account_key_content) is not used."
+    aliases: [ account_key ]
+  account_key_content:
+    description:
+      - "Content of the Let's Encrypt account RSA key."
+      - "Mutually exclusive with C(account_key_src)."
+      - "Required if C(account_key_src) is not used."
+    version_added: "2.5"
   account_email:
     description:
       - "The email address associated with this account."
@@ -98,8 +106,23 @@
 '''
 
 EXAMPLES = '''
-- letsencrypt:
-    account_key: /etc/pki/cert/private/account.key
+- name: Create a challenge for sample.com using a account key from a variable.
+  letsencrypt:
+    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
+
+- name: Create a challenge for sample.com using a account key from hashi vault.
+  letsencrypt:
+    account_key_content: "{{ lookup('hashi_vault', 'secret=secret/account_private_key:value') }}"
+    csr: /etc/pki/cert/csr/sample.com.csr
+    dest: /etc/httpd/ssl/sample.com.crt
+  register: sample_com_challenge
+
+- name: Create a challenge for sample.com using a account key file.
+  letsencrypt:
+    account_key_src: /etc/pki/cert/private/account.key
     csr: /etc/pki/cert/csr/sample.com.csr
     dest: /etc/httpd/ssl/sample.com.crt
   register: sample_com_challenge
@@ -112,8 +135,9 @@
 #     content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}"
 #     when: sample_com_challenge is changed
 
-- letsencrypt:
-    account_key: /etc/pki/cert/private/account.key
+- name: Let the challenge be validated and retrieve the cert
+  letsencrypt:
+    account_key_src: /etc/pki/cert/private/account.key
     csr: /etc/pki/cert/csr/sample.com.csr
     dest: /etc/httpd/ssl/sample.com.crt
     data: "{{ sample_com_challenge }}"
@@ -321,7 +345,10 @@ class ACMEAccount(object):
 
     def __init__(self, module):
         self.module = module
-        self.key = module.params['account_key']
+        self.agreement = module.params['agreement']
+        # account_key path and content are mutually exclusive
+        self.key = module.params['account_key_src']
+        self.key_content = module.params['account_key_content']
         self.email = module.params['account_email']
         self.data = module.params['data']
         self.directory = ACMEDirectory(module)
@@ -333,11 +360,20 @@ def __init__(self, module):
         self._authz_list_uri = None
         self._certs_list_uri = None
 
-        if not os.path.exists(self.key):
-            module.fail_json(msg="Account key %s not found" % (self.key))
-
         self._openssl_bin = module.get_bin_path('openssl', True)
 
+        # Create a key file from content, key (path) and key content are mutually exclusive
+        if self.key_content is not None:
+            _, tmpsrc = tempfile.mkstemp()
+            f = open(tmpsrc, 'wb')
+            try:
+                f.write(self.key_content)
+                self.key = tmpsrc
+            except Exception as err:
+                os.remove(tmpsrc)
+                module.fail_json(msg="failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
+            f.close()
+
         pub_hex, pub_exp = self._parse_account_key(self.key)
         self.jws_header = {
             "alg": "RS256",
@@ -801,11 +837,16 @@ def get_certificate(self):
                 self.cert_days = get_cert_days(self.module, self.dest)
                 self.changed = True
 
+        # Clean up temporary account key file
+        if self.module.params['account_key_content'] is not None:
+            os.remove(self.account.key)
+
 
 def main():
     module = AnsibleModule(
         argument_spec=dict(
-            account_key=dict(required=True, type='path'),
+            account_key_src=dict(type='path', aliases=['account_key']),
+            account_key_content=dict(type='str'),
             account_email=dict(required=False, default=None, type='str'),
             acme_directory=dict(required=False, default='https://acme-staging.api.letsencrypt.org/directory', type='str'),
             agreement=dict(required=False, type='str'),
@@ -816,6 +857,12 @@ def main():
             dest=dict(required=True, aliases=['cert'], type='path'),
             remaining_days=dict(required=False, default=10, type='int'),
         ),
+        required_one_of=(
+            ['account_key_src', 'account_key_content'],
+        ),
+        mutually_exclusive=(
+            ['account_key_src', 'account_key_content'],
+        ),
         supports_check_mode=True,
     )
 


Test patch: