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:
Felix Fontein 2019-03-18 17:34:47 +01:00 committed by John R Barker
parent 3fa39ac818
commit e00f315358
9 changed files with 81 additions and 11 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- "openssl_privatekey - add ``backup`` option."

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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