Skip to content

rootfiles_configured: populate /usr/share/rootfiles/ in remediation#14710

Draft
ggbecker wants to merge 1 commit into
ComplianceAsCode:masterfrom
ggbecker:fix-14582-branch
Draft

rootfiles_configured: populate /usr/share/rootfiles/ in remediation#14710
ggbecker wants to merge 1 commit into
ComplianceAsCode:masterfrom
ggbecker:fix-14582-branch

Conversation

@ggbecker
Copy link
Copy Markdown
Member

Description:

  • On RHEL 9.0/9.1 the rootfiles package installs dotfiles directly into /root/ and does not create /usr/share/rootfiles/. The remediation was writing a tmpfiles.d conf with C copy entries sourcing from that directory. When the destination files were absent at boot, systemd-tmpfiles failed with "No such file or directory" because the source path did not exist.

  • Fix the Bash and Ansible remediations to create /usr/share/rootfiles/ and copy each dotfile from /root/ to that directory if not already present, so the tmpfiles.d source is always valid. On RHEL 9.2+ where rootfiles already provides /usr/share/rootfiles/, these steps are no-ops.

Rationale:

On RHEL 9.0/9.1 the rootfiles package installs dotfiles directly into
/root/ and does not create /usr/share/rootfiles/. The remediation was
writing a tmpfiles.d conf with C copy entries sourcing from that
directory. When the destination files were absent at boot,
systemd-tmpfiles failed with "No such file or directory" because the
source path did not exist.

Fix the Bash and Ansible remediations to create /usr/share/rootfiles/
and copy each dotfile from /root/ to that directory if not already
present, so the tmpfiles.d source is always valid. On RHEL 9.2+ where
rootfiles already provides /usr/share/rootfiles/, these steps are
no-ops.

Fixes ComplianceAsCode#14582
@ggbecker ggbecker added this to the 0.1.81 milestone May 12, 2026
@ggbecker ggbecker added bugfix Fixes to reported bugs. STIG STIG Benchmark related. labels May 12, 2026
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Used by openshift-ci bot. label May 12, 2026
@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented May 12, 2026

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@github-actions
Copy link
Copy Markdown

This datastream diff is auto generated by the check Compare DS/Generate Diff

Click here to see the full diff
bash remediation for rule 'xccdf_org.ssgproject.content_rule_rootfiles_configured' differs.
--- xccdf_org.ssgproject.content_rule_rootfiles_configured
+++ xccdf_org.ssgproject.content_rule_rootfiles_configured
@@ -1,7 +1,13 @@
 # Remediation is applicable only in certain platforms
 if rpm --quiet -q rootfiles; then
 
-find "/etc/tmpfiles.d/" -name "*.conf" -print0 | xargs -0 sed -i  "/C[[:space:]]*\/root\/.bash_logout/d"
+mkdir -p /usr/share/rootfiles
+    [ -f "/root/.bash_logout" ] && [ ! -f "/usr/share/rootfiles/.bash_logout" ] && cp "/root/.bash_logout" "/usr/share/rootfiles/.bash_logout"
+    [ -f "/root/.bash_profile" ] && [ ! -f "/usr/share/rootfiles/.bash_profile" ] && cp "/root/.bash_profile" "/usr/share/rootfiles/.bash_profile"
+    [ -f "/root/.bashrc" ] && [ ! -f "/usr/share/rootfiles/.bashrc" ] && cp "/root/.bashrc" "/usr/share/rootfiles/.bashrc"
+    [ -f "/root/.cshrc" ] && [ ! -f "/usr/share/rootfiles/.cshrc" ] && cp "/root/.cshrc" "/usr/share/rootfiles/.cshrc"
+    [ -f "/root/.tcshrc" ] && [ ! -f "/usr/share/rootfiles/.tcshrc" ] && cp "/root/.tcshrc" "/usr/share/rootfiles/.tcshrc"
+    find "/etc/tmpfiles.d/" -name "*.conf" -print0 | xargs -0 sed -i  "/C[[:space:]]*\/root\/.bash_logout/d"
     find "/etc/tmpfiles.d/" -name "*.conf" -print0 | xargs -0 sed -i  "/C[[:space:]]*\/root\/.bash_profile/d"
     find "/etc/tmpfiles.d/" -name "*.conf" -print0 | xargs -0 sed -i  "/C[[:space:]]*\/root\/.bashrc/d"
     find "/etc/tmpfiles.d/" -name "*.conf" -print0 | xargs -0 sed -i  "/C[[:space:]]*\/root\/.cshrc/d"

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_rootfiles_configured' differs.
--- xccdf_org.ssgproject.content_rule_rootfiles_configured
+++ xccdf_org.ssgproject.content_rule_rootfiles_configured
@@ -1,6 +1,54 @@
 - name: Gather the package facts
   package_facts:
     manager: auto
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Ensure source directory
+    exists
+  ansible.builtin.file:
+    path: /usr/share/rootfiles
+    state: directory
+    mode: '0755'
+    owner: root
+    group: root
+  when: '"rootfiles" in ansible_facts.packages'
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Stat /root/.bash_logout
+  ansible.builtin.stat:
+    path: /root/.bash_logout
+  register: rootfiles_configured_bash_logout_root_stat
+  when: '"rootfiles" in ansible_facts.packages'
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Copy /root/.bash_logout
+    to /usr/share/rootfiles/.bash_logout if missing
+  ansible.builtin.copy:
+    src: /root/.bash_logout
+    dest: /usr/share/rootfiles/.bash_logout
+    remote_src: true
+    force: false
+  when:
+  - '"rootfiles" in ansible_facts.packages'
+  - rootfiles_configured_bash_logout_root_stat.stat.exists
   tags:
   - configure_strategy
   - low_complexity
@@ -55,6 +103,37 @@
   - no_reboot_needed
   - rootfiles_configured
 
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Stat /root/.bash_profile
+  ansible.builtin.stat:
+    path: /root/.bash_profile
+  register: rootfiles_configured_bash_profile_root_stat
+  when: '"rootfiles" in ansible_facts.packages'
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Copy /root/.bash_profile
+    to /usr/share/rootfiles/.bash_profile if missing
+  ansible.builtin.copy:
+    src: /root/.bash_profile
+    dest: /usr/share/rootfiles/.bash_profile
+    remote_src: true
+    force: false
+  when:
+  - '"rootfiles" in ansible_facts.packages'
+  - rootfiles_configured_bash_profile_root_stat.stat.exists
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
 - name: Ensure rootfiles tmpfile.d is Configured Correctly - Find configuration files
   ansible.builtin.find:
     paths: /etc/tmpfiles.d/
@@ -101,6 +180,37 @@
   - no_reboot_needed
   - rootfiles_configured
 
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Stat /root/.bashrc
+  ansible.builtin.stat:
+    path: /root/.bashrc
+  register: rootfiles_configured_bashrc_root_stat
+  when: '"rootfiles" in ansible_facts.packages'
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Copy /root/.bashrc to
+    /usr/share/rootfiles/.bashrc if missing
+  ansible.builtin.copy:
+    src: /root/.bashrc
+    dest: /usr/share/rootfiles/.bashrc
+    remote_src: true
+    force: false
+  when:
+  - '"rootfiles" in ansible_facts.packages'
+  - rootfiles_configured_bashrc_root_stat.stat.exists
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
 - name: Ensure rootfiles tmpfile.d is Configured Correctly - Find configuration files
   ansible.builtin.find:
     paths: /etc/tmpfiles.d/
@@ -147,6 +257,37 @@
   - no_reboot_needed
   - rootfiles_configured
 
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Stat /root/.cshrc
+  ansible.builtin.stat:
+    path: /root/.cshrc
+  register: rootfiles_configured_cshrc_root_stat
+  when: '"rootfiles" in ansible_facts.packages'
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Copy /root/.cshrc to
+    /usr/share/rootfiles/.cshrc if missing
+  ansible.builtin.copy:
+    src: /root/.cshrc
+    dest: /usr/share/rootfiles/.cshrc
+    remote_src: true
+    force: false
+  when:
+  - '"rootfiles" in ansible_facts.packages'
+  - rootfiles_configured_cshrc_root_stat.stat.exists
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
 - name: Ensure rootfiles tmpfile.d is Configured Correctly - Find configuration files
   ansible.builtin.find:
     paths: /etc/tmpfiles.d/
@@ -193,6 +334,37 @@
   - no_reboot_needed
   - rootfiles_configured
 
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Stat /root/.tcshrc
+  ansible.builtin.stat:
+    path: /root/.tcshrc
+  register: rootfiles_configured_tcshrc_root_stat
+  when: '"rootfiles" in ansible_facts.packages'
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
+- name: Ensure rootfiles tmpfile.d is Configured Correctly - Copy /root/.tcshrc to
+    /usr/share/rootfiles/.tcshrc if missing
+  ansible.builtin.copy:
+    src: /root/.tcshrc
+    dest: /usr/share/rootfiles/.tcshrc
+    remote_src: true
+    force: false
+  when:
+  - '"rootfiles" in ansible_facts.packages'
+  - rootfiles_configured_tcshrc_root_stat.stat.exists
+  tags:
+  - configure_strategy
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - rootfiles_configured
+
 - name: Ensure rootfiles tmpfile.d is Configured Correctly - Find configuration files
   ansible.builtin.find:
     paths: /etc/tmpfiles.d/

@jan-cerny jan-cerny self-assigned this May 14, 2026
@comps
Copy link
Copy Markdown
Collaborator

comps commented May 15, 2026

I think this is the wrong fix, but since Claude suggested it (per the description), I feel it's only fair for my Claude to explain why. 😄


Summary

This draft PR attempts to fix #14582systemd-tmpfiles errors at boot on RHEL 9.0/9.1 when the stig profile is applied. The approach: mkdir -p /usr/share/rootfiles during remediation and copy each dotfile from /root/ into it, so that the existing tmpfiles.d C (copy) directives have valid source paths.

The PR description openly notes: "This is a Claude suggestion to fix the issue."

The Root Cause

The rootfiles_configured rule writes /etc/tmpfiles.d/rootfiles.conf with entries like:

C /root/.bashrc 600 root root - /usr/share/rootfiles/.bashrc

On RHEL 9.2+, the rootfiles RPM ships /usr/share/rootfiles/ containing template dotfiles. On RHEL 9.0/9.1, the RPM installs dotfiles directly into /root/ and does not create /usr/share/rootfiles/. At boot, systemd-tmpfiles tries to copy from a source that doesn't exist and logs five errors.

Why This Fix Is Wrong

1. Violation of FHS and RPM conventions

/usr/share is designated by the FHS 3.0 as vendor/package-managed, read-only, architecture-independent data. Non-RPM tools (remediation scripts, Ansible, configuration management) should never write files there. The created files become "unowned" from RPM's perspective:

  • rpm -V rootfiles may report anomalies
  • rpm -qf /usr/share/rootfiles/.bashrc returns "not owned by any package"
  • A rootfiles package update could clobber or conflict with these manually placed files
  • rpm --setperms rootfiles could undo related permission changes
  • Security auditors running rpm -Va will flag these as unauthorized modifications

2. Breaks on OSTree / RHEL Image Mode

RHEL Image Mode (fully supported in RHEL 10, tech preview in RHEL 9) mounts /usr read-only at runtime. Writing to /usr/share/rootfiles/ is physically impossible on these deployments. A compliance remediation that writes to /usr is not forward-compatible.

3. Circular and semantically backwards

The remediation copies files FROM /root/ (the destination of the tmpfiles.d C directive) TO /usr/share/rootfiles/ (the source of the C directive). Then at boot, systemd-tmpfiles tries to copy them back. This is a no-op loop — the "pristine template" is manufactured from what's already in the target location. If /root/.bashrc has been customized by the admin, that customization gets frozen as the "template" in /usr/share/rootfiles/, then on every subsequent boot C skips because the destination already exists. The whole mechanism does nothing useful while polluting a vendor-managed namespace.

4. No tests added

The PR doesn't add or update tests to verify that the remediation works on RHEL 9.0/9.1. The existing test suite doesn't verify /usr/share/rootfiles/ existence or boot-time systemd-tmpfiles behavior.

Better Alternatives

There are cleaner approaches that respect system conventions:

Option A (minimal, recommended): Use the C- suffix in tmpfiles.d entries

The systemd tmpfiles.d syntax supports a - suffix on the type letter that makes line failure non-fatal during --create. Changing:

C /root/.bashrc 600 root root - /usr/share/rootfiles/.bashrc

to:

C- /root/.bashrc 600 root root - /usr/share/rootfiles/.bashrc
  • On RHEL 9.0/9.1: the line silently skips (source absent), no boot error. The dotfiles already exist in /root/ from the RPM.
  • On RHEL 9.2+: behaves identically to C (source exists, copy proceeds if destination is missing).

This requires updating the OVAL check regex to accept C-? instead of only C, plus updating the remediation templates. No writes to /usr/share. Works on OSTree. No unowned files.

Option B (architectural): Use z (set-permissions) instead of C (copy)

The z directive adjusts mode/ownership/SELinux on existing files without needing a source path:

z /root/.bashrc 0600 root root -
z /root/.bash_profile 0600 root root -
z /root/.bash_logout 0600 root root -
z /root/.cshrc 0600 root root -
z /root/.tcshrc 0600 root root -

On all RHEL 9 versions, the rootfiles RPM creates these files in /root/, so they already exist. The underlying STIG requirement (SRG-OS-000480-GPOS-00227 / RHEL-09-232045) is "local initialization files must have mode 0740 or less permissive" — it does not mandate tmpfiles.d or mention /usr/share/rootfiles/ at all. The C-from-source design was a ComplianceAsCode implementation choice, not a STIG requirement.

This is a bigger refactor (rule.yml, OVAL, remediations, policy text) but it eliminates the /usr/share/rootfiles/ dependency entirely.

Option C (hybrid): C- in tmpfiles.d + direct permission hardening in remediation

Combine Option A with a direct chmod 0600 / chown root:root on the /root/ dotfiles in the remediation. This gives:

  • Correct permissions immediately (direct remediation)
  • Boot-time enforcement on RHEL 9.2+ (tmpfiles.d C- works when source exists)
  • Graceful degradation on RHEL 9.0/9.1 (tmpfiles.d C- silently skips, but permissions were already set)
  • No writes to /usr/share

Verdict

This PR should not be merged in its current form. Writing to /usr/share/rootfiles/ from a remediation script is the wrong layer of abstraction — it manufactures vendor-managed files outside the package manager, breaks on immutable systems, and creates a circular copy pattern that doesn't accomplish anything useful.

The C- suffix approach (Option A or C) is the correct minimal fix. It solves the boot error without stepping outside the remediation's proper scope, respects RPM file ownership, and is forward-compatible with OSTree deployments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fixes to reported bugs. do-not-merge/work-in-progress Used by openshift-ci bot. STIG STIG Benchmark related.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Systemd-tmpfiles errors on RHEL 9 boot

3 participants