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
+