diff --git a/roles/ipaclient/library/ipaclient_api.py b/roles/ipaclient/library/ipaclient_api.py index 7d4b8298715aa3fa3fc0f4f9571d5dcd7b37036e..9193f603b02e42f50e16b4c551e13a75a477f7dd 100644 --- a/roles/ipaclient/library/ipaclient_api.py +++ b/roles/ipaclient/library/ipaclient_api.py @@ -55,6 +55,10 @@ options: type: bool required: no default: no + krb_name: + description: The krb5 config file name + type: str + required: yes author: - Thomas Woerner (@t-woerner) ''' @@ -65,6 +69,7 @@ EXAMPLES = ''' servers: ["server1.example.com","server2.example.com"] domain: example.com hostname: client1.example.com + krb_name: /tmp/tmpkrb5.conf register: result_ipaclient_api ''' @@ -99,6 +104,7 @@ def main(): realm=dict(required=True, type='str'), hostname=dict(required=True, type='str'), debug=dict(required=False, type='bool', default="false"), + krb_name=dict(required=True, type='str'), ), supports_check_mode=False, ) @@ -110,9 +116,11 @@ def main(): realm = module.params.get('realm') hostname = module.params.get('hostname') debug = module.params.get('debug') + krb_name = module.params.get('krb_name') host_principal = 'host/%s@%s' % (hostname, realm) os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE + os.environ['KRB5_CONFIG'] = krb_name ca_certs = x509.load_certificate_list_from_file(paths.IPA_CA_CRT) if 40500 <= NUM_VERSION < 40590: diff --git a/roles/ipaclient/library/ipaclient_join.py b/roles/ipaclient/library/ipaclient_join.py index 5d41a546a9dc00941a186ccd69129aca3a6a75f0..68379ea70fc885e9b28c7ab16f007cb0ff1a3ef1 100644 --- a/roles/ipaclient/library/ipaclient_join.py +++ b/roles/ipaclient/library/ipaclient_join.py @@ -46,10 +46,6 @@ options: type: list elements: str required: yes - domain: - description: Primary DNS domain of the IPA deployment - type: str - required: yes realm: description: Kerberos realm name of the IPA deployment type: str @@ -58,10 +54,6 @@ options: description: Fully qualified name of this host type: str required: yes - kdc: - description: The name or address of the host running the KDC - type: str - required: yes basedn: description: The basedn of the IPA server (of the form dc=example,dc=com) type: str @@ -102,6 +94,10 @@ options: description: Turn on extra debugging type: bool required: no + krb_name: + description: The krb5 config file name + type: str + required: yes author: - Thomas Woerner (@t-woerner) ''' @@ -111,27 +107,25 @@ EXAMPLES = ''' - name: Join IPA in force mode with maximum 5 kinit attempts ipaclient_join: servers: ["server1.example.com","server2.example.com"] - domain: example.com realm: EXAMPLE.COM - kdc: server1.example.com basedn: dc=example,dc=com hostname: client1.example.com principal: admin password: MySecretPassword force_join: yes kinit_attempts: 5 + krb_name: /tmp/tmpkrb5.conf # Join IPA to get the keytab using ipadiscovery return values - name: Join IPA ipaclient_join: servers: "{{ ipadiscovery.servers }}" - domain: "{{ ipadiscovery.domain }}" realm: "{{ ipadiscovery.realm }}" - kdc: "{{ ipadiscovery.kdc }}" basedn: "{{ ipadiscovery.basedn }}" hostname: "{{ ipadiscovery.hostname }}" principal: admin password: MySecretPassword + krb_name: /tmp/tmpkrb5.conf ''' RETURN = ''' @@ -147,9 +141,9 @@ import tempfile from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.ansible_ipa_client import ( setup_logging, check_imports, - SECURE_PATH, sysrestore, paths, options, configure_krb5_conf, - realm_to_suffix, kinit_keytab, GSSError, kinit_password, NUM_VERSION, - get_ca_cert, get_ca_certs, errors, run + SECURE_PATH, sysrestore, paths, options, realm_to_suffix, kinit_keytab, + GSSError, kinit_password, NUM_VERSION, get_ca_cert, get_ca_certs, errors, + run ) @@ -157,10 +151,8 @@ def main(): module = AnsibleModule( argument_spec=dict( servers=dict(required=True, type='list', elements='str'), - domain=dict(required=True, type='str'), realm=dict(required=True, type='str'), hostname=dict(required=True, type='str'), - kdc=dict(required=True, type='str'), basedn=dict(required=True, type='str'), principal=dict(required=False, type='str'), password=dict(required=False, type='str', no_log=True), @@ -170,6 +162,7 @@ def main(): force_join=dict(required=False, type='bool'), kinit_attempts=dict(required=False, type='int', default=5), debug=dict(required=False, type='bool'), + krb_name=dict(required=True, type='str'), ), supports_check_mode=False, ) @@ -179,11 +172,9 @@ def main(): setup_logging() servers = module.params.get('servers') - domain = module.params.get('domain') realm = module.params.get('realm') hostname = module.params.get('hostname') basedn = module.params.get('basedn') - kdc = module.params.get('kdc') force_join = module.params.get('force_join') principal = module.params.get('principal') password = module.params.get('password') @@ -192,6 +183,7 @@ def main(): ca_cert_file = module.params.get('ca_cert_file') kinit_attempts = module.params.get('kinit_attempts') debug = module.params.get('debug') + krb_name = module.params.get('krb_name') if password is not None and keytab is not None: module.fail_json(msg="Password and keytab cannot be used together") @@ -199,12 +191,10 @@ def main(): if password is None and admin_keytab is None: module.fail_json(msg="Password or admin_keytab is needed") - client_domain = hostname[hostname.find(".") + 1:] nolog = tuple() env = {'PATH': SECURE_PATH} fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) host_principal = 'host/%s@%s' % (hostname, realm) - sssd = True options.ca_cert_file = ca_cert_file options.principal = principal @@ -215,19 +205,6 @@ def main(): changed = False already_joined = False try: - (krb_fd, krb_name) = tempfile.mkstemp() - os.close(krb_fd) - configure_krb5_conf( - cli_realm=realm, - cli_domain=domain, - cli_server=servers, - cli_kdc=kdc, - dnsok=False, - filename=krb_name, - client_domain=client_domain, - client_hostname=hostname, - configure_sssd=sssd, - force=False) env['KRB5_CONFIG'] = krb_name ccache_dir = tempfile.mkdtemp(prefix='krbcc') ccache_name = os.path.join(ccache_dir, 'ccache') @@ -336,27 +313,17 @@ def main(): paths.IPA_DNS_CCACHE, config=krb_name, attempts=kinit_attempts) - env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE except GSSError as e: # failure to get ticket makes it impossible to login and # bind from sssd to LDAP, abort installation module.fail_json(msg="Failed to obtain host TGT: %s" % e) finally: - try: - os.remove(krb_name) - except OSError: - module.fail_json(msg="Could not remove %s" % krb_name) if ccache_dir is not None: try: os.rmdir(ccache_dir) except OSError: pass - if os.path.exists(krb_name + ".ipabkp"): - try: - os.remove(krb_name + ".ipabkp") - except OSError: - module.fail_json(msg="Could not remove %s.ipabkp" % krb_name) module.exit_json(changed=changed, already_joined=already_joined) diff --git a/roles/ipaclient/library/ipaclient_setup_certmonger.py b/roles/ipaclient/library/ipaclient_setup_certmonger.py new file mode 100644 index 0000000000000000000000000000000000000000..5c81b408dfc243290a564d94942801f401517ceb --- /dev/null +++ b/roles/ipaclient/library/ipaclient_setup_certmonger.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner <twoerner@redhat.com> +# +# Based on ipa-client-install code +# +# Copyright (C) 2017-2022 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: ipaclient_setup_certmonger +short_description: Setup certmonger for IPA client +description: Setup certmonger for IPA client +options: + realm: + description: Kerberos realm name of the IPA deployment + type: str + required: yes + hostname: + description: Fully qualified name of this host + type: str + required: yes + subject_base: + description: | + The certificate subject base (default O=<realm-name>). + RDNs are in LDAP order (most specific RDN first). + type: str + required: yes + ca_enabled: + description: Whether the Certificate Authority is enabled or not + type: bool + required: yes + request_cert: + description: Request certificate for the machine + type: bool + required: yes +author: + - Thomas Woerner (@t-woerner) +''' + +EXAMPLES = ''' +- name: Setup certmonger for IPA client + ipaclient_setup_certmonger: + realm: EXAMPLE.COM + hostname: client1.example.com + subject_base: O=EXAMPLE.COM + ca_enabled: true + request_cert: false +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + setup_logging, check_imports, + options, sysrestore, paths, ScriptError, configure_certmonger +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + realm=dict(required=True, type='str'), + hostname=dict(required=True, type='str'), + subject_base=dict(required=True, type='str'), + ca_enabled=dict(required=True, type='bool'), + request_cert=dict(required=True, type='bool'), + ), + supports_check_mode=False, + ) + + module._ansible_debug = True + check_imports(module) + setup_logging() + + cli_realm = module.params.get('realm') + hostname = module.params.get('hostname') + subject_base = module.params.get('subject_base') + ca_enabled = module.params.get('ca_enabled') + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + + options.request_cert = module.params.get('request_cert') + options.hostname = hostname + + try: + configure_certmonger(fstore, subject_base, cli_realm, hostname, + options, ca_enabled) + + except ScriptError as e: + module.fail_json(msg=str(e)) + + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_setup_nss.py b/roles/ipaclient/library/ipaclient_setup_nss.py index a14305835da62c51d861accdb8ef1459157a81f0..74ca9d424edeb2e537114bc6556b4e04cef88275 100644 --- a/roles/ipaclient/library/ipaclient_setup_nss.py +++ b/roles/ipaclient/library/ipaclient_setup_nss.py @@ -181,7 +181,7 @@ from ansible.module_utils.ansible_ipa_client import ( options, sysrestore, paths, ansible_module_get_parsed_ip_addresses, api, errors, create_ipa_nssdb, ipautil, ScriptError, CLIENT_INSTALL_ERROR, get_certs_from_ldap, DN, certstore, x509, logger, certdb, - CalledProcessError, tasks, client_dns, configure_certmonger, services, + CalledProcessError, tasks, client_dns, services, update_ssh_keys, save_state, configure_ldap_conf, configure_nslcd_conf, configure_openldap_conf, hardcode_ldap_server, getargspec, NUM_VERSION, serialization @@ -356,8 +356,6 @@ def main(): if not options.on_master: client_dns(cli_server[0], hostname, options) - configure_certmonger(fstore, subject_base, cli_realm, hostname, - options, ca_enabled) if hasattr(paths, "SSH_CONFIG_DIR"): ssh_config_dir = paths.SSH_CONFIG_DIR diff --git a/roles/ipaclient/library/ipaclient_temp_krb5.py b/roles/ipaclient/library/ipaclient_temp_krb5.py new file mode 100644 index 0000000000000000000000000000000000000000..cbe652c652ac96ba8258f294f146dab634b463c4 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_temp_krb5.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner <twoerner@redhat.com> +# +# Based on ipa-client-install code +# +# Copyright (C) 2017-2022 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: ipaclient_temp_krb5 +short_description: + Create temporary krb5 configuration. +description: + Create temporary krb5 configuration for deferring the creation of the final + krb5.conf on clients +options: + servers: + description: Fully qualified name of IPA servers to enroll to + type: list + elements: str + required: yes + domain: + description: Primary DNS domain of the IPA deployment + type: str + required: yes + realm: + description: Kerberos realm name of the IPA deployment + type: str + required: yes + hostname: + description: Fully qualified name of this host + type: str + required: yes + kdc: + description: The name or address of the host running the KDC + type: str + required: yes + on_master: + description: Whether the configuration is done on the master or not + type: bool + required: no + default: no +author: + - Thomas Woerner (@t-woerner) +''' + +EXAMPLES = ''' +# Test IPA with local keytab +- name: Test IPA in force mode with maximum 5 kinit attempts + ipaclient_test_keytab: + servers: ["server1.example.com","server2.example.com"] + domain: example.com + realm: EXAMPLE.COM + kdc: server1.example.com + hostname: client1.example.com + +# Test IPA with ipadiscovery return values +- name: Join IPA + ipaclient_test_keytab: + servers: "{{ ipadiscovery.servers }}" + domain: "{{ ipadiscovery.domain }}" + realm: "{{ ipadiscovery.realm }}" + kdc: "{{ ipadiscovery.kdc }}" + hostname: "{{ ipadiscovery.hostname }}" +''' + +RETURN = ''' +krb_name: + description: The krb5 config file name + returned: always + type: str +''' + +import os +import tempfile + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + setup_logging, check_imports, configure_krb5_conf +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + servers=dict(required=True, type='list', elements='str'), + domain=dict(required=True, type='str'), + realm=dict(required=True, type='str'), + hostname=dict(required=True, type='str'), + kdc=dict(required=True, type='str'), + on_master=dict(required=False, type='bool', default=False), + ), + supports_check_mode=False, + ) + + module._ansible_debug = True + check_imports(module) + setup_logging() + + servers = module.params.get('servers') + domain = module.params.get('domain') + realm = module.params.get('realm') + hostname = module.params.get('hostname') + kdc = module.params.get('kdc') + client_domain = hostname[hostname.find(".") + 1:] + + krb_name = None + # Create temporary krb5 configuration + try: + (krb_fd, krb_name) = tempfile.mkstemp() + os.close(krb_fd) + configure_krb5_conf( + cli_realm=realm, + cli_domain=domain, + cli_server=servers, + cli_kdc=kdc, + dnsok=False, + filename=krb_name, + client_domain=client_domain, + client_hostname=hostname, + configure_sssd=True, + force=False) + except Exception as ex: + if krb_name: + try: + os.remove(krb_name) + except OSError: + module.fail_json(msg="Could not remove %s" % krb_name) + module.fail_json( + msg="Failed to create temporary krb5 configuration: %s" % str(ex)) + + module.exit_json(changed=False, + krb_name=krb_name) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_test_keytab.py b/roles/ipaclient/library/ipaclient_test_keytab.py index 3f1c69de306a752ca78b11e1faa02684a9da145a..3bebeea49e6905f745d3633abef43278d6890d2e 100644 --- a/roles/ipaclient/library/ipaclient_test_keytab.py +++ b/roles/ipaclient/library/ipaclient_test_keytab.py @@ -244,6 +244,12 @@ def main(): os.remove(krb_name) except OSError: module.fail_json(msg="Could not remove %s" % krb_name) + if os.path.exists(krb_name + ".ipabkp"): + try: + os.remove(krb_name + ".ipabkp") + except OSError: + module.fail_json( + msg="Could not remove %s.ipabkp" % krb_name) module.exit_json(changed=False, krb5_keytab_ok=krb5_keytab_ok, diff --git a/roles/ipaclient/tasks/install.yml b/roles/ipaclient/tasks/install.yml index e76842fc29c05acbf7daf551600acb6a678dbfc2..662f09a66a506e54ec36bf8f87d0f3bede605f2e 100644 --- a/roles/ipaclient/tasks/install.yml +++ b/roles/ipaclient/tasks/install.yml @@ -239,12 +239,19 @@ hostname: "{{ result_ipaclient_test.hostname }}" when: not ipaclient_on_master | bool - - name: Install - Join IPA - ipaclient_join: + - name: Install - Create temporary krb5 configuration + ipaclient_temp_krb5: servers: "{{ result_ipaclient_test.servers }}" domain: "{{ result_ipaclient_test.domain }}" realm: "{{ result_ipaclient_test.realm }}" + hostname: "{{ result_ipaclient_test.hostname }}" kdc: "{{ result_ipaclient_test.kdc }}" + register: result_ipaclient_temp_krb5 + + - name: Install - Join IPA + ipaclient_join: + servers: "{{ result_ipaclient_test.servers }}" + realm: "{{ result_ipaclient_test.realm }}" basedn: "{{ result_ipaclient_test.basedn }}" hostname: "{{ result_ipaclient_test.hostname }}" force_join: "{{ ipaclient_force_join | default(omit) }}" @@ -255,6 +262,7 @@ admin_keytab: "{{ ipaadmin_keytab if ipaadmin_keytab is defined and not ipaclient_use_otp | bool else omit }}" # ca_cert_file: "{{ ipaclient_ca_cert_file | default(omit) }}" kinit_attempts: "{{ ipaclient_kinit_attempts | default(omit) }}" + krb_name: "{{ result_ipaclient_temp_krb5.krb_name }}" register: result_ipaclient_join when: not ipaclient_on_master | bool and (not result_ipaclient_test_keytab.krb5_keytab_ok or @@ -323,26 +331,13 @@ "{{ ipassd_no_krb5_offline_passwords | default(ipasssd_no_krb5_offline_passwords) }}" - - name: Install - Configure krb5 for IPA realm - ipaclient_setup_krb5: - realm: "{{ result_ipaclient_test.realm }}" - domain: "{{ result_ipaclient_test.domain }}" - servers: "{{ result_ipaclient_test.servers }}" - kdc: "{{ result_ipaclient_test.kdc }}" - dnsok: "{{ result_ipaclient_test.dnsok }}" - client_domain: "{{ result_ipaclient_test.client_domain }}" - hostname: "{{ result_ipaclient_test.hostname }}" - sssd: "{{ result_ipaclient_test.sssd }}" - force: "{{ ipaclient_force }}" - # on_master: "{{ ipaclient_on_master }}" - when: not ipaclient_on_master | bool - - name: Install - IPA API calls for remaining enrollment parts ipaclient_api: servers: "{{ result_ipaclient_test.servers }}" realm: "{{ result_ipaclient_test.realm }}" hostname: "{{ result_ipaclient_test.hostname }}" # debug: yes + krb_name: "{{ result_ipaclient_temp_krb5.krb_name }}" register: result_ipaclient_api - name: Install - Fix IPA ca @@ -413,6 +408,36 @@ domain: "{{ result_ipaclient_test.domain }}" nisdomain: "{{ ipaclient_nisdomain | default(omit) }}" when: not ipaclient_no_nisdomain | bool + + - name: Remove temporary krb5.conf + ansible.builtin.file: + path: "{{ result_ipaclient_temp_krb5.krb_name }}" + state: absent + when: result_ipaclient_temp_krb5.krb_name is defined + + - name: Install - Configure krb5 for IPA realm + ipaclient_setup_krb5: + realm: "{{ result_ipaclient_test.realm }}" + domain: "{{ result_ipaclient_test.domain }}" + servers: "{{ result_ipaclient_test.servers }}" + kdc: "{{ result_ipaclient_test.kdc }}" + dnsok: "{{ result_ipaclient_test.dnsok }}" + client_domain: "{{ result_ipaclient_test.client_domain }}" + hostname: "{{ result_ipaclient_test.hostname }}" + sssd: "{{ result_ipaclient_test.sssd }}" + force: "{{ ipaclient_force }}" + # on_master: "{{ ipaclient_on_master }}" + when: not ipaclient_on_master | bool + + - name: Install - Configure certmonger + ipaclient_setup_certmonger: + realm: "{{ result_ipaclient_test.realm }}" + hostname: "{{ result_ipaclient_test.hostname }}" + subject_base: "{{ result_ipaclient_api.subject_base }}" + ca_enabled: "{{ result_ipaclient_api.ca_enabled }}" + request_cert: "{{ ipaclient_request_cert }}" + when: not ipaclient_on_master | bool + always: - name: Install - Restore original admin password if overwritten by OTP no_log: yes @@ -424,3 +449,15 @@ ansible.builtin.file: path: "/etc/ipa/.dns_ccache" state: absent + + - name: Remove temporary krb5.conf + ansible.builtin.file: + path: "{{ result_ipaclient_temp_krb5.krb_name }}" + state: absent + when: result_ipaclient_temp_krb5.krb_name is defined + + - name: Remove temporary krb5.conf backup + ansible.builtin.file: + path: "{{ result_ipaclient_temp_krb5.krb_name }}.ipabkp" + state: absent + when: result_ipaclient_temp_krb5.krb_name is defined