Description of problem:
Every rule built on the lineinfile template emits an Ansible task with regexp: ''.
ansible.builtin.lineinfile uses regexp to find the line to replace, and an empty pattern
matches at position 0 of every line — so the module takes the last line of the file as its
match and overwrites it, instead of finding and updating the setting the rule manages.
On the first run this silently destroys a line of the target file. On every subsequent run it
appends another duplicate of the managed setting, without bound.
Root cause. shared/templates/lineinfile/ansible.template calls the macro without a regex
argument:
{{{ ansible_lineinfile(msg='', path=PATH, new_line=TEXT, create='yes', state='present',
insert_after='', insert_before='', rule_title=rule_title) -}}}
regex therefore falls back to its default of '' in shared/macros/10-ansible.jinja:31, and
line 39 emits it verbatim as regexp: '{{{ regex }}}'.
Of the 14 call sites of ansible_lineinfile in the repository, this is the only one that omits
regex — every other caller passes an anchored pattern (e.g.
regex='^\s*Include\s+\/usr\/etc\/ssh\/sshd_config\.d/\*\.conf' in
shared/templates/sshd_lineinfile/ansible.template).
Affected rules — all 7 rules using template: lineinfile on current master (df060c67):
| Rule |
File whose last line is overwritten |
accounts_password_pam_enforce_root |
pwquality.conf |
accounts_password_pam_enforce_local |
pwquality.conf |
accounts_password_pam_enforcing |
pwquality.conf |
accounts_password_pam_pwhistory_enforce_for_root |
/etc/security/pwhistory.conf |
set_firewalld_default_zone |
/etc/firewalld/firewalld.conf |
sssd_enable_certmap |
/etc/sssd/sssd.conf |
verify_use_mappers |
/etc/pam_pkcs11/pam_pkcs11.conf |
SCAP Security Guide Version:
Reproduced against scap-security-guide 0.1.81 (Rocky Linux 9.8 appstream package).
Root cause confirmed still present on master at df060c67.
ansible-core 2.21.3.
Operating System Version:
Rocky Linux 9.8 (Blue Onyx), aarch64, kernel 5.14.0-687.10.1.el9_8.0.1.aarch64.
Fresh install, no prior remediation.
Steps to Reproduce:
- Generate the Ansible remediation for profile
cis_server_l1
(/usr/share/scap-security-guide/ansible/rl9-playbook-cis_server_l1.yml).
- Record the stock contents of
/etc/security/pwquality.conf and
/etc/security/pwhistory.conf — in particular their final lines.
- Run the playbook against a fresh host.
- Run the identical playbook against the same host twice more, and diff both files after each run.
Actual Results:
The generated task for accounts_password_pam_enforce_root:
- name: Ensure PAM Enforces Password Requirements - Enforce for root User
ansible.builtin.lineinfile:
path: /etc/security/pwquality.conf
create: true
regexp: '' # ← matches at position 0 of every line
line: enforce_for_root
state: present
After three consecutive applies:
|
stock |
after 3 applies |
/etc/security/pwquality.conf lines |
79 |
82 |
/etc/security/pwhistory.conf lines |
21 |
24 |
occurrences of enforce_for_root in each |
0 |
3 |
stock final line of pwquality.conf |
# local_users_only |
destroyed on run 1 |
stock final line of pwhistory.conf |
# file = /etc/security/opasswd |
destroyed on run 1 |
Run 1 overwrites the file's last line. Runs 2 and 3 each append another enforce_for_root,
because the pattern still fails to find the setting it just wrote. Applied on a schedule the
duplicates grow linearly — a weekly run for a year leaves 52 copies.
Expected Results:
The task finds an existing enforce_for_root setting and updates it in place, or appends it if
absent. No pre-existing line is modified, and a second run reports ok rather than changed.
Additional Information/Debugging Steps:
Why this is easy to miss. PAM reads the last occurrence of a setting, so the end state is
compliant. The compliance score does not move (97.4 on all three passes here), the OVAL check
passes, and no before/after scan delta reports anything. It is only visible by running the
remediation twice and reading the file.
On a pristine box the destroyed line is a comment. On a system an administrator has edited — the
usual state of a real host — the last line of pwquality.conf is as likely to be a setting they
added.
Two further consequences worth noting:
- Three of the seven target the same file (
pwquality.conf). In a profile selecting more than
one, each task overwrites whatever the previous one appended, so they destroy each other's work
rather than only the stock content. I could not find a user-facing profile in this state — the
only profiles selecting two of the three are the internal hidden: true default profiles for
Fedora, Ubuntu 22.04 and Ubuntu 24.04, and I did not trace rule selection through control files.
So this is a latent hazard rather than a reproduced one.
sssd_enable_certmap and set_firewalld_default_zone write to service configuration, where
losing the final line is a plausible outage rather than a lost comment.
Suggested fix. Give the template an anchored pattern so the module finds the setting it
manages — roughly:
{{{ ansible_lineinfile(msg='', path=PATH, new_line=TEXT, regex=REGEX,
create='yes', state='present', ...) -}}}
with a per-rule regex var defaulting to an anchored match on the managed key.
sssd_enable_certmap already passes escape_text: false and a regex-shaped text, so it likely
needs handling of its own.
I have not opened a PR because the right default (derive the pattern from text, versus require an
explicit regex var on each of the 7 rules) is a judgement call for maintainers, and any fix
should be checked against the Bash and OVAL backends too. Happy to submit one if you tell me which
shape you would prefer.
Description of problem:
Every rule built on the
lineinfiletemplate emits an Ansible task withregexp: ''.ansible.builtin.lineinfileusesregexpto find the line to replace, and an empty patternmatches at position 0 of every line — so the module takes the last line of the file as its
match and overwrites it, instead of finding and updating the setting the rule manages.
On the first run this silently destroys a line of the target file. On every subsequent run it
appends another duplicate of the managed setting, without bound.
Root cause.
shared/templates/lineinfile/ansible.templatecalls the macro without aregexargument:
{{{ ansible_lineinfile(msg='', path=PATH, new_line=TEXT, create='yes', state='present', insert_after='', insert_before='', rule_title=rule_title) -}}}regextherefore falls back to its default of''inshared/macros/10-ansible.jinja:31, andline 39 emits it verbatim as
regexp: '{{{ regex }}}'.Of the 14 call sites of
ansible_lineinfilein the repository, this is the only one that omitsregex— every other caller passes an anchored pattern (e.g.regex='^\s*Include\s+\/usr\/etc\/ssh\/sshd_config\.d/\*\.conf'inshared/templates/sshd_lineinfile/ansible.template).Affected rules — all 7 rules using
template: lineinfileon currentmaster(df060c67):accounts_password_pam_enforce_rootpwquality.confaccounts_password_pam_enforce_localpwquality.confaccounts_password_pam_enforcingpwquality.confaccounts_password_pam_pwhistory_enforce_for_root/etc/security/pwhistory.confset_firewalld_default_zone/etc/firewalld/firewalld.confsssd_enable_certmap/etc/sssd/sssd.confverify_use_mappers/etc/pam_pkcs11/pam_pkcs11.confSCAP Security Guide Version:
Reproduced against
scap-security-guide0.1.81 (Rocky Linux 9.8 appstream package).Root cause confirmed still present on
masteratdf060c67.ansible-core2.21.3.Operating System Version:
Rocky Linux 9.8 (Blue Onyx), aarch64, kernel 5.14.0-687.10.1.el9_8.0.1.aarch64.
Fresh install, no prior remediation.
Steps to Reproduce:
cis_server_l1(
/usr/share/scap-security-guide/ansible/rl9-playbook-cis_server_l1.yml)./etc/security/pwquality.confand/etc/security/pwhistory.conf— in particular their final lines.Actual Results:
The generated task for
accounts_password_pam_enforce_root:After three consecutive applies:
/etc/security/pwquality.conflines/etc/security/pwhistory.conflinesenforce_for_rootin eachpwquality.conf# local_users_onlypwhistory.conf# file = /etc/security/opasswdRun 1 overwrites the file's last line. Runs 2 and 3 each append another
enforce_for_root,because the pattern still fails to find the setting it just wrote. Applied on a schedule the
duplicates grow linearly — a weekly run for a year leaves 52 copies.
Expected Results:
The task finds an existing
enforce_for_rootsetting and updates it in place, or appends it ifabsent. No pre-existing line is modified, and a second run reports
okrather thanchanged.Additional Information/Debugging Steps:
Why this is easy to miss. PAM reads the last occurrence of a setting, so the end state is
compliant. The compliance score does not move (97.4 on all three passes here), the OVAL check
passes, and no before/after scan delta reports anything. It is only visible by running the
remediation twice and reading the file.
On a pristine box the destroyed line is a comment. On a system an administrator has edited — the
usual state of a real host — the last line of
pwquality.confis as likely to be a setting theyadded.
Two further consequences worth noting:
pwquality.conf). In a profile selecting more thanone, each task overwrites whatever the previous one appended, so they destroy each other's work
rather than only the stock content. I could not find a user-facing profile in this state — the
only profiles selecting two of the three are the internal
hidden: truedefault profiles forFedora, Ubuntu 22.04 and Ubuntu 24.04, and I did not trace rule selection through control files.
So this is a latent hazard rather than a reproduced one.
sssd_enable_certmapandset_firewalld_default_zonewrite to service configuration, wherelosing the final line is a plausible outage rather than a lost comment.
Suggested fix. Give the template an anchored pattern so the module finds the setting it
manages — roughly:
{{{ ansible_lineinfile(msg='', path=PATH, new_line=TEXT, regex=REGEX, create='yes', state='present', ...) -}}}with a per-rule
regexvar defaulting to an anchored match on the managed key.sssd_enable_certmapalready passesescape_text: falseand a regex-shapedtext, so it likelyneeds handling of its own.
I have not opened a PR because the right default (derive the pattern from
text, versus require anexplicit
regexvar on each of the 7 rules) is a judgement call for maintainers, and any fixshould be checked against the Bash and OVAL backends too. Happy to submit one if you tell me which
shape you would prefer.