Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement all Keycloak settings #1203

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
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.

}

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