Skip to content
Draft
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
229 changes: 131 additions & 98 deletions manifests/config.pp
Original file line number Diff line number Diff line change
Expand Up @@ -158,128 +158,161 @@
$listen_socket = '/run/foreman.sock'

class { 'foreman::config::apache':
app_root => $foreman::app_root,
priority => $foreman::vhost_priority,
servername => $foreman::servername,
serveraliases => $foreman::serveraliases,
server_port => $foreman::server_port,
server_ssl_port => $foreman::server_ssl_port,
proxy_backend => "unix://${listen_socket}",
ssl => $foreman::ssl,
ssl_ca => $foreman::server_ssl_ca,
ssl_chain => $foreman::server_ssl_chain,
ssl_cert => $foreman::server_ssl_cert,
ssl_key => $foreman::server_ssl_key,
ssl_crl => $foreman::server_ssl_crl,
ssl_protocol => $foreman::server_ssl_protocol,
ssl_verify_client => $foreman::server_ssl_verify_client,
user => $foreman::user,
foreman_url => $foreman::foreman_url,
ipa_authentication => $foreman::ipa_authentication,
keycloak => $foreman::keycloak,
keycloak_app_name => $foreman::keycloak_app_name,
keycloak_realm => $foreman::keycloak_realm,
app_root => $foreman::app_root,
priority => $foreman::vhost_priority,
servername => $foreman::servername,
serveraliases => $foreman::serveraliases,
server_port => $foreman::server_port,
server_ssl_port => $foreman::server_ssl_port,
proxy_backend => "unix://${listen_socket}",
ssl => $foreman::ssl,
ssl_ca => $foreman::server_ssl_ca,
ssl_chain => $foreman::server_ssl_chain,
ssl_cert => $foreman::server_ssl_cert,
ssl_key => $foreman::server_ssl_key,
ssl_crl => $foreman::server_ssl_crl,
ssl_protocol => $foreman::server_ssl_protocol,
ssl_verify_client => $foreman::server_ssl_verify_client,
user => $foreman::user,
foreman_url => $foreman::foreman_url,
external_authentication => $external_foreman::authentication,
keycloak => $foreman::keycloak,
keycloak_app_name => $foreman::keycloak_app_name,
keycloak_realm => $foreman::keycloak_realm,
}

contain foreman::config::apache

$foreman_socket_override = template('foreman/foreman.socket-overrides.erb')

if $foreman::ipa_authentication {
if $facts['os']['selinux']['enabled'] {
selboolean { ['allow_httpd_mod_auth_pam', 'httpd_dbus_sssd']:
persistent => true,
value => 'on',
case $foreman::external_authentication {
'ipa', 'ipa_with_api': {
if $facts['os']['selinux']['enabled'] {
selboolean { ['allow_httpd_mod_auth_pam', 'httpd_dbus_sssd']:
persistent => true,
value => 'on',
}
}
}

if $foreman::ipa_manage_sssd {
service { 'sssd':
ensure => running,
enable => true,
require => Package['sssd-dbus'],
if $foreman::ipa_manage_sssd {
service { 'sssd':
ensure => running,
enable => true,
require => Package['sssd-dbus'],
}
}
}

file { "/etc/pam.d/${foreman::pam_service}":
ensure => file,
owner => root,
group => root,
mode => '0644',
content => template('foreman/pam_service.erb'),
}
file { "/etc/pam.d/${foreman::pam_service}":
ensure => file,
owner => root,
group => root,
mode => '0644',
content => template('foreman/pam_service.erb'),
}

$http_keytab = pick($foreman::http_keytab, "${apache::conf_dir}/http.keytab")
$http_keytab = pick($foreman::http_keytab, "${apache::conf_dir}/http.keytab")

exec { 'ipa-getkeytab':
command => "/bin/echo Get keytab \
&& KRB5CCNAME=KEYRING:session:get-http-service-keytab kinit -k \
&& KRB5CCNAME=KEYRING:session:get-http-service-keytab /usr/sbin/ipa-getkeytab -k ${http_keytab} -p HTTP/${facts['networking']['fqdn']} \
&& kdestroy -c KEYRING:session:get-http-service-keytab",
creates => $http_keytab,
}
-> file { $http_keytab:
ensure => file,
owner => $apache::user,
mode => '0600',
}
exec { 'ipa-getkeytab':
command => "/bin/echo Get keytab \
&& KRB5CCNAME=KEYRING:session:get-http-service-keytab kinit -k \
&& KRB5CCNAME=KEYRING:session:get-http-service-keytab /usr/sbin/ipa-getkeytab -k ${http_keytab} -p HTTP/${facts['networking']['fqdn']} \
&& kdestroy -c KEYRING:session:get-http-service-keytab",
creates => $http_keytab,
}
-> file { $http_keytab:
ensure => file,
owner => $apache::user,
mode => '0600',
}

$gssapi_local_name = bool2str($foreman::gssapi_local_name, 'On', 'Off')
$gssapi_local_name = bool2str($foreman::gssapi_local_name, 'On', 'Off')

foreman::config::apache::fragment { 'intercept_form_submit':
ssl_content => template('foreman/intercept_form_submit.conf.erb'),
}
foreman::config::apache::fragment { 'intercept_form_submit':
ssl_content => template('foreman/intercept_form_submit.conf.erb'),
}

foreman::config::apache::fragment { 'lookup_identity':
ssl_content => template('foreman/lookup_identity.conf.erb'),
}
foreman::config::apache::fragment { 'lookup_identity':
ssl_content => template('foreman/lookup_identity.conf.erb'),
}

foreman::config::apache::fragment { 'auth_gssapi':
ssl_content => template('foreman/auth_gssapi.conf.erb'),
}
foreman::config::apache::fragment { 'auth_gssapi':
ssl_content => template('foreman/auth_gssapi.conf.erb'),
}

foreman::config::apache::fragment { 'external_auth_api':
ssl_content => template('foreman/external_auth_api.conf.erb'),
}
foreman::config::apache::fragment { 'external_auth_api':
ssl_content => template('foreman/external_auth_api.conf.erb'),
}

if $foreman::ipa_manage_sssd {
$sssd = pick(fact('foreman_sssd'), {})
$sssd_services = join(unique(pick($sssd['services'], []) + ['ifp']), ', ')
$sssd_ldap_user_extra_attrs = join(unique(pick($sssd['ldap_user_extra_attrs'], []) + ['email:mail', 'lastname:sn', 'firstname:givenname']), ', ')
$sssd_allowed_uids = join(unique(pick($sssd['allowed_uids'], []) + [$apache::user, 'root']), ', ')
$sssd_user_attributes = join(unique(pick($sssd['user_attributes'], []) + ['+email', '+firstname', '+lastname']), ', ')
$sssd_ifp_extra_attributes = [
"set target[.=~regexp('domain/.*')]/ldap_user_extra_attrs '${sssd_ldap_user_extra_attrs}'",
"set target[.='sssd']/services '${sssd_services}'",
'set target[.=\'ifp\'] \'ifp\'',
"set target[.='ifp']/allowed_uids '${sssd_allowed_uids}'",
"set target[.='ifp']/user_attributes '${sssd_user_attributes}'",
]

$sssd_changes = $sssd_ifp_extra_attributes + ($foreman::ipa_sssd_default_realm ? {
undef => [],
default => ["set target[.='sssd']/default_domain_suffix '${$foreman::ipa_sssd_default_realm}'"],
})

augeas { 'sssd-ifp-extra-attributes':
context => '/files/etc/sssd/sssd.conf',
changes => $sssd_changes,
notify => Service['sssd'],
if $foreman::ipa_manage_sssd {
$sssd = pick(fact('foreman_sssd'), {})
$sssd_services = join(unique(pick($sssd['services'], []) + ['ifp']), ', ')
$sssd_ldap_user_extra_attrs = join(unique(pick($sssd['ldap_user_extra_attrs'], []) + ['email:mail', 'lastname:sn', 'firstname:givenname']), ', ')
$sssd_allowed_uids = join(unique(pick($sssd['allowed_uids'], []) + [$apache::user, 'root']), ', ')
$sssd_user_attributes = join(unique(pick($sssd['user_attributes'], []) + ['+email', '+firstname', '+lastname']), ', ')
$sssd_ifp_extra_attributes = [
"set target[.=~regexp('domain/.*')]/ldap_user_extra_attrs '${sssd_ldap_user_extra_attrs}'",
"set target[.='sssd']/services '${sssd_services}'",
'set target[.=\'ifp\'] \'ifp\'',
"set target[.='ifp']/allowed_uids '${sssd_allowed_uids}'",
"set target[.='ifp']/user_attributes '${sssd_user_attributes}'",
]

$sssd_changes = $sssd_ifp_extra_attributes + ($foreman::ipa_sssd_default_realm ? {
undef => [],
default => ["set target[.='sssd']/default_domain_suffix '${$foreman::ipa_sssd_default_realm}'"],
})

augeas { 'sssd-ifp-extra-attributes':
context => '/files/etc/sssd/sssd.conf',
changes => $sssd_changes,
notify => Service['sssd'],
}
}

foreman::settings_fragment { 'authorize_login_delegation.yaml':
content => template('foreman/settings-external-auth.yaml.erb'),
order => '02',
}
}

foreman::settings_fragment { 'authorize_login_delegation.yaml':
content => template('foreman/settings-external-auth.yaml.erb'),
order => '02',
foreman::settings_fragment { 'authorize_login_delegation_api.yaml':
content => template('foreman/settings-external-auth-api.yaml.erb'),
order => '03',
}
}
'keycloak': {
$foreman_socket_override = undef

unless $foreman::ssl {
fail('Keycloak requires HTTPS')
}

foreman::settings_fragment { 'authorize_login_delegation_api.yaml':
content => template('foreman/settings-external-auth-api.yaml.erb'),
order => '03',
foreman::settings_fragment { 'authorize_login_delegation.yaml':
content => template('foreman/settings-external-auth.yaml.erb'),
order => '02',
}

# TODO: parameter
$keycloak_url = 'https://keycloak.example.com'
$oidc_issuer = "${keycloak_url}/auth/realms/${foreman::keycloak_realm}"
$keycloak_settings = {
':login_delegation_logout_url' => "${foreman::foreman_url}/users/extlogout",
# TODO: parameters or obtain from ${oidc_issuer}/.well-known/openid-configuration
':oidc_algorithm' => 'RS256',
':oidc_audience' => ["${foreman::servername}-foreman-openidc"],
Copy link

@WoutResseler WoutResseler Jan 17, 2025

Choose a reason for hiding this comment

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

I would parameterize oidc_audience so that the value can be overwritten. This allows people who have a certain naming standards for their keycloak clients to keep using them.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, which is why there's a TODO above it.

Choose a reason for hiding this comment

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

Apologies! Completely over looked those

':oidc_issuer' => $oidc_issuer,
':oidc_jwks_url' => "${oidc_issuer}/protocol/openid-connect/certs",

Choose a reason for hiding this comment

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

I would also parameterize oidc_jwks_url so that the value can be overwritten. If you use non standard paths or keycloak updates their path, as they have been known for doing already you will have no option to overwrite this.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was still wondering how to best deal with this. IMHO this is also covered by the TODO above it that this isn't the final form.

One thought I had was to have keycloak as one opinionated option and one oidc as a generic one. Not sure if that makes sense or not.

Note that the OIDC standard says there should be a .well-known/openid-configuration endpoint. Quoting https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig

OpenID Providers supporting Discovery MUST make a JSON document available at the path formed by concatenating the string /.well-known/openid-configuration to the Issuer.

In particular, it should have the userinfo_endpoint metadata. That points to https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata which list jwks_uri. In other words, you should always be able to look this up. If you look at how Apache is configured by keycloak-httpd-client-install it's using OIDCProviderMetadataURL {{ keycloak_server_url }}/realms/{{ keycloak_realm }}/.well-known/openid-configuration. That's what the TODO refers to. If we have the metadata URL then you can read:

  • oidc_algorithm from id_token_signing_alg_values_supported
  • odic_issuer from issuer
  • oidc_jwks_url from jwks_uri

That only leaves oidc_audience.

Now I wonder what should read these values. Should we modify Foreman itself to do this? That feels more correct than having the installer retrieve these values and store it.

Copy link
Contributor

Choose a reason for hiding this comment

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

One thought I had was to have keycloak as one opinionated option and one oidc as a generic one. Not sure if that makes sense or not.

With keycloak being "just" a preset for the generic oidc one?

Should we modify Foreman itself to do this?

Would that mean not relying on apache's mod_auth_oidc at all?

Copy link
Member Author

Choose a reason for hiding this comment

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

One thought I had was to have keycloak as one opinionated option and one oidc as a generic one. Not sure if that makes sense or not.

With keycloak being "just" a preset for the generic oidc one?

Yes, something like that.

Should we modify Foreman itself to do this?

Would that mean not relying on apache's mod_auth_oidc at all?

Disclaimer: I only just looked this up.

It probably still should depend on mod_auth_oidc because we would get a good integration with various providers (https://github.com/OpenIDC/mod_auth_openidc?tab=readme-ov-file#quickstart-for-specific-providers)

But I was wondering how we could get rid of those settings or at least minimize what we need to manage. https://github.com/OpenIDC/mod_auth_openidc/wiki#12-how-can-i-customize-the-idp-discovery-or-initial-login-page mentions reading the mod_auth_oidc metadata cache directly, which feels very ugly.

I also see mentions that mod_auth_oidc sends ODIC_* headers and sends it to the backend application. That implies we could modify Foreman to just read those values and not do any parsing on the backend. In other words, fully offload ODIC authentication to Apache. Reading https://github.com/OpenIDC/mod_auth_openidc/wiki/Flow-Chart#6-claims-propagation talks about some specific options there. Diving deeper, you can see ODIC_PassUserInfoAs exists: https://github.com/OpenIDC/mod_auth_openidc/blob/b2e99151bc695335089c8d4bfe5793624ac0732e/auth_openidc.conf#L852-L871. There's a claims option that passes the data as individual headers, meaning you don't parse the JWT at all.

This would make it a lot more like the regular external authentication (like with LDAP) where we pass HTTP headers with the values.

}

foreman::settings_fragment { 'authorize_login_delegation-keycloak.yaml':
# TODO: does this include the document marker?
content => stdlib::to_yaml($keycloak_settings),
order => '04',
}
}
default: {
$foreman_socket_override = undef
}
}
} else {
$foreman_socket_override = undef
}

systemd::dropin_file { 'foreman-socket':
Expand Down
81 changes: 51 additions & 30 deletions manifests/config/apache.pp
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,15 @@
# @param access_log_format
# Apache log format to use
#
# @param ipa_authentication
# Whether to install support for IPA authentication
# @param external_authentication
# The authentication type to use
#
# @param http_vhost_options
# Direct options to apache::vhost for the http vhost
#
# @param https_vhost_options
# Direct options to apache::vhost for the https vhost
#
# @param keycloak
# Whether to enable keycloak support
#
# @param keycloak_app_name
# The app name as passed to keycloak-httpd-client-install
#
Expand Down Expand Up @@ -112,10 +109,9 @@
Optional[String] $user = undef,
Optional[Stdlib::HTTPUrl] $foreman_url = undef,
Optional[String] $access_log_format = undef,
Boolean $ipa_authentication = false,
Hash[String, Any] $http_vhost_options = {},
Hash[String, Any] $https_vhost_options = {},
Boolean $keycloak = false,
Optional[Enum['ipa', 'ipa_with_api', 'keycloak']] $external_authentication = undef,
String[1] $keycloak_app_name = 'foreman-openidc',
String[1] $keycloak_realm = 'ssl-realm',
Array[String[1]] $request_headers_to_unset = [
Expand Down Expand Up @@ -236,31 +232,56 @@
include apache
include apache::mod::headers

if $ipa_authentication {
include apache::mod::authnz_pam
include apache::mod::auth_basic
include apache::mod::intercept_form_submit
include apache::mod::lookup_identity
include apache::mod::auth_gssapi
} elsif $keycloak {
include apache::mod::auth_openidc
case $external_authentication {
'ipa', 'ipa_with_api': {
include apache::mod::authnz_pam
include apache::mod::auth_basic
include apache::mod::intercept_form_submit
include apache::mod::lookup_identity
include apache::mod::auth_gssapi
}
'keycloak': {
include apache::mod::auth_openidc

# This file is generated by keycloak-httpd-client-install and that manages
# the content. The command would be:
#
# keycloak-httpd-client-install --app-name ${keycloak_app_name} --keycloak-server-url $KEYCLOAK_URL --keycloak-admin-username $KEYCLOAK_USER --keycloak-realm ${keycloak_realm} --keycloak-admin-realm master --keycloak-auth-role root-admin --client-type openidc --client-hostname ${servername} --protected-locations /users/extlogin
#
# If $suburi is used, --location-root should also be passed in
#
# By defining it here we avoid purging it and also tighten the
# permissions so the world can't read its secrets.
# This is functionally equivalent to apache::custom_config without content/source
file { "${apache::confd_dir}/${keycloak_app_name}_oidc_keycloak_${keycloak_realm}.conf":
ensure => file,
owner => 'root',
group => 'root',
mode => '0640',
# TODO: parameter
$use_keycloak_httpd_client_install = true
if $use_keycloak_httpd_client_install {
# This file is generated by keycloak-httpd-client-install and that manages
# the content. The command would be:
#
# keycloak-httpd-client-install --app-name ${keycloak_app_name} --keycloak-server-url $KEYCLOAK_URL --keycloak-admin-username $KEYCLOAK_USER --keycloak-realm ${keycloak_realm} --keycloak-admin-realm master --keycloak-auth-role root-admin --client-type openidc --client-hostname ${servername} --protected-locations /users/extlogin
#
# If $suburi is used, --location-root should also be passed in
#
# By defining it here we avoid purging it and also tighten the
# permissions so the world can't read its secrets.
# This is functionally equivalent to apache::custom_config without content/source
file { "${apache::confd_dir}/${keycloak_app_name}_oidc_keycloak_${keycloak_realm}.conf":
ensure => file,
owner => 'root',
group => 'root',
mode => '0640',
}
} else {
# TODO: parameters
$oidc_parameters = {
'OIDCClientID' => '{{ clientid }}',
'OIDCProviderMetadataURL' => "{{ keycloak_server_url }}/realms/${keycloak_realm}/.well-known/openid-configuration",
'OIDCCryptoPassphrase' => '{{ crypto_passphrase }}',
'OIDCClientSecret' => '{{ oidc_client_secret }}',
'OIDCRedirectURI' => "${foreman_url}/users/extlogin/redirect_uri",
'OIDCRemoteUserClaim' => '{{ oidc_remote_user_claim }}',
}
# TODO: pass to Apache
$locations = {
'/users/extlogin' => [
'AuthType openid-connect',
'Require valid-user',
],
}
}
}
default: {}
}

file { "${apache::confd_dir}/${priority}-foreman.d":
Expand Down
Loading
Loading