1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

java_keystore: add certificate_path and private_key_path options (#2230)

* java_keystore: add `certificate_path` and `private_key_path` options

* Update DOCUMENTATION and EXAMPLES accordingly.
* Refactor integration tests to play the same tasks twice.
* Add a changelog fragment (minor_changes).

refactor DOCUMENTATION

* Add useful info for better understanding of what options allow
  keystore regeneration on the fly, and what other options lead the
  module to fail, if their values change.
* Fix indentation and tenses.
* Add myself as author.

* readability-related stuff + changelog fragment
This commit is contained in:
quidame 2021-04-17 18:17:53 +02:00 committed by GitHub
parent f77aa51ab8
commit 3a8206fe62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 328 additions and 201 deletions

View file

@ -0,0 +1,6 @@
---
minor_changes:
- "java_keystore - add options ``certificate_path`` and ``private_key_path``,
mutually exclusive with ``certificate`` and ``private_key`` respectively, and
targetting files on remote hosts rather than their contents on the controller.
(https://github.com/ansible-collections/community.general/issues/1669)."

View file

@ -1,7 +1,8 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# (c) 2016, Guillaume Grossetie <ggrossetie@yuzutech.fr> # Copyright: (c) 2016, Guillaume Grossetie <ggrossetie@yuzutech.fr>
# Copyright: (c) 2021, quidame <quidame@poivron.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
@ -11,68 +12,98 @@ __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: java_keystore module: java_keystore
short_description: Create or delete a Java keystore in JKS format. short_description: Create a Java keystore in JKS format
description: description:
- Create or delete a Java keystore in JKS format for a given certificate. - Bundle a x509 certificate and its private key into a Java Keystore in JKS format.
options: options:
name: name:
type: str description:
description: - Name of the certificate in the keystore.
- Name of the certificate. - If the provided name does not exist in the keystore, the module fails.
required: true This behavior will change in a next release.
certificate: type: str
type: str required: true
description: certificate:
- Certificate that should be used to create the key store. description:
required: true - Content of the certificate used to create the keystore.
private_key: - If the fingerprint of the provided certificate does not match the
type: str fingerprint of the certificate bundled in the keystore, the keystore
description: is regenerated with the provided certificate.
- Private key that should be used to create the key store. - Exactly one of I(certificate) or I(certificate_path) is required.
required: true type: str
private_key_passphrase: certificate_path:
description: description:
- Pass phrase for reading the private key, if required. - Location of the certificate used to create the keystore.
type: str - If the fingerprint of the provided certificate does not match the
required: false fingerprint of the certificate bundled in the keystore, the keystore
version_added: '0.2.0' is regenerated with the provided certificate.
password: - Exactly one of I(certificate) or I(certificate_path) is required.
type: str type: path
description: version_added: '3.0.0'
- Password that should be used to secure the key store. private_key:
required: true description:
dest: - Content of the private key used to create the keystore.
type: path - Exactly one of I(private_key) or I(private_key_path) is required.
description: type: str
- Absolute path where the jks should be generated. private_key_path:
required: true description:
owner: - Location of the private key used to create the keystore.
description: - Exactly one of I(private_key) or I(private_key_path) is required.
- Name of the user that should own jks file. type: path
required: false version_added: '3.0.0'
group: private_key_passphrase:
description: description:
- Name of the group that should own jks file. - Passphrase used to read the private key, if required.
required: false type: str
mode: version_added: '0.2.0'
description: password:
- Mode the file should be. description:
required: false - Password that should be used to secure the keystore.
force: - If the provided password fails to unlock the keystore, the module
description: fails. This behavior will change in a next release.
- Key store will be created even if it already exists. type: str
required: false required: true
type: bool dest:
default: 'no' description:
requirements: [openssl, keytool] - Absolute path of the generated keystore.
author: Guillaume Grossetie (@Mogztter) type: path
required: true
force:
description:
- Keystore is created even if it already exists.
type: bool
default: 'no'
owner:
description:
- Name of the user that should own jks file.
required: false
group:
description:
- Name of the group that should own jks file.
required: false
mode:
description:
- Mode the file should be.
required: false
requirements:
- openssl in PATH
- keytool in PATH
author:
- Guillaume Grossetie (@Mogztter)
- quidame (@quidame)
extends_documentation_fragment: extends_documentation_fragment:
- files - files
seealso:
- module: community.general.java_cert
notes:
- I(certificate) and I(private_key) require that their contents are available
on the controller (either inline in a playbook, or with the C(file) lookup),
while I(certificate_path) and I(private_key_path) require that the files are
available on the target host.
''' '''
EXAMPLES = ''' EXAMPLES = '''
- name: Create a key store for the given certificate (inline) - name: Create a keystore for the given certificate/private key pair (inline)
community.general.java_keystore: community.general.java_keystore:
name: example name: example
certificate: | certificate: |
@ -88,11 +119,19 @@ EXAMPLES = '''
password: changeit password: changeit
dest: /etc/security/keystore.jks dest: /etc/security/keystore.jks
- name: Create a key store for the given certificate (lookup) - name: Create a keystore for the given certificate/private key pair (with files on controller)
community.general.java_keystore: community.general.java_keystore:
name: example name: example
certificate: "{{lookup('file', '/path/to/certificate.crt') }}" certificate: "{{ lookup('file', '/path/to/certificate.crt') }}"
private_key: "{{lookup('file', '/path/to/private.key') }}" private_key: "{{ lookup('file', '/path/to/private.key') }}"
password: changeit
dest: /etc/security/keystore.jks
- name: Create a keystore for the given certificate/private key pair (with files on target host)
community.general.java_keystore:
name: snakeoil
certificate_path: /etc/ssl/certs/ssl-cert-snakeoil.pem
private_key_path: /etc/ssl/private/ssl-cert-snakeoil.key
password: changeit password: changeit
dest: /etc/security/keystore.jks dest: /etc/security/keystore.jks
''' '''
@ -198,22 +237,32 @@ def create_tmp_private_key(module):
def cert_changed(module, openssl_bin, keytool_bin, keystore_path, keystore_pass, alias): def cert_changed(module, openssl_bin, keytool_bin, keystore_path, keystore_pass, alias):
certificate_path = create_tmp_certificate(module) certificate_path = module.params['certificate_path']
if certificate_path is None:
certificate_path = create_tmp_certificate(module)
try: try:
current_certificate_fingerprint = read_certificate_fingerprint(module, openssl_bin, certificate_path) current_certificate_fingerprint = read_certificate_fingerprint(module, openssl_bin, certificate_path)
stored_certificate_fingerprint = read_stored_certificate_fingerprint(module, keytool_bin, alias, keystore_path, keystore_pass) stored_certificate_fingerprint = read_stored_certificate_fingerprint(module, keytool_bin, alias, keystore_path, keystore_pass)
return current_certificate_fingerprint != stored_certificate_fingerprint return current_certificate_fingerprint != stored_certificate_fingerprint
finally: finally:
os.remove(certificate_path) if module.params['certificate_path'] is None:
os.remove(certificate_path)
def create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass): def create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass):
if module.check_mode: if module.check_mode:
return module.exit_json(changed=True) return module.exit_json(changed=True)
certificate_path = create_tmp_certificate(module) certificate_path = module.params['certificate_path']
private_key_path = create_tmp_private_key(module) if certificate_path is None:
certificate_path = create_tmp_certificate(module)
private_key_path = module.params['private_key_path']
if private_key_path is None:
private_key_path = create_tmp_private_key(module)
keystore_p12_path = create_path() keystore_p12_path = create_path()
try: try:
if os.path.exists(keystore_path): if os.path.exists(keystore_path):
os.remove(keystore_path) os.remove(keystore_path)
@ -257,8 +306,10 @@ def create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password,
cmd=import_keystore_cmd, cmd=import_keystore_cmd,
rc=rc) rc=rc)
finally: finally:
os.remove(certificate_path) if module.params['certificate_path'] is None:
os.remove(private_key_path) os.remove(certificate_path)
if module.params['private_key_path'] is None:
os.remove(private_key_path)
os.remove(keystore_p12_path) os.remove(keystore_p12_path)
@ -301,23 +352,33 @@ class ArgumentSpec(object):
self.supports_check_mode = True self.supports_check_mode = True
self.add_file_common_args = True self.add_file_common_args = True
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(type='str', required=True),
certificate=dict(required=True, no_log=True), dest=dict(type='path', required=True),
private_key=dict(required=True, no_log=True), certificate=dict(type='str', no_log=True),
password=dict(required=True, no_log=True), certificate_path=dict(type='path'),
dest=dict(required=True, type='path'), private_key=dict(type='str', no_log=True),
force=dict(required=False, default=False, type='bool'), private_key_path=dict(type='path', no_log=False),
private_key_passphrase=dict(required=False, no_log=True, type='str') private_key_passphrase=dict(type='str', no_log=True),
password=dict(type='str', required=True, no_log=True),
force=dict(type='bool', default=False),
)
choose_between = (
['certificate', 'certificate_path'],
['private_key', 'private_key_path'],
) )
self.argument_spec = argument_spec self.argument_spec = argument_spec
self.required_one_of = choose_between
self.mutually_exclusive = choose_between
def main(): def main():
spec = ArgumentSpec() spec = ArgumentSpec()
module = AnsibleModule( module = AnsibleModule(
argument_spec=spec.argument_spec, argument_spec=spec.argument_spec,
required_one_of=spec.required_one_of,
mutually_exclusive=spec.mutually_exclusive,
supports_check_mode=spec.supports_check_mode,
add_file_common_args=spec.add_file_common_args, add_file_common_args=spec.add_file_common_args,
supports_check_mode=spec.supports_check_mode
) )
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
process_jks(module) process_jks(module)

View file

@ -0,0 +1,16 @@
---
java_keystore_certs:
- name: cert
commonName: example.com
- name: cert-pw
passphrase: hunter2
commonName: example.com
java_keystore_new_certs:
- name: cert2
keyname: cert
commonName: example.org
- name: cert2-pw
keyname: cert-pw
passphrase: hunter2
commonName: example.org

View file

@ -4,134 +4,22 @@
# and should not be used as examples of how to write Ansible roles # # and should not be used as examples of how to write Ansible roles #
#################################################################### ####################################################################
- when: has_java_keytool - when: has_java_keytool
connection: local
block: block:
- name: Create private keys - name: Include tasks to create ssl materials on the controller
community.crypto.openssl_privatekey: include_tasks: prepare.yml
path: "{{ output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.key' }}"
size: 2048 # this should work everywhere
# The following is more efficient, but might not work everywhere:
# type: ECC
# curve: secp384r1
cipher: "{{ 'auto' if item.passphrase is defined else omit }}"
passphrase: "{{ item.passphrase | default(omit) }}"
loop:
- name: cert
- name: cert-pw
passphrase: hunter2
- name: Create CSRs - when: has_java_keytool
community.crypto.openssl_csr: block:
path: "{{ output_dir ~ '/' ~ item.name ~ '.csr' }}" - name: Include tasks to play with 'certificate' and 'private_key' contents
privatekey_path: "{{ output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.key' }}" include_tasks: tests.yml
privatekey_passphrase: "{{ item.passphrase | default(omit) }}" vars:
commonName: "{{ item.commonName }}" remote_cert: false
loop:
- name: cert
commonName: example.com
- name: cert-pw
passphrase: hunter2
commonName: example.com
- name: cert2
keyname: cert
commonName: example.org
- name: cert2-pw
keyname: cert-pw
passphrase: hunter2
commonName: example.org
- name: Create certificates - name: Include tasks to create ssl materials on the remote host
community.crypto.x509_certificate: include_tasks: prepare.yml
path: "{{ output_dir ~ '/' ~ item.name ~ '.pem' }}"
csr_path: "{{ output_dir ~ '/' ~ item.name ~ '.csr' }}"
privatekey_path: "{{ output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.key' }}"
privatekey_passphrase: "{{ item.passphrase | default(omit) }}"
provider: selfsigned
loop:
- name: cert
commonName: example.com
- name: cert-pw
passphrase: hunter2
commonName: example.com
- name: cert2
keyname: cert
commonName: example.org
- name: cert2-pw
keyname: cert-pw
passphrase: hunter2
commonName: example.org
- name: Create a Java key store for the given certificates (check mode) - name: Include tasks to play with 'certificate_path' and 'private_key_path' locations
community.general.java_keystore: &create_key_store_data include_tasks: tests.yml
name: example vars:
certificate: "{{ lookup('file', output_dir ~ '/' ~ item.name ~ '.pem') }}" remote_cert: true
private_key: "{{ lookup('file', output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.key') }}"
private_key_passphrase: "{{ item.passphrase | default(omit) }}"
password: changeit
dest: "{{ output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.jks' }}"
loop: &create_key_store_loop
- name: cert
- name: cert-pw
passphrase: hunter2
check_mode: yes
register: result_check
- name: Create a Java key store for the given certificates
community.general.java_keystore: *create_key_store_data
loop: *create_key_store_loop
register: result
- name: Create a Java key store for the given certificates (idempotency, check mode)
community.general.java_keystore: *create_key_store_data
loop: *create_key_store_loop
check_mode: yes
register: result_idem_check
- name: Create a Java key store for the given certificates (idempotency)
community.general.java_keystore: *create_key_store_data
loop: *create_key_store_loop
register: result_idem
- name: Create a Java key store for the given certificates (certificate changed, check mode)
community.general.java_keystore: *create_key_store_data
loop: &create_key_store_loop_new_certs
- name: cert2
keyname: cert
- name: cert2-pw
keyname: cert-pw
passphrase: hunter2
check_mode: yes
register: result_change_check
- name: Create a Java key store for the given certificates (certificate changed)
community.general.java_keystore: *create_key_store_data
loop: *create_key_store_loop_new_certs
register: result_change
- name: Create a Java key store for the given certificates (password changed, check mode)
community.general.java_keystore:
<<: *create_key_store_data
password: hunter2
loop: *create_key_store_loop_new_certs
check_mode: yes
register: result_pw_change_check
when: false # FIXME: module currently crashes
- name: Create a Java key store for the given certificates (password changed)
community.general.java_keystore:
<<: *create_key_store_data
password: hunter2
loop: *create_key_store_loop_new_certs
register: result_pw_change
when: false # FIXME: module currently crashes
- name: Validate results
assert:
that:
- result is changed
- result_check is changed
- result_idem is not changed
- result_idem_check is not changed
- result_change is changed
- result_change_check is changed
# - result_pw_change is changed # FIXME: module currently crashes
# - result_pw_change_check is changed # FIXME: module currently crashes

View file

@ -0,0 +1,33 @@
---
- name: Create test directory
ansible.builtin.file:
path: "{{ output_dir }}"
state: directory
- name: Create private keys
community.crypto.openssl_privatekey:
path: "{{ output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.key' }}"
size: 2048 # this should work everywhere
# The following is more efficient, but might not work everywhere:
# type: ECC
# curve: secp384r1
cipher: "{{ 'auto' if item.passphrase is defined else omit }}"
passphrase: "{{ item.passphrase | default(omit) }}"
loop: "{{ java_keystore_certs }}"
- name: Create CSRs
community.crypto.openssl_csr:
path: "{{ output_dir ~ '/' ~ item.name ~ '.csr' }}"
privatekey_path: "{{ output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.key' }}"
privatekey_passphrase: "{{ item.passphrase | default(omit) }}"
commonName: "{{ item.commonName }}"
loop: "{{ java_keystore_certs + java_keystore_new_certs }}"
- name: Create certificates
community.crypto.x509_certificate:
path: "{{ output_dir ~ '/' ~ item.name ~ '.pem' }}"
csr_path: "{{ output_dir ~ '/' ~ item.name ~ '.csr' }}"
privatekey_path: "{{ output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.key' }}"
privatekey_passphrase: "{{ item.passphrase | default(omit) }}"
provider: selfsigned
loop: "{{ java_keystore_certs + java_keystore_new_certs }}"

View file

@ -0,0 +1,123 @@
---
- name: Create test directory
ansible.builtin.file:
path: "{{ output_dir }}"
state: directory
- name: Ensure the Java keystore does not exist (cleanup between tests)
ansible.builtin.file:
path: "{{ output_dir ~ '/' ~ item.name ~ '.jks' }}"
state: absent
loop: "{{ java_keystore_certs }}"
loop_control:
label: "{{ output_dir ~ '/' ~ item.name ~ '.jks' }}"
- name: Create a Java keystore for the given ({{ 'remote' if remote_cert else 'local' }}) certificates (check mode)
community.general.java_keystore: &java_keystore_params
name: example
dest: "{{ output_dir ~ '/' ~ (item.keyname | d(item.name)) ~ '.jks' }}"
certificate: "{{ omit if remote_cert else lookup('file', output_dir ~ '/' ~ item.name ~ '.pem') }}"
private_key: "{{ omit if remote_cert else lookup('file', output_dir ~ '/' ~ (item.keyname | d(item.name)) ~ '.key') }}"
certificate_path: "{{ omit if not remote_cert else output_dir ~ '/' ~ item.name ~ '.pem' }}"
private_key_path: "{{ omit if not remote_cert else output_dir ~ '/' ~ (item.keyname | d(item.name)) ~ '.key' }}"
private_key_passphrase: "{{ item.passphrase | d(omit) }}"
password: changeit
loop: "{{ java_keystore_certs }}"
check_mode: yes
register: result_check
- name: Create a Java keystore for the given certificates
community.general.java_keystore: *java_keystore_params
loop: "{{ java_keystore_certs }}"
register: result
- name: Create a Java keystore for the given certificates (idempotency, check mode)
community.general.java_keystore: *java_keystore_params
loop: "{{ java_keystore_certs }}"
check_mode: yes
register: result_idem_check
- name: Create a Java keystore for the given certificates (idempotency)
community.general.java_keystore: *java_keystore_params
loop: "{{ java_keystore_certs }}"
register: result_idem
- name: Create a Java keystore for the given certificates (certificate changed, check mode)
community.general.java_keystore: *java_keystore_params
loop: "{{ java_keystore_new_certs }}"
check_mode: yes
register: result_change_check
- name: Create a Java keystore for the given certificates (certificate changed)
community.general.java_keystore: *java_keystore_params
loop: "{{ java_keystore_new_certs }}"
register: result_change
- name: Create a Java keystore for the given certificates (alias changed, check mode)
community.general.java_keystore:
<<: *java_keystore_params
name: foobar
loop: "{{ java_keystore_new_certs }}"
check_mode: yes
register: result_alias_change_check
when: false # FIXME: module currently crashes
- name: Create a Java keystore for the given certificates (alias changed)
community.general.java_keystore:
<<: *java_keystore_params
name: foobar
loop: "{{ java_keystore_new_certs }}"
register: result_alias_change
when: false # FIXME: module currently crashes
- name: Create a Java keystore for the given certificates (password changed, check mode)
community.general.java_keystore:
<<: *java_keystore_params
name: foobar
password: hunter2
loop: "{{ java_keystore_new_certs }}"
check_mode: yes
register: result_pw_change_check
when: false # FIXME: module currently crashes
- name: Create a Java keystore for the given certificates (password changed)
community.general.java_keystore:
<<: *java_keystore_params
name: foobar
password: hunter2
loop: "{{ java_keystore_new_certs }}"
register: result_pw_change
when: false # FIXME: module currently crashes
- name: Check that the remote certificates have not been removed
ansible.builtin.file:
path: "{{ output_dir ~ '/' ~ item.name ~ '.pem' }}"
state: file
loop: "{{ java_keystore_certs + java_keystore_new_certs }}"
when: remote_cert
- name: Check that the remote private keys have not been removed
ansible.builtin.file:
path: "{{ output_dir ~ '/' ~ item.name ~ '.key' }}"
state: file
loop: "{{ java_keystore_certs }}"
when: remote_cert
- name: Validate results
assert:
that:
- result is changed
- result_check is changed
- result_idem is not changed
- result_idem_check is not changed
- result_change is changed
- result_change_check is changed
# - result_alias_change is changed # FIXME: module currently crashes
# - result_alias_change_check is changed # FIXME: module currently crashes
# - result_pw_change is changed # FIXME: module currently crashes
# - result_pw_change_check is changed # FIXME: module currently crashes