ComplianceAsCode / ComplianceAsCode/content

`lineinfile` template generates `regexp: ''`, so remediation overwrites the last line of the target file

Open
#15,034 0 comments 0 reactions 0 assignees View on GitHub
Ansible triaged
Dominant language
Shell
Stars
2.8k
Forks
828
Avg merge
3d 8m
Merged PRs (30d)
80

Description

#### 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:

```jinja
{{{ 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:

1. Generate the Ansible remediation for profile `cis_server_l1`
(`/usr/share/scap-security-guide/ansible/rl9-playbook-cis_server_l1.yml`).
2. Record the stock contents of `/etc/security/pwquality.conf` and
`/etc/security/pwhistory.conf` — in particular their final lines.
3. Run the playbook against a fresh host.
4. 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`:

```yaml
- 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:

```jinja
{{{ 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.