Skip to content

Commit

Permalink
feat(step_ca_renew): add ca_renew module (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxhoesel committed Mar 29, 2021
1 parent a152d58 commit 6c6be57
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 0 deletions.
131 changes: 131 additions & 0 deletions plugins/modules/step_ca_renew.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2021, Max Hösel <ansible@maxhoesel.de>
# 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
__metaclass__ = type

DOCUMENTATION = r"""
---
module: step_ca_renew
author: Max Hösel (@maxhoesel)
short_description: Renew a valid certificate
version_added: '0.3.0'
description: Renew a valid certificate
requirements:
- A C(step-ca) server, either remote or local
notes:
- Check mode is supported.
options:
crt_file:
description: The certificate in PEM format that we want to renew.
required: yes
type: path
expires_in:
description: >
The amount of time remaining before certificate expiration, at which point a renewal should be attempted.
The certificate renewal will not be performed if the time to expiration is greater than the I(expires_in) value.
A random jitter (duration/20) will be added to avoid multiple services hitting the renew endpoint at the same time.
The duration is a sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m".
Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
type: str
force:
description: Force the overwrite of files without asking.
type: bool
exec:
description: The command to run after the certificate has been renewed.
type: str
key_file:
description: They key file of the certificate.
required: yes
type: path
output_file:
description: The new certificate file path. Defaults to overwriting the crt-file positional argument.
type: path
password_file:
description: The path to the file containing the password to encrypt or decrypt the private key.
type: path
pid:
description: >
The process id to signal after the certificate has been renewed. By default the the SIGHUP (1) signal will be used,
but this can be configured with the I(signal) parameter.
type: int
pid_file:
description: >
The path from which to read the process id that will be signaled after the certificate has been renewed.
By default the the SIGHUP (1) signal will be used, but this can be configured with the I(signal) parameter.
type: path
signal:
description: >
The signal number to send to the selected PID, so it can reload the configuration and load the new certificate.
Default value is SIGHUP (1).
type: int
extends_documentation_fragment:
- maxhoesel.smallstep.step_cli
- maxhoesel.smallstep.ca_remote_local
"""

EXAMPLES = r"""
# See https://smallstep.com/docs/step-cli/reference/ca/renew for more examples
- name: Renew a certificate
maxhoesel.smallstep.step_ca_renew:
crt_file: internal.crt
key_file: internal.key
ca_url: https://ca.smallstep.com:9000
force: yes
"""

from ansible.module_utils.basic import AnsibleModule
from ..module_utils.validation import check_step_cli_install
from ..module_utils.run import run_step_cli_command


def run_module():
module_args = dict(
crt_file=dict(type="path", required=True),
expires_in=dict(type="str"),
force=dict(type="bool"),
exec=dict(type="str"),
key_file=dict(type="path", required=True),
output_file=dict(type="path"),
password_file=dict(type="path", no_log=False),
pid=dict(type="int"),
pid_file=dict(type="path"),
signal=dict(type="int"),
root=dict(type="path"),
step_cli_executable=dict(type="path", default="step-cli"),
ca_url=dict(type="str"),
ca_config=dict(type="path"),
offline=dict(type="bool"),
)
result = dict(changed=False, stdout="", stderr="", msg="")
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)

check_step_cli_install(module, module.params["step_cli_executable"], result)

# Positional Parameters
params = ["ca", "renew", module.params["crt_file"], module.params["key_file"]]
# Regular args
args = ["expires_in", "force", "exec", "output_file", "password_file", "pid", "pid_file",
"signal", "root", "ca_url", "ca_config", "offline"]
# All parameters can be converted to a mapping by just appending -- and replacing the underscores
args = {arg: "--{a}".format(a=arg.replace("_", "-")) for arg in args}

result = run_step_cli_command(
module.params["step_cli_executable"], params,
module, result, args
)
if "Your certificate has been saved in" in result["stderr"]:
result["changed"] = True
module.exit_json(**result)


def main():
run_module()


if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions tests/integration/targets/step_ca_renew/files/tests_crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBaDCCAQ2gAwIBAgIQFMqlUCL64sx6g0MV8OZnhDAKBggqhkjOPQQDAjASMRAw
DgYDVQQDEwdyb290LWNhMB4XDTIwMTExMDIzMzUwMloXDTMwMTEwODIzMzUwMlow
EjEQMA4GA1UEAxMHcm9vdC1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAtY
ZTIL4F+MjNgdrtshVTQ6Kc/yPB8/tfm+i0CXMNq/133ygQTeuZtbC7sZUpLGK86t
rLadRmwJjrcpW+alYy2jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG
AQH/AgEBMB0GA1UdDgQWBBQYL8oI1OD5GwqHsrc1Nh+aTc2dEjAKBggqhkjOPQQD
AgNJADBGAiEAxN0zGIjVNrs0ifLYJz3p+LUv1sl+ACkgHLjo/C5gIwcCIQDu+iJZ
x27/c6kqM6jTz2vrQY21ylCfq9KN5Zw23F3qSw==
-----END CERTIFICATE-----
5 changes: 5 additions & 0 deletions tests/integration/targets/step_ca_renew/files/tests_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIET5OC19GaXMueCJM6GPSvUKMfnwa4FCZMps/2VhmC/+oAoGCCqGSM49
AwEHoUQDQgAEC1hlMgvgX4yM2B2u2yFVNDopz/I8Hz+1+b6LQJcw2r/XffKBBN65
m1sLuxlSksYrzq2stp1GbAmOtylb5qVjLQ==
-----END EC PRIVATE KEY-----
2 changes: 2 additions & 0 deletions tests/integration/targets/step_ca_renew/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
- setup_smallstep
101 changes: 101 additions & 0 deletions tests/integration/targets/step_ca_renew/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
- name: Testing keys are present
copy:
src: "{{ item }}"
dest: "/tmp/"
owner: "{{ step_ca_user }}"
group: "{{ step_ca_user }}"
mode: 0600
become: yes
become_user: "{{ step_ca_user }}"
loop:
- tests_key
- tests_crt
- name: Testing password file is present
copy:
content: "{{ step_ca_password }}"
dest: "/tmp/tests_passfile"
owner: "{{ step_ca_user }}"
group: "{{ step_ca_user }}"
mode: 0600
become: yes
become_user: "{{ step_ca_user }}"
- name: Create test provisioners
maxhoesel.smallstep.step_ca_provisioner: "{{ item }}"
become: yes
become_user: "{{ step_ca_user }}"
environment:
STEPPATH: "{{ step_ca_path }}"
loop:
- name: tests-JWK
type: JWK
jwk_password_file: "/tmp/tests_passfile"
- name: Reload Server
service:
name: step-ca
state: reloaded

- name: Get token for certificate creation
maxhoesel.smallstep.step_ca_token:
name: "127.0.0.1"
provisioner: tests-JWK
provisioner_password_file: "/tmp/tests_passfile"
return_token: yes
become: yes
become_user: "{{ step_ca_user }}"
register: generated_token
environment:
STEPPATH: "{{ step_ca_path }}"
- name: Create certificate with token
maxhoesel.smallstep.step_ca_certificate:
name: "127.0.0.1"
crt_file: /tmp/generated_certificate
key_file: /tmp/generated_key
provisioner: tests-JWK
provisioner_password_file: "/tmp/tests_passfile"
force: yes
not_after: 1h
token: "{{ generated_token.token }}"
ca_url: "{{ step_ca_url }}"

- name: Try to renew early
maxhoesel.smallstep.step_ca_renew:
crt_file: /tmp/generated_certificate
key_file: /tmp/generated_key
ca_url: "{{ step_ca_url }}"
expires_in: 5m
force: yes
register: early_renewal
- name: Verify that early renew didn't change anything
assert:
that: not early_renewal.changed
- name: Force renewal of the cert
maxhoesel.smallstep.step_ca_renew:
crt_file: /tmp/generated_certificate
key_file: /tmp/generated_key
ca_url: "{{ step_ca_url }}"
force: yes
expires_in: 61m
register: forced_renewal
- name: Verify that forced renewal worked
assert:
that: forced_renewal.changed

- name: Delete generated files
file:
path: "{{ item }}"
state: absent
loop:
- /tmp/generated_certificate
- /tmp/generated_key
- /tmp/tests_passfile
- name: Remove test provisioners
maxhoesel.smallstep.step_ca_provisioner:
name: "{{ item.0 }}"
type: "{{ item.1 }}"
state: absent
become: yes
become_user: "{{ step_ca_user }}"
environment:
STEPPATH: "{{ step_ca_path }}"
loop:
- ["tests-JWK", "JWK"]

0 comments on commit 6c6be57

Please sign in to comment.