openssh_keypair and openssl_privatekey: add regenerate option (#67038)

* Add regenerate option to openssh_keypair and openssl_privatekey.

* Add changelog.
pull/67442/head
Felix Fontein 4 years ago committed by GitHub
parent 55cb8c5388
commit b1de5d43fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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."

@ -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:

@ -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)

@ -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

@ -0,0 +1,7 @@
---
regenerate_values:
- never
- fail
- partial_idempotence
- full_idempotence
- always

@ -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", ">=")'

@ -0,0 +1,7 @@
---
regenerate_values:
- never
- fail
- partial_idempotence
- full_idempotence
- always
Loading…
Cancel
Save