diff --git a/docs/deployment.md b/docs/deployment.md index 3efd1f8f..0aebfbbd 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -213,3 +213,21 @@ For secure connections with certificate verification: --candlepin-database-password=secure_candlepin_password \ --pulp-database-password=secure_pulp_password ``` + +## External authentication support + +The deployment utility supports setting up necessary services to allow leveraging kerberos for user authentication if the host machine is enrolled in a FreeIPA/IDM or Active Directory realm. + +### Prerequisites + +Before configuring external authentication support, ensure the following requirements are met: +- the host machine is enrolled in FreeIPA/IDM or Active Directory realm +- a keytab for the Kerberos service principal is available at the host machine + +### External Database Configuration Parameters + +The external authentication configuration is managed through `foremanctl` command line parameters: +- `--external-authentication`: Set to `ipa` to enable kerberos authentication in WebUI, set to `ipa_with_api` to enable kerberos authentication in WebUI, API and hammer CLI +- `--external-authentication-pam-server`: PAM service name to use when authenticating users, can be changed in case a specific FreeIPA/IDM HBAC service should be used (default: `foreman`) + +If `hammer` feature is enabled and `--external-authentication` is set to `ipa_with_api`, `hammer` will be configured to use negotiate-based authentication. diff --git a/docs/parameters.md b/docs/parameters.md index 7b2b4a63..d262629d 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -52,6 +52,8 @@ There are multiple use cases from the users perspective that dictate what parame | `--foreman-puma-workers` | Number of workers for Puma | `--foreman-foreman-service-puma-workers` | | `--pulp-worker-count` | Number of pulp workers | `--foreman-proxy-content-pulpcore-worker-count` | | `--tuning` | Sets the tuning profile | `--tuning` | +| `--external-authentication={ipa,ipa_with_api}` | Enable configuration for external authentication via IPA for web UI (or webUI and API for `ipa_with_api`), expects the target machine to [be enrolled into FreeIPA/IDM](https://docs.theforeman.org/3.16/Configuring_User_Authentication/index-katello.html#enrolling-foreman-server-in-freeipa-domain) | `--foreman-ipa-authentication`
`--foreman-ipa-authentication-api` | +| `--external-authentication-pam-service` | PAM service used for host-based access control in IPA | `--foreman-pam-service` | #### Certs @@ -86,8 +88,6 @@ There are multiple use cases from the users perspective that dictate what parame | `--certs-reset` | Parameter to reset all certificates to default | foreman-installer | No | | `--foreman-initial-location` | | | | `--foreman-initial-organization` | | | -| `--foreman-ipa-authentication` | | | -| `--foreman-ipa-authentication-api` | | | | `--foreman-keycloak` | | | | `--foreman-keycloak-app-name` | | | | `--foreman-keycloak-realm` | | | diff --git a/src/playbooks/deploy/metadata.obsah.yaml b/src/playbooks/deploy/metadata.obsah.yaml index 997aaec9..f36a827f 100644 --- a/src/playbooks/deploy/metadata.obsah.yaml +++ b/src/playbooks/deploy/metadata.obsah.yaml @@ -11,6 +11,13 @@ variables: help: Number of workers for Puma. pulp_worker_count: help: Number of Pulp workers. Defaults to 8 or the number of CPU cores, whichever is smaller. + external_authentication: + help: External authentication method to use + choices: + - ipa + - ipa_with_api + external_authentication_pam_service: + help: Name of the PAM service to use for IPA authentication include: - _certificate_source diff --git a/src/requirements.yml b/src/requirements.yml index 01a19ec6..1587cb83 100644 --- a/src/requirements.yml +++ b/src/requirements.yml @@ -1,6 +1,7 @@ collections: - community.postgresql - community.crypto + - community.general - ansible.posix - name: containers.podman version: ">=1.16.4" diff --git a/src/roles/foreman/templates/settings.yaml.j2 b/src/roles/foreman/templates/settings.yaml.j2 index 321426c6..c5527cb3 100644 --- a/src/roles/foreman/templates/settings.yaml.j2 +++ b/src/roles/foreman/templates/settings.yaml.j2 @@ -20,3 +20,11 @@ :oauth_consumer_key: {{ foreman_oauth_consumer_key }} :oauth_consumer_secret: {{ foreman_oauth_consumer_secret }} {% endif %} + +{% if httpd_external_authentication in ['ipa', 'ipa_with_api'] %} +:authorize_login_delegation: true +:authorize_login_delegation_auth_source_user_autocreate: External +{% endif %} +{% if httpd_external_authentication == 'ipa_with_api' %} +:authorize_login_delegation_api: true +{% endif %} diff --git a/src/roles/hammer/README.md b/src/roles/hammer/README.md index 603a4f35..21987741 100644 --- a/src/roles/hammer/README.md +++ b/src/roles/hammer/README.md @@ -8,6 +8,7 @@ variables - `hammer_foreman_server_url`: The URL of the Foreman server to configure (default: `https://{{ ansible_facts['fqdn'] }}`) - `hammer_ca_certificate`: The CA bundle to verify the connection to Foreman. By default this is empty and Hammer uses the system store. Alternatively you can use `hammer --fetch-ca-cert` to obtain the cert of the configured Foreman server. - `hammer_packages`: Which plugin packages to install. +- `hammer_kerberos_auth_enabled`: Enable Kerberos/negotiate authentication for Hammer CLI (default: `false`). When enabled, Hammer will use session-based authentication with negotiate auth type, allowing users to authenticate using `kinit`. usage inside foremanctl ----------------------- diff --git a/src/roles/hammer/defaults/main.yml b/src/roles/hammer/defaults/main.yml index ab69142d..98171f78 100644 --- a/src/roles/hammer/defaults/main.yml +++ b/src/roles/hammer/defaults/main.yml @@ -5,3 +5,4 @@ hammer_default_plugins: - foreman hammer_plugins: [] hammer_packages: "{{ (hammer_default_plugins+hammer_plugins) | map('regex_replace', '^', 'hammer-cli-plugin-') }}" +hammer_kerberos_auth_enabled: "{{ external_authentication is defined and external_authentication == 'ipa_with_api' }}" diff --git a/src/roles/hammer/templates/cli.modules.d-foreman.yml.j2 b/src/roles/hammer/templates/cli.modules.d-foreman.yml.j2 index ebf50a31..dbc8ac60 100644 --- a/src/roles/hammer/templates/cli.modules.d-foreman.yml.j2 +++ b/src/roles/hammer/templates/cli.modules.d-foreman.yml.j2 @@ -8,7 +8,11 @@ # Enable using sessions # When sessions are enabled, hammer ignores credentials stored in the config file # and asks for them interactively at the begining of each session. - :use_sessions: false + :use_sessions: {{ hammer_kerberos_auth_enabled | ternary(true, false) }} +{% if hammer_kerberos_auth_enabled %} + # Default authentication type for Kerberos/negotiate authentication + :default_auth_type: 'Negotiate_Auth' +{% endif %} # Check API documentation cache status on each request :refresh_cache: false diff --git a/src/roles/hammer/templates/hammer-root.yml.j2 b/src/roles/hammer/templates/hammer-root.yml.j2 index c8f0b3e9..c1c1a0f9 100644 --- a/src/roles/hammer/templates/hammer-root.yml.j2 +++ b/src/roles/hammer/templates/hammer-root.yml.j2 @@ -2,3 +2,9 @@ # Credentials. You'll be asked for them interactively if you leave them blank here :username: '{{ foreman_initial_admin_username }}' :password: '{{ foreman_initial_admin_password }}' +{% if hammer_kerberos_auth_enabled %} + # Enable using sessions for Kerberos authentication + :use_sessions: true + # Default authentication type for Kerberos/negotiate authentication + :default_auth_type: 'Negotiate_Auth' +{% endif %} diff --git a/src/roles/httpd/defaults/main.yml b/src/roles/httpd/defaults/main.yml index d8172964..19961685 100644 --- a/src/roles/httpd/defaults/main.yml +++ b/src/roles/httpd/defaults/main.yml @@ -3,3 +3,10 @@ httpd_pulp_api_backend: http://localhost:24817 httpd_pulp_content_backend: http://localhost:24816 httpd_foreman_backend: http://localhost:3000 httpd_pub_dir: /var/www/html/pub + +# External authentication configuration +httpd_external_authentication: "{{ external_authentication | default(None) }}" +httpd_ipa_manage_sssd: true +httpd_ipa_keytab: /etc/httpd/conf/http.keytab +httpd_ipa_pam_service: "{{ external_authentication_pam_service | default('foreman') }}" +httpd_ipa_gssapi_local_name: true diff --git a/src/roles/httpd/handlers/main.yml b/src/roles/httpd/handlers/main.yml index f171e66b..8c444312 100644 --- a/src/roles/httpd/handlers/main.yml +++ b/src/roles/httpd/handlers/main.yml @@ -3,3 +3,8 @@ ansible.builtin.systemd: name: httpd state: restarted + +- name: Restart sssd + ansible.builtin.systemd: + name: sssd + state: restarted diff --git a/src/roles/httpd/tasks/external_auth/cleanup.yml b/src/roles/httpd/tasks/external_auth/cleanup.yml new file mode 100644 index 00000000..11fb4199 --- /dev/null +++ b/src/roles/httpd/tasks/external_auth/cleanup.yml @@ -0,0 +1,22 @@ +--- +- name: Remove external authentication configuration + ansible.builtin.file: + path: "/etc/httpd/conf.d/05-{{ item }}.d/external_auth.conf" + state: absent + notify: + - Restart httpd + loop: + - foreman + - foreman-ssl + +- name: Remove Apache module configuration files for IPA authentication + ansible.builtin.file: + path: /etc/httpd/conf.modules.d/55-{{ item }}.conf + state: absent + loop: + - authnz_pam + - intercept_form_submit + - lookup_identity + - auth_gssapi + notify: + - Restart httpd diff --git a/src/roles/httpd/tasks/external_auth/ipa.yml b/src/roles/httpd/tasks/external_auth/ipa.yml new file mode 100644 index 00000000..a732263f --- /dev/null +++ b/src/roles/httpd/tasks/external_auth/ipa.yml @@ -0,0 +1,91 @@ +--- +- name: Install Apache modules for IPA authentication + ansible.builtin.package: + name: + - mod_authnz_pam + - mod_intercept_form_submit + - mod_lookup_identity + - mod_auth_gssapi + state: present + +- name: Create directory for Apache module configuration + ansible.builtin.file: + path: /etc/httpd/conf.modules.d + state: directory + mode: "0755" + +- name: Load Apache modules for IPA authentication + ansible.builtin.copy: + dest: /etc/httpd/conf.modules.d/55-{{ item }}.conf + content: | + LoadModule {{ item }}_module modules/mod_{{ item }}.so + mode: "0644" + loop: + - authnz_pam + - intercept_form_submit + - lookup_identity + - auth_gssapi + notify: + - Restart httpd + +- name: Set SELinux booleans for IPA authentication + ansible.posix.seboolean: + name: "{{ item }}" + state: true + persistent: true + loop: + - allow_httpd_mod_auth_pam + - httpd_dbus_sssd + when: ansible_facts['selinux']['status'] == "enabled" + +- name: Configure SSSD for IPA authentication + ansible.builtin.import_tasks: ../sssd.yml + when: httpd_ipa_manage_sssd | bool + +- name: Create PAM service file for IPA authentication + ansible.builtin.template: + src: pam_service.j2 + dest: "/etc/pam.d/{{ httpd_ipa_pam_service }}" + mode: "0644" + +- name: Ensure keytab directory exists + ansible.builtin.file: + path: "{{ httpd_ipa_keytab | dirname }}" + state: directory + mode: "0755" + +- name: Get keytab for HTTP service + ansible.builtin.shell: + cmd: | + KRB5CCNAME=KEYRING:session:get-http-service-keytab kinit -k || true + KRB5CCNAME=KEYRING:session:get-http-service-keytab /usr/sbin/ipa-getkeytab -k {{ httpd_ipa_keytab }} -p HTTP/{{ ansible_facts['fqdn'] }} + kdestroy -c KEYRING:session:get-http-service-keytab || true + creates: "{{ httpd_ipa_keytab }}" + changed_when: false + +- name: Set keytab file permissions + ansible.builtin.file: + path: "{{ httpd_ipa_keytab }}" + owner: apache + group: apache + mode: "0600" + +- name: Create directory for Apache configuration fragments + ansible.builtin.file: + path: /etc/httpd/conf.d/05-{{ item }}.d + state: directory + mode: "0755" + loop: + - foreman + - foreman-ssl + +- name: Deploy external authentication configuration + ansible.builtin.template: + src: external_auth.conf.j2 + dest: /etc/httpd/conf.d/05-{{ item }}.d/external_auth.conf + mode: "0644" + notify: + - Restart httpd + loop: + - foreman + - foreman-ssl diff --git a/src/roles/httpd/tasks/external_auth/ipa_with_api.yml b/src/roles/httpd/tasks/external_auth/ipa_with_api.yml new file mode 100644 index 00000000..e8106648 --- /dev/null +++ b/src/roles/httpd/tasks/external_auth/ipa_with_api.yml @@ -0,0 +1,3 @@ +--- +- name: Configure IPA authentication with API support + ansible.builtin.import_tasks: ipa.yml diff --git a/src/roles/httpd/tasks/main.yml b/src/roles/httpd/tasks/main.yml index 1ee492fd..ae436413 100644 --- a/src/roles/httpd/tasks/main.yml +++ b/src/roles/httpd/tasks/main.yml @@ -58,6 +58,14 @@ remote_src: true mode: "0644" +- name: Configure external authentication + ansible.builtin.include_tasks: "external_auth/{{ httpd_external_authentication }}.yml" + when: httpd_external_authentication in ['ipa', 'ipa_with_api'] + +- name: Remove external authentication configuration if not enabled + ansible.builtin.include_tasks: external_auth/cleanup.yml + when: httpd_external_authentication is none + - name: Configure foreman vhost ansible.builtin.template: src: foreman-vhost.conf.j2 diff --git a/src/roles/httpd/tasks/sssd.yml b/src/roles/httpd/tasks/sssd.yml new file mode 100644 index 00000000..3dae192b --- /dev/null +++ b/src/roles/httpd/tasks/sssd.yml @@ -0,0 +1,55 @@ +--- +- name: Install sssd-dbus package + ansible.builtin.package: + name: sssd-dbus + state: present + +- name: Ensure SSSD service is running and enabled + ansible.builtin.systemd: + name: sssd + state: started + enabled: true + +- name: Read existing SSSD configuration + ansible.builtin.slurp: + src: /etc/sssd/sssd.conf + register: httpd_sssd_config + ignore_errors: true + +- name: Parse SSSD services configuration + ansible.builtin.set_fact: + httpd_sssd_existing_services: "{{ (httpd_sssd_config.content | default('') | b64decode | + regex_search('\\[sssd\\][\\s\\S]*?services\\s*=\\s*([^\\n]+)', '\\1', multiline=True) | + default(['']) | first) | trim }}" + when: httpd_sssd_config.content is defined + +- name: Configure SSSD services to include ifp + community.general.ini_file: + path: /etc/sssd/sssd.conf + section: sssd + option: services + value: "{{ httpd_sssd_existing_services }}{% if httpd_sssd_existing_services != '' %}, {% endif %}ifp" + mode: "0600" + when: httpd_sssd_existing_services | regex_search('\\bifp\\b') != 'ifp' + notify: + - Restart sssd + +- name: Configure SSSD IFP allowed_uids + community.general.ini_file: + path: /etc/sssd/sssd.conf + section: ifp + option: allowed_uids + value: "root, apache" + mode: "0600" + notify: + - Restart sssd + +- name: Configure SSSD IFP user_attributes + community.general.ini_file: + path: /etc/sssd/sssd.conf + section: ifp + option: user_attributes + value: "+email, +firstname, +lastname" + mode: "0600" + notify: + - Restart sssd diff --git a/src/roles/httpd/templates/external_auth.conf.j2 b/src/roles/httpd/templates/external_auth.conf.j2 new file mode 100644 index 00000000..05e1d7d2 --- /dev/null +++ b/src/roles/httpd/templates/external_auth.conf.j2 @@ -0,0 +1,69 @@ +{% if httpd_external_authentication in ['ipa', 'ipa_with_api'] %} +# Intercept form submissions for PAM authentication + + InterceptFormPAMService {{ httpd_ipa_pam_service }} + InterceptFormLogin login[login] + InterceptFormPassword login[password] + + +# Lookup user attributes from SSSD + + LookupUserAttr email REMOTE_USER_EMAIL + LookupUserAttr firstname REMOTE_USER_FIRSTNAME + LookupUserAttr lastname REMOTE_USER_LASTNAME + LookupUserGroups REMOTE_USER_GROUPS : + LookupUserGroupsIter REMOTE_USER_GROUP + + # Set headers for proxy requests + RequestHeader set REMOTE_USER %{REMOTE_USER}e + RequestHeader set REMOTE_USER_EMAIL %{REMOTE_USER_EMAIL}e + RequestHeader set REMOTE_USER_FIRSTNAME %{REMOTE_USER_FIRSTNAME}e + RequestHeader set REMOTE_USER_LASTNAME %{REMOTE_USER_LASTNAME}e + RequestHeader set REMOTE_USER_GROUPS %{REMOTE_USER_GROUPS}e + + +# GSSAPI/Kerberos authentication for web UI + + SSLRequireSSL + AuthType GSSAPI + AuthName "GSSAPI Single Sign On Login" + GssapiCredStore keytab:{{ httpd_ipa_keytab }} + GssapiSSLonly On + GssapiLocalName {{ httpd_ipa_gssapi_local_name | ternary('On', 'Off') }} + # require valid-user + require pam-account {{ httpd_ipa_pam_service }} + ErrorDocument 401 'Kerberos authentication did not pass.' + # The following is needed as a workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1020087 + ErrorDocument 500 'Kerberos authentication did not pass.' + + +# External authentication for API endpoints + + SSLRequireSSL +{% if httpd_external_authentication == 'ipa_with_api' %} + + AuthType Basic + AuthName "PAM Authentication" + AuthBasicProvider PAM + AuthPAMService {{ httpd_ipa_pam_service }} + + + AuthType GSSAPI + AuthName "GSSAPI Single Sign On Login" + GssapiCredStore keytab:{{ httpd_ipa_keytab }} + GssapiSSLonly On + GssapiLocalName {{ httpd_ipa_gssapi_local_name | ternary('On', 'Off') }} + +{% else %} + AuthType Basic + AuthName "PAM Authentication" + AuthBasicProvider PAM + AuthPAMService {{ httpd_ipa_pam_service }} +{% endif %} + require pam-account {{ httpd_ipa_pam_service }} + ErrorDocument 401 '{ "error": "External authentication did not pass." }' + # The following is needed as a workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1020087 + ErrorDocument 500 '{ "error": "External authentication did not pass." }' + +{% endif %} + diff --git a/src/roles/httpd/templates/pam_service.j2 b/src/roles/httpd/templates/pam_service.j2 new file mode 100644 index 00000000..f2a6a054 --- /dev/null +++ b/src/roles/httpd/templates/pam_service.j2 @@ -0,0 +1,3 @@ +auth required pam_sss.so +account required pam_sss.so +