Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dist: $(NAME)-$(VERSION).tar.gz

$(NAME)-$(VERSION).tar.gz: build/collections/foremanctl
git archive --prefix $(NAME)-$(VERSION)/ --output $(NAME)-$(VERSION).tar HEAD
tar --append --file $(NAME)-$(VERSION).tar --transform='s#^#$(NAME)-$(VERSION)/#' build/collections/foremanctl
tar --append --file $(NAME)-$(VERSION).tar --transform='s#^#$(NAME)-$(VERSION)/#' --exclude='build/collections/foremanctl/ansible_collections/*/*/tests/*' build/collections/foremanctl
gzip $(NAME)-$(VERSION).tar

build/collections/foremanctl: $(REQUIREMENTS_YML)
Expand Down
18 changes: 18 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions docs/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ There are multiple use cases from the users perspective that dictate what parame
| `--foreman-initial-admin-password` | Initial password for the admin user | `--foreman-initial-admin-password` |
| `--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` |
| `--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`<br/> `--foreman-ipa-authentication-api` |
| `--external-authentication-pam-service` | PAM service used for host-based access control in IPA | `--foreman-pam-service` |

#### Certs

Expand Down Expand Up @@ -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` | | |
Expand Down
7 changes: 7 additions & 0 deletions src/playbooks/deploy/metadata.obsah.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +17 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between the two? ipa is UI only, while ipa_with_api does UI and API?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And to extend that, later we can add other methods here, like oidc etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ipa is UI only, while ipa_with_api does UI and API?

exactly

later we can add other methods here, like oidc etc?

Yes, that was the intent here

Copy link
Member

@evgeni evgeni Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and those are mutually exclusive, right? as in, there can be only one external auth?

(LDAP probably always works, but that's not terminated on the httpd level)

Copy link
Contributor Author

@adamruzicka adamruzicka Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. I wanted to avoid the situation we had in the old installer where all the methods could be enabled individually, but some conflicted with others.

external_authentication_pam_service:
help: Name of the PAM service to use for IPA authentication

include:
- _certificate_source
Expand Down
1 change: 1 addition & 0 deletions src/requirements.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
collections:
- community.postgresql
- community.crypto
- community.general
- ansible.posix
- name: containers.podman
version: ">=1.16.4"
Expand Down
8 changes: 8 additions & 0 deletions src/roles/foreman/templates/settings.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
1 change: 1 addition & 0 deletions src/roles/hammer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------------
Expand Down
1 change: 1 addition & 0 deletions src/roles/hammer/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}"
6 changes: 5 additions & 1 deletion src/roles/hammer/templates/cli.modules.d-foreman.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/roles/hammer/templates/hammer-root.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
7 changes: 7 additions & 0 deletions src/roles/httpd/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions src/roles/httpd/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
ansible.builtin.systemd:
name: httpd
state: restarted

- name: Restart sssd
ansible.builtin.systemd:
name: sssd
state: restarted
22 changes: 22 additions & 0 deletions src/roles/httpd/tasks/external_auth/cleanup.yml
Original file line number Diff line number Diff line change
@@ -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
91 changes: 91 additions & 0 deletions src/roles/httpd/tasks/external_auth/ipa.yml
Original file line number Diff line number Diff line change
@@ -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"

Comment on lines +45 to +50
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory this is not needed if httpd_ipa_pam_service != foreman

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But when is it not foreman?

Copy link
Contributor Author

@adamruzicka adamruzicka Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the service either needs to be defined manually by the user as a yet another pam service or it has to be the name of a user-created HBAC service in ipa

Edit: in either case, deploying our custom service config shouldn't be needed, but also doesn't really hurt anything

- 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
3 changes: 3 additions & 0 deletions src/roles/httpd/tasks/external_auth/ipa_with_api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
- name: Configure IPA authentication with API support
ansible.builtin.import_tasks: ipa.yml
8 changes: 8 additions & 0 deletions src/roles/httpd/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions src/roles/httpd/tasks/sssd.yml
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions src/roles/httpd/templates/external_auth.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{% if httpd_external_authentication in ['ipa', 'ipa_with_api'] %}
# Intercept form submissions for PAM authentication
<Location /users/login>
InterceptFormPAMService {{ httpd_ipa_pam_service }}
InterceptFormLogin login[login]
InterceptFormPassword login[password]
</Location>

# Lookup user attributes from SSSD
<LocationMatch ^(/api(/v2)?)?/users/(ext)?login/?$>
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
</LocationMatch>

# GSSAPI/Kerberos authentication for web UI
<LocationMatch ^/users/extlogin/?$>
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 '<html><meta http-equiv="refresh" content="0; URL=/users/login"><body>Kerberos authentication did not pass.</body></html>'
# The following is needed as a workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1020087
ErrorDocument 500 '<html><meta http-equiv="refresh" content="0; URL=/users/login"><body>Kerberos authentication did not pass.</body></html>'
</LocationMatch>

# External authentication for API endpoints
<LocationMatch ^/api(/v2)?/users/extlogin/?$>
SSLRequireSSL
{% if httpd_external_authentication == 'ipa_with_api' %}
<If "%{HTTP:Authorization} =~ /^Basic/">
AuthType Basic
AuthName "PAM Authentication"
AuthBasicProvider PAM
AuthPAMService {{ httpd_ipa_pam_service }}
</If>
<Else>
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>
{% 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." }'
</LocationMatch>
{% endif %}

Loading
Loading