From 9932b1dc98f60e975aa17163966545713f15717c Mon Sep 17 00:00:00 2001
From: Thomas Woerner <twoerner@redhat.com>
Date: Mon, 13 Jun 2022 10:37:32 +0200
Subject: [PATCH] New roles for smartcard server and client setup

There are new smartcard roles in the roles folder:

    roles/ipasmartcard_server
    roles/ipasmartcard_client

This roles allows to setup smartcard for servers and clients.

Here is the documentation for the roles:

    roles/ipasmartcard_server/README.md
    roles/ipasmartcard_client/README.md

New example playbooks have been added:

    playbooks/install-smartcard-server.yml
    playbooks/install-smartcard-replicas.yml
    playbooks/install-smartcard-servers.yml
    playbooks/install-smartcard-clients.yml
---
 README.md                                     |   3 +
 playbooks/install-smartcard-clients.yml       |   8 +
 playbooks/install-smartcard-replicas.yml      |   8 +
 playbooks/install-smartcard-server.yml        |   8 +
 playbooks/install-smartcard-servers.yml       |   8 +
 roles/ipasmartcard_client/README.md           | 111 ++++++++
 roles/ipasmartcard_client/defaults/main.yml   |   4 +
 ...rtcard_client_add_ca_to_sssd_auth_ca_db.sh |  30 +++
 ...martcard_client_add_ca_to_systemwide_db.sh |  31 +++
 ...ient_add_pkcs11_module_to_systemwide_db.sh |  36 +++
 .../library/ipasmartcard_client_get_vars.py   |  83 ++++++
 .../ipasmartcard_client_validate_ca_certs.py  | 110 ++++++++
 roles/ipasmartcard_client/meta/main.yml       |  22 ++
 roles/ipasmartcard_client/tasks/main.yml      | 173 ++++++++++++
 roles/ipasmartcard_client/vars/default.yml    |   3 +
 roles/ipasmartcard_server/README.md           | 169 ++++++++++++
 roles/ipasmartcard_server/defaults/main.yml   |   4 +
 ...rtcard_server_add_ca_to_sssd_auth_ca_db.sh |  30 +++
 ...martcard_server_add_ca_to_systemwide_db.sh |  31 +++
 ...asmartcard_server_enable_ocsp_directive.sh |  35 +++
 ...tcard_server_mark_httpd_cert_as_trusted.sh |  35 +++
 .../library/ipasmartcard_server_get_vars.py   | 159 +++++++++++
 .../ipasmartcard_server_validate_ca_certs.py  | 110 ++++++++
 roles/ipasmartcard_server/meta/main.yml       |  22 ++
 roles/ipasmartcard_server/tasks/main.yml      | 247 ++++++++++++++++++
 roles/ipasmartcard_server/vars/default.yml    |   2 +
 26 files changed, 1482 insertions(+)
 create mode 100644 playbooks/install-smartcard-clients.yml
 create mode 100644 playbooks/install-smartcard-replicas.yml
 create mode 100644 playbooks/install-smartcard-server.yml
 create mode 100644 playbooks/install-smartcard-servers.yml
 create mode 100644 roles/ipasmartcard_client/README.md
 create mode 100644 roles/ipasmartcard_client/defaults/main.yml
 create mode 100644 roles/ipasmartcard_client/files/ipasmartcard_client_add_ca_to_sssd_auth_ca_db.sh
 create mode 100644 roles/ipasmartcard_client/files/ipasmartcard_client_add_ca_to_systemwide_db.sh
 create mode 100644 roles/ipasmartcard_client/files/ipasmartcard_client_add_pkcs11_module_to_systemwide_db.sh
 create mode 100644 roles/ipasmartcard_client/library/ipasmartcard_client_get_vars.py
 create mode 100644 roles/ipasmartcard_client/library/ipasmartcard_client_validate_ca_certs.py
 create mode 100644 roles/ipasmartcard_client/meta/main.yml
 create mode 100644 roles/ipasmartcard_client/tasks/main.yml
 create mode 100644 roles/ipasmartcard_client/vars/default.yml
 create mode 100644 roles/ipasmartcard_server/README.md
 create mode 100644 roles/ipasmartcard_server/defaults/main.yml
 create mode 100644 roles/ipasmartcard_server/files/ipasmartcard_server_add_ca_to_sssd_auth_ca_db.sh
 create mode 100644 roles/ipasmartcard_server/files/ipasmartcard_server_add_ca_to_systemwide_db.sh
 create mode 100644 roles/ipasmartcard_server/files/ipasmartcard_server_enable_ocsp_directive.sh
 create mode 100644 roles/ipasmartcard_server/files/ipasmartcard_server_mark_httpd_cert_as_trusted.sh
 create mode 100644 roles/ipasmartcard_server/library/ipasmartcard_server_get_vars.py
 create mode 100644 roles/ipasmartcard_server/library/ipasmartcard_server_validate_ca_certs.py
 create mode 100644 roles/ipasmartcard_server/meta/main.yml
 create mode 100644 roles/ipasmartcard_server/tasks/main.yml
 create mode 100644 roles/ipasmartcard_server/vars/default.yml

diff --git a/README.md b/README.md
index 98bbc5ad..a3ddc3aa 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ Features
 * One-time-password (OTP) support for client installation
 * Repair mode for clients
 * Backup and restore, also to and from controller
+* Smartcard setup for servers and clients
 * Modules for automembership rule management
 * Modules for automount key management
 * Modules for automount location management
@@ -425,6 +426,8 @@ Roles
 * [Replica](roles/ipareplica/README.md)
 * [Client](roles/ipaclient/README.md)
 * [Backup](roles/ipabackup/README.md)
+* [SmartCard server](roles/ipasmartcard_server/README.md)
+* [SmartCard client](roles/ipasmartcard_client/README.md)
 
 Modules in plugin/modules
 =========================
diff --git a/playbooks/install-smartcard-clients.yml b/playbooks/install-smartcard-clients.yml
new file mode 100644
index 00000000..d78e7834
--- /dev/null
+++ b/playbooks/install-smartcard-clients.yml
@@ -0,0 +1,8 @@
+---
+- name: Playbook to setup smartcard for IPA clients
+  hosts: ipaclients
+  become: true
+
+  roles:
+  - role: ipasmartcard_client
+    state: present
diff --git a/playbooks/install-smartcard-replicas.yml b/playbooks/install-smartcard-replicas.yml
new file mode 100644
index 00000000..1482d196
--- /dev/null
+++ b/playbooks/install-smartcard-replicas.yml
@@ -0,0 +1,8 @@
+---
+- name: Playbook to setup smartcard for IPA replicas
+  hosts: ipareplicas
+  become: true
+
+  roles:
+  - role: ipasmartcard_server
+    state: present
diff --git a/playbooks/install-smartcard-server.yml b/playbooks/install-smartcard-server.yml
new file mode 100644
index 00000000..ebca03cb
--- /dev/null
+++ b/playbooks/install-smartcard-server.yml
@@ -0,0 +1,8 @@
+---
+- name: Playbook to setup smartcard for IPA server
+  hosts: ipaserver
+  become: true
+
+  roles:
+  - role: ipasmartcard_server
+    state: present
diff --git a/playbooks/install-smartcard-servers.yml b/playbooks/install-smartcard-servers.yml
new file mode 100644
index 00000000..4986fb4b
--- /dev/null
+++ b/playbooks/install-smartcard-servers.yml
@@ -0,0 +1,8 @@
+---
+- name: Playbook to setup smartcard for IPA server and replicas
+  hosts: ipaserver, ipareplicas
+  become: true
+
+  roles:
+  - role: ipasmartcard_server
+    state: present
diff --git a/roles/ipasmartcard_client/README.md b/roles/ipasmartcard_client/README.md
new file mode 100644
index 00000000..afe0ae1c
--- /dev/null
+++ b/roles/ipasmartcard_client/README.md
@@ -0,0 +1,111 @@
+ipasmartcard_client role
+========================
+
+Description
+-----------
+
+This role allows to configure IPA clients for Smart Card authentication.
+
+**Note**: The ansible-freeipa smartcard client role requires an enrolled IPA client.
+
+
+Features
+--------
+* Client setup for Smart Card authentication
+
+
+Supported FreeIPA Versions
+--------------------------
+
+FreeIPA versions 4.5 and up are supported by this role.
+
+
+Supported Distributions
+-----------------------
+
+* RHEL/CentOS 7.6+
+* Fedora 26+
+
+
+Requirements
+------------
+
+**Controller**
+* Ansible version: 2.8+
+
+**Node**
+* Supported FreeIPA version (see above)
+* Supported distribution (needed for package installation only, see above)
+* Enrolled IPA client
+
+
+Limitations
+-----------
+
+Only the enablement of smartcards is supported by the role, there is no disablement.
+
+
+Usage
+=====
+
+Example inventory file with IPA clients:
+
+```ini
+[ipaclients]
+ipaclient1.example.com
+ipaclient2.example.com
+
+[ipaclients:vars]
+ipaadmin_password=SomeADMINpassword
+ipasmartcard_client_ca_certs=/etc/ipa/ca.crt
+```
+
+Example playbook to setup smartcard for the IPA clients using admin password and ipasmartcard_client_ca_certs from inventory file:
+
+```yaml
+---
+- name: Playbook to setup smartcard for IPA clients
+  hosts: ipaclients
+  become: true
+
+  roles:
+  - role: ipasmartcard_client
+    state: present
+```
+
+Playbooks
+=========
+
+The playbooks needed to setup smartcard for the IPA clients is part of the repository in the playbooks folder.
+
+```
+install-smartcard-clients.yml
+```
+
+Please remember to link or copy the playbooks to the base directory of ansible-freeipa if you want to use the roles within the source archive.
+
+
+How to setup smartcard for clients
+----------------------------------
+
+```bash
+ansible-playbook -v -i inventory/hosts install-smartcard-clients.yml
+```
+This will setup the clients for smartcard use.
+
+
+Variables
+=========
+
+Variable | Description | Required
+-------- | ----------- | --------
+`ipaadmin_principal` | The kerberos principal used for admin. Will be set to `admin` if not set. (string) | no
+`ipaadmin_password` | The password for the IPA admin user. As an alternative an admin user keytab can be used instead with `ipaadmin_keytab`. (string) | yes
+`ipaadmin_keytab` | The admin keytab as an alternative to `ipaadmin_password`. (string) | no
+`ipasmartcard_client_ca_certs` | The CA certificates for smartcard use. If `ipasmartcard_client_ca_certs` is not set, but `ipasmartcard_server_ca_certs`, then `ipasmartcard_server_ca_certs` will be used. | yes
+
+
+Authors
+=======
+
+Thomas Woerner
diff --git a/roles/ipasmartcard_client/defaults/main.yml b/roles/ipasmartcard_client/defaults/main.yml
new file mode 100644
index 00000000..15602768
--- /dev/null
+++ b/roles/ipasmartcard_client/defaults/main.yml
@@ -0,0 +1,4 @@
+---
+# defaults file for ipasmartcard_client role
+
+ipaclient_install_packages: yes
diff --git a/roles/ipasmartcard_client/files/ipasmartcard_client_add_ca_to_sssd_auth_ca_db.sh b/roles/ipasmartcard_client/files/ipasmartcard_client_add_ca_to_sssd_auth_ca_db.sh
new file mode 100644
index 00000000..cdb89127
--- /dev/null
+++ b/roles/ipasmartcard_client/files/ipasmartcard_client_add_ca_to_sssd_auth_ca_db.sh
@@ -0,0 +1,30 @@
+#!/bin/bash -eu
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 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/>.
+
+cert_file=$1
+db=$2
+
+if [ -z "${cert_file}" ] || [ -z "${db}" ]; then
+    echo "Usage: $0 <ca cert> <db file>"
+    exit 1
+fi
+
+cat "${cert_file}" >> "${db}"
diff --git a/roles/ipasmartcard_client/files/ipasmartcard_client_add_ca_to_systemwide_db.sh b/roles/ipasmartcard_client/files/ipasmartcard_client_add_ca_to_systemwide_db.sh
new file mode 100644
index 00000000..6da15801
--- /dev/null
+++ b/roles/ipasmartcard_client/files/ipasmartcard_client_add_ca_to_systemwide_db.sh
@@ -0,0 +1,31 @@
+#!/bin/bash -eu
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 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/>.
+
+cert_file=$1
+db=$2
+
+if [ -z "${cert_file}" ] || [ -z "${db}" ]; then
+    echo "Usage: $0 <ca cert> <db file>"
+    exit 1
+fi
+
+uuid=$(uuidgen)
+certutil -d "${db}" -A -i "${cert_file}" -n "Smart Card CA ${uuid}" -t CT,C,C
diff --git a/roles/ipasmartcard_client/files/ipasmartcard_client_add_pkcs11_module_to_systemwide_db.sh b/roles/ipasmartcard_client/files/ipasmartcard_client_add_pkcs11_module_to_systemwide_db.sh
new file mode 100644
index 00000000..12f3e750
--- /dev/null
+++ b/roles/ipasmartcard_client/files/ipasmartcard_client_add_pkcs11_module_to_systemwide_db.sh
@@ -0,0 +1,36 @@
+#!/bin/bash -eu
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 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/>.
+
+nssdb=$1
+module_name="OpenSC"
+pkcs11_shared_lib="/usr/lib64/opensc-pkcs11.so"
+
+if [ -z "${nssdb}" ]; then
+    echo "Usage: $0 <nssdb>"
+    exit 1
+fi
+
+if modutil -dbdir "${nssdb}" -list | grep -q "${module_name}" || p11-kit list-modules | grep -i "${module_name}" -q
+then
+    echo "${module_name} PKCS#11 module already configured"
+else
+    echo "" | modutil -dbdir "${nssdb}" -add "${module_name}" -libfile "${pkcs11_shared_lib}"
+fi
diff --git a/roles/ipasmartcard_client/library/ipasmartcard_client_get_vars.py b/roles/ipasmartcard_client/library/ipasmartcard_client_get_vars.py
new file mode 100644
index 00000000..66c65912
--- /dev/null
+++ b/roles/ipasmartcard_client/library/ipasmartcard_client_get_vars.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 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: ipasmartcard_client_get_vars
+short description:
+  Get variables from ipaplatform and python interpreter used for the module.
+description:
+  Get variables from ipaplatform and python interpreter used for the module.
+options:
+author:
+    - Thomas Woerner
+'''
+
+EXAMPLES = '''
+- name: Get VARS from IPA
+  ipasmartcard_client_get_vars:
+  register: ipasmartcard_client_vars
+'''
+
+RETURN = '''
+NSS_DB_DIR:
+  description: paths.NSS_DB_DIR from ipaplatform
+  returned: always
+  type: str
+USE_AUTHSELECT:
+  description: True if "AUTHSELECT" is defined in paths
+  returned: always
+  type: bool
+python_interpreter:
+  description: Python interpreter from sys.executable
+  returned: always
+  type: str
+'''
+
+import sys
+from ansible.module_utils.basic import AnsibleModule
+from ipaplatform.paths import paths
+
+
+def main():
+    ansible_module = AnsibleModule(
+        argument_spec={},
+        supports_check_mode=False,
+    )
+
+    ansible_module.exit_json(changed=False,
+                             NSS_DB_DIR=paths.NSS_DB_DIR,
+                             USE_AUTHSELECT=hasattr(paths, "AUTHSELECT"),
+                             python_interpreter=sys.executable)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/roles/ipasmartcard_client/library/ipasmartcard_client_validate_ca_certs.py b/roles/ipasmartcard_client/library/ipasmartcard_client_validate_ca_certs.py
new file mode 100644
index 00000000..c89427b1
--- /dev/null
+++ b/roles/ipasmartcard_client/library/ipasmartcard_client_validate_ca_certs.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Based on ipa-replica-install code
+#
+# Copyright (C) 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: ipasmartcard_server_validate_ca_certs
+short description: Validate CA certs
+description: Validate CA certs
+options:
+  ca_cert_files:
+    description:
+      List of files containing CA certificates for the service certificate
+      files
+    required: yes
+author:
+    - Thomas Woerner
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+import os.path
+from ansible.module_utils.basic import AnsibleModule
+try:
+    from ipalib import x509
+except ImportError:
+    x509 = None
+
+
+def main():
+    ansible_module = AnsibleModule(
+        argument_spec=dict(
+            ca_cert_files=dict(required=False, type='list', default=[]),
+        ),
+        supports_check_mode=False,
+    )
+
+    # get parameters #
+
+    ca_cert_files = ansible_module.params.get('ca_cert_files')
+
+    # import check #
+
+    if x509 is None:
+        ansible_module.fail_json(msg="Failed to import x509 from ipalib")
+
+    # validate ca certs #
+
+    if ca_cert_files is not None:
+        if not isinstance(ca_cert_files, list):
+            ansible_module.fail_json(
+                msg="Expected list, got %s" % repr(ca_cert_files))
+        # remove duplicates
+        ca_cert_files = list(dict.fromkeys(ca_cert_files))
+        # validate
+        for cert in ca_cert_files:
+            if not os.path.exists(cert):
+                ansible_module.fail_json(msg="'%s' does not exist" % cert)
+            if not os.path.isfile(cert):
+                ansible_module.fail_json(msg="'%s' is not a file" % cert)
+            if not os.path.isabs(cert):
+                ansible_module.fail_json(
+                    msg="'%s' is not an absolute file path" % cert)
+            try:
+                x509.load_certificate_from_file(cert)
+            except Exception:
+                ansible_module.fail_json(
+                    msg="'%s' is not a valid certificate file" % cert)
+
+    # exit #
+
+    ansible_module.exit_json(changed=False,
+                             ca_cert_files=ca_cert_files)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/roles/ipasmartcard_client/meta/main.yml b/roles/ipasmartcard_client/meta/main.yml
new file mode 100644
index 00000000..26ef55df
--- /dev/null
+++ b/roles/ipasmartcard_client/meta/main.yml
@@ -0,0 +1,22 @@
+---
+dependencies: []
+
+galaxy_info:
+  author: Thomas Woerner
+  description: A role to setup IPA server(s) for Smart Card authentication
+  company: Red Hat, Inc
+  license: GPLv3
+  min_ansible_version: 2.8
+  platforms:
+  - name: Fedora
+    versions:
+    - all
+  - name: EL
+    versions:
+    - 7
+    - 8
+  galaxy_tags:
+    - identity
+    - ipa
+    - freeipa
+    - smartcard
diff --git a/roles/ipasmartcard_client/tasks/main.yml b/roles/ipasmartcard_client/tasks/main.yml
new file mode 100644
index 00000000..e4a560bc
--- /dev/null
+++ b/roles/ipasmartcard_client/tasks/main.yml
@@ -0,0 +1,173 @@
+---
+# tasks file for ipasmartcard_client role
+
+- name: Uninstall smartcard client
+  ansible.builtin.fail: msg="Uninstalling smartcard for IPA is not supported"
+  when: state|default('present') == 'absent'
+
+- name: Import variables specific to distribution
+  ansible.builtin.include_vars: "{{ item }}"
+  with_first_found:
+    - "vars/{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_version'] }}.yml"
+    - "vars/{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}.yml"
+    - "vars/{{ ansible_facts['distribution'] }}.yml"
+    # os_family is used as a fallback for distros which are not currently
+    # supported, but are based on a supported distro family. For example,
+    # Oracle, Rocky, Alma and Alibaba linux, which are all "RedHat" based.
+    - "vars/{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_version'] }}.yml"
+    - "vars/{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_major_version'] }}.yml"
+    - "vars/{{ ansible_facts['os_family'] }}.yml"
+    # If neither distro nor family is supported, try a default configuration.
+    - "vars/default.yml"
+
+- block:
+
+  # CA CERTS
+
+  # Use "ipasmartcard_server_ca_certs"
+
+  - name: Use "ipasmartcard_server_ca_certs"
+    ansible.builtin.set_fact:
+      ipasmartcard_client_ca_certs: "{{ ipasmartcard_server_ca_certs }}"
+    when: ipasmartcard_client_ca_certs is not defined and
+          ipasmartcard_server_ca_certs is defined
+
+  # Fail on empty "ipasmartcard_client_ca_certs"
+
+  - name: Fail on empty "ipasmartcard_client_ca_certs"
+    ansible.builtin.fail: msg="No CA certs given in 'ipasmartcard_client_ca_certs'"
+    when: ipasmartcard_client_ca_certs is not defined or
+          ipasmartcard_client_ca_certs | length < 1
+
+  # Validate ipasmartcard_client_ca_certs
+
+  - name: Validate CA certs "{{ ipasmartcard_client_ca_certs }}"
+    ipasmartcard_client_validate_ca_certs:
+      ca_cert_files: "{{ ipasmartcard_client_ca_certs }}"
+    register: result_validate_ca_certs
+
+  # INSTALL needed packages: opensc, dconf and krb5-pkinit-openssl
+
+  - name: Ensure needed packages are installed
+    ansible.builtin.package:
+      name: "{{ ipasmartcard_client_packages }}"
+      state: present
+
+  # REMOVE pam_pkcs11
+
+  - name: Ensure pam_pkcs11 is missing
+    ansible.builtin.package:
+      name: "{{ ipasmartcard_client_remove_pam_pkcs11_packages }}"
+      state: absent
+
+  # KINIT
+
+  - name: Set default principal if not given
+    ansible.builtin.set_fact:
+      ipaadmin_principal: admin
+    when: ipaadmin_principal is undefined
+
+  - name: kinit using "{{ ipaadmin_principal }}" password
+    ansible.builtin.command: kinit "{{ ipaadmin_principal }}"
+    args:
+      stdin: "{{ ipaadmin_password }}"
+    when: ipaadmin_password is defined
+
+  - name: kinit using "{{ ipaadmin_principal }}" keytab
+    ansible.builtin.command: kinit -kt "{{ ipaadmin_keytab }}" "{{ ipaadmin_principal }}"
+    when: ipaadmin_keytab is defined
+
+  # Enable and start smartcard daemon
+
+  - name: Enable and start smartcard daemon
+    ansible.builtin.service:
+      name: pcscd
+      enabled: true
+      state: started
+
+  # GET VARS FROM IPA
+
+  - name: Get VARS from IPA
+    ipasmartcard_client_get_vars:
+    register: ipasmartcard_client_vars
+
+  # Add pkcs11 module to systemwide db
+
+  - name: Add pkcs11 module to systemwide db
+    ansible.builtin.script: ipasmartcard_client_add_pkcs11_module_to_systemwide_db.sh
+                            "{{ ipasmartcard_client_vars.NSS_DB_DIR }}"
+
+  # Ensure /etc/sssd/pki exists
+
+  - block:
+    - name: Ensure /etc/sssd/pki exists
+      ansible.builtin.file:
+        path: /etc/sssd/pki
+        state: directory
+        mode: 0711
+
+    - name: Ensure /etc/sssd/pki/sssd_auth_ca_db.pem is absent
+      ansible.builtin.file:
+        path: /etc/sssd/pki/sssd_auth_ca_db.pem
+        state: absent
+
+    when: ipasmartcard_client_vars.USE_AUTHSELECT
+
+  # Upload smartcard CA certificates to systemwide db
+
+  - name: Upload smartcard CA certificates to systemwide db
+    ansible.builtin.script: ipasmartcard_client_add_ca_to_systemwide_db.sh
+                            "{{ item }}"
+                            "{{ ipasmartcard_client_vars.NSS_DB_DIR }}"
+    with_items: "{{ result_validate_ca_certs.ca_cert_files }}"
+
+  # Newer version of sssd use OpenSSL and read the CA certs
+  # from /etc/sssd/pki/sssd_auth_ca_db.pem
+
+  - name: Add CA certs to /etc/sssd/pki/sssd_auth_ca_db.pem
+    ansible.builtin.script: ipasmartcard_client_add_ca_to_sssd_auth_ca_db.sh
+                            "{{ item }}"
+                            /etc/sssd/pki/sssd_auth_ca_db.pem
+    with_items: "{{ result_validate_ca_certs.ca_cert_files }}"
+    when: ipasmartcard_client_vars.USE_AUTHSELECT
+
+  # Update ipa CA certificate store
+
+  - name: Update ipa CA certificate store
+    ansible.builtin.command: ipa-certupdate
+
+  # Run authselect or authconfig to configure smartcard auth
+
+  - name: Use authselect to enable Smart Card authentication
+    ansible.builtin.command: authselect enable-feature with-smartcard
+    when: ipasmartcard_client_vars.USE_AUTHSELECT
+
+  - name: Use authconfig to enable Smart Card authentication
+    ansible.builtin.command: authconfig --enablesssd --enablesssdauth --enablesmartcard --smartcardmodule=sssd --smartcardaction=1 --updateall
+    when: not ipasmartcard_client_vars.USE_AUTHSELECT
+
+  # Set pam_cert_auth=True in /etc/sssd/sssd.conf
+
+  - name: Store NSS OCSP upgrade state
+    ansible.builtin.command: "{{ ipasmartcard_client_vars.python_interpreter }}"
+    args:
+      stdin: |
+        from SSSDConfig import SSSDConfig
+        c = SSSDConfig()
+        c.import_config()
+        c.set("pam", "pam_cert_auth", "True")
+        c.write()
+    when: ipasmartcard_client_vars.USE_AUTHSELECT
+
+  # Restart sssd
+
+  - name: Restart sssd
+    ansible.builtin.service:
+      name: sssd
+      state: restarted
+
+  ### ALWAYS ###
+
+  always:
+  - name: kdestroy
+    ansible.builtin.command: kdestroy -A
diff --git a/roles/ipasmartcard_client/vars/default.yml b/roles/ipasmartcard_client/vars/default.yml
new file mode 100644
index 00000000..15e5ae2c
--- /dev/null
+++ b/roles/ipasmartcard_client/vars/default.yml
@@ -0,0 +1,3 @@
+---
+ipasmartcard_client_remove_pam_pkcs11_packages: [ "pam_pkcs11" ]
+ipasmartcard_client_packages: [ "opensc", "dconf", "krb5-pkinit-openssl" ]
diff --git a/roles/ipasmartcard_server/README.md b/roles/ipasmartcard_server/README.md
new file mode 100644
index 00000000..299b49fc
--- /dev/null
+++ b/roles/ipasmartcard_server/README.md
@@ -0,0 +1,169 @@
+ipasmartcard_server role
+========================
+
+Description
+-----------
+
+This role allows to configure an IPA server (master or replica) for Smart Card authentication.
+
+**Note**: The ansible-freeipa smartcard server role requires a configured IPA server with ipa-ca.DOMAIN resolvable by the DNS server.
+
+With external DNS ipa-ca.DOMAIN needs to be set.
+
+
+Features
+--------
+* Server setup for Smart Card authentication
+
+
+Supported FreeIPA Versions
+--------------------------
+
+FreeIPA versions 4.5 and up are supported by this role.
+
+
+Supported Distributions
+-----------------------
+
+* RHEL/CentOS 7.6+
+* Fedora 26+
+
+
+Requirements
+------------
+
+**Controller**
+* Ansible version: 2.8+
+
+**Node**
+* Supported FreeIPA version (see above)
+* Supported distribution (needed for package installation only, see above)
+* Deployed IPA server
+
+
+Limitations
+-----------
+
+Only the enablement of smartcards is supported by the role, there is no disablement. The disablement of features in IPA in not supported.
+
+
+Usage
+=====
+
+Example inventory file with ipa server and replicas:
+
+```ini
+[ipaserver]
+ipaserver.example.com
+
+[ipareplicas]
+ipareplica1.example.com
+ipareplica2.example.com
+
+[ipacluster:children]
+ipaserver
+ipareplicas
+
+[ipacluster:vars]
+ipaadmin_password=SomeADMINpassword
+ipasmartcard_server_ca_certs=/etc/ipa/ca.crt
+```
+
+Example playbook to setup smartcard for the IPA server using admin password and ipasmartcard_server_ca_certs from inventory file:
+
+```yaml
+---
+- name: Playbook to setup smartcard for IPA server
+  hosts: ipaserver
+  become: true
+
+  roles:
+  - role: ipasmartcard_server
+    state: present
+```
+
+Example playbook to setup smartcard for the IPA servers in ipareplicas group using admin password and ipasmartcard_server_ca_certs from inventory file:
+
+```yaml
+---
+- name: Playbook to setup smartcard for IPA replicas
+  hosts: ipareplicas
+  become: true
+
+  roles:
+  - role: ipasmartcard_server
+    state: present
+```
+
+Example playbook to setup smartcard for the IPA servers in ipaserver and ipareplicas group using admin password and ipasmartcard_server_ca_certs from inventory file:
+
+```yaml
+---
+- name: Playbook to setup smartcard for IPA server and replicas
+  hosts: ipaserver, ipareplicas
+  become: true
+
+  roles:
+  - role: ipasmartcard_server
+    state: present
+```
+
+
+Playbooks
+=========
+
+The playbooks needed to setup smartcard for the IPA server and the replicas are part of the repository in the playbooks folder.
+
+```
+install-smartcard-server.yml
+install-smartcard-servers.yml
+install-smartcard-replicas.yml
+```
+
+Please remember to link or copy the playbooks to the base directory of ansible-freeipa if you want to use the roles within the source archive.
+
+
+How to setup smartcard for server
+---------------------------------
+
+```bash
+ansible-playbook -v -i inventory/hosts install-smartcard-server.yml
+```
+This will setup the server for smartcard use.
+
+
+How to setup smartcard for replicas
+-----------------------------------
+
+```bash
+ansible-playbook -v -i inventory/hosts install-smartcard-replicas.yml
+```
+This will setup the replicas for smartcard use.
+
+
+How to setup smartcard for server and replicas
+----------------------------------------------
+
+```bash
+ansible-playbook -v -i inventory/hosts install-smartcard-servers.yml
+```
+This will setup the replicas for smartcard use.
+
+
+Variables
+=========
+
+Variable | Description | Required
+-------- | ----------- | --------
+`ipaadmin_principal` | The kerberos principal used for admin. Will be set to `admin` if not set. (string) | no
+`ipaadmin_password` | The password for the IPA admin user. As an alternative an admin user keytab can be used instead with `ipaadmin_keytab`. (string) | yes
+`ipaadmin_keytab` | The admin keytab as an alternative to `ipaadmin_password`. (string) | no
+`ipaserver_hostname` | Fully qualified name of the server. By default `ansible_facts['fqdn']` will be used. (string) | no
+`ipaserver_domain` | The primary DNS domain of an existing IPA deployment. By default the domain will be used from ipa server-find result. (string)  | no
+`ipasmartcard_server_ca_certs` | The CA certificates for smartcard use. (list of string) | yes
+
+
+Authors
+=======
+
+Thomas Woerner
diff --git a/roles/ipasmartcard_server/defaults/main.yml b/roles/ipasmartcard_server/defaults/main.yml
new file mode 100644
index 00000000..d852e46e
--- /dev/null
+++ b/roles/ipasmartcard_server/defaults/main.yml
@@ -0,0 +1,4 @@
+---
+# defaults file for ipasmartcard_server role
+
+ipaserver_install_packages: yes
diff --git a/roles/ipasmartcard_server/files/ipasmartcard_server_add_ca_to_sssd_auth_ca_db.sh b/roles/ipasmartcard_server/files/ipasmartcard_server_add_ca_to_sssd_auth_ca_db.sh
new file mode 100644
index 00000000..cdb89127
--- /dev/null
+++ b/roles/ipasmartcard_server/files/ipasmartcard_server_add_ca_to_sssd_auth_ca_db.sh
@@ -0,0 +1,30 @@
+#!/bin/bash -eu
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 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/>.
+
+cert_file=$1
+db=$2
+
+if [ -z "${cert_file}" ] || [ -z "${db}" ]; then
+    echo "Usage: $0 <ca cert> <db file>"
+    exit 1
+fi
+
+cat "${cert_file}" >> "${db}"
diff --git a/roles/ipasmartcard_server/files/ipasmartcard_server_add_ca_to_systemwide_db.sh b/roles/ipasmartcard_server/files/ipasmartcard_server_add_ca_to_systemwide_db.sh
new file mode 100644
index 00000000..6da15801
--- /dev/null
+++ b/roles/ipasmartcard_server/files/ipasmartcard_server_add_ca_to_systemwide_db.sh
@@ -0,0 +1,31 @@
+#!/bin/bash -eu
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 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/>.
+
+cert_file=$1
+db=$2
+
+if [ -z "${cert_file}" ] || [ -z "${db}" ]; then
+    echo "Usage: $0 <ca cert> <db file>"
+    exit 1
+fi
+
+uuid=$(uuidgen)
+certutil -d "${db}" -A -i "${cert_file}" -n "Smart Card CA ${uuid}" -t CT,C,C
diff --git a/roles/ipasmartcard_server/files/ipasmartcard_server_enable_ocsp_directive.sh b/roles/ipasmartcard_server/files/ipasmartcard_server_enable_ocsp_directive.sh
new file mode 100644
index 00000000..fb4bb32d
--- /dev/null
+++ b/roles/ipasmartcard_server/files/ipasmartcard_server_enable_ocsp_directive.sh
@@ -0,0 +1,35 @@
+#!/bin/bash -eu
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 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/>.
+
+directive=$1
+conf_file=$2
+
+if [ -z "${directive}" ] || [ -z "${conf_file}" ]; then
+    echo "Usage: $0 <directive> <config file>"
+    exit 1
+fi
+
+if grep -q "${directive} " "${conf_file}"
+then
+    sed -i.ipabkp -r "s/^#*[[:space:]]*${directive}[[:space:]]+(on|off)$/${directive} on/" "${conf_file}"
+else
+    sed -i.ipabkp "/<\/VirtualHost>/i ${directive} on" "${conf_file}"
+fi
diff --git a/roles/ipasmartcard_server/files/ipasmartcard_server_mark_httpd_cert_as_trusted.sh b/roles/ipasmartcard_server/files/ipasmartcard_server_mark_httpd_cert_as_trusted.sh
new file mode 100644
index 00000000..16b529d9
--- /dev/null
+++ b/roles/ipasmartcard_server/files/ipasmartcard_server_mark_httpd_cert_as_trusted.sh
@@ -0,0 +1,35 @@
+#!/bin/bash -eu
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 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/>.
+
+directive=$1
+nss_conf=$2
+nickname=$3
+alias_dir=$4
+
+if [ -z "${directive}" ] || [ -z "${nss_conf}" ] || [ -z "${nickname}" ] || 
+   [ -z "${alias_dir}" ]
+then
+    echo "Usage: $0 <directive> <nss conf> <nickname directive> <alias directory>"
+    exit 1
+fi
+
+http_cert_nick=$(grep "${nickname}" "${nss_conf}" | cut -f 2 -d ' ')
+certutil -M -n "$http_cert_nick" -d "${alias_dir}" -f "${alias_dir}/pwdfile.txt" -t "Pu,u,u"
diff --git a/roles/ipasmartcard_server/library/ipasmartcard_server_get_vars.py b/roles/ipasmartcard_server/library/ipasmartcard_server_get_vars.py
new file mode 100644
index 00000000..97b1d8c9
--- /dev/null
+++ b/roles/ipasmartcard_server/library/ipasmartcard_server_get_vars.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 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: ipasmartcard_server_get_vars
+short description:
+  Get variables from ipaplatform and ipaserver and python interpreter.
+description:
+  Get variables from ipaplatform and ipaserver and python interpreter.
+options:
+author:
+    - Thomas Woerner
+'''
+
+EXAMPLES = '''
+- name: Get VARS from IPA
+  ipasmartcard_server_get_vars:
+  register: ipasmartcard_server_vars
+'''
+
+RETURN = '''
+NSS_OCSP_ENABLED:
+  description:
+    Empty string for newer systems using ssl.conf and not nss.conf for
+    HTTP instance where OCSP_ENABLED and OCSP_DIRECTIVE are defined in
+    ipaserver.install.httpinstance, else NSS_OCSP_ENABLED imported from
+    ipaserver.install.httpinstance.
+  returned: always
+  type: str
+NSS_OCSP_DIRECTIVE:
+  description:
+    Empty string for newer systems using ssl.conf and not nss.conf for
+    HTTP instance where OCSP_ENABLED and OCSP_DIRECTIVE are defined in
+    ipaserver.install.httpinstance, else NSSOCSP.
+  returned: always
+  type: str
+NSS_NICKNAME_DIRECTIVE:
+  description:
+    Empty string for newer systems using ssl.conf and not nss.conf for
+    HTTP instance where OCSP_ENABLED and OCSP_DIRECTIVE are defined in
+    ipaserver.install.httpinstance, else NSSNickname
+  returned: always
+  type: str
+OCSP_ENABLED:
+  description:
+    OCSP_ENABLED imported from ipaserver.install.httpinstance, if import
+    succeeds, else ""
+  returned: always
+  type: str
+OCSP_DIRECTIVE:
+  description:
+    OCSP_DIRECTIVE imported from ipaserver.install.httpinstance, if import
+    succeeds, else ""
+  returned: always
+  type: str
+HTTPD_SSL_CONF:
+  description: paths.HTTPD_SSL_CONF from ipaplatform
+  returned: always
+  type: str
+HTTPD_NSS_CONF:
+  description: paths.HTTPD_NSS_CONF from ipaplatform
+  returned: always
+  type: str
+HTTPD_ALIAS_DIR:
+  description: paths.HTTPD_ALIAS_DIR from ipaplatform
+  returned: always
+  type: str
+allow_httpd_ifp:
+  description:
+    True if sssd_enable_ifp can be imported from ipaclient.install.client,
+    else false.
+  returned: always
+  type: bool
+NSS_DB_DIR:
+  description: paths.NSS_DB_DIR from ipaplatform
+  returned: always
+  type: str
+USE_AUTHSELECT:
+  description: True if "AUTHSELECT" is defined in paths
+  returned: always
+  type: bool
+python_interpreter:
+  description: Python interpreter from sys.executable
+  returned: always
+  type: str
+'''
+
+import sys
+from ansible.module_utils.basic import AnsibleModule
+from ipaplatform.paths import paths
+try:
+    from ipaserver.install.httpinstance import OCSP_ENABLED, OCSP_DIRECTIVE
+    NSS_OCSP_ENABLED = ""
+    NSS_OCSP_DIRECTIVE = ""
+    NSS_NICKNAME_DIRECTIVE = ""
+except ImportError:
+    from ipaserver.install.httpinstance import NSS_OCSP_ENABLED
+    NSS_OCSP_DIRECTIVE = "NSSOCSP"
+    NSS_NICKNAME_DIRECTIVE = "NSSNickname"
+    OCSP_ENABLED = ""
+    OCSP_DIRECTIVE = ""
+try:
+    from ipaclient.install.client import sssd_enable_ifp
+except ImportError:
+    sssd_enable_ifp = None
+
+
+def main():
+    ansible_module = AnsibleModule(
+        argument_spec={},
+        supports_check_mode=False,
+    )
+
+    ansible_module.exit_json(changed=False,
+                             NSS_OCSP_ENABLED=NSS_OCSP_ENABLED,
+                             NSS_OCSP_DIRECTIVE=NSS_OCSP_DIRECTIVE,
+                             NSS_NICKNAME_DIRECTIVE=NSS_NICKNAME_DIRECTIVE,
+                             OCSP_ENABLED=OCSP_ENABLED,
+                             OCSP_DIRECTIVE=OCSP_DIRECTIVE,
+                             HTTPD_SSL_CONF=paths.HTTPD_SSL_CONF,
+                             HTTPD_NSS_CONF=paths.HTTPD_NSS_CONF,
+                             HTTPD_ALIAS_DIR=paths.HTTPD_ALIAS_DIR,
+                             allow_httpd_ifp=sssd_enable_ifp is not None,
+                             NSS_DB_DIR=paths.NSS_DB_DIR,
+                             USE_AUTHSELECT=hasattr(paths, "AUTHSELECT"),
+                             python_interpreter=sys.executable)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/roles/ipasmartcard_server/library/ipasmartcard_server_validate_ca_certs.py b/roles/ipasmartcard_server/library/ipasmartcard_server_validate_ca_certs.py
new file mode 100644
index 00000000..c89427b1
--- /dev/null
+++ b/roles/ipasmartcard_server/library/ipasmartcard_server_validate_ca_certs.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Based on ipa-replica-install code
+#
+# Copyright (C) 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: ipasmartcard_server_validate_ca_certs
+short description: Validate CA certs
+description: Validate CA certs
+options:
+  ca_cert_files:
+    description:
+      List of files containing CA certificates for the service certificate
+      files
+    required: yes
+author:
+    - Thomas Woerner
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+import os.path
+from ansible.module_utils.basic import AnsibleModule
+try:
+    from ipalib import x509
+except ImportError:
+    x509 = None
+
+
+def main():
+    ansible_module = AnsibleModule(
+        argument_spec=dict(
+            ca_cert_files=dict(required=False, type='list', default=[]),
+        ),
+        supports_check_mode=False,
+    )
+
+    # get parameters #
+
+    ca_cert_files = ansible_module.params.get('ca_cert_files')
+
+    # import check #
+
+    if x509 is None:
+        ansible_module.fail_json(msg="Failed to import x509 from ipalib")
+
+    # validate ca certs #
+
+    if ca_cert_files is not None:
+        if not isinstance(ca_cert_files, list):
+            ansible_module.fail_json(
+                msg="Expected list, got %s" % repr(ca_cert_files))
+        # remove duplicates
+        ca_cert_files = list(dict.fromkeys(ca_cert_files))
+        # validate
+        for cert in ca_cert_files:
+            if not os.path.exists(cert):
+                ansible_module.fail_json(msg="'%s' does not exist" % cert)
+            if not os.path.isfile(cert):
+                ansible_module.fail_json(msg="'%s' is not a file" % cert)
+            if not os.path.isabs(cert):
+                ansible_module.fail_json(
+                    msg="'%s' is not an absolute file path" % cert)
+            try:
+                x509.load_certificate_from_file(cert)
+            except Exception:
+                ansible_module.fail_json(
+                    msg="'%s' is not a valid certificate file" % cert)
+
+    # exit #
+
+    ansible_module.exit_json(changed=False,
+                             ca_cert_files=ca_cert_files)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/roles/ipasmartcard_server/meta/main.yml b/roles/ipasmartcard_server/meta/main.yml
new file mode 100644
index 00000000..26ef55df
--- /dev/null
+++ b/roles/ipasmartcard_server/meta/main.yml
@@ -0,0 +1,22 @@
+---
+dependencies: []
+
+galaxy_info:
+  author: Thomas Woerner
+  description: A role to setup IPA server(s) for Smart Card authentication
+  company: Red Hat, Inc
+  license: GPLv3
+  min_ansible_version: 2.8
+  platforms:
+  - name: Fedora
+    versions:
+    - all
+  - name: EL
+    versions:
+    - 7
+    - 8
+  galaxy_tags:
+    - identity
+    - ipa
+    - freeipa
+    - smartcard
diff --git a/roles/ipasmartcard_server/tasks/main.yml b/roles/ipasmartcard_server/tasks/main.yml
new file mode 100644
index 00000000..c3a89a4e
--- /dev/null
+++ b/roles/ipasmartcard_server/tasks/main.yml
@@ -0,0 +1,247 @@
+---
+# tasks file for ipasmartcard_server role
+
+- name: Uninstall smartcard server
+  ansible.builtin.fail: msg="Uninstalling smartcard for IPA is not supported"
+  when: state|default('present') == 'absent'
+
+- name: Import variables specific to distribution
+  ansible.builtin.include_vars: "{{ item }}"
+  with_first_found:
+    - "vars/{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_version'] }}.yml"
+    - "vars/{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}.yml"
+    - "vars/{{ ansible_facts['distribution'] }}.yml"
+    # os_family is used as a fallback for distros which are not currently
+    # supported, but are based on a supported distro family. For example,
+    # Oracle, Rocky, Alma and Alibaba linux, which are all "RedHat" based.
+    - "vars/{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_version'] }}.yml"
+    - "vars/{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_major_version'] }}.yml"
+    - "vars/{{ ansible_facts['os_family'] }}.yml"
+    # If neither distro nor family is supported, try a default configuration.
+    - "vars/default.yml"
+
+- block:
+
+  # CA CERTS
+
+  # Fail on empty "ipasmartcard_server_ca_certs"
+  - name: Fail on empty "ipasmartcard_server_ca_certs"
+    ansible.builtin.fail: msg="No CA certs given in 'ipasmartcard_server_ca_certs'"
+    when: ipasmartcard_server_ca_certs is not defined or
+          ipasmartcard_server_ca_certs | length < 1
+
+  # Validate ipasmartcard_server_ca_certs
+
+  - name: Validate CA certs "{{ ipasmartcard_server_ca_certs }}"
+    ipasmartcard_server_validate_ca_certs:
+      ca_cert_files: "{{ ipasmartcard_server_ca_certs }}"
+    register: result_validate_ca_certs
+
+  # INSTALL bind-utils
+
+  - name: Ensure {{ ipasmartcard_server_bindutils_packages }} are installed
+    ansible.builtin.package:
+      name: "{{ ipasmartcard_server_bindutils_packages }}"
+      state: present
+    when: ipaserver_install_packages | bool
+
+  # KINIT
+
+  - name: Set default principal if not given
+    ansible.builtin.set_fact:
+      ipaadmin_principal: admin
+    when: ipaadmin_principal is undefined
+
+  - name: kinit using "{{ ipaadmin_principal }}" password
+    ansible.builtin.command: kinit "{{ ipaadmin_principal }}"
+    args:
+      stdin: "{{ ipaadmin_password }}"
+    when: ipaadmin_password is defined
+
+  - name: kinit using "{{ ipaadmin_principal }}" keytab
+    ansible.builtin.command: kinit -kt "{{ ipaadmin_keytab }}" "{{ ipaadmin_principal }}"
+    when: ipaadmin_keytab is defined
+
+  # IS MASTER
+
+  - name: Check that this is an IPA master
+    ansible.builtin.command: ipa server-show --raw "{{ ipaserver_hostname | default(ansible_facts['fqdn']) }}"
+    register: result_ipa_server_show
+
+  - name: Fail if not an IPA server
+    ansible.builtin.fail: msg="Not an IPA server"
+    when: result_ipa_server_show.failed
+
+  - name: Get Domain from server-find server name
+    ansible.builtin.set_fact:
+      ipaserver_domain: "{{ (result_ipa_server_show.stdout | regex_search('cn: (.+)', '\\1'))[0].split('.')[1:] | join ('.') }}"
+    when: ipaserver_domain is not defined
+
+  - name: Get ipa-ca records
+    ansible.builtin.command: "dig +short ipa-ca.{{ ipaserver_domain }}"
+    register: result_get_ipaca_records
+
+  - name: Fail if ipa-ca records are not resolvable
+    ansible.builtin.fail: msg="ipa-ca records are not resolvable"
+    when: result_get_ipaca_records.failed or
+          result_get_ipaca_records.stdout | length == 0
+
+  # GET VARS FROM IPA
+
+  - name: Get VARS from IPA
+    ipasmartcard_server_get_vars:
+    register: ipasmartcard_server_vars
+
+  # ENABLE NSS OCSP
+
+  - name: Enable the OCSP directive in nss.conf
+    ansible.builtin.script: ipasmartcard_server_enable_ocsp_directive.sh
+                            "{{ ipasmartcard_server_vars.NSS_OCSP_DIRECTIVE }}"
+                            "{{ ipasmartcard_server_vars.HTTPD_NSS_CONF }}"
+    when: ipasmartcard_server_vars.NSS_OCSP_ENABLED | length > 0
+
+  # MARK NSS HTTPD CERT AS TRUSTED
+
+  - name: Mark HTTPD CERT as trusted
+    ansible.builtin.script: ipasmartcard_server_mark_httpd_cert_as_trusted.sh
+                            "{{ ipasmartcard_server_vars.NSS_OCSP_DIRECTIVE }}"
+                            "{{ ipasmartcard_server_vars.HTTPD_NSS_CONF }}"
+                            "{{ ipasmartcard_server_vars.NSS_NICKNAME_DIRECTIVE }}"
+                            "{{ ipasmartcard_server_vars.HTTPD_ALIAS_DIR }}"
+    when: ipasmartcard_server_vars.NSS_OCSP_ENABLED | length > 0
+
+  # ENABLE SSL OCSP
+
+  - name: Enable the OCSP directive in ssl.conf
+    ansible.builtin.script: ipasmartcard_server_enable_ocsp_directive.sh
+                            "{{ ipasmartcard_server_vars.OCSP_DIRECTIVE }}"
+                            "{{ ipasmartcard_server_vars.HTTPD_SSL_CONF }}"
+    when: ipasmartcard_server_vars.OCSP_ENABLED | length > 0
+
+  # Restart apache
+
+  - name: Restart apache
+    ansible.builtin.service:
+      name: httpd
+      state: restarted
+
+  # RECORD HTTPD OCSP STATUS
+
+  # Store the NSS OCSP upgrade state
+
+  - name: Store NSS OCSP upgrade state
+    ansible.builtin.command: "{{ ipasmartcard_server_vars.python_interpreter }}"
+    args:
+      stdin: |
+        from ipaserver.install import sysupgrade
+        sysupgrade.set_upgrade_state("httpd", "{{ ipasmartcard_server_vars.NSS_OCSP_DIRECTIVE }}", True)
+    when: ipasmartcard_server_vars.NSS_OCSP_ENABLED | length > 0
+
+  # Store the SSL OCSP upgrade state
+
+  - name: Store SSL OCSP upgrade state
+    ansible.builtin.command: "{{ ipasmartcard_server_vars.python_interpreter }}"
+    args:
+      stdin: |
+        from ipaserver.install import sysupgrade
+        sysupgrade.set_upgrade_state("httpd", "{{ ipasmartcard_server_vars.OCSP_DIRECTIVE }}", True)
+    when: ipasmartcard_server_vars.OCSP_ENABLED | length > 0
+
+  # check whether PKINIT is configured on the master
+
+  - name: Enable PKINIT
+    ansible.builtin.command: ipa-pkinit-manage enable
+
+  # Enable OK-AS-DELEGATE flag on the HTTP principal
+  # This enables smart card login to WebUI
+
+  - name: Enable OK-AS-DELEGATE flag on the HTTP principal
+    ipaservice:
+      name: "HTTP/{{ ipaserver_hostname | default(ansible_facts['fqdn']) }}"
+      ok_to_auth_as_delegate: yes
+
+  # HTTPD IFP
+
+  - block:
+
+    # Allow Apache to access SSSD IFP
+
+    - name: Allow Apache to access SSSD IFP
+      ansible.builtin.command: "{{ ipasmartcard_server_vars.python_interpreter }}"
+      args:
+        stdin: |
+          import SSSDConfig
+          from ipaclient.install.client import sssd_enable_ifp
+          from ipaplatform.paths import paths
+          c = SSSDConfig.SSSDConfig()
+          c.import_config()
+          sssd_enable_ifp(c, allow_httpd=True)
+          c.write(paths.SSSD_CONF)
+      when: ipasmartcard_server_vars.OCSP_ENABLED | length > 0
+
+    # Restart sssd
+
+    - name: Restart sssd
+      ansible.builtin.service:
+        name: sssd
+        state: restarted
+
+    when: ipasmartcard_server_vars.allow_httpd_ifp
+
+  # Ensure /etc/sssd/pki exists
+
+  - block:
+    - name: Ensure /etc/sssd/pki exists
+      ansible.builtin.file:
+        path: /etc/sssd/pki
+        state: directory
+        mode: 0711
+
+    - name: Ensure /etc/sssd/pki/sssd_auth_ca_db.pem is absent
+      ansible.builtin.file:
+        path: /etc/sssd/pki/sssd_auth_ca_db.pem
+        state: absent
+
+    when: ipasmartcard_server_vars.USE_AUTHSELECT
+
+  # Upload smartcard CA certificates to systemwide db
+
+  - name: Upload smartcard CA certificates to systemwide db
+    ansible.builtin.script: ipasmartcard_server_add_ca_to_systemwide_db.sh
+                            "{{ item }}"
+                            "{{ ipasmartcard_server_vars.NSS_DB_DIR }}"
+    with_items: "{{ result_validate_ca_certs.ca_cert_files }}"
+
+  # Newer version of sssd use OpenSSL and read the CA certs
+  # from /etc/sssd/pki/sssd_auth_ca_db.pem
+
+  - name: Add CA certs to /etc/sssd/pki/sssd_auth_ca_db.pem
+    ansible.builtin.script: ipasmartcard_server_add_ca_to_sssd_auth_ca_db.sh
+                            "{{ item }}"
+                            /etc/sssd/pki/sssd_auth_ca_db.pem
+    with_items: "{{ result_validate_ca_certs.ca_cert_files }}"
+    when: ipasmartcard_server_vars.USE_AUTHSELECT
+
+  # Install smartcard signing CA certs
+
+  - name: Install smartcard signing CA certs
+    ansible.builtin.command: ipa-cacert-manage install "{{ item }}" -t CT,C,C
+    with_items: "{{ result_validate_ca_certs.ca_cert_files }}"
+
+  # Update ipa CA certificate store
+
+  - name: Update ipa CA certificate store
+    ansible.builtin.command: ipa-certupdate
+
+  # Restart krb5kdc
+
+  - name: Restart krb5kdc
+    ansible.builtin.service:
+      name: krb5kdc
+      state: restarted
+
+  ### ALWAYS ###
+
+  always:
+  - name: kdestroy
+    ansible.builtin.command: kdestroy -A
diff --git a/roles/ipasmartcard_server/vars/default.yml b/roles/ipasmartcard_server/vars/default.yml
new file mode 100644
index 00000000..ff88a90d
--- /dev/null
+++ b/roles/ipasmartcard_server/vars/default.yml
@@ -0,0 +1,2 @@
+---
+ipasmartcard_server_bindutils_packages: [ "bind-utils" ]
-- 
GitLab