From b1de5d43fce1ba08668179360e98e0fa53ee69a0 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sat, 15 Feb 2020 15:39:36 +0100 Subject: [PATCH] openssh_keypair and openssl_privatekey: add regenerate option (#67038) * Add regenerate option to openssh_keypair and openssl_privatekey. * Add changelog. --- .../67038-openssl-openssh-key-regenerate.yml | 3 + lib/ansible/modules/crypto/openssh_keypair.py | 132 +++++-- .../modules/crypto/openssl_privatekey.py | 140 ++++++-- .../targets/openssh_keypair/tasks/main.yml | 264 ++++++++++++++ .../targets/openssh_keypair/vars/main.yml | 7 + .../targets/openssl_privatekey/tasks/impl.yml | 338 ++++++++++++++++++ .../targets/openssl_privatekey/vars/main.yml | 7 + 7 files changed, 828 insertions(+), 63 deletions(-) create mode 100644 changelogs/fragments/67038-openssl-openssh-key-regenerate.yml create mode 100644 test/integration/targets/openssh_keypair/vars/main.yml create mode 100644 test/integration/targets/openssl_privatekey/vars/main.yml diff --git a/changelogs/fragments/67038-openssl-openssh-key-regenerate.yml b/changelogs/fragments/67038-openssl-openssh-key-regenerate.yml new file mode 100644 index 00000000000..de40804b427 --- /dev/null +++ b/changelogs/fragments/67038-openssl-openssh-key-regenerate.yml @@ -0,0 +1,3 @@ +minor_changes: +- "openssh_keypair - the ``regenerate`` option allows to configure the module's behavior when it should or needs to regenerate private keys." +- "openssl_privatekey - the ``regenerate`` option allows to configure the module's behavior when it should or needs to regenerate private keys." diff --git a/lib/ansible/modules/crypto/openssh_keypair.py b/lib/ansible/modules/crypto/openssh_keypair.py index 70f77f5aec0..86667f4f938 100644 --- a/lib/ansible/modules/crypto/openssh_keypair.py +++ b/lib/ansible/modules/crypto/openssh_keypair.py @@ -63,6 +63,39 @@ options: - Provides a new comment to the public key. When checking if the key is in the correct state this will be ignored. type: str version_added: "2.9" + regenerate: + description: + - Allows to configure in which situations the module is allowed to regenerate private keys. + The module will always generate a new key if the destination file does not exist. + - By default, the key will be regenerated when it doesn't match the module's options, + except when the key cannot be read or the passphrase does not match. Please note that + this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence) + is specified. + - If set to C(never), the module will fail if the key cannot be read or the passphrase + isn't matching, and will never regenerate an existing key. + - If set to C(fail), the module will fail if the key does not correspond to the module's + options. + - If set to C(partial_idempotence), the key will be regenerated if it does not conform to + the module's options. The key is B(not) regenerated if it cannot be read (broken file), + the key is protected by an unknown passphrase, or when they key is not protected by a + passphrase, but a passphrase is specified. + - If set to C(full_idempotence), the key will be regenerated if it does not conform to the + module's options. This is also the case if the key cannot be read (broken file), the key + is protected by an unknown passphrase, or when they key is not protected by a passphrase, + but a passphrase is specified. Make sure you have a B(backup) when using this option! + - If set to C(always), the module will always regenerate the key. This is equivalent to + setting I(force) to C(yes). + - Note that adjusting the comment and the permissions can be changed without regeneration. + Therefore, even for C(never), the task can result in changed. + type: str + choices: + - never + - fail + - partial_idempotence + - full_idempotence + - always + default: partial_idempotence + version_added: '2.10' notes: - In case the ssh key is broken or password protected, the module will fail. Set the I(force) option to C(yes) if you want to regenerate the keypair. @@ -149,6 +182,9 @@ class Keypair(object): self.privatekey = None self.fingerprint = {} self.public_key = {} + self.regenerate = module.params['regenerate'] + if self.regenerate == 'always': + self.force = True if self.type in ('rsa', 'rsa1'): self.size = 4096 if self.size is None else self.size @@ -236,42 +272,50 @@ class Keypair(object): if module.set_fs_attributes_if_different(file_args, False): self.changed = True + def _check_pass_protected_or_broken_key(self, module): + key_state = module.run_command([module.get_bin_path('ssh-keygen', True), + '-P', '', '-yf', self.path], check_rc=False) + if key_state[0] == 255 or 'is not a public key file' in key_state[2]: + return True + if 'incorrect passphrase' in key_state[2] or 'load failed' in key_state[2]: + return True + return False + def isPrivateKeyValid(self, module, perms_required=True): # check if the key is correct def _check_state(): return os.path.exists(self.path) - def _check_pass_protected_or_broken_key(): - key_state = module.run_command([module.get_bin_path('ssh-keygen', True), - '-P', '', '-yf', self.path], check_rc=False) - if key_state[0] == 255 or 'is not a public key file' in key_state[2]: - return True - if 'incorrect passphrase' in key_state[2] or 'load failed' in key_state[2]: - return True + if not _check_state(): return False - if _check_state(): - if _check_pass_protected_or_broken_key(): - module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.' - ' Will not proceed. To force regeneration, call the module with `force=yes`.') + if self._check_pass_protected_or_broken_key(module): + if self.regenerate in ('full_idempotence', 'always'): + return False + module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `full_idempotence` or `always`, or with `force=yes`.') - proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path], check_rc=False) - if not proc[0] == 0: - if os.path.isdir(self.path): - module.fail_json(msg='%s is a directory. Please specify a path to a file.' % (self.path)) + proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path], check_rc=False) + if not proc[0] == 0: + if os.path.isdir(self.path): + module.fail_json(msg='%s is a directory. Please specify a path to a file.' % (self.path)) + if self.regenerate in ('full_idempotence', 'always'): return False + module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `full_idempotence` or `always`, or with `force=yes`.') - fingerprint = proc[1].split() - keysize = int(fingerprint[0]) - keytype = fingerprint[-1][1:-1].lower() - else: - return False + fingerprint = proc[1].split() + keysize = int(fingerprint[0]) + keytype = fingerprint[-1][1:-1].lower() - def _check_perms(module): - file_args = module.load_file_common_arguments(module.params) - return not module.set_fs_attributes_if_different(file_args, False) + self.fingerprint = fingerprint + + if self.regenerate == 'never': + return True def _check_type(): return self.type == keytype @@ -279,12 +323,18 @@ class Keypair(object): def _check_size(): return self.size == keysize - self.fingerprint = fingerprint + if not (_check_type() and _check_size()): + if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'): + return False + module.fail_json(msg='Key has wrong type and/or size.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.') - if not perms_required: - return _check_state() and _check_type() and _check_size() + def _check_perms(module): + file_args = module.load_file_common_arguments(module.params) + return not module.set_fs_attributes_if_different(file_args, False) - return _check_state() and _check_perms(module) and _check_type() and _check_size() + return not perms_required or _check_perms(module) def isPublicKeyValid(self, module, perms_required=True): @@ -299,11 +349,13 @@ class Keypair(object): def _parse_pubkey(pubkey_content): if pubkey_content: parts = pubkey_content.split(' ', 2) + if len(parts) < 2: + return False return parts[0], parts[1], '' if len(parts) <= 2 else parts[2] return False def _pubkey_valid(pubkey): - if pubkey_parts: + if pubkey_parts and _parse_pubkey(pubkey): return pubkey_parts[:2] == _parse_pubkey(pubkey)[:2] return False @@ -317,19 +369,24 @@ class Keypair(object): file_args['path'] = file_args['path'] + '.pub' return not module.set_fs_attributes_if_different(file_args, False) + pubkey_parts = _parse_pubkey(_get_pubkey_content()) + pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) pubkey = pubkey[1].strip('\n') - pubkey_parts = _parse_pubkey(_get_pubkey_content()) if _pubkey_valid(pubkey): self.public_key = pubkey + else: + return False - if not self.comment: - return _pubkey_valid(pubkey) + if self.comment: + if not _comment_valid(): + return False - if not perms_required: - return _pubkey_valid(pubkey) and _comment_valid() + if perms_required: + if not _check_perms(module): + return False - return _pubkey_valid(pubkey) and _comment_valid() and _check_perms(module) + return True def dump(self): # return result as a dict @@ -382,6 +439,11 @@ def main(): force=dict(type='bool', default=False), path=dict(type='path', required=True), comment=dict(type='str'), + regenerate=dict( + type='str', + default='partial_idempotence', + choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] + ), ), supports_check_mode=True, add_file_common_args=True, @@ -401,7 +463,7 @@ def main(): if module.check_mode: result = keypair.dump() - result['changed'] = module.params['force'] or not keypair.isPrivateKeyValid(module) or not keypair.isPublicKeyValid(module) + result['changed'] = keypair.force or not keypair.isPrivateKeyValid(module) or not keypair.isPublicKeyValid(module) module.exit_json(**result) try: diff --git a/lib/ansible/modules/crypto/openssl_privatekey.py b/lib/ansible/modules/crypto/openssl_privatekey.py index 3725a335dec..2fdfdab10c9 100644 --- a/lib/ansible/modules/crypto/openssl_privatekey.py +++ b/lib/ansible/modules/crypto/openssl_privatekey.py @@ -164,6 +164,39 @@ options: type: bool default: no version_added: "2.10" + regenerate: + description: + - Allows to configure in which situations the module is allowed to regenerate private keys. + The module will always generate a new key if the destination file does not exist. + - By default, the key will be regenerated when it doesn't match the module's options, + except when the key cannot be read or the passphrase does not match. Please note that + this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence) + is specified. + - If set to C(never), the module will fail if the key cannot be read or the passphrase + isn't matching, and will never regenerate an existing key. + - If set to C(fail), the module will fail if the key does not correspond to the module's + options. + - If set to C(partial_idempotence), the key will be regenerated if it does not conform to + the module's options. The key is B(not) regenerated if it cannot be read (broken file), + the key is protected by an unknown passphrase, or when they key is not protected by a + passphrase, but a passphrase is specified. + - If set to C(full_idempotence), the key will be regenerated if it does not conform to the + module's options. This is also the case if the key cannot be read (broken file), the key + is protected by an unknown passphrase, or when they key is not protected by a passphrase, + but a passphrase is specified. Make sure you have a B(backup) when using this option! + - If set to C(always), the module will always regenerate the key. This is equivalent to + setting I(force) to C(yes). + - Note that if I(format_mismatch) is set to C(convert) and everything matches except the + format, the key will always be converted, except if I(regenerate) is set to C(always). + type: str + choices: + - never + - fail + - partial_idempotence + - full_idempotence + - always + default: full_idempotence + version_added: '2.10' extends_documentation_fragment: - files seealso: @@ -321,6 +354,9 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject): self.format_mismatch = module.params['format_mismatch'] self.privatekey_bytes = None self.return_content = module.params['return_content'] + self.regenerate = module.params['regenerate'] + if self.regenerate == 'always': + self.force = True self.backup = module.params['backup'] self.backup_file = None @@ -333,6 +369,11 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject): """(Re-)Generate private key.""" pass + @abc.abstractmethod + def _ensure_private_key_loaded(self): + """Make sure that the private key has been loaded.""" + pass + @abc.abstractmethod def _get_private_key_data(self): """Return bytes for self.privatekey""" @@ -359,6 +400,7 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject): # Convert if self.backup: self.backup_file = module.backup_local(self.path) + self._ensure_private_key_loaded() privatekey_data = self._get_private_key_data() if self.return_content: self.privatekey_bytes = privatekey_data @@ -390,19 +432,42 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject): def check(self, module, perms_required=True, ignore_conversion=True): """Ensure the resource is in its desired state.""" - state_and_perms = super(PrivateKeyBase, self).check(module, perms_required) + state_and_perms = super(PrivateKeyBase, self).check(module, perms_required=False) - if not state_and_perms or not self._check_passphrase(): + if not state_and_perms: + # key does not exist return False - if not self._check_size_and_type(): - return False + if not self._check_passphrase(): + if self.regenerate in ('full_idempotence', 'always'): + return False + module.fail_json(msg='Unable to read the key. The key is protected with a another passphrase / no passphrase or broken.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `full_idempotence` or `always`, or with `force=yes`.') + + if self.regenerate != 'never': + if not self._check_size_and_type(): + if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'): + return False + module.fail_json(msg='Key has wrong type and/or size.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.') if not self._check_format(): - if not ignore_conversion or self.format_mismatch != 'convert': + # During conversion step, convert if format does not match and format_mismatch == 'convert' + if not ignore_conversion and self.format_mismatch == 'convert': return False - - return True + # During generation step, regenerate if format does not match and format_mismatch == 'regenerate' + if ignore_conversion and self.format_mismatch == 'regenerate' and self.regenerate != 'never': + if not ignore_conversion or self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'): + return False + module.fail_json(msg='Key has wrong format.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.' + ' To convert the key, set `format_mismatch` to `convert`.') + + # check whether permissions are correct (in case that needs to be checked) + return not perms_required or super(PrivateKeyBase, self).check(module, perms_required=perms_required) def dump(self): """Serialize the object into a dictionary.""" @@ -453,6 +518,14 @@ class PrivateKeyPyOpenSSL(PrivateKeyBase): except (TypeError, ValueError) as exc: raise PrivateKeyError(exc) + def _ensure_private_key_loaded(self): + """Make sure that the private key has been loaded.""" + if self.privatekey is None: + try: + self.privatekey = privatekey = crypto_utils.load_privatekey(self.path, self.passphrase) + except crypto_utils.OpenSSLBadPassphraseError as exc: + raise PrivateKeyError(exc) + def _get_private_key_data(self): """Return bytes for self.privatekey""" if self.cipher and self.passphrase: @@ -478,12 +551,8 @@ class PrivateKeyPyOpenSSL(PrivateKeyBase): def _check_type(privatekey): return self.type == privatekey.type() - try: - privatekey = crypto_utils.load_privatekey(self.path, self.passphrase) - except crypto_utils.OpenSSLBadPassphraseError as exc: - raise PrivateKeyError(exc) - - return _check_size(privatekey) and _check_type(privatekey) + self._ensure_private_key_loaded() + return _check_size(self.privatekey) and _check_type(self.privatekey) def _check_format(self): # Not supported by this backend @@ -606,6 +675,11 @@ class PrivateKeyCryptography(PrivateKeyBase): except cryptography.exceptions.UnsupportedAlgorithm as dummy: self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type)) + def _ensure_private_key_loaded(self): + """Make sure that the private key has been loaded.""" + if self.privatekey is None: + self.privatekey = self._load_privatekey() + def _get_private_key_data(self): """Return bytes for self.privatekey""" # Select export format and encoding @@ -697,7 +771,11 @@ class PrivateKeyCryptography(PrivateKeyBase): data = f.read() format = crypto_utils.identify_private_key_format(data) if format == 'raw': - # Raw keys cannot be encrypted + # Raw keys cannot be encrypted. To avoid incompatibilities, we try to + # actually load the key (and return False when this fails). + self._load_privatekey() + # Loading the key succeeded. Only return True when no passphrase was + # provided. return self.passphrase is None else: return cryptography.hazmat.primitives.serialization.load_pem_private_key( @@ -709,27 +787,26 @@ class PrivateKeyCryptography(PrivateKeyBase): return False def _check_size_and_type(self): - privatekey = self._load_privatekey() - self.privatekey = privatekey - - if isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): - return self.type == 'RSA' and self.size == privatekey.key_size - if isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): - return self.type == 'DSA' and self.size == privatekey.key_size - if CRYPTOGRAPHY_HAS_X25519 and isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): + self._ensure_private_key_loaded() + + if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + return self.type == 'RSA' and self.size == self.privatekey.key_size + if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): + return self.type == 'DSA' and self.size == self.privatekey.key_size + if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): return self.type == 'X25519' - if CRYPTOGRAPHY_HAS_X448 and isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey): + if CRYPTOGRAPHY_HAS_X448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey): return self.type == 'X448' - if CRYPTOGRAPHY_HAS_ED25519 and isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): return self.type == 'Ed25519' - if CRYPTOGRAPHY_HAS_ED448 and isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): return self.type == 'Ed448' - if isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): if self.type != 'ECC': return False if self.curve not in self.curves: return False - return self.curves[self.curve]['verify'](privatekey) + return self.curves[self.curve]['verify'](self.privatekey) return False @@ -777,6 +854,11 @@ def main(): format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']), select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'), return_content=dict(type='bool', default=False), + regenerate=dict( + type='str', + default='full_idempotence', + choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] + ), ), supports_check_mode=True, add_file_common_args=True, @@ -837,7 +919,9 @@ def main(): if private_key.state == 'present': if module.check_mode: result = private_key.dump() - result['changed'] = module.params['force'] or not private_key.check(module) + result['changed'] = private_key.force \ + or not private_key.check(module, ignore_conversion=True) \ + or not private_key.check(module, ignore_conversion=False) module.exit_json(**result) private_key.generate(module) diff --git a/test/integration/targets/openssh_keypair/tasks/main.yml b/test/integration/targets/openssh_keypair/tasks/main.yml index 5bdb242b094..3458f8ecf13 100644 --- a/test/integration/targets/openssh_keypair/tasks/main.yml +++ b/test/integration/targets/openssh_keypair/tasks/main.yml @@ -109,3 +109,267 @@ register: privatekey8_result_force - import_tasks: ../tests/validate.yml + + +# Test regenerate option + +- name: Regenerate - setup simple keys + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1024 + loop: "{{ regenerate_values }}" +- name: Regenerate - setup password protected keys + command: 'ssh-keygen -f {{ output_dir }}/regenerate-b-{{ item }} -N password' + loop: "{{ regenerate_values }}" +- name: Regenerate - setup broken keys + copy: + dest: '{{ output_dir }}/regenerate-c-{{ item.0 }}{{ item.1 }}' + content: 'broken key' + mode: '0700' + with_nested: + - "{{ regenerate_values }}" + - [ '', '.pub' ] + +- name: Regenerate - modify broken keys (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-c-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify broken keys + openssh_keypair: + path: '{{ output_dir }}/regenerate-c-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify password protected keys (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-b-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify password protected keys + openssh_keypair: + path: '{{ output_dir }}/regenerate-b-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - not modify regular keys (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: Regenerate - not modify regular keys + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: Regenerate - adjust key size (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1048 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - adjust key size + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1048 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always{{ item.1 }}' + dest: '{{ output_dir }}/regenerate-a-{{ item.0 }}{{ item.1 }}' + remote_src: true + with_nested: + - "{{ regenerate_values }}" + - [ '', '.pub' ] + when: "item.0 != 'always'" + +- name: Regenerate - adjust key type (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - adjust key type + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always{{ item.1 }}' + dest: '{{ output_dir }}/regenerate-a-{{ item.0 }}{{ item.1 }}' + remote_src: true + with_nested: + - "{{ regenerate_values }}" + - [ '', '.pub' ] + when: "item.0 != 'always'" + +- name: Regenerate - adjust comment (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + comment: test comment + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result is changed + +- name: Regenerate - adjust comment + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + comment: test comment + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result is changed + # for all values but 'always', the key should have not been regenerated. + # verify this by comparing fingerprints: + - result.results[0].fingerprint == result.results[1].fingerprint + - result.results[0].fingerprint == result.results[2].fingerprint + - result.results[0].fingerprint == result.results[3].fingerprint + - result.results[0].fingerprint != result.results[4].fingerprint diff --git a/test/integration/targets/openssh_keypair/vars/main.yml b/test/integration/targets/openssh_keypair/vars/main.yml new file mode 100644 index 00000000000..81eb611f8af --- /dev/null +++ b/test/integration/targets/openssh_keypair/vars/main.yml @@ -0,0 +1,7 @@ +--- +regenerate_values: + - never + - fail + - partial_idempotence + - full_idempotence + - always diff --git a/test/integration/targets/openssl_privatekey/tasks/impl.yml b/test/integration/targets/openssl_privatekey/tasks/impl.yml index 06416c95bf1..03f92e94fd5 100644 --- a/test/integration/targets/openssl_privatekey/tasks/impl.yml +++ b/test/integration/targets/openssl_privatekey/tasks/impl.yml @@ -465,3 +465,341 @@ - privatekey_fmt_2_step_6.privatekey == lookup('file', output_dir ~ '/privatekey_fmt_2.pem', rstrip=False) when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=")' + + + +# Test regenerate option + +- name: Regenerate - setup simple keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1024 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" +- name: Regenerate - setup password protected keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-b-{{ item }}.pem' + type: RSA + size: 1024 + passphrase: hunter2 + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" +- name: Regenerate - setup broken keys + copy: + dest: '{{ output_dir }}/regenerate-c-{{ item }}.pem' + content: 'broken key' + mode: '0700' + loop: "{{ regenerate_values }}" + +- name: Regenerate - modify broken keys (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-c-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg or 'Cannot load raw key' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg or 'Cannot load raw key' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg or 'Cannot load raw key' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify broken keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-c-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg or 'Cannot load raw key' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg or 'Cannot load raw key' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg or 'Cannot load raw key' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify password protected keys (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-b-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify password protected keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-b-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - not modify regular keys (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: Regenerate - not modify regular keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: Regenerate - adjust key size (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1048 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - adjust key size + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1048 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always.pem' + dest: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + remote_src: true + loop: "{{ regenerate_values }}" + when: "item != 'always'" + +- name: Regenerate - adjust key type (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - adjust key type + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- block: + - name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always.pem' + dest: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + remote_src: true + loop: "{{ regenerate_values }}" + when: "item != 'always'" + + - name: Regenerate - format mismatch (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + format: pkcs8 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result + - assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong format. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + + - name: Regenerate - format mismatch + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + format: pkcs8 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result + - assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong format. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + + - name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always.pem' + dest: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + remote_src: true + loop: "{{ regenerate_values }}" + when: "item != 'always'" + + - name: Regenerate - convert format (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + format: pkcs1 + format_mismatch: convert + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + register: result + - assert: + that: + - result.results[0] is changed + - result.results[1] is changed + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + + - name: Regenerate - convert format + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + format: pkcs1 + format_mismatch: convert + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + register: result + - assert: + that: + - result.results[0] is changed + - result.results[1] is changed + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + # for all values but 'always', the key should have not been regenerated. + # verify this by comparing fingerprints: + - result.results[0].fingerprint == result.results[1].fingerprint + - result.results[0].fingerprint == result.results[2].fingerprint + - result.results[0].fingerprint == result.results[3].fingerprint + - result.results[0].fingerprint != result.results[4].fingerprint + when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=")' diff --git a/test/integration/targets/openssl_privatekey/vars/main.yml b/test/integration/targets/openssl_privatekey/vars/main.yml new file mode 100644 index 00000000000..81eb611f8af --- /dev/null +++ b/test/integration/targets/openssl_privatekey/vars/main.yml @@ -0,0 +1,7 @@ +--- +regenerate_values: + - never + - fail + - partial_idempotence + - full_idempotence + - always