diff --git a/docs/docsite/rst/scenario_guides/guide_aci.rst b/docs/docsite/rst/scenario_guides/guide_aci.rst index 356a5e5623d..e71e39bd659 100644 --- a/docs/docsite/rst/scenario_guides/guide_aci.rst +++ b/docs/docsite/rst/scenario_guides/guide_aci.rst @@ -204,10 +204,14 @@ Every Ansible ACI module accepts the following parameters that influence the mod Password for ``username`` to log on to the APIC, using password-based authentication. private_key - Private key for ``username`` to log on to APIC, using signature-based authentication. *New in version 2.5* + Private key for ``username`` to log on to APIC, using signature-based authentication. + This could either be the raw private key content (include header/footer) or a file that stores the key content. + *New in version 2.5* certificate_name - Name of the certificate in the ACI Web GUI. (Defaults to ``private_key`` file base name) *New in version 2.5* + Name of the certificate in the ACI Web GUI. + This defaults to either the ``username`` value or the ``private_key`` file base name). + *New in version 2.5* timeout Timeout value for socket-level communication. @@ -367,11 +371,70 @@ You need the following parameters with your ACI module(s) for it to work: private_key: pki/admin.key certificate_name: admin # This could be left out ! +or you can use the private key content: + +.. code-block:: yaml + :emphasize-lines: 2,3 + + username: admin + private_key: | + -----BEGIN PRIVATE KEY----- + <> + -----END PRIVATE KEY----- + certificate_name: admin # This could be left out ! + + .. hint:: If you use a certificate name in ACI that matches the private key's basename, you can leave out the ``certificate_name`` parameter like the example above. + +Using Ansible Vault to encrypt the private key +`````````````````````````````````````````````` +.. versionadded:: 2.8 + +To start, encrypt the private key and give it a strong password. + +.. code-block:: bash + + ansible-vault encrypt admin.key + +Use a text editor to open the private-key. You should have an encrypted cert now. + +.. code-block:: bash + + $ANSIBLE_VAULT;1.1;AES256 + 56484318584354658465121889743213151843149454864654151618131547984132165489484654 + 45641818198456456489479874513215489484843614848456466655432455488484654848489498 + .... + +Copy and paste the new encrypted cert into your playbook as a new variable. + +.. code-block:: yaml + + private_key: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 56484318584354658465121889743213151843149454864654151618131547984132165489484654 + 45641818198456456489479874513215489484843614848456466655432455488484654848489498 + .... + +Use the new variable for the private_key: + +.. code-block:: yaml + + username: admin + private_key: "{{ private_key }}" + certificate_name: admin # This could be left out ! + +When running the playbook, use "--ask-vault-pass" to decrypt the private key. + +.. code-block:: bash + + ansible-playbook site.yaml --ask-vault-pass + + More information ```````````````` -Detailed information about Signature-based Authentication is available from `Cisco APIC Signature-Based Transactions `_. +- Detailed information about Signature-based Authentication is available from `Cisco APIC Signature-Based Transactions `_. +- More information on Ansible Vault can be found on the :ref:`Ansible Vault ` page. .. _aci_guide_rest: diff --git a/docs/docsite/rst/user_guide/vault.rst b/docs/docsite/rst/user_guide/vault.rst index cb9bb177182..edcbe67c27d 100644 --- a/docs/docsite/rst/user_guide/vault.rst +++ b/docs/docsite/rst/user_guide/vault.rst @@ -1,3 +1,5 @@ +.. _vault: + Ansible Vault ============= diff --git a/lib/ansible/module_utils/network/aci/aci.py b/lib/ansible/module_utils/network/aci/aci.py index 13f686bdd81..017099b27b3 100644 --- a/lib/ansible/module_utils/network/aci/aci.py +++ b/lib/ansible/module_utils/network/aci/aci.py @@ -69,7 +69,7 @@ def aci_argument_spec(): port=dict(type='int', required=False), username=dict(type='str', default='admin', aliases=['user']), password=dict(type='str', no_log=True), - private_key=dict(type='path', aliases=['cert_key']), # Beware, this is not the same as client_key ! + private_key=dict(type='str', aliases=['cert_key'], no_log=True), # Beware, this is not the same as client_key ! certificate_name=dict(type='str', aliases=['cert_name']), # Beware, this is not the same as client_cert ! output_level=dict(type='str', default='normal', choices=['debug', 'info', 'normal']), timeout=dict(type='int', default=30), @@ -226,16 +226,39 @@ class ACIModule(object): if payload is None: payload = '' - # Use the private key basename (without extension) as certificate_name - if self.params['certificate_name'] is None: - self.params['certificate_name'] = os.path.basename(os.path.splitext(self.params['private_key'])[0]) - - try: - with open(self.params['private_key'], 'r') as priv_key_fh: - private_key_content = priv_key_fh.read() - sig_key = load_privatekey(FILETYPE_PEM, private_key_content) - except Exception: - self.module.fail_json(msg='Cannot load private key %s' % self.params['private_key']) + # Check if we got a private key. This allows the use of vaulting the private key. + if self.params['private_key'].startswith('-----BEGIN PRIVATE KEY-----'): + try: + sig_key = load_privatekey(FILETYPE_PEM, self.params['private_key']) + except Exception: + self.module.fail_json(msg="Cannot load provided 'private_key' parameter.") + # Use the username as the certificate_name value + if self.params['certificate_name'] is None: + self.params['certificate_name'] = self.params['username'] + elif self.params['private_key'].startswith('-----BEGIN CERTIFICATE-----'): + self.module.fail_json(msg="Provided 'private_key' parameter value appears to be a certificate. Please correct.") + else: + # If we got a private key file, read from this file. + # NOTE: Avoid exposing any other credential as a filename in output... + if not os.path.exists(self.params['private_key']): + self.module.fail_json(msg="The provided private key file does not appear to exist. Is it a filename?") + try: + with open(self.params['private_key'], 'r') as fh: + private_key_content = fh.read() + except Exception: + self.module.fail_json(msg="Cannot open private key file '%s'." % self.params['private_key']) + if private_key_content.startswith('-----BEGIN PRIVATE KEY-----'): + try: + sig_key = load_privatekey(FILETYPE_PEM, private_key_content) + except Exception: + self.module.fail_json(msg="Cannot load private key file '%s'." % self.params['private_key']) + # Use the private key basename (without extension) as certificate_name + if self.params['certificate_name'] is None: + self.params['certificate_name'] = os.path.basename(os.path.splitext(self.params['private_key'])[0]) + elif private_key_content.startswith('-----BEGIN CERTIFICATE-----'): + self.module.fail_json(msg="Provided private key file %s appears to be a certificate. Please correct." % self.params['private_key']) + else: + self.module.fail_json(msg="Provided private key file '%s' does not appear to be a private key. Please correct." % self.params['private_key']) # NOTE: ACI documentation incorrectly adds a space between method and path sig_request = method + path + payload @@ -312,7 +335,7 @@ class ACIModule(object): self.url = '%(protocol)s://%(host)s/' % self.params + path.lstrip('/') # Sign and encode request as to APIC's wishes - if self.params['private_key'] is not None: + if not self.params['private_key']: self.cert_auth(path=path, payload=payload) # Perform request @@ -349,7 +372,7 @@ class ACIModule(object): self.url = '%(protocol)s://%(host)s/' % self.params + path.lstrip('/') # Sign and encode request as to APIC's wishes - if self.params['private_key'] is not None: + if not self.params['private_key']: self.cert_auth(path=path, method='GET') # Perform request @@ -636,7 +659,7 @@ class ACIModule(object): elif not self.module.check_mode: # Sign and encode request as to APIC's wishes - if self.params['private_key'] is not None: + if not self.params['private_key']: self.cert_auth(method='DELETE') resp, info = fetch_url(self.module, self.url, @@ -772,7 +795,7 @@ class ACIModule(object): uri = self.url + self.filter_string # Sign and encode request as to APIC's wishes - if self.params['private_key'] is not None: + if not self.params['private_key']: self.cert_auth(path=self.path + self.filter_string, method='GET') resp, info = fetch_url(self.module, uri, @@ -873,7 +896,7 @@ class ACIModule(object): return elif not self.module.check_mode: # Sign and encode request as to APIC's wishes - if self.params['private_key'] is not None: + if not self.params['private_key']: self.cert_auth(method='POST', payload=json.dumps(self.config)) resp, info = fetch_url(self.module, self.url, diff --git a/lib/ansible/plugins/doc_fragments/aci.py b/lib/ansible/plugins/doc_fragments/aci.py index 54b371108fa..f0eec66dbc4 100644 --- a/lib/ansible/plugins/doc_fragments/aci.py +++ b/lib/ansible/plugins/doc_fragments/aci.py @@ -34,16 +34,17 @@ options: required: yes private_key: description: - - PEM formatted file that contains your private key to be used for signature-based authentication. - - The name of the key (without extension) is used as the certificate name in ACI, unless C(certificate_name) is specified. + - Either a PEM-formatted private key file or the private key content used for signature-based authentication. + - This value also influences the default C(certificate_name) that is used. - This option is mutual exclusive with C(password). If C(password) is provided too, it will be ignored. - type: path + type: str required: yes aliases: [ cert_key ] certificate_name: description: - The X.509 certificate name attached to the APIC AAA user used for signature-based authentication. - - It defaults to the C(private_key) basename, without extension. + - If a C(private_key) filename was provided, this defaults to the C(private_key) basename, without extension. + - If PEM-formatted content was provided for C(private_key), this defaults to the C(username) value. type: str aliases: [ cert_name ] output_level: