New certificate management module.

There is a new certificate management module placed in the plugins
folder:

    plugins/modules/ipacert.py

The certificate module allows to request, revoke, release and retrieve
certificates for users, hosts and services.

Here is the documentation for the module:

    README-cert.md

New example playbooks have been added:

    playbooks/cert/cert-hold.yml
    playbooks/cert/cert-release.yml
    playbooks/cert/cert-request-host.yml
    playbooks/cert/cert-request-service.yml
    playbooks/cert/cert-request-user.yml
    playbooks/cert/cert-retrieve.yml
    playbooks/cert/cert-revoke.yml

New tests for the module can be found at:

    tests/cert/test_cert_client_context.yml
    tests/cert/test_cert_host.yml
    tests/cert/test_cert_service.yml
    tests/cert/test_cert_user.yml

The module has been co-authored by Sam Morris (@yrro) and Rafael
Guterres Jeffman (@rjeffman).
This commit is contained in:
Sam Morris
2021-11-18 00:25:56 +00:00
committed by Rafael Guterres Jeffman
parent 180afd7586
commit 87e1edf575
16 changed files with 1610 additions and 1 deletions

175
README-cert.md Normal file
View File

@@ -0,0 +1,175 @@
Cert module
============
Description
-----------
The cert module makes it possible to request, revoke and retrieve SSL certificates for hosts, services and users.
Features
--------
* Certificate request
* Certificate hold/release
* Certificate revocation
* Certificate retrieval
Supported FreeIPA Versions
--------------------------
FreeIPA versions 4.4.0 and up are supported by the ipacert module.
Requirements
------------
**Controller**
* Ansible version: 2.8+
* Some tool to generate a certificate signing request (CSR) might be needed, like `openssl`.
**Node**
* Supported FreeIPA version (see above)
Usage
=====
Example inventory file
```ini
[ipaserver]
ipaserver.test.local
```
Example playbook to request a new certificate for a service:
```yaml
---
- name: Certificate request
hosts: ipaserver
tasks:
- name: Request a certificate for a web server
ipacert:
ipaadmin_password: SomeADMINpassword
state: requested
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIGYMEwCAQAwGTEXMBUGA1UEAwwOZnJlZWlwYSBydWxlcyEwKjAFBgMrZXADIQBs
HlqIr4b/XNK+K8QLJKIzfvuNK0buBhLz3LAzY7QDEqAAMAUGAytlcANBAF4oSCbA
5aIPukCidnZJdr491G4LBE+URecYXsPknwYb+V+ONnf5ycZHyaFv+jkUBFGFeDgU
SYaXm/gF8cDYjQI=
-----END CERTIFICATE REQUEST-----
principal: HTTP/www.example.com
register: cert
```
Example playbook to revoke an existing certificate:
```yaml
---
- name: Revoke certificate
hosts: ipaserver
tasks:
- name Revoke a certificate
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 123456789
state: revoked
```
Example to hold a certificate (alias for revoking a certificate with reason `certificateHold (6)`):
```yaml
---
- name: Hold a certificate
hosts: ipaserver
tasks:
- name: Hold certificate
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 0xAB1234
state: held
```
Example playbook to release hold of certificate (may be used with any revoked certificates, despite of the rovoke reason):
```yaml
---
- name: Release hold
hosts: ipaserver
tasks:
- name: Take a revoked certificate off hold
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 0xAB1234
state: released
```
Example playbook to retrieve a certificate and save it to a file in the target node:
```yaml
---
- name: Retriev certificate
hosts: ipaserver
tasks:
- name: Retrieve a certificate and save it to file 'cert.pem'
ipacert:
ipaadmin_password: SomeADMINpassword
certificate_out: cert.pem
state: retrieved
```
ipacert
-------
Variable | Description | Required
-------- | ----------- | --------
`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no
`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to yes. (bool) | no
`csr` | X509 certificate signing request, in PEM format. | yes, if `state: requested`
`principal` | Host/service/user principal for the certificate. | yes, if `state: requested`
`add` \| `add_principal` | Automatically add the principal if it doesn't exist (service principals only). (bool) | no
`profile_id` \| `profile` | Certificate Profile to use | no
`ca` | Name of the issuing certificate authority. | no
`chain` | Include certificate chain in output. (bool) | no
`serial_number` | Certificate serial number. (int) | yes, if `state` is `retrieved`, `held`, `released` or `revoked`.
`revocation_reason` \| `reason` | Reason for revoking the certificate. Use one of the reason strings, or the corresponding value: "unspecified" (0), "keyCompromise" (1), "cACompromise" (2), "affiliationChanged" (3), "superseded" (4), "cessationOfOperation" (5), "certificateHold" (6), "removeFromCRL" (8), "privilegeWithdrawn" (9), "aACompromise" (10) | yes, if `state: revoked`
`certificate_out` | Write certificate (chain if `chain` is set) to this file, on the target node. | no
`state` | The state to ensure. It can be one of `requested`, `held`, `released`, `revoked`, or `retrieved`. `held` is the same as revoke with reason "certificateHold" (6). `released` is the same as `cert-revoke-hold` on IPA CLI, releasing the hold status of a certificate. | yes
Return Values
=============
Values are returned only if `state` is `requested` or `retrieved` and if `certificate_out` is not defined.
Variable | Description | Returned When
-------- | ----------- | -------------
`certificate` | Certificate fields and data. (dict) <br>Options: | if `state` is `requested` or `retrieved` and if `certificate_out` is not defined
&nbsp; | `certificate` - Issued X509 certificate in PEM encoding. Will include certificate chain if `chain: true`. (list) | always
&nbsp; | `san_dnsname` - X509 Subject Alternative Name. | When DNSNames are present in the Subject Alternative Name extension of the issued certificate.
&nbsp; | `issuer` - X509 distinguished name of issuer. | always
&nbsp; | `subject` - X509 distinguished name of certificate subject. | always
&nbsp; | `serial_number` - Serial number of the issued certificate. (int) | always
&nbsp; | `revoked` - Revoked status of the certificate. (bool) | if certificate was revoked
&nbsp; | `owner_user` - The username that owns the certificate. | if `state: retrieved` and certificate is owned by a user
&nbsp; | `owner_host` - The host that owns the certificate. | if `state: retrieved` and certificate is owned by a host
&nbsp; | `owner_service` - The service that owns the certificate. | if `state: retrieved` and certificate is owned by a service
&nbsp; | `valid_not_before` - Time when issued certificate becomes valid, in GeneralizedTime format (YYYYMMDDHHMMSSZ) | always
&nbsp; | `valid_not_after` - Time when issued certificate ceases to be valid, in GeneralizedTime format (YYYYMMDDHHMMSSZ) | always
Authors
=======
Sam Morris
Rafael Jeffman

View File

@@ -17,6 +17,7 @@ Features
* Modules for automount key management
* Modules for automount location management
* Modules for automount map management
* Modules for certificate management
* Modules for config management
* Modules for delegation management
* Modules for dns config management
@@ -436,6 +437,7 @@ Modules in plugin/modules
* [ipaautomountkey](README-automountkey.md)
* [ipaautomountlocation](README-automountlocation.md)
* [ipaautomountmap](README-automountmap.md)
* [ipacert](README-cert.md)
* [ipaconfig](README-config.md)
* [ipadelegation](README-delegation.md)
* [ipadnsconfig](README-dnsconfig.md)

View File

@@ -0,0 +1,14 @@
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Temporarily hold a certificate
ipacert:
serial_number: 12345
state: held

View File

@@ -0,0 +1,15 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Release a certificate hold
ipacert:
serial_number: 12345
state: released

View File

@@ -0,0 +1,26 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Request a certificate for a host
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIIBWjCBxAIBADAbMRkwFwYDVQQDDBBob3N0LmV4YW1wbGUuY29tMIGfMA0GCSqG
SIb3DQEBAQUAA4GNADCBiQKBgQCzR3Vd4Cwl0uVgwB3+wxz+4JldFk3x526bPeuK
g8EEc+rEHILzJWeXC8ywCYPOgK9n7hrdMfVQiIx3yHYrY+0IYuLehWow4o1iJEf5
urPNAP9K9C4Y7MMXzzoQmoWR3IFQQpOYwvWOtiZfvrhmtflnYEGLE2tgz53gOQHD
NnbCCwIDAQABoAAwDQYJKoZIhvcNAQELBQADgYEAgF+6YC39WhnvmFgNz7pjAh5E
2ea3CgG+zrzAyiSBGG6WpXEjqMRnAQxciQNGxQacxjwWrscZidZzqg8URJPugewq
tslYB1+RkZn+9UWtfnWvz89+xnOgco7JlytnbH10Nfxt5fXXx13rY0tl54jBtk2W
422eYZ12wb4gjNcQy3A=
-----END CERTIFICATE REQUEST-----
principal: host/host.example.com
state: requested

View File

@@ -0,0 +1,23 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Request a certificate for a service
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIGYMEwCAQAwGTEXMBUGA1UEAwwOZnJlZWlwYSBydWxlcyEwKjAFBgMrZXADIQBs
HlqIr4b/XNK+K8QLJKIzfvuNK0buBhLz3LAzY7QDEqAAMAUGAytlcANBAF4oSCbA
5aIPukCidnZJdr491G4LBE+URecYXsPknwYb+V+ONnf5ycZHyaFv+jkUBFGFeDgU
SYaXm/gF8cDYjQI=
-----END CERTIFICATE REQUEST-----
principal: HTTP/www.example.com
add: true
state: requested

View File

@@ -0,0 +1,27 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Request a certificate for a user with a specific profile
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIIBejCB5AIBADAQMQ4wDAYDVQQDDAVwaW5reTCBnzANBgkqhkiG9w0BAQEFAAOB
jQAwgYkCgYEA7uChccy1Is1FTM0SF23WPYW472E3ozeLh2kzhKR9Ni6FLmeEGgu7
/hicR1VwvXHYkNwI1tpW9LqxRVvgr6vheqHySljrBcoRfshfYvKejp03l2327Bfq
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: pinky
profile: IECUserRoles
state: requested

View File

@@ -0,0 +1,16 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Retrieve a certificate
ipacert:
serial_number: 12345
state: retrieved
register: cert_retrieved

View File

@@ -0,0 +1,18 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Permanently revoke a certificate issued by a lightweight sub-CA
ipacert:
serial_number: 12345
ca: vpn-ca
# reason: keyCompromise (1)
reason: 1
state: revoked

View File

@@ -29,7 +29,8 @@ __all__ = ["gssapi", "netaddr", "api", "ipalib_errors", "Env",
"DEFAULT_CONFIG", "LDAP_GENERALIZED_TIME_FORMAT",
"kinit_password", "kinit_keytab", "run", "DN", "VERSION",
"paths", "tasks", "get_credentials_if_valid", "Encoding",
"load_pem_x509_certificate", "DNSName", "getargspec"]
"DNSName", "getargspec", "certificate_loader",
"write_certificate_list"]
import os
# ansible-freeipa requires locale to be C, IPA requires utf-8.
@@ -106,6 +107,7 @@ try:
except ImportError:
from ipalib.x509 import load_certificate
certificate_loader = load_certificate
from ipalib.x509 import write_certificate_list
# Try to import is_ipa_configured or use a fallback implementation.
try:

571
plugins/modules/ipacert.py Normal file
View File

@@ -0,0 +1,571 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Authors:
# Sam Morris <sam@robots.org.uk>
# Rafael Guterres Jeffman <rjeffman@redhat.com>
#
# Copyright (C) 2021 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
"metadata_version": "1.0",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = """
---
module: ipacert
short description: Manage FreeIPA certificates
description: Manage FreeIPA certificates
extends_documentation_fragment:
- ipamodule_base_docs
options:
csr:
description: |
X509 certificate signing request, in RFC 7468 PEM encoding.
Only available if `state: requested`, required if `csr_file` is not
provided.
type: str
csr_file:
description: |
Path to file with X509 certificate signing request, in RFC 7468 PEM
encoding. Only available if `state: requested`, required if `csr_file`
is not provided.
type: str
principal:
description: |
Host/service/user principal for the certificate.
Required if `state: requested`. Only available if `state: requested`.
type: str
add:
description: |
Automatically add the principal if it doesn't exist (service
principals only). Only available if `state: requested`.
type: bool
aliases: ["add_principal"]
required: false
ca:
description: Name of the issuing certificate authority.
type: str
required: false
serial_number:
description: |
Certificate serial number. Cannot be used with `state: requested`.
Required for all states, except `requested`.
type: int
profile:
description: Certificate Profile to use.
type: str
aliases: ["profile_id"]
required: false
revocation_reason:
description: |
Reason for revoking the certificate. Use one of the reason strings,
or the corresponding value: "unspecified" (0), "keyCompromise" (1),
"cACompromise" (2), "affiliationChanged" (3), "superseded" (4),
"cessationOfOperation" (5), "certificateHold" (6), "removeFromCRL" (8),
"privilegeWithdrawn" (9), "aACompromise" (10).
Use only if `state: revoked`. Required if `state: revoked`.
type: raw
aliases: ['reason']
certificate_out:
description: |
Write certificate (chain if `chain` is set) to this file, on the target
node.. Use only when `state` is `requested` or `retrieved`.
type: str
required: false
state:
description: |
The state to ensure. `held` is the same as revoke with reason
"certificateHold" (6). `released` is the same as `cert-revoke-hold`
on IPA CLI, releasing the hold status of a certificate.
choices: ["requested", "held", "released", "revoked", "retrieved"]
required: true
type: str
author:
authors:
- Sam Morris (@yrro)
- Rafael Guterres Jeffman (@rjeffman)
"""
EXAMPLES = """
- name: Request a certificate for a web server
ipacert:
ipaadmin_password: SomeADMINpassword
state: requested
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIGYMEwCAQAwGTEXMBUGA1UEAwwOZnJlZWlwYSBydWxlcyEwKjAFBgMrZXADIQBs
HlqIr4b/XNK+K8QLJKIzfvuNK0buBhLz3LAzY7QDEqAAMAUGAytlcANBAF4oSCbA
5aIPukCidnZJdr491G4LBE+URecYXsPknwYb+V+ONnf5ycZHyaFv+jkUBFGFeDgU
SYaXm/gF8cDYjQI=
-----END CERTIFICATE REQUEST-----
principal: HTTP/www.example.com
register: cert
- name: Request certificate for a user, with an appropriate profile.
ipacert:
ipaadmin_password: SomeADMINpassword
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIIBejCB5AIBADAQMQ4wDAYDVQQDDAVwaW5reTCBnzANBgkqhkiG9w0BAQEFAAOB
jQAwgYkCgYEA7uChccy1Is1FTM0SF23WPYW472E3ozeLh2kzhKR9Ni6FLmeEGgu7
/hicR1VwvXHYkNwI1tpW9LqxRVvgr6vheqHySljrBcoRfshfYvKejp03l2327Bfq
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: pinky
profile_id: IECUserRoles
state: requested
- name: Temporarily hold a certificate
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 12345
state: held
- name: Remove a certificate hold
ipacert:
ipaadmin_password: SomeADMINpassword
state: released
serial_number: 12345
- name: Permanently revoke a certificate issued by a lightweight sub-CA
ipacert:
ipaadmin_password: SomeADMINpassword
state: revoked
ca: vpn-ca
serial_number: 0x98765
reason: keyCompromise
- name: Retrieve a certificate
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 12345
state: retrieved
register: cert_retrieved
"""
RETURN = """
certificate:
description: Certificate fields and data.
returned: |
if `state` is `requested` or `retrived` and `certificate_out`
is not defined.
type: dict
contains:
certificate:
description: |
Issued X509 certificate in PEM encoding. Will include certificate
chain if `chain: true` is used.
type: list
elements: str
returned: always
issuer:
description: X509 distinguished name of issuer.
type: str
sample: CN=Certificate Authority,O=EXAMPLE.COM
returned: always
serial_number:
description: Serial number of the issued certificate.
type: int
sample: 902156300
returned: always
valid_not_after:
description: |
Time when issued certificate ceases to be valid,
in GeneralizedTime format (YYYYMMDDHHMMSSZ).
type: str
returned: always
valid_not_before:
description: |
Time when issued certificate becomes valid, in
GeneralizedTime format (YYYYMMDDHHMMSSZ).
type: str
returned: always
subject:
description: X509 distinguished name of certificate subject.
type: str
sample: CN=www.example.com,O=EXAMPLE.COM
returned: always
san_dnsname:
description: X509 Subject Alternative Name.
type: list
elements: str
sample: ['www.example.com', 'other.example.com']
returned: |
when DNSNames are present in the Subject Alternative Name
extension of the issued certificate.
revoked:
description: Revoked status of the certificate.
type: bool
returned: always
owner_user:
description: The username that owns the certificate.
type: str
returned: when `state` is `retrieved`
owner_host:
description: The host that owns the certificate.
type: str
returned: when `state` is `retrieved`
owner_service:
description: The service that owns the certificate.
type: str
returned: when `state` is `retrieved`
"""
import base64
import time
import ssl
from ansible.module_utils import six
from ansible.module_utils._text import to_text
from ansible.module_utils.ansible_freeipa_module import (
IPAAnsibleModule, certificate_loader, write_certificate_list,
)
if six.PY3:
unicode = str
# Reasons are defined in RFC 5280 sec. 5.3.1; removeFromCRL is not present in
# this list; run the module with state=released instead.
REVOCATION_REASONS = {
'unspecified': 0,
'keyCompromise': 1,
'cACompromise': 2,
'affiliationChanged': 3,
'superseded': 4,
'cessationOfOperation': 5,
'certificateHold': 6,
'removeFromCRL': 8,
'privilegeWithdrawn': 9,
'aACompromise': 10,
}
def gen_args(
module, principal=None, add_principal=None, ca=None, chain=None,
profile=None, certificate_out=None, reason=None
):
args = {}
if principal is not None:
args['principal'] = principal
if add_principal is not None:
args['add'] = add_principal
if ca is not None:
args['cacn'] = ca
if profile is not None:
args['profile_id'] = profile
if certificate_out is not None:
args['out'] = certificate_out
if chain:
args['chain'] = True
if ca:
args['cacn'] = ca
if reason is not None:
args['revocation_reason'] = get_revocation_reason(module, reason)
return args
def get_revocation_reason(module, reason):
"""Ensure revocation reasion is a valid integer code."""
reason_int = -1
try:
reason_int = int(reason)
except ValueError:
reason_int = REVOCATION_REASONS.get(reason, -1)
if reason_int not in REVOCATION_REASONS.values():
module.fail_json(msg="Invalid revocation reason: %s" % reason)
return reason_int
def parse_cert_timestamp(dt):
"""Ensure time is in GeneralizedTime format (YYYYMMDDHHMMSSZ)."""
return time.strftime(
"%Y%m%d%H%M%SZ",
time.strptime(dt, "%a %b %d %H:%M:%S %Y UTC")
)
def result_handler(_module, result, _command, _name, _args, exit_args, chain):
"""Split certificate into fields."""
if chain:
exit_args['certificate'] = [
ssl.DER_cert_to_PEM_cert(c)
for c in result['result'].get('certificate_chain', [])
]
else:
exit_args['certificate'] = [
ssl.DER_cert_to_PEM_cert(
base64.b64decode(result['result']['certificate'])
)
]
exit_args['san_dnsname'] = [
str(dnsname)
for dnsname in result['result'].get('san_dnsname', [])
]
exit_args.update({
key: result['result'][key]
for key in [
'issuer', 'subject', 'serial_number',
'revoked', 'revocation_reason'
]
if key in result['result']
})
exit_args.update({
key: result['result'][key][0]
for key in ['owner_user', 'owner_host', 'owner_service']
if key in result['result']
})
exit_args.update({
key: parse_cert_timestamp(result['result'][key])
for key in ['valid_not_after', 'valid_not_before']
if key in result['result']
})
def do_cert_request(
module, csr, principal, add_principal=None, ca=None, profile=None,
chain=None, certificate_out=None
):
"""Request a certificate."""
args = gen_args(
module, principal=principal, ca=ca, chain=chain,
add_principal=add_principal, profile=profile,
)
exit_args = {}
commands = [[to_text(csr), "cert_request", args]]
changed = module.execute_ipa_commands(
commands,
result_handler=result_handler,
exit_args=exit_args,
chain=chain
)
if certificate_out is not None:
certs = (
certificate_loader(cert.encode("utf-8"))
for cert in exit_args['certificate']
)
write_certificate_list(certs, certificate_out)
exit_args = {}
return changed, exit_args
def do_cert_revoke(ansible_module, serial_number, reason=None, ca=None):
"""Revoke a certificate."""
_ign, cert = do_cert_retrieve(ansible_module, serial_number, ca)
if not cert or cert.get('revoked', False):
return False, cert
args = gen_args(ansible_module, ca=ca, reason=reason)
commands = [[serial_number, "cert_revoke", args]]
changed = ansible_module.execute_ipa_commands(commands)
return changed, cert
def do_cert_release(ansible_module, serial_number, ca=None):
"""Release hold on certificate."""
_ign, cert = do_cert_retrieve(ansible_module, serial_number, ca)
revoked = cert.get('revoked', True)
reason = cert.get('revocation_reason', -1)
if cert and not revoked:
return False, cert
if revoked and reason != 6: # can only release held certificates
ansible_module.fail_json(
msg="Cannot release hold on certificate revoked with"
" reason: %d" % reason
)
args = gen_args(ansible_module, ca=ca)
commands = [[serial_number, "cert_remove_hold", args]]
changed = ansible_module.execute_ipa_commands(commands)
return changed, cert
def do_cert_retrieve(
module, serial_number, ca=None, chain=None, outfile=None
):
"""Retrieve a certificate with 'cert-show'."""
args = gen_args(module, ca=ca, chain=chain, certificate_out=outfile)
exit_args = {}
commands = [[serial_number, "cert_show", args]]
module.execute_ipa_commands(
commands,
result_handler=result_handler,
exit_args=exit_args,
chain=chain,
)
if outfile is not None:
exit_args = {}
return False, exit_args
def main():
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# requested
csr=dict(type="str"),
csr_file=dict(type="str"),
principal=dict(type="str"),
add_principal=dict(type="bool", required=False, aliases=["add"]),
profile_id=dict(type="str", aliases=["profile"], required=False),
# revoked
revocation_reason=dict(type="raw", aliases=["reason"]),
# general
serial_number=dict(type="int"),
ca=dict(type="str"),
chain=dict(type="bool", required=False),
certificate_out=dict(type="str", required=False),
# state
state=dict(
type="str",
required=True,
choices=[
"requested", "held", "released", "revoked", "retrieved"
]
),
),
mutually_exclusive=[["csr", "csr_file"]],
required_if=[
('state', 'requested', ['principal']),
('state', 'retrieved', ['serial_number']),
('state', 'held', ['serial_number']),
('state', 'released', ['serial_number']),
('state', 'revoked', ['serial_number', 'revocation_reason']),
],
supports_check_mode=False,
)
ansible_module._ansible_debug = True
# Get parameters
# requested
csr = ansible_module.params_get("csr")
csr_file = ansible_module.params_get("csr_file")
principal = ansible_module.params_get("principal")
add_principal = ansible_module.params_get("add_principal")
profile = ansible_module.params_get("profile_id")
# revoked
reason = ansible_module.params_get("revocation_reason")
# general
serial_number = ansible_module.params.get("serial_number")
ca = ansible_module.params_get("ca")
chain = ansible_module.params_get("chain")
certificate_out = ansible_module.params_get("certificate_out")
# state
state = ansible_module.params_get("state")
# Check parameters
if ansible_module.params_get("ipaapi_context") == "server":
ansible_module.fail_json(
msg="Context 'server' for ipacert is not yet supported."
)
invalid = []
if state == "requested":
invalid = ["serial_number", "revocation_reason"]
if csr is None and csr_file is None:
ansible_module.fail_json(
msg="Required 'csr' or 'csr_file' with 'state: requested'.")
else:
invalid = [
"csr", "principal", "add_principal", "profile"
"certificate_out"
]
if state in ["released", "held"]:
invalid.extend(["revocation_reason", "certificate_out", "chain"])
if state == "retrieved":
invalid.append("revocation_reason")
if state == "revoked":
invalid.extend(["certificate_out", "chain"])
elif state == "held":
reason = 6 # certificateHold
ansible_module.params_fail_used_invalid(invalid, state)
# Init
changed = False
exit_args = {}
# Connect to IPA API
# If executed on 'server' contexot, cert plugin uses the IPA RA agent
# TLS client certificate/key, which users are not able to access,
# resulting in a 'permission denied' exception when attempting to connect
# the CA service. Therefore 'client' context in forced for this module.
with ansible_module.ipa_connect(context="client"):
if state == "requested":
if csr_file is not None:
with open(csr_file, "rt") as csr_in:
csr = "".join(csr_in.readlines())
changed, exit_args = do_cert_request(
ansible_module,
csr,
principal,
add_principal,
ca,
profile,
chain,
certificate_out
)
elif state in ("held", "revoked"):
changed, exit_args = do_cert_revoke(
ansible_module, serial_number, reason, ca)
elif state == "released":
changed, exit_args = do_cert_release(
ansible_module, serial_number, ca)
elif state == "retrieved":
changed, exit_args = do_cert_retrieve(
ansible_module, serial_number, ca, chain, certificate_out)
# Done
ansible_module.exit_json(changed=changed, certificate=exit_args)
if __name__ == "__main__":
main()

View File

@@ -60,6 +60,7 @@ disable =
[pylint.BASIC]
good-names =
ex, i, j, k, Run, _, e, x, dn, cn, ip, os, unicode, __metaclass__, ds,
dt, ca,
# These are utils tools, and not part of the released collection.
galaxyfy-playbook, galaxyfy-README, galaxyfy-module-EXAMPLES,
module_EXAMPLES

View File

@@ -0,0 +1,60 @@
---
- name: Test cert
hosts: ipaclients, ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_contetx: "{{ ipa_context | default(omit) }}"
tasks:
- name: Include FreeIPA facts.
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
# Test will only be executed if host is not a server.
- name: Execute with server context in the client.
ipacert:
ipaapi_context: server
name: ThisShouldNotWork
register: result
failed_when: not (result.failed and result.msg is regex("No module named '*ipaserver'*"))
when: ipa_host_is_client
# Import basic module tests, and execute with ipa_context set to 'client'.
# If ipaclients is set, it will be executed using the client, if not,
# ipaserver will be used.
#
# With this setup, tests can be executed against an IPA client, against
# an IPA server using "client" context, and ensure that tests are executed
# in upstream CI.
- name: Test host certs using client context, in client host.
ansible.builtin.import_playbook: test_cert_host.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test service certs using client context, in client host.
ansible.builtin.import_playbook: test_cert_service.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test user certs using client context, in client host.
ansible.builtin.import_playbook: test_cert_user.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test host certs using client context, in server host.
ansible.builtin.import_playbook: test_cert_host.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']
- name: Test service certs using client context, in server host.
ansible.builtin.import_playbook: test_cert_service.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']
- name: Test user certs using client context, in server host.
ansible.builtin.import_playbook: test_cert_user.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']

View File

@@ -0,0 +1,214 @@
---
- name: Test host certificate requests
hosts: "{{ ipa_test_host | default('ipaserver') }}"
become: false
gather_facts: false
module_defaults:
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipacert:
ipaadmin_password: SomeADMINpassword
# ipacert only supports client context
ipaapi_context: "client"
tasks:
# SETUP
- name: Ensure test files do not exist
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/host.csr"
# Ensure test items exist
- name: Ensure domain name is set
ansible.builtin.set_fact:
ipa_domain: ipa.test
when: ipa_domain is not defined
- name: Ensure test host exists
ipahost:
name: "certhost.{{ ipa_domain }}"
state: present
force: true
- name: Create CSR
ansible.builtin.shell:
cmd: "openssl req -newkey rsa:1024 -keyout /dev/null -nodes -subj /CN=certhost.{{ ipa_domain }}"
register: host_req
- name: Create CSR file
ansible.builtin.copy:
dest: "/root/host.csr"
content: "{{ host_req.stdout }}"
mode: 0644
# TESTS
- name: Request certificate for host
ipacert:
csr: '{{ host_req.stdout }}'
principal: "host/certhost.{{ ipa_domain }}"
state: requested
register: host_cert
failed_when: not host_cert.changed or host_cert.failed
- name: Display data from the requested certificate.
ansible.builtin.debug:
var: host_cert
- name: Retrieve certificate for host
ipacert:
serial_number: "{{ host_cert.certificate.serial_number }}"
state: retrieved
register: retrieved
failed_when: retrieved.certificate.serial_number != host_cert.certificate.serial_number
- name: Display data from the retrieved certificate.
ansible.builtin.debug:
var: retrieved
- name: Place certificate on hold
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: held
register: result
failed_when: not result.changed or result.failed
- name: Place certificate on hold, again
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: held
register: result
failed_when: result.changed or result.failed
- name: Release hold on certificate
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.changed or result.failed
- name: Release hold on certificate, again
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: released
register: result
failed_when: result.changed or result.failed
- name: Revoke certificate
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: not result.changed or result.failed
- name: Revoke certificate, again
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: result.changed or result.failed
- name: Try to revoke inexistent certificate
ipacert:
serial_number: 0x123456789
reason: 9
state: revoked
register: result
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or "Certificate serial number 0x123456789 not found" in result.msg))
- name: Try to release revoked certificate
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.failed or "Cannot release hold on certificate revoked with reason" not in result.msg
- name: Request certificate for host and save to file
ipacert:
csr: '{{ host_req.stdout }}'
principal: "host/certhost.{{ ipa_domain }}"
certificate_out: "/root/cert_1.pem"
state: requested
register: result
failed_when: not result.changed or result.failed or result.certificate
- name: Check requested certificate file
ansible.builtin.file:
path: "/root/cert_1.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Retrieve certificate for host to a file
ipacert:
serial_number: "{{ host_cert.certificate.serial_number }}"
certificate_out: "/root/retrieved.pem"
state: retrieved
register: result
failed_when: result.changed or result.failed or result.certificate
- name: Check retrieved certificate file
ansible.builtin.file:
path: "/root/retrieved.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Request with invalid CSR.
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: "host/certhost.{{ ipa_domain }}"
state: requested
register: result
failed_when: not (result.failed and "Failure decoding Certificate Signing Request" in result.msg)
- name: Request certificate using a file
ipacert:
csr_file: "/root/host.csr"
principal: "host/certhost.{{ ipa_domain }}"
state: requested
register: result
failed_when: not result.changed or result.failed
- name: Request certificate using an invalid profile
ipacert:
csr_file: "/root/host.csr"
principal: "host/certhost.{{ ipa_domain }}"
profile: invalid_profile
state: requested
register: result
failed_when: not (result.failed and "Request failed with status 400" in result.msg)
# CLEANUP TEST ITEMS
- name: Removet test host
ipahost:
name: "certhost.{{ ipa_domain }}"
state: absent
- name: Ensure test files do not exist
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/host.csr"

View File

@@ -0,0 +1,232 @@
---
- name: Test service certificate requests
hosts: "{{ ipa_test_host | default('ipaserver') }}"
# Change "become" or "gather_facts" to "yes",
# if you test playbook requires any.
become: false
gather_facts: false
module_defaults:
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipacert:
ipaadmin_password: SomeADMINpassword
# ipacert only supports client context
ipaapi_context: "client"
tasks:
# SETUP
- name: Ensure test files do not exist
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/service.csr"
# Ensure test items exist
- name: Ensure domain name is set
ansible.builtin.set_fact:
ipa_domain: ipa.test
when: ipa_domain is not defined
- name: Ensure test host exist
ipahost:
name: "certservice.{{ ipa_domain }}"
force: true
state: present
- name: Ensure service exist
ipaservice:
name: "HTTP/certservice.{{ ipa_domain }}"
force: true
state: present
- name: Create signing request for certificate
ansible.builtin.shell:
cmd: "openssl req -newkey rsa:1024 -keyout /dev/null -nodes -subj /CN=certservice.{{ ipa_domain }}"
register: service_req
- name: Create CSR file
ansible.builtin.copy:
dest: "/root/service.csr"
content: "{{ service_req.stdout }}"
mode: '0644'
# TESTS
- name: Request certificate for service
ipacert:
csr: '{{ service_req.stdout }}'
principal: "HTTP/certservice.{{ ipa_domain }}"
add_principal: true
state: requested
register: service_cert
failed_when: not service_cert.changed or service_cert.failed
- name: Display data from the requested certificate.
ansible.builtin.debug:
var: service_cert
- name: Retrieve certificate for service
ipacert:
serial_number: "{{ service_cert.certificate.serial_number }}"
state: retrieved
register: retrieved
failed_when: retrieved.certificate.serial_number != service_cert.certificate.serial_number
- name: Display data from the retrieved certificate.
ansible.builtin.debug:
var: retrieved
- name: Place certificate on hold
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: held
register: result
failed_when: not result.changed or result.failed
- name: Place certificate on hold, again
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: held
register: result
failed_when: result.changed or result.failed
- name: Release hold on certificate
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.changed or result.failed
- name: Release hold on certificate, again
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: released
register: result
failed_when: result.changed or result.failed
- name: Revoke certificate
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: not result.changed or result.failed
- name: Revoke certificate, again
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: result.changed or result.failed
- name: Try to revoke inexistent certificate
ipacert:
serial_number: 0x123456789
reason: 9
state: revoked
register: result
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or "Certificate serial number 0x123456789 not found" in result.msg))
- name: Try to release revoked certificate
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.failed or "Cannot release hold on certificate revoked with reason" not in result.msg
- name: Request certificate for service and save to file
ipacert:
csr: '{{ service_req.stdout }}'
principal: "HTTP/certservice.{{ ipa_domain }}"
add_principal: true
certificate_out: "/root/cert_1.pem"
state: requested
register: result
failed_when: not result.changed or result.failed or result.certificate
- name: Check requested certificate file
ansible.builtin.file:
path: "/root/cert_1.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Retrieve certificate for service to a file
ipacert:
serial_number: "{{ service_cert.certificate.serial_number }}"
certificate_out: "/root/retrieved.pem"
state: retrieved
register: result
failed_when: result.changed or result.failed or result.certificate
- name: Check retrieved certificate file
ansible.builtin.file:
path: "/root/retrieved.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Request with invalid CSR.
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: "HTTP/certservice.{{ ipa_domain }}"
state: requested
register: result
failed_when: not (result.failed and "Failure decoding Certificate Signing Request" in result.msg)
- name: Request certificate using a file
ipacert:
csr_file: "/root/service.csr"
principal: "HTTP/certservice.{{ ipa_domain }}"
state: requested
register: result
failed_when: not result.changed or result.failed
- name: Request certificate using an invalid profile
ipacert:
csr_file: "/root/service.csr"
principal: "HTTP/certservice.{{ ipa_domain }}"
profile: invalid_profile
state: requested
register: result
failed_when: not (result.failed and "Request failed with status 400" in result.msg)
# CLEANUP TEST ITEMS
- name: Remove test service
ipaservice:
name: "HTTP/certservice.{{ ipa_domain }}"
state: absent
continue: true
- name: Remove test host
ipahost:
name: certservice.example.com
state: absent
- name: Ensure test files do not exist
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/service.csr"

View File

@@ -0,0 +1,213 @@
---
- name: Test user certificate requests
hosts: "{{ ipa_test_host | default('ipaserver') }}"
become: false
gather_facts: false
module_defaults:
ipauser:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipacert:
ipaadmin_password: SomeADMINpassword
# ipacert only supports client context
ipaapi_context: "client"
tasks:
# Ensure test files do not exist
- name: Check retrieved certificate file
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/user.csr"
# Ensure test items exist.
- name: Ensure test user exists
ipauser:
name: certuser
first: certificate
last: user
- name: Crete CSR
ansible.builtin.shell:
cmd:
'openssl req -newkey rsa:1024 -keyout /dev/null -nodes -subj /CN=certuser -reqexts IECUserRoles
-config <(cat /etc/pki/tls/openssl.cnf; printf "[IECUserRoles]\n1.2.840.10070.8.1=ASN1:UTF8String:hello world")'
executable: /bin/bash
register: user_req
- name: Create CSR file
ansible.builtin.copy:
dest: "/root/user.csr"
content: "{{ user_req.stdout }}"
mode: 0644
# TESTS
- name: Request certificate for user
ipacert:
csr: '{{ user_req.stdout }}'
principal: certuser
profile: IECUserRoles
state: requested
register: user_cert
failed_when: not user_cert.changed or user_cert.failed
- name: Display data from the requested certificate.
ansible.builtin.debug:
var: user_cert
- name: Retrieve certificate for user
ipacert:
serial_number: "{{ user_cert.certificate.serial_number }}"
state: retrieved
register: retrieved
failed_when: retrieved.certificate.serial_number != user_cert.certificate.serial_number
- name: Display data from the retrieved certificate.
ansible.builtin.debug:
var: retrieved
- name: Place certificate on hold
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: held
register: result
failed_when: not result.changed or result.failed
- name: Place certificate on hold, again
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: held
register: result
failed_when: result.changed or result.failed
- name: Release hold on certificate
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.changed or result.failed
- name: Release hold on certificate, again
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: released
register: result
failed_when: result.changed or result.failed
- name: Revoke certificate
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: not result.changed or result.failed
- name: Revoke certificate, again
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: result.changed or result.failed
- name: Try to revoke inexistent certificate
ipacert:
serial_number: 0x123456789
reason: 9
state: revoked
register: result
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or "Certificate serial number 0x123456789 not found" in result.msg))
- name: Try to release revoked certificate
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.failed or "Cannot release hold on certificate revoked with reason" not in result.msg
- name: Request certificate for user and save to file
ipacert:
csr: '{{ user_req.stdout }}'
principal: certuser
profile: IECUserRoles
certificate_out: "/root/cert_1.pem"
state: requested
register: result
failed_when: not result.changed or result.failed or result.certificate
- name: Check requested certificate file
ansible.builtin.file:
path: "/root/cert_1.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Retrieve certificate for user to a file
ipacert:
serial_number: "{{ user_cert.certificate.serial_number }}"
certificate_out: "/root/retrieved.pem"
state: retrieved
register: result
failed_when: result.changed or result.failed or result.certificate
- name: Check retrieved certificate file
ansible.builtin.file:
path: "/root/retrieved.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Request with invalid CSR.
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: certuser
state: requested
register: result
failed_when: not (result.failed and "Failure decoding Certificate Signing Request" in result.msg)
- name: Request certificate using a file
ipacert:
csr_file: "/root/user.csr"
principal: certuser
state: requested
register: result
failed_when: not result.changed or result.failed
- name: Request certificate using an invalid profile
ipacert:
csr_file: "/root/user.csr"
principal: certuser
profile: invalid_profile
state: requested
register: result
failed_when: not (result.failed and "Request failed with status 400" in result.msg)
# CLEANUP TEST ITEMS
- name: Remove test user
ipauser:
name: certuser
state: absent
- name: Check retrieved certificate file
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/user.csr"