diff --git a/client/man/ipa-client-install.1 b/client/man/ipa-client-install.1 index 48a2e54ea..bd8818807 100644 --- a/client/man/ipa-client-install.1 +++ b/client/man/ipa-client-install.1 @@ -11,7 +11,7 @@ Configures a client machine to use IPA for authentication and identity services. By default this configures SSSD to connect to an IPA server for authentication and authorization. Optionally one can instead configure PAM and NSS (Name Switching Service) to work with an IPA server over Kerberos and LDAP. -An authorized user is required to join a client machine to IPA. This can take the form of a kerberos principal or a one\-time password associated with the machine. +An authorized account is required to join a client machine to IPA. This can take the form of a kerberos principal, a one\-time password associated with the machine, or PKINIT identity associated with the machine. This same tool is used to unconfigure IPA and attempts to return the machine to its previous state. Part of this process is to unenroll the host from the IPA server. Unenrollment consists of disabling the principal key on the IPA server so that it may be re\-enrolled. The machine principal in /etc/krb5.keytab (host/@REALM) is used to authenticate to the IPA server to unenroll itself. If this principal does not exist then unenrollment will fail and an administrator will need to disable the host principal (ipa host\-disable ). @@ -229,6 +229,22 @@ first. When this option is not specified, \fBipa\-client\-install\fR will back up SSSD config and create new one. The back up version will be restored during uninstall. +.SS "PKINIT OPTIONS" +.TP +\fB\-\-pkinit\-identity=\fIDENTITY\fR +Identity string for PKINIT authentication to use to join the IPA realm, +for example 'FILE:/path/to/cert.pem,/path/to/key.pem'. See krb5.conf(5) +for more information. The option is mutually exclusive with +\fB\-\-password\fR and \fB\-\-keytab\fR. +.TP +\fB\-\-pkinit\-anchor\fR=\fIFILEDIR\fR +Trust anchors (root and intermediate CA certs) for PKINIT. \fIFILEDIR\fR is +either the absolute path to a PEM bundle (for example +'FILE:/etc/pki/tls/cert.pem') or to an OpenSSL hash directory (for example +'DIR:/etc/ssl/certs/'). The option can be used multiple times. PKINIT +requires the full trust chain of the Kerberos KDC server as well as the full +trust chain of the identity certificate. + .SS "UNINSTALL OPTIONS" .TP \fB\-\-uninstall\fR diff --git a/doc/designs/client-install-pkinit.md b/doc/designs/client-install-pkinit.md new file mode 100644 index 000000000..d58b5f139 --- /dev/null +++ b/doc/designs/client-install-pkinit.md @@ -0,0 +1,219 @@ +# IPA client enrollment with PKINIT + +This design document proposes PKINIT as an additional authentication +mechanism for `ipa-client-install`, ticket +[https://pagure.io/freeipa/issue/9271](https://pagure.io/freeipa/issue/9271). + +## Overview + +PKINIT is an authentication mechanism for Kerberos that uses X.509 +certificates and private keys to authenticate Kerberos KDC server to client +and optionally clients to the server. Mutual authentication is almost the +same as mTLS with TLS server and client certificates in HTTPS. + +PKINIT can be used instead of user/password or OTP to authorize client +enrollment. This enables admins to re-use existing host certificates to +automate client installations. + + +## Assumptions + +- The client machine has access to a X.509 certificate / key pair that + uniquely identifies an account with necessary permissions to enroll the + host. The certificate does not have to contain special extensions for + PKINIT. It only requires an attribute that can be uniquely mapped to an + account, e.g. subject alternative name (SAN) dNSName maps to FQDN of + an existing host entry or RFC 822 name maps to an user account with + enrollment privileges. +- A certmap rule exists matches the certificate, and maps the unique + identifier to a single account like an existing host entry or user with + enrollment privileges. +- The certificate is signed by a CA chain which is known to and trusted + by the Kerberos KDC server. This can be accomplished by installing the + root CA and intermediate CAs as additional CA certificates to IPA + with `ipa-cacert-manage install` on one IPA server, then running + `ipa-certupdate` on every IPA servers. +- The CA chain of the certificate and the CA chain of the Kerberos KDC + server certificate are available on the host. On an IPA server, the file + `/var/lib/ipa-client/pki/kdc-ca-bundle.pem` typically contains all + necessary CAs. + +How the certificates and CAs are generated and distributed and how host +entries are created is out of scope for this document. The certificates +and key can be provided as files or by a PKCS#11 provider like OpenSC +from a smart card or p11-kit proxy from a remote PKCS#11 source. + + +### Host self-enrollment + +Host self-enrollment allows a host to enroll itself using a host-specific +certificate. The approach use the fact that a host account has the necessary +permissions to self-manage its host entry. A host entry must be pre-created +by a privileged account and a certmap rule must map a unique identifier to +a host entry. + +For example a certmap rule like + +```console +$ ipa certmaprule-add pkinit-host \ + --matchrule 'CN=Certificate Authority,O=IPA.EXAMPLE' \ + --maprule='(fqdn={subject_dns_name})' +``` + +allows an host with hostname `host.ipa.example` and a certificate with +properties like + +```console +Issuer: O = HMSIDM.TEST, CN = Certificate Authority +Subject: O = HMSIDM.TEST, CN = host.ipa.example +X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:host.ipa.example +``` + +to enroll itself with a command like + +```console +$ ipa-client-install \ + --pkinit-identity=FILE:/path/to/cert.pem,/path/to/key.pem \ + --pkinit-anchor=FILE:/path/to/kdc-ca-bundle.pem \ + ... +``` + +A privileged account only has to pre-created its host entry with + +```console +$ ipa host-add host.ipa.example +``` + +first. + + +### Privileged user account + +A user account with e.g. `Host Administrators` privilege can be used to +create and enroll new hosts. + +Example certmap rule: +```console +$ ipa certmaprule-add pkinit-user \ + --matchrule 'CN=Certificate Authority,O=IPA.EXAMPLE' \ + --maprule='(&(mail={subject_rfc822_name})(objectclass=inetorgperson))' +``` + +In this case you have to pass the principal name of the user account to +`ipa-client-install`: + +```console +$ ipa-client-install \ + --pkinit-identity=FILE:/path/to/cert.pem,/path/to/key.pem \ + --pkinit-anchor=FILE:/path/to/kdc-ca-bundle.pem \ + -p enrollmentuser + ... +``` + + +## New options for ipa-client-install + +The `ipa-client-install` command is extended with two new options: + +`--pkinit-identity=IDENTITY` specifies the PKINIT identity information. The +option is mutually exclusive with `--keytab` and `--password` option. +See [man krb5.conf(5)](https://web.mit.edu/kerberos/krb5-1.19/doc/admin/conf_files/krb5_conf.html#pkinit-options) +for more information. Possible values are + +- `FILE:/path/to/cert.pem,/path/to/key.pem` or `FILE:/path/to/combined.pem` + for certificate and private key in PEM format. +- `PKCS12:/path/to/file.p12` for PKCS#12 file with a certificate and private + key. +- `PKCS11:...` to use a PKCS#11 provider +- `DIR:/path/to/directory` with `*.crt` and `*.key` files. + +`--pkinit-anchor=FILEDIR` to load trust anchors (root and intermediate CA +certs) for KDC server and host identity. *FILE* is either an absolute path to +a PEM bundle (for example `FILE:/etc/pki/tls/cert.pem`) or to an OpenSSL hash +directory (for example `DIR:/etc/ssl/certs/`). The option can be used multiple +times to load trust anchors from several locations. + +By default `ipa-client-install` attempts to authenticate with the host +principal `host/hostname@REALM`. Use `-p` option to authenticate as a +different account. + +Example: + +```console +$ ipa-client-install \ + --pkinit-identity=FILE:/path/to/cert.pem,/path/to/key.pem \ + --pkinit-anchor=FILE:/path/to/kdc-ca-bundle.pem \ + ... +``` + + +## Testing + +The `ipa certmap-match` command does not support hosts, yet. To test a +rule and certificate, you can run kinit: + +```console +$ kinit \ + -X X509_user_identity=FILE:/path/to/cert.pem,/path/to/key.pem \ + -X X509_anchors=FILE:/path/to/kdc-ca-bundle.pem \ + host/host.ipa.example +``` + +Set the environment variable `KRB5_CONFIG=/dev/stderr` for additional debug +information. On IPA servers the log file `/var/log/krb5kdc.log` contains +information about cert authentication and filters: + +```console +Initializing IPA certauth plugin. +Doing certauth authorize for [host/host.ipa.example@IPA.EXAMPLE] +Got cert filter [(fqdn=host.ipa.example)] +PKINIT: freshness token received from host/host.ipa.example@IPA.EXAMPLE +``` + +The KDC caches certmap rules for 5 minutes. To test a new or modified certmap +rule immediately, the KDC must be restarted with the command +`systemctl restart krb5kdc.service`. + + +## cert map rules + +See [SSSD: Certificate mapping and matching rules](https://sssd.io/design-pages/matching_and_mapping_certificates.html) +and [man sss-certmap(5)](https://www.mankier.com/5/sss-certmap). + +### certmap matching rule + +certmap *matching rules* use RFC 4514 string representation of subject and +issuer distinguished names (DN) in LDAP order with NSS-style attribute type +names. RFC 4514 is the successor to RFC 2253. LDAP order is reverse to X.509 +order of relative distinguised names, so common name (CN) typically comes +before organization and country. + +- Matching rules are regular expressions. Characters `^.[$()|*+?{\\` must be + quoted, e.g. `.` becomes `\.`. +- Characters `=#+,;<=>\` have special meaning in in RDN attribute values + and may be escaped. A `,` becomes `\,`, which is then quoted again as + `\\,`. +- Some fields use different attribute type names than OpenSSL, e.g. + `emailAddress` attributes is just `E`. + +To match a cert with issuer DN +`O = "ACME, Inc.", CN = host.acme.example, emailAddress = info@acme.example`, +use the matching rule +`^E=info@acme.example,CN=host.acme.example,O=ACME\\, Inc\.$`. + +### certmap mapping rules + +certmap mapping rules are used to associate a certificate with an account. +Mapping rules are LDAP queries with templating, e.g. +`(fqdn={subject_dns_name})` uses the dNSName value from the certificate +subject alternative name extension. + +- Combine the filter with `(!(krbprincipalkey=*))` to prevent further PKINIT + authenticate after the host has been enrolled: + `(&(fqdn={subject_dns_name})(!(krbprincipalkey=*)))` +- Use `(!(memberof=cn=ipaservers,cn=hostgroups,cn=accounts,.*))` to prevent + PKINIT as an IPA server host or + `(memberof=pkinit-hosts,cn=hostgroups,cn=accounts,.*)` to limit PKINIT to + hosts in a custom `pkinit-hosts` host group. diff --git a/doc/designs/index.rst b/doc/designs/index.rst index 17184db21..23561444c 100644 --- a/doc/designs/index.rst +++ b/doc/designs/index.rst @@ -26,3 +26,4 @@ FreeIPA design documentation external-idp/external-idp.md external-idp/idp-api.md random-serial-numbers.md + client-install-pkinit.md diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py index eaec931e9..e5d3e8223 100644 --- a/ipaclient/install/client.py +++ b/ipaclient/install/client.py @@ -40,7 +40,7 @@ from ipalib.constants import FQDN, IPAAPI_USER, MAXHOSTNAMELEN from ipalib.install import certmonger, certstore, service from ipalib.install import hostname as hostname_ from ipalib.facts import is_ipa_client_configured, is_ipa_configured -from ipalib.install.kinit import kinit_keytab, kinit_password +from ipalib.install.kinit import kinit_keytab, kinit_password, kinit_pkinit from ipalib.install.service import enroll_only, prepare_only from ipalib.rpc import delete_persistent_client_session_data from ipalib.util import ( @@ -2203,16 +2203,26 @@ def install_check(options): pass if options.unattended and ( - options.password is None and - options.principal is None and - options.keytab is None and - options.prompt_password is False and - not options.on_master + options.password is None + and options.principal is None + and options.keytab is None + and options.pkinit_identity is None + and options.prompt_password is False + and not options.on_master ): raise ScriptError( "One of password / principal / keytab is required.", rval=CLIENT_INSTALL_ERROR) + if options.pkinit_identity is not None and ( + options.password is not None + or options.keytab is not None + ): + raise ScriptError( + "pkinit_identity is mutually exclusive with password / keytab.", + rval=CLIENT_INSTALL_ERROR + ) + if options.hostname: hostname = options.hostname hostname_source = 'Provided as option' @@ -2739,7 +2749,8 @@ def _install(options, tdict): if not options.unattended: if (options.principal is None and options.password is None and - options.prompt_password is False and options.keytab is None): + options.prompt_password is False and options.keytab is None + and options.pkinit_identity is None): options.principal = user_input("User authorized to enroll " "computers", allow_empty=False) logger.debug( @@ -2773,6 +2784,7 @@ def _install(options, tdict): env['XMLRPC_TRACE_CURL'] = 'yes' if options.force_join: join_args.append("-f") + if options.principal is not None: stdin = None principal = options.principal @@ -2832,6 +2844,33 @@ def _install(options, tdict): "Keytab file could not be found: {}".format( options.keytab), rval=CLIENT_INSTALL_ERROR) + elif options.pkinit_identity: + join_args.append("-f") + with open(paths.CA_BUNDLE_PEM, "w"): + # HACK: kinit fails when "pkinit_pool" file is missing. + # Create an empty file and remove it after PKINIT. + pass + # if no principal is set, use host principal + if options.principal is None: + pkinit_principal = host_principal + else: + pkinit_principal = options.principal + try: + kinit_pkinit( + pkinit_principal, + options.pkinit_identity, + ccache_name=ccache_name, + config=krb_name, + pkinit_anchors=options.pkinit_anchors + ) + except CalledProcessError as e: + print_port_conf_info() + raise ScriptError( + f"Kerberos PKINIT authentication failed: {e}", + rval=CLIENT_INSTALL_ERROR + ) + finally: + remove_file(paths.CA_BUNDLE_PEM) elif options.password: nolog = (options.password,) join_args.append("-w") @@ -3212,9 +3251,11 @@ def _install(options, tdict): user = options.principal if user is None: user = "admin@%s" % cli_domain - logger.info("Principal is not set when enrolling with OTP" - "; using principal '%s' for 'getent passwd'", - user) + logger.info( + "Principal is not set when enrolling with OTP " + "or PKINIT; using principal '%s' for 'getent passwd'.", + user + ) elif '@' not in user: user = "%s@%s" % (user, cli_domain) n = 0 @@ -3902,7 +3943,67 @@ class ClientInstallInterface(hostname_.HostNameInstallInterface, "--all-ip-addresses") +@group +class PKINITInstallInterface(service.ServiceInstallInterface): + description = "PKINIT" + + pkinit_identity = knob( + type=str, + default=None, + description=( + "PKINIT identity information (for example " + "FILE:/path/to/cert.pem,/path/to/key.pem)" + ), + cli_metavar="IDENTITY", + ) + + @pkinit_identity.validator + def pkinit_identity(self, value): + # see pkinit_crypto_openssl.c:crypto_load_certs() + # ignore "ENV:" prefix + if not value.startswith( + ("FILE:", "PKCS11:", "PKCS12:", "DIR:", "ENV:") + ): + raise ValueError( + "identity must start with FILE:, PKCS11:, PKCS12:, DIR:, " + "or ENV:" + ) + + pkinit_anchors = knob( + type=typing.List[str], + default=None, + description=( + "PKINIT trust anchors, prefixed with FILE: for CA PEM bundle " + "file or DIR: for an OpenSSL hash dir. The option can be used " + "used multiple times." + ), + cli_names="--pkinit-anchor", + cli_metavar="FILEDIR", + ) + + @pkinit_anchors.validator + def pkinit_anchors(self, value): + # see pkinit_crypto_openssl.c:crypto_load_cas_and_crls() + for part in value: + prefix, sep, path = part.partition(":") + if not sep or prefix not in {"FILE", "DIR", "ENV:"}: + raise ValueError( + "Invalid pkinit_anchor '{part}' is not prefixed with " + "FILE: or DIR:." + ) + if prefix == "FILE" and not os.path.isfile(path): + raise ValueError( + f"pkinit anchor path '{path}' does not exist or is not " + "a file." + ) + if prefix == "DIR" and not os.path.isdir(path): + raise ValueError( + f"pkinit anchor path '{path}' does not exist or is not " + "a file." + ) + class ClientInstall(ClientInstallInterface, + PKINITInstallInterface, automount.AutomountInstallInterface): """ Client installer diff --git a/ipaclient/install/ipa_certupdate.py b/ipaclient/install/ipa_certupdate.py index 3ff58ca76..d7bc4cc32 100644 --- a/ipaclient/install/ipa_certupdate.py +++ b/ipaclient/install/ipa_certupdate.py @@ -103,7 +103,9 @@ def run_with_args(api): else: lwcas = [] - if is_ipa_configured(): + ipa_configured = is_ipa_configured() + + if ipa_configured: # look up CA servers before service restarts resp = api.Command.server_role_find( role_servrole=u'CA server', @@ -141,6 +143,11 @@ def run_with_args(api): update_client(certs) + # update_client() may have updated KDC cert bundle; restart KDC to pick + # up changes. + if ipa_configured and services.knownservices.krb5kdc.is_running(): + services.knownservices.krb5kdc.restart() + def update_client(certs): update_file(paths.IPA_CA_CRT, certs) diff --git a/ipalib/install/kinit.py b/ipalib/install/kinit.py index a4cf0cbe3..968605c05 100644 --- a/ipalib/install/kinit.py +++ b/ipalib/install/kinit.py @@ -127,3 +127,49 @@ def kinit_armor(ccache_name, pkinit_anchors=None): # this workaround enables us to capture stderr and put it # into the raised exception in case of unsuccessful authentication run(args, env=env, raiseonerr=True, capture_error=True) + + +def kinit_pkinit( + principal, + user_identity, + ccache_name, + config=None, + pkinit_anchors=None, +): + """Perform kinit with X.509 identity (PKINIT) + + :param principal: principal name + :param user_identity: X509_user_identity paramemter + :param ccache_name: location of ccache + :param config: path to krb5.conf (default: default location) + :param pkinit_anchor: if not None, the PKINIT anchors to use. Otherwise + the value from Kerberos client library configuration is used. Entries + must be prefixed with FILE: or DIR: + + user identity example: + FILE:filename[,keyfilename] + PKCS12:filename + PKCS11:... + DIR:directoryname + + :raises: CalledProcessError if PKINIT fails + """ + logger.debug( + "Initializing principal %s using PKINIT %s", principal, user_identity + ) + + env = {"LC_ALL": "C"} + if config is not None: + env["KRB5_CONFIG"] = config + + args = [paths.KINIT, "-c", ccache_name] + if pkinit_anchors is not None: + for pkinit_anchor in pkinit_anchors: + assert pkinit_anchor.startswith(("FILE:", "DIR:", "ENV:")) + args.extend(["-X", f"X509_anchors={pkinit_anchor}"]) + args.extend(["-X", f"X509_user_identity={user_identity}"]) + args.append(principal) + + # this workaround enables us to capture stderr and put it + # into the raised exception in case of unsuccessful authentication + run(args, env=env, raiseonerr=True, capture_error=True) diff --git a/ipatests/prci_definitions/nightly_latest.yaml b/ipatests/prci_definitions/nightly_latest.yaml index bc88888bc..d44c40961 100644 --- a/ipatests/prci_definitions/nightly_latest.yaml +++ b/ipatests/prci_definitions/nightly_latest.yaml @@ -1462,6 +1462,18 @@ jobs: timeout: 3600 topology: *master_1repl + fedora-latest/test_pkinit_install: + requires: [fedora-latest/build] + priority: 50 + job: + class: RunPytest + args: + build_url: '{fedora-latest/build_url}' + test_suite: test_integration/test_pkinit_install.py + template: *ci-master-latest + timeout: 3600 + topology: *master_1repl_1client + fedora-latest/test_pkinit_manage: requires: [fedora-latest/build] priority: 50 diff --git a/ipatests/prci_definitions/nightly_latest_pki.yaml b/ipatests/prci_definitions/nightly_latest_pki.yaml index 0fafcccf3..d0d7e72a3 100644 --- a/ipatests/prci_definitions/nightly_latest_pki.yaml +++ b/ipatests/prci_definitions/nightly_latest_pki.yaml @@ -887,6 +887,20 @@ jobs: timeout: 5400 topology: *master_1repl + pki-fedora/test_pkinit_install: + requires: [pki-fedora/build] + priority: 50 + job: + class: RunPytest + args: + build_url: '{pki-fedora/build_url}' + update_packages: True + copr: '@pki/master' + test_suite: test_integration/test_pkinit_install.py + template: *ci-master-latest + timeout: 3600 + topology: *master_1repl_1client + pki-fedora/test_pkinit_manage: requires: [pki-fedora/build] priority: 50 diff --git a/ipatests/prci_definitions/nightly_latest_selinux.yaml b/ipatests/prci_definitions/nightly_latest_selinux.yaml index ff717abbe..bd6d71fe5 100644 --- a/ipatests/prci_definitions/nightly_latest_selinux.yaml +++ b/ipatests/prci_definitions/nightly_latest_selinux.yaml @@ -1577,6 +1577,19 @@ jobs: timeout: 3600 topology: *master_1repl + fedora-latest/test_pkinit_install: + requires: [fedora-latest/build] + priority: 50 + job: + class: RunPytest + args: + build_url: '{fedora-latest/build_url}' + selinux_enforcing: True + test_suite: test_integration/test_pkinit_install.py + template: *ci-master-latest + timeout: 3600 + topology: *master_1repl_1client + fedora-latest/test_pkinit_manage: requires: [fedora-latest/build] priority: 50 diff --git a/ipatests/prci_definitions/nightly_latest_testing.yaml b/ipatests/prci_definitions/nightly_latest_testing.yaml index 05eff05c7..4b4f08936 100644 --- a/ipatests/prci_definitions/nightly_latest_testing.yaml +++ b/ipatests/prci_definitions/nightly_latest_testing.yaml @@ -1693,6 +1693,20 @@ jobs: timeout: 3600 topology: *master_1repl + testing-fedora/test_pkinit_install: + requires: [testing-fedora/build] + priority: 50 + job: + class: RunPytest + args: + build_url: '{testing-fedora/build_url}' + update_packages: True + enable_testing_repo: True + test_suite: test_integration/test_pkinit_install.py + template: *ci-master-latest + timeout: 3600 + topology: *master_1repl_1client + testing-fedora/test_pkinit_manage: requires: [testing-fedora/build] priority: 50 diff --git a/ipatests/prci_definitions/nightly_latest_testing_selinux.yaml b/ipatests/prci_definitions/nightly_latest_testing_selinux.yaml index 2b9bd74f7..e70e9c6d3 100644 --- a/ipatests/prci_definitions/nightly_latest_testing_selinux.yaml +++ b/ipatests/prci_definitions/nightly_latest_testing_selinux.yaml @@ -1808,6 +1808,21 @@ jobs: timeout: 3600 topology: *master_1repl + testing-fedora/test_pkinit_install: + requires: [testing-fedora/build] + priority: 50 + job: + class: RunPytest + args: + build_url: '{testing-fedora/build_url}' + update_packages: True + selinux_enforcing: True + enable_testing_repo: True + test_suite: test_integration/test_pkinit_install.py + template: *ci-master-latest + timeout: 3600 + topology: *master_1repl_1client + testing-fedora/test_pkinit_manage: requires: [testing-fedora/build] priority: 50 diff --git a/ipatests/prci_definitions/nightly_previous.yaml b/ipatests/prci_definitions/nightly_previous.yaml index e4d2f5e48..3da0c63a6 100644 --- a/ipatests/prci_definitions/nightly_previous.yaml +++ b/ipatests/prci_definitions/nightly_previous.yaml @@ -1462,6 +1462,18 @@ jobs: timeout: 3600 topology: *master_1repl + fedora-previous/test_pkinit_install: + requires: [fedora-previous/build] + priority: 50 + job: + class: RunPytest + args: + build_url: '{fedora-previous/build_url}' + test_suite: test_integration/test_pkinit_install.py + template: *ci-master-previous + timeout: 3600 + topology: *master_1repl_1client + fedora-previous/test_pkinit_manage: requires: [fedora-previous/build] priority: 50 diff --git a/ipatests/prci_definitions/nightly_rawhide.yaml b/ipatests/prci_definitions/nightly_rawhide.yaml index 9ccbb95fd..71060f017 100644 --- a/ipatests/prci_definitions/nightly_rawhide.yaml +++ b/ipatests/prci_definitions/nightly_rawhide.yaml @@ -1577,6 +1577,19 @@ jobs: timeout: 3600 topology: *master_1repl + fedora-rawhide/test_pkinit_install: + requires: [fedora-rawhide/build] + priority: 50 + job: + class: RunPytest + args: + build_url: '{fedora-rawhide/build_url}' + update_packages: True + test_suite: test_integration/test_pkinit_install.py + template: *ci-master-frawhide + timeout: 3600 + topology: *master_1repl_1client + fedora-rawhide/test_pkinit_manage: requires: [fedora-rawhide/build] priority: 50 diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py index 5dc20887c..08544d496 100755 --- a/ipatests/pytest_ipa/integration/tasks.py +++ b/ipatests/pytest_ipa/integration/tasks.py @@ -533,8 +533,8 @@ def install_replica(master, replica, setup_ca=True, setup_dns=False, def install_client(master, client, extra_args=[], user=None, - password=None, unattended=True, stdin_text=None, - nameservers='master'): + password=None, pkinit_identity=None, unattended=True, + stdin_text=None, nameservers='master'): """ :param nameservers: nameservers to write in resolver config. Possible values: @@ -556,20 +556,23 @@ def install_client(master, client, extra_args=[], user=None, if nameservers == 'master': nameservers = master.ip client.resolver.setup_resolver(nameservers, master.domain.name) - if user is None: - user = client.config.admin_name - if password is None: - password = client.config.admin_password args = [ 'ipa-client-install', '--domain', client.domain.name, '--realm', client.domain.realm, - '-p', user, - '-w', password, '--server', master.hostname ] + if pkinit_identity: + args.extend(["--pkinit-identity", pkinit_identity]) + else: + if user is None: + user = client.config.admin_name + if password is None: + password = client.config.admin_password + args.extend(['-p', user, '-w', password]) + if unattended: args.append('-U') diff --git a/ipatests/test_integration/test_pkinit_install.py b/ipatests/test_integration/test_pkinit_install.py new file mode 100644 index 000000000..caa0e6a34 --- /dev/null +++ b/ipatests/test_integration/test_pkinit_install.py @@ -0,0 +1,100 @@ +# +# Copyright (C) 2022 FreeIPA Contributors see COPYING for license +# + +""" +Module provides tests for ipa-client-install with PKINIT +""" +import os + +from ipaplatform.paths import paths +from ipatests.test_integration.base import IntegrationTest +from ipatests.pytest_ipa.integration import tasks + + +class TestPkinitClientInstall(IntegrationTest): + num_clients = 1 + + certfile = "/etc/pki/tls/certs/client.pem" + keyfile = "/etc/pki/tls/private/client.key" + tmpbundle = "/tmp/kdc-ca-bundle.pme" + + @classmethod + def install(cls, mh): + tasks.install_master(cls.master) + + def add_certmaperule(self): + """add certmap rule to map SAN dNSName to host entry""" + self.master.run_command( + [ + "ipa", + "certmaprule-add", + "pkinit-host", + "--matchrule=CN=Certificate Authority,.*", + "--maprule=(fqdn={subject_dns_name})", + ] + ) + + def add_host(self): + """Add host entry for client + + Allow master to manage client so it can create a certificate. + """ + client = self.clients[0] + self.master.run_command( + ["ipa", "host-add", "--force", client.hostname] + ) + self.master.run_command( + [ + "ipa", + "host-add-managedby", + f"--hosts={self.master.hostname}", + client.hostname, + ] + ) + + def create_cert(self): + """Create and copy certificate for client""" + client = self.clients[0] + self.master.run_command( + [ + "mkdir", + "-p", + os.path.dirname(self.certfile), + os.path.dirname(self.keyfile), + ] + ) + self.master.run_command( + [ + "ipa-getcert", + "request", + "-w", + # fmt: off + "-f", self.certfile, + "-k", self.keyfile, + "-N", client.hostname, + "-D", client.hostname, + "-K", f"host/{client.hostname}", + # fmt: on + ] + ) + # copy cert, key, and bundle to client + for filename in (self.certfile, self.keyfile): + data = self.master.get_file_contents(filename) + client.put_file_contents(filename, data) + + cabundle = self.master.get_file_contents(paths.KDC_CA_BUNDLE_PEM) + client.put_file_contents(self.tmpbundle, cabundle) + + def test_client_install_pkinit(self): + tasks.kinit_admin(self.master) + self.add_certmaperule() + self.add_host() + self.create_cert() + + tasks.install_client( + self.master, + self.clients[0], + pkinit_identity=f"FILE:{self.certfile},{self.keyfile}", + extra_args=[f"--pkinit-anchor=FILE:{self.tmpbundle}"], + )