From aa52f49fd176e8d8ca3f3ae578f328c11ee5972a Mon Sep 17 00:00:00 2001
From: teacup-on-rockingchair
<315160+teacup-on-rockingchair@users.noreply.github.com>
Date: Thu, 19 Feb 2026 08:36:45 +0200
Subject: [PATCH 1/2] Change pam_options template to support pam configuration
in /usr and /etc directories In SLE16 it is the case the distribution default
configuration comes in /usr subdirs and system-wide custom configuration sits
in /etc so we need to handle both in the template - add ability to specify
the external variable name to template - add ability to specify variable
type: integer or string - for now this template is only used for 2 rules:
use_pam_wheel_for_su and use_pam_wheel_group_for_su, but the approach needs
to be applied to other PAM related rules
---
shared/templates/pam_options/ansible.template | 36 ++++---
shared/templates/pam_options/bash.template | 16 ++-
shared/templates/pam_options/oval.template | 97 +++++++++++++++++--
3 files changed, 124 insertions(+), 25 deletions(-)
diff --git a/shared/templates/pam_options/ansible.template b/shared/templates/pam_options/ansible.template
index 5308b0625466..8da6a86a3fbc 100644
--- a/shared/templates/pam_options/ansible.template
+++ b/shared/templates/pam_options/ansible.template
@@ -10,13 +10,14 @@
# updated the Ansible pamd module to do that, we will need to use regexp
# for now.
-
-# declare the XCCDF vars if any
-{{% for arg in ARGUMENTS %}}
-{{% if arg['variable']|length %}}
-- (xccdf-var var_password_pam_{{{ arg['variable'] }}})
+{{% if product == 'sle16' %}}
+- name: Copy default /usr/lib/pam.d/{{ '{{{ PATH }}}' | basename }} to {{{ PATH }}}
+ ansible.builtin.copy:
+ src: /usr/lib/pam.d/{{ '{{{ PATH }}}' | basename }}
+ dest: {{{ PATH }}}
+ force: no
+ mode: '0644'
{{% endif %}}
-{{% endfor %}}
- name: Set control_flag fact
ansible.builtin.set_fact:
@@ -33,7 +34,7 @@
path: {{{ PATH }}}
line: '{{{ TYPE }}} {{{ CONTROL_FLAG }}} {{{ MODULE }}}'
state: present
- when: check_pam_module_result.stdout is defined and '"{{{ MODULE }}}" not in check_pam_module_result.stdout'
+ when: check_pam_module_result is not skipped and check_pam_module_result.stdout is defined and "{{{ MODULE }}}" not in check_pam_module_result.stdout
- name: Ensure '{{{ MODULE }}}' module has conforming control flag
ansible.builtin.lineinfile:
@@ -41,7 +42,7 @@
regexp: '^(\s*{{{ TYPE }}}\s+)\S+(\s+{{{ MODULE }}}\s+.*)'
line: '\g<1>{{{ CONTROL_FLAG }}}\g<2>'
backrefs: yes
- when: control_flag|length
+ when: check_pam_module_result is not skipped and control_flag|length
{{% for arg in ARGUMENTS %}}
# NOTE: if 'remove_argument' is present and set to some value, we assume
@@ -56,13 +57,22 @@
{{% elif arg['variable']|length %}}
# NOTE(gyee): if 'var' is used, user is meant to set the argument to a
# static value
+{{% if arg['variable_name'] %}}
+{{% set pam_variable_name = arg['variable_name'] %}}
+{{% else %}}
+{{% set pam_variable_name = "var_password_pam_" + arg['variable'] %}}
+{{% endif %}}
+{{{ ansible_instantiate_variables(pam_variable_name) }}}
+
+{{% set pam_variable_value = "{{ " + pam_variable_name + " }}" %}}
-- name: Ensure "{{{ MODULE }}}" module has argument "{{{ arg['variable'] }}}={{ var_password_pam_{{{ arg['variable'] }}} }}"
+- name: Ensure "{{{ MODULE }}}" module has argument "{{{ arg['variable'] }}}={{{ pam_variable_value }}}"
ansible.builtin.lineinfile:
path: {{{ PATH }}}
regexp: '^(\s*{{{ TYPE }}}\s+{{{ CONTROL_FLAG }}}\s+{{{ MODULE }}}(?:\s+\S+)*\s+{{{ arg['variable'] }}}=)(?:\S+)((\s+\S+)*\s*\\*\s*)$'
- line: '\g<1>{{ var_password_pam_{{{ arg['variable'] }}} }}\g<2>'
+ line: '\g<1>{{{ pam_variable_value }}}\g<2>'
backrefs: yes
+ when: check_pam_module_result is not skipped
- name: Check the presence of "{{{ arg['variable'] }}}" argument in "{{{ MODULE }}}" module
ansible.builtin.shell: |
@@ -74,9 +84,9 @@
ansible.builtin.lineinfile:
path: {{{ PATH }}}
regexp: '^(\s*{{{ TYPE }}}\s+{{{ CONTROL_FLAG }}}\s+{{{ MODULE }}})((\s+\S+)*\s*(\\)*$)'
- line: '\g<1> {{{ arg['variable'] }}}={{ var_password_pam_{{{ arg['variable'] }}} }}\g<2>'
+ line: '\g<1> {{{ arg['variable'] }}}={{{ pam_variable_value }}}\g<2>'
backrefs: yes
- when: check_pam_module_argument_result is not skipped and '"{{{ arg['variable'] }}}" not in check_pam_module_argument_result.stdout'
+ when: check_pam_module_argument_result is not skipped and "{{{ arg['variable'] }}}" not in check_pam_module_argument_result.stdout
{{% else %}}
- name: Set argument_value fact
ansible.builtin.set_fact:
@@ -102,6 +112,6 @@
regexp: '^(\s*{{{ TYPE }}}\s+{{{ CONTROL_FLAG }}}\s+{{{ MODULE }}})((\s+\S+)*\s*(\\)*$)'
line: '\g<1> {{{ arg['new_argument'] }}}\g<2>'
backrefs: yes
- when: check_pam_module_argument_result is not skipped and '"{{{ arg['argument'] }}}" not in check_pam_module_argument_result.stdout'
+ when: check_pam_module_argument_result is not skipped and "{{{ arg['argument'] }}}" not in check_pam_module_argument_result.stdout
{{% endif %}}
{{% endfor %}}
diff --git a/shared/templates/pam_options/bash.template b/shared/templates/pam_options/bash.template
index 49b717c6cb45..a3b4ca92863a 100644
--- a/shared/templates/pam_options/bash.template
+++ b/shared/templates/pam_options/bash.template
@@ -10,10 +10,22 @@ declare -a ARGS=()
declare -a NEW_ARGS=()
declare -a DEL_ARGS=()
+{{% if product == 'sle16' %}}
+PAM_DEFAULTS_FILE_NAME="/usr/lib/pam.d/$(basename "{{{ PATH }}}")"
+if ! [ -e "{{{ PATH }}}" ] ; then
+ cp "${PAM_DEFAULTS_FILE_NAME}" "{{{ PATH }}}"
+fi
+{{% endif %}}
+
{{% for arg in ARGUMENTS -%}}
{{% if arg['variable'] | length -%}}
-{{{ bash_instantiate_variables("var_password_pam_" + arg['variable']) }}}
-VALUES+=("${{{ "var_password_pam_" + arg['variable'] }}}")
+{{% if arg['variable_name'] %}}
+{{% set pam_variable_name = arg['variable_name'] %}}
+{{% else %}}
+{{% set pam_variable_name = "var_password_pam_" + arg['variable'] %}}
+{{% endif %}}
+{{{ bash_instantiate_variables(pam_variable_name) }}}
+VALUES+=("${{{ pam_variable_name }}}")
VALUE_NAMES+=("{{{ arg['variable'] }}}")
{{%- else %}}
VALUES+=("")
diff --git a/shared/templates/pam_options/oval.template b/shared/templates/pam_options/oval.template
index e517a837f463..9751c025adb6 100644
--- a/shared/templates/pam_options/oval.template
+++ b/shared/templates/pam_options/oval.template
@@ -4,22 +4,63 @@
{{% set MATCH_CONTROL_FLAG = '\S+' %}}
{{% endif %}}
+{{% if product == 'sle16' %}}
+{{% set PAM_VENDOR_FILE = "/usr/lib/pam.d/" + PATH.split('/') | last %}}
+{{% endif %}}
+
{{{ oval_metadata("Configure PAM module", rule_title=rule_title) }}}
-
-{{% for arg in ARGUMENTS %}}
-{{% if arg['variable']|length %}}
-
+{{% if product == 'sle16' %}}
+
+
+
+ {{% for arg in ARGUMENTS %}}
+ {{% if arg['variable']|length %}}
+
+ {{% else %}}
+
+ {{% endif %}}
+ {{% endfor %}}
+
+
+ {{{ oval_config_file_exists_criterion(PATH, rule_id=rule_id) }}}
+ {{% for arg in ARGUMENTS %}}
+ {{% if arg['variable']|length %}}
+
+ {{% else %}}
+
+ {{% endif %}}
+ {{% endfor %}}
+
+
{{% else %}}
-
-{{% endif %}}
-{{% endfor %}}
+
+ {{% for arg in ARGUMENTS %}}
+ {{% if arg['variable']|length %}}
+
+ {{% else %}}
+
+ {{% endif %}}
+ {{% endfor %}}
+{{% endif %}}
+{{% if product == 'sle16' %}}
+{{{ oval_config_file_exists_test(PATH, rule_id=rule_id) }}}
+{{{ oval_config_file_exists_object(PATH, rule_id=rule_id) }}}
+{{% endif %}}
+
{{% for arg in ARGUMENTS %}}
{{% if arg['variable']|length %}}
+{{% if arg['variable_name'] %}}
+{{% set pam_variable_name = arg['variable_name'] %}}
+{{% else %}}
+{{% set pam_variable_name = "var_password_pam_" + arg['variable'] %}}
+{{% endif %}}
+
+
@@ -29,15 +70,33 @@
{{{ PATH }}}
- ^\s*{{{ TYPE }}}\s+{{{ MATCH_CONTROL_FLAG }}}\s+{{{ MODULE }}}.*\s{{{ arg['variable'] }}}=(-?\d+)(?:\s+.*)?
+ ^\s*{{{ TYPE }}}\s+{{{ MATCH_CONTROL_FLAG }}}\s+{{{ MODULE }}}.*\s{{{ arg['variable'] }}}=(-?[a-zA-Z0-9]+)(?:\s+.*)?
1
-
+
-
+
+
+{{% if product == 'sle16' %}}
+
+
+
+
+
+
+ {{{ PAM_VENDOR_FILE }}}
+ ^\s*{{{ TYPE }}}\s+{{{ MATCH_CONTROL_FLAG }}}\s+{{{ MODULE }}}.*\s{{{ arg['variable'] }}}=(-?[a-zA-Z0-9]+)(?:\s+.*)?
+ 1
+
+{{% endif %}}
+
+
{{% else %}}
1
+
+{{% if product == 'sle16' %}}
+
+
+
+
+
+ {{{ PAM_VENDOR_FILE }}}
+{{% if arg['argument_match']|length %}}
+ ^\s*{{{ TYPE }}}(?:(?!\n)\s)+{{{ MATCH_CONTROL_FLAG }}}(?:(?!\n)\s)+{{{ MODULE }}}((?!\n)\s[^\n]+)?(?!\n)\s+{{{ arg['argument'] }}}={{{ arg['argument_match'] }}}((\s+\S+)*\s*\\*\s*)$
+{{% else %}}
+ ^\s*{{{ TYPE }}}(?:(?!\n)\s)+{{{ MATCH_CONTROL_FLAG }}}(?:(?!\n)\s)+{{{ MODULE }}}((?!\n)\s[^\n]+)?(?!\n)\s+{{{ arg['argument'] }}}((\s+\S+)*\s*\\*\s*)$
+{{% endif %}}
+ 1
+
+{{% endif %}}
{{% endif %}}
{{% endfor %}}
From 4965420ec1d287232b7d15e52d3a23028048f2b5 Mon Sep 17 00:00:00 2001
From: teacup-on-rockingchair
<315160+teacup-on-rockingchair@users.noreply.github.com>
Date: Thu, 19 Feb 2026 08:46:44 +0200
Subject: [PATCH 2/2] Apply the pam_options template for use_pam_wheel_for_su
and use_pam_wheel_group_for_su
---
.../use_pam_wheel_for_su/ansible/shared.yml | 11 -------
.../use_pam_wheel_for_su/bash/shared.sh | 4 ---
.../use_pam_wheel_for_su/oval/shared.xml | 21 -------------
.../root_logins/use_pam_wheel_for_su/rule.yml | 11 +++++++
.../ansible/shared.yml | 14 ---------
.../use_pam_wheel_group_for_su/bash/shared.sh | 15 ---------
.../oval/shared.xml | 31 -------------------
.../use_pam_wheel_group_for_su/rule.yml | 17 +++++++++-
8 files changed, 27 insertions(+), 97 deletions(-)
delete mode 100644 linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/ansible/shared.yml
delete mode 100644 linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/bash/shared.sh
delete mode 100644 linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/oval/shared.xml
delete mode 100644 linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/ansible/shared.yml
delete mode 100644 linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/bash/shared.sh
delete mode 100644 linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/oval/shared.xml
diff --git a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/ansible/shared.yml b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/ansible/shared.yml
deleted file mode 100644
index b6f60d4bc946..000000000000
--- a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/ansible/shared.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-# platform = multi_platform_rhel,multi_platform_fedora,multi_platform_ol,multi_platform_rhv,multi_platform_sle,multi_platform_almalinux
-# reboot = false
-# strategy = restrict
-# complexity = low
-# disruption = low
-
-- name: "Restrict usage of su command only to members of wheel group"
- ansible.builtin.replace:
- path: "/etc/pam.d/su"
- regexp: '^[\s]*#[\s]*auth[\s]+required[\s]+pam_wheel\.so[\s]+use_uid$'
- replace: "auth required pam_wheel.so use_uid"
diff --git a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/bash/shared.sh b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/bash/shared.sh
deleted file mode 100644
index 5bd381d1210f..000000000000
--- a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/bash/shared.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-# platform = multi_platform_rhel,multi_platform_fedora,multi_platform_ol,multi_platform_rhv,multi_platform_sle,multi_platform_almalinux
-
-# uncomment the option if commented
-sed '/^[[:space:]]*#[[:space:]]*auth[[:space:]]\+required[[:space:]]\+pam_wheel\.so[[:space:]]\+use_uid$/s/^[[:space:]]*#//' -i /etc/pam.d/su
diff --git a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/oval/shared.xml b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/oval/shared.xml
deleted file mode 100644
index d7932de398fd..000000000000
--- a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/oval/shared.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
- {{{ oval_metadata("Only members of the wheel group should be able to authenticate through the su command.", rule_title=rule_title) }}}
-
-
-
-
-
-
-
-
-
-
- /etc/pam.d/su
- ^[\s]*auth[\s]+required[\s]+pam_wheel\.so[\s]+\buse_uid\b
- 1
-
-
diff --git a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/rule.yml b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/rule.yml
index 6c0ca626469e..ad32af129980 100644
--- a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/rule.yml
+++ b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_for_su/rule.yml
@@ -61,3 +61,14 @@ vuldiscussion: |-
When operating systems provide the capability to escalate a functional capability, it is critical the user re-authenticate.
platform: package[pam]
+
+template:
+ name: pam_options
+ vars:
+ path: /etc/pam.d/su
+ type: auth
+ control_flag: required
+ module: pam_wheel.so
+ arguments:
+ - argument: use_uid
+ new_argument: use_uid
diff --git a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/ansible/shared.yml b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/ansible/shared.yml
deleted file mode 100644
index 6d79f4e9d2de..000000000000
--- a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/ansible/shared.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-# platform = multi_platform_fedora,multi_platform_rhel,multi_platform_sle,multi_platform_slmicro,multi_platform_ubuntu
-# reboot = false
-# strategy = restrict
-# complexity = low
-# disruption = low
-
-{{{ ansible_instantiate_variables("var_pam_wheel_group_for_su") }}}
-
-- name: {{{ rule_title }}} - Add the group to the /etc/pam.d/su file
- ansible.builtin.lineinfile:
- path: "/etc/pam.d/su"
- state: present
- regexp: '^[\s]*#[\s]*auth[\s]+required[\s]+pam_wheel\.so[\s]+use_uid group=$'
- line: "auth required pam_wheel.so use_uid group={{ var_pam_wheel_group_for_su }}"
diff --git a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/bash/shared.sh b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/bash/shared.sh
deleted file mode 100644
index 35df572f9f8d..000000000000
--- a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/bash/shared.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-# platform = multi_platform_fedora,multi_platform_rhel,multi_platform_sle,multi_platform_slmicro,multi_platform_ubuntu,multi_platform_debian
-{{{ bash_instantiate_variables("var_pam_wheel_group_for_su") }}}
-
-PAM_CONF=/etc/pam.d/su
-
-pamstr=$(grep -P '^auth\s+required\s+pam_wheel\.so\s+(?=[^#]*\buse_uid\b)(?=[^#]*\bgroup=)' ${PAM_CONF})
-if [ -z "$pamstr" ]; then
- sed -Ei '/^auth\b.*\brequired\b.*\bpam_wheel\.so/d' ${PAM_CONF} # remove any remaining uncommented pam_wheel.so line
- sed -Ei "/^auth\s+sufficient\s+pam_rootok\.so.*$/a auth required pam_wheel.so use_uid group=${var_pam_wheel_group_for_su}" ${PAM_CONF}
-else
- group_val=$(echo -n "$pamstr" | grep -Eo '\bgroup=[_a-z][-0-9_a-z]*' | cut -d '=' -f 2)
- if [ -z "${group_val}" ] || [ ${group_val} != ${var_pam_wheel_group_for_su} ]; then
- sed -Ei "s/(^auth\s+required\s+pam_wheel.so\s+[^#]*group=)[_a-z][-0-9_a-z]*/\1${var_pam_wheel_group_for_su}/" ${PAM_CONF}
- fi
-fi
diff --git a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/oval/shared.xml b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/oval/shared.xml
deleted file mode 100644
index b841da12e366..000000000000
--- a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/oval/shared.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
- {{{ oval_metadata("Only members of the group set in variable 'var_pam_wheel_group_for_su' should be able to authenticate through the su command.", rule_title=rule_title) }}}
-
-
-
-
-
-
-
-
-
-
-
- /etc/pam.d/su
- ^\s*auth\s+required\s+pam_wheel\.so\s+(?=[^#]*\buse_uid\b)[^#]*\bgroup=([_a-z][-0-9_a-z]*)
- 1
-
-
-
-
-
-
-
-
diff --git a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/rule.yml b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/rule.yml
index 3d00846e44a7..d1cf0a7ad598 100644
--- a/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/rule.yml
+++ b/linux_os/guide/system/accounts/accounts-restrictions/root_logins/use_pam_wheel_group_for_su/rule.yml
@@ -37,9 +37,24 @@ ocil: |-
Run the following command to check if the line is present:
grep pam_wheel /etc/pam.d/su
The output should contain the following line:
- auth required pam_wheel.so use_uid group={{{ xccdf_value("var_pam_wheel_group_for_su") }}}
+ auth required pam_wheel.so use_uid group={{{ xccdf_value("var_pam_wheel_group_for_su.var") }}}
warnings:
- general: |-
Note that ensure_pam_wheel_group_empty rule complements this requirement by
ensuring the referenced group exists and has no members.
+
+template:
+ name: pam_options
+ vars:
+ path: /etc/pam.d/su
+ type: auth
+ control_flag: required
+ module: pam_wheel.so
+ arguments:
+ - variable: group
+ variable_name: var_pam_wheel_group_for_su
+ operation: equals
+ datatype: string
+ - argument: use_uid
+ new_argument: use_uid