From 1d6fa880261d3e01bfc768bb4e3e6e8049cd5766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=B6sel?= Date: Tue, 9 Apr 2024 01:05:16 +0200 Subject: [PATCH] modules: add `_password` args as alternative to password_files (#402) this PR adds a `_password` parameter to go along with every currently existing `password_file` module parameter. As discussed in #352, this makes passing in passwords to tasks much more convenient, as users no longer need to create and delete password files manually. To ensure no sensitive password files remain on the system, they are created in a temporary directory that is handled through a context manager. --- plugins/doc_fragments/ca_admin.py | 11 +- plugins/module_utils/cli_wrapper.py | 12 +- plugins/module_utils/params/ca_admin.py | 17 +- plugins/modules/step_ca_certificate.py | 30 +- plugins/modules/step_ca_provisioner.py | 30 +- plugins/modules/step_ca_renew.py | 24 +- plugins/modules/step_ca_token.py | 33 +- .../step_ca_certificate/tasks/main.yml | 350 +++++++++--------- .../step_ca_provisioner/tasks/main.yml | 290 ++++++++------- .../targets/step_ca_renew/tasks/main.yml | 2 + .../targets/step_ca_token/tasks/main.yml | 8 + 11 files changed, 448 insertions(+), 359 deletions(-) diff --git a/plugins/doc_fragments/ca_admin.py b/plugins/doc_fragments/ca_admin.py index d694e853..a084bb0d 100644 --- a/plugins/doc_fragments/ca_admin.py +++ b/plugins/doc_fragments/ca_admin.py @@ -20,7 +20,16 @@ class ModuleDocFragment: type: str aliases: - admin_name + admin_password: + description: > + The password to encrypt or decrypt the private key. + Will be passed to step-cli through a temporary file. + Mutually exclusive with I(admin_password_file) + type: str admin_password_file: - description: The path to the file containing the password to encrypt or decrypt the private key. + description: > + The path to the file containing the password to encrypt or decrypt the private key. + Must already be present on the remote host. + Mutually exclusive with I(admin_password) type: path ''' diff --git a/plugins/module_utils/cli_wrapper.py b/plugins/module_utils/cli_wrapper.py index ea51a6a9..8d5dc5dc 100644 --- a/plugins/module_utils/cli_wrapper.py +++ b/plugins/module_utils/cli_wrapper.py @@ -90,7 +90,7 @@ def build(self, module: AnsibleModule, tmpdir: Path) -> List[str]: # Create temporary files for any parameters that need to point to files, such as password-file # Since these files may contain sensitive data, we first create the fd with locked-down permissions, # then write the actual content - for module_arg in self.module_tmpfile_args: + for module_arg in [arg for arg in self.module_tmpfile_args if module_params[arg]]: path = tmpdir / module_arg path.touch(0o700, exist_ok=False) with open(path, "w", encoding="utf-8") as f: @@ -98,15 +98,13 @@ def build(self, module: AnsibleModule, tmpdir: Path) -> List[str]: args.extend([self.module_tmpfile_args[module_arg], path.as_posix()]) # transform the values in module_params into valid step-coi arguments using module_args_params mapping - for param_name in self.module_param_args: + for param_name in [arg for arg in self.module_param_args if module_params[arg]]: if param_name not in module_params: raise CliError(f"Could not build command parameters: " f"param '{param_name}' not in module argspec, this is most likely a bug") + param_type = module.argument_spec[param_name].get("type", "str") - if not module_params[param_name]: - # param not set - pass - elif param_type == "bool" and bool(module_params[param_name]): + if param_type == "bool" and bool(module_params[param_name]): args.append(self.module_param_args[param_name]) elif param_type == "list": for item in cast(List, module_params[param_name]): @@ -147,7 +145,7 @@ def run(self, module: AnsibleModule) -> CliCommandResult: CliError if the module args don't match with the provided params """ # use a context manager to ensure that our sensitive temporary files are *always* deleted - with tempfile.TemporaryDirectory("ansible-smallstep") as tmpdir: + with tempfile.TemporaryDirectory("-ansible-smallstep") as tmpdir: cmd = [self.executable.path] + self.args.build(module, Path(tmpdir)) if module.check_mode and not self.run_in_check_mode: diff --git a/plugins/module_utils/params/ca_admin.py b/plugins/module_utils/params/ca_admin.py index 284b2730..1a18cc7a 100644 --- a/plugins/module_utils/params/ca_admin.py +++ b/plugins/module_utils/params/ca_admin.py @@ -17,22 +17,29 @@ class AdminParams(ParamsHelper): admin_key=dict(type="path"), admin_provisioner=dict(type="str", aliases=["admin_issuer"]), admin_subject=dict(type="str", aliases=["admin_name"]), + admin_password=dict(type="str", no_log=True), admin_password_file=dict(type="path", no_log=False) ) @classmethod def cli_args(cls) -> CliCommandArgs: - return CliCommandArgs([], {key: f"--{key.replace('_', '-')}" for key in cls.argument_spec}) + return CliCommandArgs([], { + "admin_cert": "--admin-cert", + "admin_key": "--admin-key", + "admin_provisioner": "--admin-provisioner", + "admin_subject": "--admin-subject", + "admin_password_file": "--admin-password-file", + }, { + "admin_password": "--admin-password-file" + }) # pylint: disable=useless-parent-delegation def __init__(self, module: AnsibleModule) -> None: super().__init__(module) def check(self): - try: - validation.check_required_together(["admin_cert", "admin_key"], self.module.params) - except ValueError: - self.module.fail_json(msg="admin_cert and admin_key must be specified together") + validation.check_required_together(["admin_cert", "admin_key"], self.module.params) + validation.check_mutually_exclusive(["admin_password", "admin_password_file"], self.module.params) def is_defined(self): return bool(self.module.params["admin_cert"]) # type: ignore diff --git a/plugins/modules/step_ca_certificate.py b/plugins/modules/step_ca_certificate.py index a17fa1b5..12e1768f 100644 --- a/plugins/modules/step_ca_certificate.py +++ b/plugins/modules/step_ca_certificate.py @@ -126,8 +126,16 @@ - issuer description: The provisioner name to use. Required if I(state=present). type: str + provisioner_password: + description: > + The password to decrypt the one-time token generating key. + Will be passed to step-cli through a temporary file. + Mutually exclusive with I(provisioner_password_file) + type: str provisioner_password_file: - description: The path to the file containing the password to decrypt the one-time token generating key. + description: > + The path to the file containing the password to decrypt the one-time token generating key. + Mutually exclusive with I(provisioner_password) type: path revoke_on_delete: description: If I(state=absent), attempt to revoke the certificate before deleting it @@ -264,7 +272,7 @@ from typing import cast, Dict, Any from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.common.validation import check_required_if +from ansible.module_utils.common.validation import check_required_if, check_mutually_exclusive from ..module_utils.params.ca_connection import CaConnectionParams from ..module_utils.cli_wrapper import CliCommand, CliCommandArgs, StepCliExecutable @@ -297,7 +305,8 @@ def create_certificate(executable: StepCliExecutable, module: AnsibleModule, for if force: args.append("--force") - create_args = CaConnectionParams.cli_args().join(CliCommandArgs(args, cert_cliarg_map)) + create_args = CaConnectionParams.cli_args().join(CliCommandArgs( + args, cert_cliarg_map, {"provisioner_password": "--provisioner-password-file"})) create_cmd = CliCommand(executable, create_args) create_cmd.run(module) return {"changed": True} @@ -408,6 +417,7 @@ def run_module(): not_after=dict(type="str"), not_before=dict(type="str"), provisioner=dict(type="str", aliases=["issuer"]), + provisioner_password=dict(type="str", no_log=True), provisioner_password_file=dict(type="path", no_log=False), revoke_on_delete=dict(type="bool", default=True), revoke_reason=dict(type="str"), @@ -431,11 +441,17 @@ def run_module(): **CaConnectionParams.argument_spec, **argument_spec, }, supports_check_mode=True) - CaConnectionParams(module).check() module_params = cast(Dict, module.params) - check_required_if([ - ["state", "present", ["name", "provisioner"], True], - ], module_params) + + try: + CaConnectionParams(module).check() + check_required_if([ + ["state", "present", ["name", "provisioner"], True], + ], module_params) + check_mutually_exclusive(["provisioner_password", "provisioner_password_file"], module_params) + except TypeError as e: + module.fail_json(f"Parameter validation failed: {e}") + executable = StepCliExecutable(module, module_params["step_cli_executable"]) crt_exists = Path(module_params["crt_file"]).exists() diff --git a/plugins/modules/step_ca_provisioner.py b/plugins/modules/step_ca_provisioner.py index 06f83ec4..0a1e5acc 100644 --- a/plugins/modules/step_ca_provisioner.py +++ b/plugins/modules/step_ca_provisioner.py @@ -164,8 +164,16 @@ version_added: 0.20.0 aliases: - tenant_id + password: + description: > + The password to encrypt or decrypt the private key. + Will be passed to step-cli through a temporary file. + Mutually exclusive with I(password_file) + type: str password_file: - description: The path to the file containing the password to encrypt or decrypt the private key. + description: > + The path to the file containing the password to encrypt or decrypt the private key. + Mutually exclusive with I(password) type: path public_key: description: > @@ -440,6 +448,7 @@ from typing import cast, Dict, Any from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.validation import check_mutually_exclusive from ..module_utils.params.ca_admin import AdminParams from ..module_utils.cli_wrapper import CliCommand, CliCommandArgs, StepCliExecutable @@ -502,12 +511,16 @@ "x509_default_dur": "--x509-default-dur", "x5c_root": "--x5c-root", } +CREATE_UPDATE_TMPFILE_ARGS = { + "password": "--password-file" +} def add_provisioner(name: str, provisioner_type: str, executable: StepCliExecutable, module: AnsibleModule): args = AdminParams.cli_args().join(CliCommandArgs( ["ca", "provisioner", "add", name, "--type", provisioner_type], - {**CREATE_UPDATE_CLIARGS, **CONNECTION_CLIARG_MAP} + {**CREATE_UPDATE_CLIARGS, **CONNECTION_CLIARG_MAP}, + CREATE_UPDATE_TMPFILE_ARGS )) cmd = CliCommand(executable, args) cmd.run(module) @@ -517,7 +530,8 @@ def add_provisioner(name: str, provisioner_type: str, executable: StepCliExecuta def update_provisioner(name: str, executable: StepCliExecutable, module: AnsibleModule): args = AdminParams.cli_args().join(CliCommandArgs( ["ca", "provisioner", "update", name], - {**CREATE_UPDATE_CLIARGS, **CONNECTION_CLIARG_MAP} + {**CREATE_UPDATE_CLIARGS, **CONNECTION_CLIARG_MAP}, + CREATE_UPDATE_TMPFILE_ARGS )) cmd = CliCommand(executable, args) cmd.run(module) @@ -564,6 +578,7 @@ def run_module(): oidc_groups=dict(type="list", elements="str", aliases=["group", "oidc_group"]), oidc_listen_address=dict(type="str", aliases=["listen_address", "oidc_client_address"]), oidc_tenant_id=dict(type="str", aliases=["tenant_id"]), + password=dict(type="str", no_log=True), password_file=dict(type="path", no_log=False), public_key=dict(type="path", aliases=["jwk_public_key", "k8ssa_public_key", "k8s_pem_keys_file"]), require_eab=dict(type="bool"), @@ -598,9 +613,14 @@ def run_module(): **AdminParams.argument_spec, **argument_spec }, supports_check_mode=True) - admin_params = AdminParams(module) - admin_params.check() module_params = cast(Dict, module.params) + admin_params = AdminParams(module) + + try: + admin_params.check() + check_mutually_exclusive(["password", "password_file"], module_params) + except TypeError as e: + module.fail_json(f"Parameter validation failed: {e}") executable = StepCliExecutable(module, module_params["step_cli_executable"]) diff --git a/plugins/modules/step_ca_renew.py b/plugins/modules/step_ca_renew.py index 6c4ef598..7a66f167 100644 --- a/plugins/modules/step_ca_renew.py +++ b/plugins/modules/step_ca_renew.py @@ -38,8 +38,16 @@ output_file: description: The new certificate file path. Defaults to overwriting the crt-file positional argument. type: path + password: + description: > + The password to encrypt or decrypt the private key. + Will be passed to step-cli through a temporary file. + Mutually exclusive with I(password_file) + type: str password_file: - description: The path to the file containing the password to encrypt or decrypt the private key. + description: > + The path to the file containing the password to encrypt or decrypt the private key. + Mutually exclusive with I(password) type: path pid: description: > @@ -76,6 +84,7 @@ from typing import Dict, cast, Any from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.validation import check_mutually_exclusive from ..module_utils.cli_wrapper import CliCommand, CliCommandArgs, StepCliExecutable from ..module_utils.params.ca_connection import CaConnectionParams @@ -90,6 +99,7 @@ def run_module(): exec=dict(type="str"), key_file=dict(type="path", required=True), output_file=dict(type="path"), + password=dict(type="str", no_log=True), password_file=dict(type="path", no_log=False), pid=dict(type="int"), pid_file=dict(type="path"), @@ -101,9 +111,14 @@ def run_module(): **CaConnectionParams.argument_spec, **argument_spec }, supports_check_mode=True) - CaConnectionParams(module).check() module_params = cast(Dict, module.params) + try: + CaConnectionParams(module).check() + check_mutually_exclusive(["password", "password_file"], module_params) + except TypeError as e: + module.fail_json(f"Parameter validation failed: {e}") + executable = StepCliExecutable(module, module_params["step_cli_executable"]) # Regular args @@ -112,7 +127,10 @@ def run_module(): renew_cliarg_map = {arg: f"--{arg.replace('_', '-')}" for arg in renew_cliargs} renew_args = CaConnectionParams.cli_args().join(CliCommandArgs( - ["ca", "renew", module_params["crt_file"], module_params["key_file"]], renew_cliarg_map)) + ["ca", "renew", module_params["crt_file"], module_params["key_file"]], + renew_cliarg_map, + {"password": "--password-file"} + )) renew_cmd = CliCommand(executable, renew_args) renew_res = renew_cmd.run(module) if "Your certificate has been saved in" in renew_res.stderr: diff --git a/plugins/modules/step_ca_token.py b/plugins/modules/step_ca_token.py index f49ee01e..80742ce2 100644 --- a/plugins/modules/step_ca_token.py +++ b/plugins/modules/step_ca_token.py @@ -77,8 +77,16 @@ - issuer description: The provisioner name to use. type: str + provisioner_password: + description: > + The password to encrypt or decrypt the one-time token generating key. + Will be passed to step-cli through a temporary file. + Mutually exclusive with I(password_file) + type: str provisioner_password_file: - description: The path to the file containing the password to decrypt the one-time token generating key. + description: > + The path to the file containing the password to decrypt the one-time token generating key. + Mutually exclusive with I(provisioner_password_file) type: path return_token: description: > @@ -137,8 +145,7 @@ """ from typing import cast, Dict, Any -from ansible.module_utils.common.validation import check_required_one_of -from ansible.module_utils.common.validation import check_mutually_exclusive +from ansible.module_utils.common.validation import check_required_one_of, check_mutually_exclusive from ansible.module_utils.basic import AnsibleModule from ..module_utils.cli_wrapper import CliCommandArgs, StepCliExecutable, CliCommand @@ -161,6 +168,7 @@ def run_module(): output_file=dict(type="path"), principal=dict(type="list", elements="str"), provisioner=dict(type="str", aliases=["issuer"]), + provisioner_password=dict(type="str", no_log=True), provisioner_password_file=dict(type="path", no_log=False), return_token=dict(type="bool"), revoke=dict(type="bool"), @@ -183,15 +191,11 @@ def run_module(): module_params = cast(Dict, module.params) try: - check_mutually_exclusive(["return_token", "output_file"], module.params) - except TypeError: - result["msg"] = "return_token and output_file cannot be specified at the same time" - module.fail_json(**result) - try: - check_required_one_of(["return_token", "output_file"], module.params) - except TypeError: - result["msg"] = "At least one of return_token and output_file must be specified" - module.fail_json(**result) + check_mutually_exclusive(["return_token", "output_file"], module_params) + check_required_one_of(["return_token", "output_file"], module_params) + check_mutually_exclusive(["provisioner_password", "provisioner_password_file"], module_params) + except TypeError as e: + module.fail_json(f"Parameter validation failed: {e}") executable = StepCliExecutable(module, module_params["step_cli_executable"]) @@ -204,7 +208,10 @@ def run_module(): token_cliarg_map = {arg: f"--{arg.replace('_', '-')}" for arg in token_cliargs} token_args = CaConnectionParams.cli_args().join(CliCommandArgs( - ["ca", "token", module_params["name"]], token_cliarg_map)) + ["ca", "token", module_params["name"]], + token_cliarg_map, + {"provisioner_password": "--provisioner-password-file"} + )) token_cmd = CliCommand(executable, token_args) token_res = token_cmd.run(module) diff --git a/tests/integration/targets/step_ca_certificate/tasks/main.yml b/tests/integration/targets/step_ca_certificate/tasks/main.yml index 10e5b8ed..3d2ff80e 100644 --- a/tests/integration/targets/step_ca_certificate/tasks/main.yml +++ b/tests/integration/targets/step_ca_certificate/tasks/main.yml @@ -3,191 +3,191 @@ key_file: "/tmp/key.pem" - block: - - name: Create normal certificate on CA - maxhoesel.smallstep.step_ca_certificate: - name: "127.0.0.1" - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - provisioner: "{{ ca_provisioner }}" - provisioner_password_file: "{{ ca_provisioner_password_file }}" - san: - - foo.bar - kty: RSA - size: 4096 - not_after: 3h + - name: Create normal certificate on CA + maxhoesel.smallstep.step_ca_certificate: + name: "127.0.0.1" + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + provisioner: "{{ ca_provisioner }}" + provisioner_password_file: "{{ ca_provisioner_password_file }}" + san: + - foo.bar + kty: RSA + size: 4096 + not_after: 3h - - name: Certificate is still present (idempotency check) - maxhoesel.smallstep.step_ca_certificate: - name: "127.0.0.1" - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - provisioner: "{{ ca_provisioner }}" - provisioner_password_file: "{{ ca_provisioner_password_file }}" - san: - - foo.bar - kty: RSA - size: 4096 - not_after: 3h - verify_roots: "/root/.step/certs/root_ca.crt" - register: cert_idempotency - - name: Check that cert did not change - ansible.builtin.assert: - that: not cert_idempotency.changed + - name: Certificate is still present (idempotency check) + maxhoesel.smallstep.step_ca_certificate: + name: "127.0.0.1" + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + provisioner: "{{ ca_provisioner }}" + provisioner_password_file: "{{ ca_provisioner_password_file }}" + san: + - foo.bar + kty: RSA + size: 4096 + not_after: 3h + verify_roots: "/root/.step/certs/root_ca.crt" + register: cert_idempotency + - name: Check that cert did not change + ansible.builtin.assert: + that: not cert_idempotency.changed - - name: Certificate stays the same if parameters are omitted - maxhoesel.smallstep.step_ca_certificate: - name: "127.0.0.1" - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - provisioner: "{{ ca_provisioner }}" - provisioner_password_file: "{{ ca_provisioner_password_file }}" - verify_roots: "/root/.step/certs/root_ca.crt" - register: cert_missing_parameters_idempotency - - name: Check that cert did not change - ansible.builtin.assert: - that: not cert_missing_parameters_idempotency.changed + - name: Certificate stays the same if parameters are omitted + maxhoesel.smallstep.step_ca_certificate: + name: "127.0.0.1" + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + provisioner: "{{ ca_provisioner }}" + provisioner_password_file: "{{ ca_provisioner_password_file }}" + verify_roots: "/root/.step/certs/root_ca.crt" + register: cert_missing_parameters_idempotency + - name: Check that cert did not change + ansible.builtin.assert: + that: not cert_missing_parameters_idempotency.changed - - name: Certificate gets reissued on force - maxhoesel.smallstep.step_ca_certificate: - force: true - name: "127.0.0.1" - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - provisioner: "{{ ca_provisioner }}" - provisioner_password_file: "{{ ca_provisioner_password_file }}" - san: - - foo.bar - kty: RSA - size: 4096 - not_after: 3h - verify_roots: "/root/.step/certs/root_ca.crt" - register: cert_force - - name: Check that cert changed - ansible.builtin.assert: - that: cert_force.changed + - name: Certificate gets reissued on force + maxhoesel.smallstep.step_ca_certificate: + force: true + name: "127.0.0.1" + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + provisioner: "{{ ca_provisioner }}" + provisioner_password: "{{ ca_provisioner_password }}" + san: + - foo.bar + kty: RSA + size: 4096 + not_after: 3h + verify_roots: "/root/.step/certs/root_ca.crt" + register: cert_force + - name: Check that cert changed + ansible.builtin.assert: + that: cert_force.changed - - name: Certificate gets reissued on SAN change - maxhoesel.smallstep.step_ca_certificate: - name: "127.0.0.1" - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - provisioner: "{{ ca_provisioner }}" - provisioner_password_file: "{{ ca_provisioner_password_file }}" - san: - - foo.bar - - another.san - kty: RSA - size: 4096 - not_after: 3h - verify_roots: "/root/.step/certs/root_ca.crt" - register: cert_san - - name: Check that cert changed - ansible.builtin.assert: - that: cert_san.changed + - name: Certificate gets reissued on SAN change + maxhoesel.smallstep.step_ca_certificate: + name: "127.0.0.1" + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + provisioner: "{{ ca_provisioner }}" + provisioner_password: "{{ ca_provisioner_password }}" + san: + - foo.bar + - another.san + kty: RSA + size: 4096 + not_after: 3h + verify_roots: "/root/.step/certs/root_ca.crt" + register: cert_san + - name: Check that cert changed + ansible.builtin.assert: + that: cert_san.changed - - name: Certificate gets reissued on size change - maxhoesel.smallstep.step_ca_certificate: - name: "127.0.0.1" - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - provisioner: "{{ ca_provisioner }}" - provisioner_password_file: "{{ ca_provisioner_password_file }}" - san: - - foo.bar - - another.san - kty: RSA - size: 2048 - not_after: 3h - verify_roots: "/root/.step/certs/root_ca.crt" - register: cert_size - - name: Check that cert changed - ansible.builtin.assert: - that: cert_size.changed + - name: Certificate gets reissued on size change + maxhoesel.smallstep.step_ca_certificate: + name: "127.0.0.1" + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + provisioner: "{{ ca_provisioner }}" + provisioner_password: "{{ ca_provisioner_password }}" + san: + - foo.bar + - another.san + kty: RSA + size: 2048 + not_after: 3h + verify_roots: "/root/.step/certs/root_ca.crt" + register: cert_size + - name: Check that cert changed + ansible.builtin.assert: + that: cert_size.changed - - name: Certificate gets reissued on kty change - maxhoesel.smallstep.step_ca_certificate: - name: "127.0.0.1" - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - provisioner: "{{ ca_provisioner }}" - provisioner_password_file: "{{ ca_provisioner_password_file }}" - san: - - foo.bar - - another.san - kty: EC - not_after: 3h - verify_roots: "/root/.step/certs/root_ca.crt" - register: cert_kty - - name: Check that cert changed - ansible.builtin.assert: - that: cert_kty.changed + - name: Certificate gets reissued on kty change + maxhoesel.smallstep.step_ca_certificate: + name: "127.0.0.1" + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + provisioner: "{{ ca_provisioner }}" + provisioner_password: "{{ ca_provisioner_password }}" + san: + - foo.bar + - another.san + kty: EC + not_after: 3h + verify_roots: "/root/.step/certs/root_ca.crt" + register: cert_kty + - name: Check that cert changed + ansible.builtin.assert: + that: cert_kty.changed - - name: Certificate gets reissued on crv change - maxhoesel.smallstep.step_ca_certificate: - name: "127.0.0.1" - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - provisioner: "{{ ca_provisioner }}" - provisioner_password_file: "{{ ca_provisioner_password_file }}" - san: - - foo.bar - - another.san - kty: EC - crv: P-521 - not_after: 3h - verify_roots: "/root/.step/certs/root_ca.crt" - register: cert_crv - - name: Check that cert changed - ansible.builtin.assert: - that: cert_crv.changed + - name: Certificate gets reissued on crv change + maxhoesel.smallstep.step_ca_certificate: + name: "127.0.0.1" + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + provisioner: "{{ ca_provisioner }}" + provisioner_password: "{{ ca_provisioner_password }}" + san: + - foo.bar + - another.san + kty: EC + crv: P-521 + not_after: 3h + verify_roots: "/root/.step/certs/root_ca.crt" + register: cert_crv + - name: Check that cert changed + ansible.builtin.assert: + that: cert_crv.changed - - name: Revoke certificate - maxhoesel.smallstep.step_ca_certificate: - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - state: revoked - register: revoked - - name: Check that cert got revoked - ansible.builtin.assert: - that: revoked.changed + - name: Revoke certificate + maxhoesel.smallstep.step_ca_certificate: + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + state: revoked + register: revoked + - name: Check that cert got revoked + ansible.builtin.assert: + that: revoked.changed - - name: Revoke certificate again - maxhoesel.smallstep.step_ca_certificate: - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - state: revoked - register: revoked_again - - name: Check that cert revocation didn't change - ansible.builtin.assert: - that: not revoked_again.changed + - name: Revoke certificate again + maxhoesel.smallstep.step_ca_certificate: + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + state: revoked + register: revoked_again + - name: Check that cert revocation didn't change + ansible.builtin.assert: + that: not revoked_again.changed - - name: Delete certificate - maxhoesel.smallstep.step_ca_certificate: - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - revoke_on_delete: true # already the default - state: absent - register: deleted - - name: Check that cert got deleted - ansible.builtin.assert: - that: revoked.changed + - name: Delete certificate + maxhoesel.smallstep.step_ca_certificate: + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + revoke_on_delete: true # already the default + state: absent + register: deleted + - name: Check that cert got deleted + ansible.builtin.assert: + that: revoked.changed - - name: Delete certificate again - maxhoesel.smallstep.step_ca_certificate: - crt_file: "{{ crt_file }}" - key_file: "{{ key_file }}" - revoke_on_delete: true # already the default - state: absent - register: deleted_again - - name: Check that cert did not change - ansible.builtin.assert: - that: not deleted_again.changed + - name: Delete certificate again + maxhoesel.smallstep.step_ca_certificate: + crt_file: "{{ crt_file }}" + key_file: "{{ key_file }}" + revoke_on_delete: true # already the default + state: absent + register: deleted_again + - name: Check that cert did not change + ansible.builtin.assert: + that: not deleted_again.changed always: - - name: Delete generated files - file: - path: "{{ item }}" - state: absent - loop: - - "{{ crt_file }}" - - "{{ key_file }}" + - name: Delete generated files + file: + path: "{{ item }}" + state: absent + loop: + - "{{ crt_file }}" + - "{{ key_file }}" diff --git a/tests/integration/targets/step_ca_provisioner/tasks/main.yml b/tests/integration/targets/step_ca_provisioner/tasks/main.yml index 4f898908..c969aa38 100644 --- a/tests/integration/targets/step_ca_provisioner/tasks/main.yml +++ b/tests/integration/targets/step_ca_provisioner/tasks/main.yml @@ -1,162 +1,166 @@ - block: - - name: Testing keys are present - copy: - src: "{{ item }}" - dest: "/tmp/" - owner: "{{ ca_user }}" - group: "{{ ca_user }}" - mode: 0600 - loop: - - tests_key - - tests_crt + - name: Testing keys are present + copy: + src: "{{ item }}" + dest: "/tmp/" + owner: "{{ ca_user }}" + group: "{{ ca_user }}" + mode: 0600 + loop: + - tests_key + - tests_crt - - name: Testing password file is present - copy: - content: "password-testing" - dest: "/tmp/tests_passfile" - owner: "{{ ca_user }}" - group: "{{ ca_user }}" - mode: 0644 # needs to be readable by the client requesting the cert + - name: Testing password file is present + copy: + content: "password-testing" + dest: "/tmp/tests_passfile" + owner: "{{ ca_user }}" + group: "{{ ca_user }}" + mode: 0644 # needs to be readable by the client requesting the cert - # The values for these test provisioners are mostly identical to the ones in the smallstep documentation, - # so many of the online provisioners do not actually work. Good enough to test our module - # functionality still. - - name: Create test provisioners - maxhoesel.smallstep.step_ca_provisioner: "{{ item | combine({'step_cli_executable': cli_binary}) }}" - loop: - - name: tests-JWK - type: JWK - password_file: "/tmp/tests_passfile" - create: yes - - name: tests-OIDC - type: OIDC - oidc_client_id: 1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com - oidc_configuration_endpoint: https://accounts.google.com/.well-known/openid-configuration - oidc_admin_email: - - mariano@smallstep.com - - max@smallstep.com - - name: tests-Amazon - type: AWS - aws_account: 123456789 - instance_age: 1h - - name: tests-Google - type: GCP - gcp_service_account: - - 1234567890-compute@developer.gserviceaccount.com - - 9876543210-compute@developer.gserviceaccount.com - gcp_project: - - identity - - accounting - - name: tests-ACME - type: ACME - - name: tests-x5c - type: X5C - x5c_root_file: "/tmp/tests_crt" - - name: tests-k8s - type: K8SSA - k8s_pem_keys_file: "/tmp/tests_crt" + # The values for these test provisioners are mostly identical to the ones in the smallstep documentation, + # so many of the online provisioners do not actually work. Good enough to test our module + # functionality still. + - name: Create test provisioners + maxhoesel.smallstep.step_ca_provisioner: "{{ item | combine({'step_cli_executable': cli_binary}) }}" + loop: + - name: tests-JWK + type: JWK + password_file: "/tmp/tests_passfile" + create: yes + - name: tests-JWK-passfile + type: JWK + password: "flightofthefirebird" + create: yes + - name: tests-OIDC + type: OIDC + oidc_client_id: 1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com + oidc_configuration_endpoint: https://accounts.google.com/.well-known/openid-configuration + oidc_admin_email: + - mariano@smallstep.com + - max@smallstep.com + - name: tests-Amazon + type: AWS + aws_account: 123456789 + instance_age: 1h + - name: tests-Google + type: GCP + gcp_service_account: + - 1234567890-compute@developer.gserviceaccount.com + - 9876543210-compute@developer.gserviceaccount.com + gcp_project: + - identity + - accounting + - name: tests-ACME + type: ACME + - name: tests-x5c + type: X5C + x5c_root_file: "/tmp/tests_crt" + - name: tests-k8s + type: K8SSA + k8s_pem_keys_file: "/tmp/tests_crt" + + - name: Test creation idempotency + maxhoesel.smallstep.step_ca_provisioner: "{{ item | combine({'step_cli_executable': cli_binary })}}" + loop: + - name: tests-JWK + type: JWK + create: yes + password_file: "/tmp/tests_passfile" + - name: tests-OIDC + type: OIDC + oidc_client_id: 1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com + oidc_configuration_endpoint: https://accounts.google.com/.well-known/openid-configuration + oidc_admin_email: + - mariano@smallstep.com + - max@smallstep.com + - name: tests-Amazon + type: AWS + aws_account: 123456789 + instance_age: 1h + - name: tests-Google + type: GCP + gcp_service_account: + - 1234567890-compute@developer.gserviceaccount.com + - 9876543210-compute@developer.gserviceaccount.com + gcp_project: + - identity + - accounting + - name: tests-ACME + type: ACME + - name: tests-x5c + type: X5C + x5c_root_file: "/tmp/tests_crt" + - name: tests-k8s + type: K8SSA + k8s_pem_keys_file: "/tmp/tests_crt" + register: second_run + + - name: Verify that nothing changed on the second run + assert: + that: not second_run.changed - - name: Test creation idempotency - maxhoesel.smallstep.step_ca_provisioner: "{{ item | combine({'step_cli_executable': cli_binary })}}" - loop: - - name: tests-JWK - type: JWK - create: yes - password_file: "/tmp/tests_passfile" - - name: tests-OIDC + - name: Test updating provisioners + maxhoesel.smallstep.step_ca_provisioner: + name: tests-OIDC type: OIDC oidc_client_id: 1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com oidc_configuration_endpoint: https://accounts.google.com/.well-known/openid-configuration oidc_admin_email: - mariano@smallstep.com - max@smallstep.com - - name: tests-Amazon - type: AWS - aws_account: 123456789 - instance_age: 1h - - name: tests-Google - type: GCP - gcp_service_account: - - 1234567890-compute@developer.gserviceaccount.com - - 9876543210-compute@developer.gserviceaccount.com - gcp_project: - - identity - - accounting - - name: tests-ACME - type: ACME - - name: tests-x5c - type: X5C - x5c_root_file: "/tmp/tests_crt" - - name: tests-k8s - type: K8SSA - k8s_pem_keys_file: "/tmp/tests_crt" - register: second_run - - - name: Verify that nothing changed on the second run - assert: - that: not second_run.changed - - - name: Test updating provisioners - maxhoesel.smallstep.step_ca_provisioner: - name: tests-OIDC - type: OIDC - oidc_client_id: 1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com - oidc_configuration_endpoint: https://accounts.google.com/.well-known/openid-configuration - oidc_admin_email: - - mariano@smallstep.com - - max@smallstep.com - - new@admin.com - state: "updated" - step_cli_executable: "{{ cli_binary }}" - register: update_test + - new@admin.com + state: "updated" + step_cli_executable: "{{ cli_binary }}" + register: update_test - - name: Verify that provisioner got updated - ansible.builtin.assert: - that: - update_test.changed + - name: Verify that provisioner got updated + ansible.builtin.assert: + that: update_test.changed - # Remove the online provisioners before restarting as they may imapct server - # functionality - - name: Remove online provisioners - maxhoesel.smallstep.step_ca_provisioner: - name: "{{ item.0 }}" - type: "{{ item.1 }}" - state: absent - step_cli_executable: "{{ cli_binary }}" - loop: - - ["tests-OIDC", "OIDC"] - - ["tests-Amazon", "AWS"] - - ["tests-Google", "GCP"] + # Remove the online provisioners before restarting as they may imapct server + # functionality + - name: Remove online provisioners + maxhoesel.smallstep.step_ca_provisioner: + name: "{{ item.0 }}" + type: "{{ item.1 }}" + state: absent + step_cli_executable: "{{ cli_binary }}" + loop: + - ["tests-OIDC", "OIDC"] + - ["tests-Amazon", "AWS"] + - ["tests-Google", "GCP"] - - name: Get Server PID - shell: pgrep -fa step-ca | grep -v step-ca.sh | cut -d ' ' -f 1 - register: _pid - - name: Reload Server - command: "kill -1 {{ _pid.stdout_lines[0] }}" - become: false + - name: Get Server PID + shell: pgrep -fa step-ca | grep -v step-ca.sh | cut -d ' ' -f 1 + register: _pid + - name: Reload Server + command: "kill -1 {{ _pid.stdout_lines[0] }}" + become: false - - name: Check server health - command: "{{ cli_binary }} ca health" - changed_when: no + - name: Check server health + command: "{{ cli_binary }} ca health" + changed_when: no - - name: Remove test provisioners - maxhoesel.smallstep.step_ca_provisioner: - name: "{{ item }}" - state: absent - step_cli_executable: "{{ cli_binary }}" - loop: - - "tests-JWK" - - "tests-ACME" - - "tests-x5c" - - "tests-k8s" + - name: Remove test provisioners + maxhoesel.smallstep.step_ca_provisioner: + name: "{{ item }}" + state: absent + step_cli_executable: "{{ cli_binary }}" + loop: + - "tests-JWK" + - "tests-JWK-passfile" + - "tests-ACME" + - "tests-x5c" + - "tests-k8s" - - name: Get step-ca config - command: "cat {{ ca_path }}/config/ca.json" - register: step_ca_config - - name: Verify that all provisioners are absent - assert: - that: - - (step_ca_config.stdout | from_json).authority.provisioners is not defined + - name: Get step-ca config + command: "cat {{ ca_path }}/config/ca.json" + register: step_ca_config + - name: Verify that all provisioners are absent + assert: + that: + - (step_ca_config.stdout | from_json).authority.provisioners is not defined become: yes become_user: "{{ ca_user }}" environment: diff --git a/tests/integration/targets/step_ca_renew/tasks/main.yml b/tests/integration/targets/step_ca_renew/tasks/main.yml index 4e27a183..15a7bdd9 100644 --- a/tests/integration/targets/step_ca_renew/tasks/main.yml +++ b/tests/integration/targets/step_ca_renew/tasks/main.yml @@ -12,6 +12,7 @@ maxhoesel.smallstep.step_ca_renew: crt_file: /tmp/generated_certificate key_file: /tmp/generated_key + password_file: "{{ ca_provisioner_password_file }}" expires_in: 5m force: yes register: early_renewal @@ -24,6 +25,7 @@ maxhoesel.smallstep.step_ca_renew: crt_file: /tmp/generated_certificate key_file: /tmp/generated_key + password: "{{ ca_provisioner_password }}" force: yes expires_in: 61m register: forced_renewal diff --git a/tests/integration/targets/step_ca_token/tasks/main.yml b/tests/integration/targets/step_ca_token/tasks/main.yml index b39818b2..a48cc106 100644 --- a/tests/integration/targets/step_ca_token/tasks/main.yml +++ b/tests/integration/targets/step_ca_token/tasks/main.yml @@ -9,3 +9,11 @@ - name: Verify that token got returned assert: that: generated_token.token + +- name: Test token creation with direct provisioner password + maxhoesel.smallstep.step_ca_token: + name: "127.0.0.1" + provisioner: "{{ ca_provisioner }}" + provisioner_password: "{{ ca_provisioner_password }}" + return_token: yes + register: generated_token