openssl_privatekey: add backup option (#53593)
* Add backup option to openssl_privatekey. * Add changelog fragment. * Make module available in remove(). * Add tests for backup. * Update lib/ansible/modules/crypto/openssl_privatekey.py Co-Authored-By: felixfontein <felix@fontein.de> * Update lib/ansible/modules/crypto/openssl_privatekey.py Co-Authored-By: felixfontein <felix@fontein.de> * Update lib/ansible/modules/crypto/openssl_privatekey.py Co-Authored-By: felixfontein <felix@fontein.de> * Update lib/ansible/modules/crypto/openssl_privatekey.py
This commit is contained in:
parent
3fa39ac818
commit
e00f315358
9 changed files with 81 additions and 11 deletions
2
changelogs/fragments/53593-openssl_privatekey-backup.yml
Normal file
2
changelogs/fragments/53593-openssl_privatekey-backup.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- "openssl_privatekey - add ``backup`` option."
|
|
@ -230,7 +230,7 @@ class OpenSSLObject(object):
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def remove(self):
|
def remove(self, module):
|
||||||
"""Remove the resource from the filesystem."""
|
"""Remove the resource from the filesystem."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1220,7 +1220,7 @@ def main():
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
certificate.remove()
|
certificate.remove(module)
|
||||||
except CertificateError as exc:
|
except CertificateError as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|
||||||
|
|
|
@ -1046,7 +1046,7 @@ def main():
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
csr.remove()
|
csr.remove(module)
|
||||||
except (CertificateSigningRequestError, crypto_utils.OpenSSLObjectError) as exc:
|
except (CertificateSigningRequestError, crypto_utils.OpenSSLObjectError) as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|
||||||
|
|
|
@ -241,7 +241,7 @@ class Pkcs(crypto_utils.OpenSSLObject):
|
||||||
self.pkcs12 = crypto.PKCS12()
|
self.pkcs12 = crypto.PKCS12()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.remove()
|
self.remove(module)
|
||||||
except PkcsError as exc:
|
except PkcsError as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|
||||||
|
@ -274,14 +274,14 @@ class Pkcs(crypto_utils.OpenSSLObject):
|
||||||
self.iter_size, self.maciter_size))
|
self.iter_size, self.maciter_size))
|
||||||
os.close(pkcs12_file)
|
os.close(pkcs12_file)
|
||||||
except (IOError, OSError) as exc:
|
except (IOError, OSError) as exc:
|
||||||
self.remove()
|
self.remove(module)
|
||||||
raise PkcsError(exc)
|
raise PkcsError(exc)
|
||||||
|
|
||||||
def parse(self, module):
|
def parse(self, module):
|
||||||
"""Read PKCS#12 file."""
|
"""Read PKCS#12 file."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.remove()
|
self.remove(module)
|
||||||
with open(self.src, 'rb') as pkcs12_fh:
|
with open(self.src, 'rb') as pkcs12_fh:
|
||||||
pkcs12_content = pkcs12_fh.read()
|
pkcs12_content = pkcs12_fh.read()
|
||||||
p12 = crypto.load_pkcs12(pkcs12_content,
|
p12 = crypto.load_pkcs12(pkcs12_content,
|
||||||
|
@ -298,7 +298,7 @@ class Pkcs(crypto_utils.OpenSSLObject):
|
||||||
os.close(pkcs12_file)
|
os.close(pkcs12_file)
|
||||||
|
|
||||||
except IOError as exc:
|
except IOError as exc:
|
||||||
self.remove()
|
self.remove(module)
|
||||||
raise PkcsError(exc)
|
raise PkcsError(exc)
|
||||||
|
|
||||||
|
|
||||||
|
@ -378,7 +378,7 @@ def main():
|
||||||
|
|
||||||
if os.path.exists(module.params['path']):
|
if os.path.exists(module.params['path']):
|
||||||
try:
|
try:
|
||||||
pkcs12.remove()
|
pkcs12.remove(module)
|
||||||
changed = True
|
changed = True
|
||||||
except PkcsError as exc:
|
except PkcsError as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|
|
@ -23,6 +23,11 @@ description:
|
||||||
L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography)
|
L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography)
|
||||||
private keys.
|
private keys.
|
||||||
- Keys are generated in PEM format.
|
- Keys are generated in PEM format.
|
||||||
|
- "Please note that the module regenerates private keys if they don't match
|
||||||
|
the module's options. In particular, if you provide another passphrase
|
||||||
|
(or specify none), change the keysize, etc., the private key will be
|
||||||
|
regenerated. If you are concerned that this could overwrite your private key,
|
||||||
|
consider using the I(backup) option."
|
||||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||||
library. By default, it tries to detect which one is available. This can be
|
library. By default, it tries to detect which one is available. This can be
|
||||||
overridden with the I(select_crypto_backend) option."
|
overridden with the I(select_crypto_backend) option."
|
||||||
|
@ -111,6 +116,13 @@ options:
|
||||||
default: auto
|
default: auto
|
||||||
choices: [ auto, cryptography, pyopenssl ]
|
choices: [ auto, cryptography, pyopenssl ]
|
||||||
version_added: "2.8"
|
version_added: "2.8"
|
||||||
|
backup:
|
||||||
|
description:
|
||||||
|
- Create a backup file including a timestamp so you can get
|
||||||
|
the original private key back if you overwrote it with a new one by accident.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
version_added: "2.8"
|
||||||
extends_documentation_fragment:
|
extends_documentation_fragment:
|
||||||
- files
|
- files
|
||||||
seealso:
|
seealso:
|
||||||
|
@ -182,6 +194,11 @@ fingerprint:
|
||||||
sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7"
|
sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7"
|
||||||
sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d"
|
sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d"
|
||||||
sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b"
|
sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b"
|
||||||
|
backup_file:
|
||||||
|
description: Name of backup file created.
|
||||||
|
returned: changed and if I(backup) is C(yes)
|
||||||
|
type: str
|
||||||
|
sample: /path/to/privatekey.pem.2019-03-09@11:22~
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
@ -255,6 +272,9 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject):
|
||||||
self.privatekey = None
|
self.privatekey = None
|
||||||
self.fingerprint = {}
|
self.fingerprint = {}
|
||||||
|
|
||||||
|
self.backup = module.params['backup']
|
||||||
|
self.backup_path = None
|
||||||
|
|
||||||
self.mode = module.params.get('mode', None)
|
self.mode = module.params.get('mode', None)
|
||||||
if self.mode is None:
|
if self.mode is None:
|
||||||
self.mode = 0o600
|
self.mode = 0o600
|
||||||
|
@ -271,6 +291,8 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject):
|
||||||
"""Generate a keypair."""
|
"""Generate a keypair."""
|
||||||
|
|
||||||
if not self.check(module, perms_required=False) or self.force:
|
if not self.check(module, perms_required=False) or self.force:
|
||||||
|
if self.backup:
|
||||||
|
self.backup_file = module.backup_local(self.path)
|
||||||
privatekey_data = self._generate_private_key_data()
|
privatekey_data = self._generate_private_key_data()
|
||||||
try:
|
try:
|
||||||
privatekey_file = os.open(self.path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
|
privatekey_file = os.open(self.path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
|
||||||
|
@ -298,6 +320,11 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject):
|
||||||
if module.set_fs_attributes_if_different(file_args, False):
|
if module.set_fs_attributes_if_different(file_args, False):
|
||||||
self.changed = True
|
self.changed = True
|
||||||
|
|
||||||
|
def remove(self, module):
|
||||||
|
if self.backup:
|
||||||
|
self.backup_file = module.backup_local(self.path)
|
||||||
|
super(PrivateKeyBase, self).remove(module)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _check_passphrase(self):
|
def _check_passphrase(self):
|
||||||
pass
|
pass
|
||||||
|
@ -325,6 +352,8 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject):
|
||||||
'changed': self.changed,
|
'changed': self.changed,
|
||||||
'fingerprint': self.fingerprint,
|
'fingerprint': self.fingerprint,
|
||||||
}
|
}
|
||||||
|
if self.backup_path:
|
||||||
|
result['backup_path'] = self.backup_path
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -583,6 +612,7 @@ def main():
|
||||||
path=dict(type='path', required=True),
|
path=dict(type='path', required=True),
|
||||||
passphrase=dict(type='str', no_log=True),
|
passphrase=dict(type='str', no_log=True),
|
||||||
cipher=dict(type='str'),
|
cipher=dict(type='str'),
|
||||||
|
backup=dict(type='bool', default=False),
|
||||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||||
),
|
),
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
|
@ -656,7 +686,7 @@ def main():
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
private_key.remove()
|
private_key.remove(module)
|
||||||
except PrivateKeyError as exc:
|
except PrivateKeyError as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|
||||||
|
|
|
@ -205,7 +205,7 @@ class PublicKey(crypto_utils.OpenSSLObject):
|
||||||
except (IOError, OSError) as exc:
|
except (IOError, OSError) as exc:
|
||||||
raise PublicKeyError(exc)
|
raise PublicKeyError(exc)
|
||||||
except AttributeError as exc:
|
except AttributeError as exc:
|
||||||
self.remove()
|
self.remove(module)
|
||||||
raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys')
|
raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys')
|
||||||
|
|
||||||
self.fingerprint = crypto_utils.get_fingerprint(
|
self.fingerprint = crypto_utils.get_fingerprint(
|
||||||
|
@ -315,7 +315,7 @@ def main():
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
public_key.remove()
|
public_key.remove(module)
|
||||||
except PublicKeyError as exc:
|
except PublicKeyError as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,7 @@
|
||||||
passphrase: hunter2
|
passphrase: hunter2
|
||||||
cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}"
|
cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}"
|
||||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
backup: yes
|
||||||
register: passphrase_1
|
register: passphrase_1
|
||||||
|
|
||||||
- name: Generate privatekey with passphrase (idempotent)
|
- name: Generate privatekey with passphrase (idempotent)
|
||||||
|
@ -157,18 +158,21 @@
|
||||||
passphrase: hunter2
|
passphrase: hunter2
|
||||||
cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}"
|
cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}"
|
||||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
backup: yes
|
||||||
register: passphrase_2
|
register: passphrase_2
|
||||||
|
|
||||||
- name: Regenerate privatekey without passphrase
|
- name: Regenerate privatekey without passphrase
|
||||||
openssl_privatekey:
|
openssl_privatekey:
|
||||||
path: '{{ output_dir }}/privatekeypw.pem'
|
path: '{{ output_dir }}/privatekeypw.pem'
|
||||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
backup: yes
|
||||||
register: passphrase_3
|
register: passphrase_3
|
||||||
|
|
||||||
- name: Regenerate privatekey without passphrase (idempotent)
|
- name: Regenerate privatekey without passphrase (idempotent)
|
||||||
openssl_privatekey:
|
openssl_privatekey:
|
||||||
path: '{{ output_dir }}/privatekeypw.pem'
|
path: '{{ output_dir }}/privatekeypw.pem'
|
||||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
backup: yes
|
||||||
register: passphrase_4
|
register: passphrase_4
|
||||||
|
|
||||||
- name: Regenerate privatekey with passphrase
|
- name: Regenerate privatekey with passphrase
|
||||||
|
@ -177,4 +181,25 @@
|
||||||
passphrase: hunter2
|
passphrase: hunter2
|
||||||
cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}"
|
cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}"
|
||||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
backup: yes
|
||||||
register: passphrase_5
|
register: passphrase_5
|
||||||
|
|
||||||
|
- name: Remove module
|
||||||
|
openssl_privatekey:
|
||||||
|
path: '{{ output_dir }}/privatekeypw.pem'
|
||||||
|
passphrase: hunter2
|
||||||
|
cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}"
|
||||||
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
backup: yes
|
||||||
|
state: absent
|
||||||
|
register: remove_1
|
||||||
|
|
||||||
|
- name: Remove module (idempotent)
|
||||||
|
openssl_privatekey:
|
||||||
|
path: '{{ output_dir }}/privatekeypw.pem'
|
||||||
|
passphrase: hunter2
|
||||||
|
cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}"
|
||||||
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
backup: yes
|
||||||
|
state: absent
|
||||||
|
register: remove_2
|
||||||
|
|
|
@ -113,3 +113,16 @@
|
||||||
- passphrase_3 is changed
|
- passphrase_3 is changed
|
||||||
- passphrase_4 is not changed
|
- passphrase_4 is not changed
|
||||||
- passphrase_5 is changed
|
- passphrase_5 is changed
|
||||||
|
- passphrase_1.backup_file is undefined
|
||||||
|
- passphrase_2.backup_file is undefined
|
||||||
|
- passphrase_3.backup_file is not none
|
||||||
|
- passphrase_4.backup_file is undefined
|
||||||
|
- passphrase_5.backup_file is not none
|
||||||
|
|
||||||
|
- name: Validate remove
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remove_1 is changed
|
||||||
|
- remove_2 is not changed
|
||||||
|
- remove_1.backup_file is not none
|
||||||
|
- remove_2.backup_file is undefined
|
||||||
|
|
Loading…
Reference in a new issue