Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
962170b
Mark unavoidable command-instead-of-module tasks and skip the rule
ggbecker Oct 2, 2026
ab0f372
Tighten the Ansible role lint gate for certification rules
ggbecker Oct 2, 2026
3a766c9
Fix lint violations in shared Ansible remediation macros
ggbecker Oct 2, 2026
e50c280
Fix risky-shell-pipe in Ansible remediations
ggbecker Oct 2, 2026
dc735b0
Replace command tasks with Ansible modules
ggbecker Oct 2, 2026
ed36b6b
Add changed_when to Ansible command/shell remediations
ggbecker Oct 2, 2026
61c1410
Replace raw command remediations with ssg_hardening module in collection
ggbecker Oct 2, 2026
ce981b0
Set file modes in Ansible remediations to satisfy risky-file-permissions
ggbecker Oct 2, 2026
58919e0
Gather service facts before special_service_block tasks
ggbecker Oct 5, 2026
8d8bf68
Re-gather service facts after special_service_block tasks
ggbecker Oct 5, 2026
abaff6f
Gate Ansible collection on command-instead-of-module in CI
ggbecker Oct 5, 2026
b35c237
Preserve existing file modes in Ansible remediations
ggbecker Oct 8, 2026
4a9b2bd
Run pipefail shell remediations under /bin/bash
ggbecker Oct 8, 2026
776ba07
Restore rpm --import for the Red Hat GPG key and route it through ssg…
ggbecker Oct 8, 2026
5286237
Use |- block scalar for pipefail shell remediations
ggbecker Oct 8, 2026
5e857bb
ssg_hardening: derive module metadata from a single OPERATIONS table
ggbecker Oct 8, 2026
ad8a5e6
Bundle .ansible-lint into the generated collection
ggbecker Oct 8, 2026
591e422
Bump minimum Ansible version to 2.16
ggbecker Oct 8, 2026
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
72 changes: 53 additions & 19 deletions .config/ansible-lint.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,56 @@
---
# Ansible-lint configuration aligned with the Red Hat Automation Hub partner
# certification requirements:
# https://docs.ansible.com/projects/partner-certification-requirements/static-analysis/
#
# Certification runs the "production" profile. A fixed set of rules "cannot be
# skipped" and must stay enforced; every other rule listed there is allowed to
# be skipped for machine-generated collections past version 1.0.0. This config
# therefore skips only the allowed rules and leaves the non-skippable ones
# active, so that `ansible-lint` run bare (auto-discovering this file) against
# the generated Ansible roles reports exactly the certification backlog.
#
# NOTE: content's own CI lints generated playbooks with an explicit
# `-c tests/ansible-lint_config.yml` (see cmake/SSGCommon.cmake), so this file
# does NOT change that gate; it is picked up only by bare `ansible-lint` runs,
# i.e. when measuring the generated collection against the certification bar.
profile: production

exclude_paths:
- changelogs
- .ansible
- .github

# Rules the certification explicitly allows to be skipped. SCAP variable names
# are not role-prefixed and do not follow Ansible's var naming pattern; yamllint
# is run separately upstream; the generated set_fact/command tasks use free-form.
skip_list:
- fqcn-builtins # We should be using fqcn
- key-order[task]
- no-free-form
- risky-file-permissions
- no-changed-when
- var-naming
- ignore-errors
- schema[meta]
- yaml[indentation] # Violations reported by yamllint.
- jinja[spacing] # Rule that looks inside jinja2 templates.
- name[template] # Rule for checking task and play names.
- fqcn[action] # Use FQCN for builtin actions.
- yaml # yamllint is run separately upstream
- no-free-form # generated set_fact/command use free-form
- jinja[spacing]
- key-order[task]
- fqcn[action-core]
- name[template]
- no-tabs
- var-naming[no-role-prefix] # SCAP variable names are not role-prefixed
- var-naming[pattern] # SCAP variable names
- ignore-errors
# A handful of remediations must call a binary directly because no Ansible
# module provides the behavior: `service auditd restart` (auditd sets
# RefuseManualStop=yes, so the service/systemd modules cannot restart it),
# and `rpm -Va`/`rpm -qf`/`rpm -qV`/`rpm --restore`/`rpm --setperms`/
# `rpm --setugids` (the package module has no verify/query-owner/restore
# semantics). These tasks carry an inline `# noqa command-instead-of-module`
# in the rule source playbooks, but the build re-serializes YAML and strips
# comments before the collection is assembled, so a global skip is required.
- command-instead-of-module
Comment on lines +44 to +46

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do I understand it well that it means that the gating CI job can't verify whether our code meets the certification requirements because of this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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


# Required by the certification to be set to warn (not skip).
warn_list:
- name[missing] # Rule for checking task and play names.
- template-instead-of-copy # Templated files should use template instead of copy
- yaml[empty-lines] # Violations reported by yamllint.
- yaml[line-length] # Violations reported by yamllint.
- name[casing] # Rule for checking task and play names.
- no-handler # Style choice, skipping for now
- risky-shell-pipe # Skipping for now due to authselect rules
- no-log-password

# Rules that "cannot be skipped" per the certification and are intentionally
# NOT in skip_list -- these constitute the remediation backlog:
# risky-shell-pipe, command-instead-of-shell, meta-no-info, package-latest,
# no-handler, meta-runtime[unsupported-version], no-changed-when,
# risky-file-permissions, unnamed-task
2 changes: 1 addition & 1 deletion .github/workflows/gate-lint-ansible-roles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ jobs:
- name: Set git safe directory
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Lint Ansible Roles
run: ansible-lint -x 204 -x experimental -x command-instead-of-module ./build/ansible_roles/*
run: ansible-lint -x 204 -x experimental ./build/ansible_roles/*
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@
register: auditctl_faillock_update_result
with_items: "{{ faillock_audit_rules }}"

- name: "{{{ rule_title }}} - Restart auditd"
# noqa command-instead-of-module: auditd sets RefuseManualStop=yes, so the service/systemd
# modules cannot restart it; the SysV 'service auditd restart' wrapper is the supported method.
- name: "{{{ rule_title }}} - Restart auditd" # noqa: command-instead-of-module
ansible.builtin.command: /usr/sbin/service auditd restart
changed_when: true
when:
- augenrules_faillock_update_result is changed or auditctl_faillock_update_result is changed
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@
register: auditctl_lastlog_update_result
with_items: "{{ lastlog_audit_rules }}"

- name: "{{{ rule_title }}} - Restart auditd"
# noqa command-instead-of-module: auditd sets RefuseManualStop=yes, so the service/systemd
# modules cannot restart it; the SysV 'service auditd restart' wrapper is the supported method.
- name: "{{{ rule_title }}} - Restart auditd" # noqa: command-instead-of-module
ansible.builtin.command: /usr/sbin/service auditd restart
changed_when: true
when:
- augenrules_lastlog_update_result is changed or auditctl_lastlog_update_result is changed
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@
register: auditctl_audit_rules_privilege_function_update_result
with_items: "{{ suid_audit_rules }}"
# restarting auditd through systemd doesn't work, see: https://access.redhat.com/solutions/5515011
- name: Restart Auditd
# noqa command-instead-of-module: auditd sets RefuseManualStop=yes, so the service/systemd
# modules cannot restart it; the SysV 'service auditd restart' wrapper is the supported method.
- name: Restart Auditd # noqa: command-instead-of-module
ansible.builtin.command: /usr/sbin/service auditd restart
changed_when: true
when:
- (augenrules_audit_rules_privilege_function_update_result.changed or
auditctl_audit_rules_privilege_function_update_result.changed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@
name: auditd.service
state: restarted
{{%- else %}} # restarting auditd through systemd doesn't work, see: https://access.redhat.com/solutions/5515011
- name: Restart Auditd
# noqa command-instead-of-module: auditd sets RefuseManualStop=yes, so the service/systemd
# modules cannot restart it; the SysV 'service auditd restart' wrapper is the supported method.
- name: Restart Auditd # noqa: command-instead-of-module
ansible.builtin.command: /usr/sbin/service auditd restart
changed_when: true
{{%- endif %}}
when:
- augenrules_audit_rules_privilege_function_update_result.changed or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@
daemon_reload: true

# IMPORTANT: this is necessary to ensure the dropin is loaded and the permissions for /etc/audit/ and /etc/audit/rules.d/ files are set correctly.
- name: "{{{ rule_title }}} - Restart auditd.service"
# noqa command-instead-of-module: auditd sets RefuseManualStop=yes, so the service/systemd
# modules cannot restart it; the SysV 'service auditd restart' wrapper is the supported method.
- name: "{{{ rule_title }}} - Restart auditd.service" # noqa: command-instead-of-module
ansible.builtin.command: service auditd restart
changed_when: true
when:
- audit_config_perms.changed or rules_dir_perms.changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
# complexity = low
# disruption = low

- name: "{{{ rule_title }}} - Restore ownership of cronie files"
# noqa command-instead-of-module: the package module cannot restore RPM-owned file ownership
# or permissions (rpm --setugids / --setperms); rpm must be invoked directly.
- name: "{{{ rule_title }}} - Restore ownership of cronie files" # noqa: command-instead-of-module
ansible.builtin.command: rpm --setugids cronie
changed_when: true

- name: "{{{ rule_title }}} - Restore permissions of cronie files"
- name: "{{{ rule_title }}} - Restore permissions of cronie files" # noqa: command-instead-of-module
ansible.builtin.command: rpm --setperms cronie
changed_when: true

- name: "{{{ rule_title }}} - Restore ownership of crontabs files"
- name: "{{{ rule_title }}} - Restore ownership of crontabs files" # noqa: command-instead-of-module
ansible.builtin.command: rpm --setugids crontabs
changed_when: true

- name: "{{{ rule_title }}} - Restore permissions of crontabs files"
- name: "{{{ rule_title }}} - Restore permissions of crontabs files" # noqa: command-instead-of-module
ansible.builtin.command: rpm --setperms crontabs
changed_when: true
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- name: {{{ rule_title }}} - Update postfix aliases
ansible.builtin.command:
cmd: newaliases
changed_when: true
when:
- aliases_root_mail_alias_changed is changed
- result_newaliases_present.stat.exists
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- name: {{{ rule_title }}} - Update postfix aliases
ansible.builtin.command:
cmd: newaliases
changed_when: true
when:
- result_newaliases_present.stat.exists
- aliases_postmaster_changed is changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

- name: "Extract sourcedir paths from chrony configuration"
ansible.builtin.shell:
cmd: grep '^[[:space:]]*sourcedir[[:space:]]\+' {{{ chrony_conf_path }}} | awk '{print $2}'
cmd: |-
set -o pipefail
grep '^[[:space:]]*sourcedir[[:space:]]\+' {{{ chrony_conf_path }}} | awk '{print $2}'
register: chrony_sourcedir_paths
changed_when: false
failed_when: false
Expand All @@ -25,7 +27,7 @@
cmd: |
for dir in {{ chrony_sourcedir_paths.stdout_lines | join(' ') }}; do
if [ -d "$dir" ]; then
grep -q '^[[:space:]]*\(server\|pool\)[[:space:]]\+[[:graph:]]\+' "$dir"/*.sources 2>/dev/null && exit 0
grep -q -e '^[[:space:]]*server[[:space:]]\+[[:graph:]]\+' -e '^[[:space:]]*pool[[:space:]]\+[[:graph:]]\+' "$dir"/*.sources 2>/dev/null && exit 0
fi
done
exit 1
Expand All @@ -38,7 +40,9 @@

- name: "Extract confdir paths from chrony configuration"
ansible.builtin.shell:
cmd: grep '^[[:space:]]*confdir[[:space:]]\+' {{{ chrony_conf_path }}} | awk '{print $2}'
cmd: |-
set -o pipefail
grep '^[[:space:]]*confdir[[:space:]]\+' {{{ chrony_conf_path }}} | awk '{print $2}'
register: chrony_confdir_paths
changed_when: false
failed_when: false
Expand All @@ -51,7 +55,7 @@
cmd: |
for dir in {{ chrony_confdir_paths.stdout_lines | join(' ') }}; do
if [ -d "$dir" ]; then
grep -q '^[[:space:]]*\(server\|pool\)[[:space:]]\+[[:graph:]]\+' "$dir"/*.conf 2>/dev/null && exit 0
grep -q -e '^[[:space:]]*server[[:space:]]\+[[:graph:]]\+' -e '^[[:space:]]*pool[[:space:]]\+[[:graph:]]\+' "$dir"/*.conf 2>/dev/null && exit 0
fi
done
exit 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@
block:
- name: '{{{ rule_title }}} - Collect NetworkManager connections names'
ansible.builtin.shell:
cmd: nmcli -g UUID,TYPE con | grep -v loopback | awk -F ':' '{ print $1 }'
cmd: |-
set -o pipefail
nmcli -g UUID,TYPE con | grep -v loopback | awk -F ':' '{ print $1 }'
register: result_nmcli_cmd_connections_names
check_mode: false
changed_when: false
failed_when: false

- name: '{{{ rule_title }}} - Collect NetworkManager connections zones'
ansible.builtin.shell:
cmd: nmcli -f connection.zone connection show {{ item | trim }} | awk '{ print $2}'
cmd: |-
set -o pipefail
nmcli -f connection.zone connection show {{ item | trim }} | awk '{ print $2}'
register: result_nmcli_cmd_connections_zones
changed_when: false
failed_when: false
Expand Down Expand Up @@ -65,7 +69,9 @@

- name: '{{{ rule_title }}} - Collect firewalld active zones'
ansible.builtin.shell:
cmd: firewall-cmd --get-active-zones | grep -v "^ " | cut -d " " -f 1
cmd: |-
set -o pipefail

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Allegedly (AI says) that we need to add executable: /bin/bash so that we can use set -o pipefail in the command

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added the bash part as well

firewall-cmd --get-active-zones | grep -v "^ " | cut -d " " -f 1
register: result_firewall_cmd_zones_names
changed_when: false
failed_when: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- name: Create USBGuard Policy configuration
ansible.builtin.command: usbguard generate-policy
register: policy
changed_when: true
when: not policy_file.stat.exists or policy_file.stat.size == 0

- name: Copy the Generated Policy configuration to a persistent file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@

- name: Dconf Update
ansible.builtin.command: dconf update
changed_when: true
when: result_ini is changed or result_lineinfile is changed
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@

- name: Dconf Update
ansible.builtin.command: dconf update
changed_when: true
when: result_ini is changed or result_lineinfile is changed
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

- name: {{{ rule_title }}} - Get directories from faillock
ansible.builtin.shell: |-
set -o pipefail
grep -oP '^\s*(?:auth.*pam_faillock.so.*)?dir\s*=\s*(\S+)' "{{ item }}" | sed -r 's/.*=\s*(\S+)/\1/'
register: faillock_output
changed_when: false
check_mode: false
failed_when: false
with_items:
- /etc/security/faillock.conf
{{% if 'ol' not in families %}}
Expand Down Expand Up @@ -42,6 +44,7 @@
if ! semanage fcontext -a -t faillog_t "{{ item.item }}(/.*)?"; then
semanage fcontext -m -t faillog_t "{{ item.item }}(/.*)?"
fi
changed_when: true
with_items: "{{ faillock_selinux_context.results }}"
when:
- item.item != ""
Expand All @@ -50,6 +53,7 @@

- name: {{{ rule_title }}} - Restore SELinux context
ansible.builtin.command: restorecon -R -v "{{ item.item }}"
changed_when: true
with_items: "{{ faillock_selinux_context.results }}"
when:
- item.item != ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

- name: '{{{ rule_title }}} - Get authselect current profile'
ansible.builtin.shell:
cmd: authselect current -r | awk '{ print $1 }'
cmd: |-
set -o pipefail
authselect current -r | awk '{ print $1 }'
register: result_authselect_profile
changed_when: false
when:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@
ansible.builtin.command:
cmd: restorecon -R "{{ var_accounts_passwords_pam_faillock_dir }}"
register: result_accounts_passwords_pam_faillock_dir_restorecon
changed_when: true
when: '"faillog_t" not in faillock_selinux_context.stdout'
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
{{% else %}}
ansible.builtin.command: /sbin/grubby --update-kernel=ALL --remove-args="systemd.confirm_spawn"
{{% endif %}}
changed_when: true
when: grub_confirm_spawn_changed is changed

- name: {{{ rule_title }}} - Regen grub.cfg handle updated GRUB_DISABLE_RECOVERY and confirm_spawn
ansible.builtin.command: grub2-mkconfig -o {{{ grub2_boot_path }}}/grub.cfg
changed_when: true
when: grub_disable_recovery_changed is changed or grub_confirm_spawn_changed is changed
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- name: "{{{ rule_title }}}"
ansible.builtin.command: |
/usr/bin/opensc-tool -S app:default:card_drivers:{{ var_smartcard_drivers }}
changed_when: true
when:
- card_drivers.stdout != var_smartcard_drivers
when:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@
when: pkcs11switch.stat.exists

- name: "{{{ rule_title }}} - Select opensc Module"
ansible.builtin.shell: echo -e "\n" | /usr/bin/pkcs11-switch opensc
ansible.builtin.shell: |-
set -o pipefail
echo -e "\n" | /usr/bin/pkcs11-switch opensc
when: pkcs11switch.stat.exists and pkcsw_output.stdout != "opensc"
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- name: "{{{ rule_title }}}"
ansible.builtin.command: |
/usr/bin/opensc-tool -S app:default:force_card_driver:{{ var_smartcard_drivers }}
changed_when: true
when:
- force_card_driver.stdout != var_smartcard_drivers
when:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
ansible.builtin.command: >
chage -m {{ var_accounts_minimum_age_login_defs }} {{ item }}
{{% endif %}}
changed_when: true
with_items: "{{ user_names.stdout_lines }}"
when: user_names.stdout_lines | length > 0
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
- name: {{{ rule_title }}} - Ensure the Number of Days of Warning Before Password Expires
ansible.builtin.command:
cmd: chage --warndays {{ var_accounts_password_warn_age_login_defs }} {{ item }}
changed_when: true
with_items: '{{ result_pass_warn_age_user_names.stdout_lines }}'
when: result_pass_warn_age_user_names is not skipped and result_pass_warn_age_user_names.stdout_lines | length > 0
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
- name: Change the period of inactivity
ansible.builtin.command:
cmd: chage --inactive {{ var_account_disable_post_pw_expiration }} {{ item }}
changed_when: true
with_items: '{{ user_names.stdout_lines }}'
when: user_names is not skipped and user_names.stdout_lines | length > 0
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
- name: Lock users with no password
ansible.builtin.command: >
passwd -l {{ item }}
changed_when: true
with_items: "{{ users_nopasswd.stdout_lines }}"
when: users_nopasswd is not skipped and users_nopasswd.stdout_lines | length > 0
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@

- name: Lock the password of the user accounts other than root with uid 0
ansible.builtin.command: passwd -l {{ item.key }}
changed_when: true
loop: "{{ getent_passwd | dict2items | rejectattr('key', 'equalto', 'root') | list }}"
when: item.value.1 == '0'
Loading