From 2487faa678e6853f076415cbcc74c9de52f7b204 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?=
<2031761+viniciusferrao@users.noreply.github.com>
Date: Thu, 27 Aug 2026 01:46:17 -0300
Subject: [PATCH] fix(rpm): preserve local xcat.conf changes across upgrades
xcat.conf was installed as an ordinary payload file and then deleted and
recreated from the Apache-version template in %post. rpm therefore held
no record of what was on disk, and an upgrade replaced an edited file
silently, leaving neither .rpmnew nor .rpmsave. A site that had added
Indexes to the /install block lost it on upgrade and directory listings
began returning 403.
Select the Apache 2.2 or 2.4 configuration at build time, using the same
distribution macros the rest of the spec already relies on, and mark
both /etc/httpd/conf.d/xcat.conf and /etc/apache2/conf.d/xcat.conf as
%config(noreplace). rpm then keeps a modified file and installs the new
vendor version alongside it as xcat.conf.rpmnew.
The old payload recorded the 2.2 file while %post wrote the 2.4 one, so
rpm cannot distinguish a stock file from an edited one across the
transition. A migration compares the active file with the templates the
outgoing package saved under conf.orig and removes it only when it is a
regular file still byte-for-byte identical to one of them. A stock
upgrade then completes without an unnecessary .rpmnew, and anything that
differs is left untouched.
That migration runs in %pretrans, not %pre. rpm fixes each config file's
fate before %pre, so removing the active file there can happen after rpm
has already resolved to write only xcat.conf.rpmnew, leaving the system
with no active configuration at all. %pretrans runs before that decision.
It is an embedded Lua scriptlet because a pre-transaction scriptlet
cannot rely on any dependency being unpacked yet, which also means the
comparison needs no external tool.
bc was needed only by the version check the service-node package no
longer performs.
The Apache directives are unchanged. Document a later-loading conf.d
file as the place for site rules, since that survives upgrades without a
merge.
---
.../advanced/security/apache_hardening.rst | 24 ++++++
xCAT/xCAT.spec | 75 ++++++++++++-------
xCATsn/xCATsn.spec | 69 +++++++++++------
3 files changed, 121 insertions(+), 47 deletions(-)
diff --git a/docs/source/advanced/security/apache_hardening.rst b/docs/source/advanced/security/apache_hardening.rst
index a07ac58ef..44cde248c 100644
--- a/docs/source/advanced/security/apache_hardening.rst
+++ b/docs/source/advanced/security/apache_hardening.rst
@@ -107,6 +107,30 @@ provisioning scripts crawl recursively.
download all files from these directories and depend on Apache directory
listings to discover file paths.
+Local Customizations
+--------------------
+
+``xcat.conf`` belongs to the xCAT package. Keep site-specific rules in a
+separate file that Apache reads after it:
+
+* EL: ``/etc/httpd/conf.d/zz-xcat-local.conf``
+* SUSE: ``/etc/apache2/conf.d/zz-xcat-local.conf``
+
+Apache reads ``conf.d`` in alphabetical order, so the ``zz-`` prefix puts the
+file after ``xcat.conf`` and its directives take precedence. Use the relative
+form of ``Options`` (``+`` or ``-``): it adjusts the options inherited from the
+xCAT block, where the absolute form replaces all of them. For example, to allow
+browsing of one site directory::
+
+ # /etc/httpd/conf.d/zz-xcat-local.conf
+
+ Options +Indexes
+
+
+Editing ``xcat.conf`` in place still works: an upgrade keeps the modified file.
+If the packaged configuration changed too, the new version is written alongside
+it as ``xcat.conf.rpmnew`` and has to be merged by hand.
+
Security Response Headers and Server Banner
-------------------------------------------
diff --git a/xCAT/xCAT.spec b/xCAT/xCAT.spec
index 998f8d6cd..34cbfdf04 100644
--- a/xCAT/xCAT.spec
+++ b/xCAT/xCAT.spec
@@ -125,6 +125,44 @@ tar -xf postscripts.tar
%build
+%ifos linux
+# rpm fixes each config file's fate before %pre runs, so a migration there can
+# delete an active file rpm has already decided to leave as xcat.conf.rpmnew.
+# %pretrans runs before that decision. It is Lua because a pre-transaction
+# scriptlet cannot rely on any dependency being unpacked yet.
+%pretrans -p
+-- Older packages shipped xcat.conf as ordinary payload and then rewrote it
+-- from conf.orig in %post, so rpm's recorded digest cannot tell a stock file
+-- from a customised one. Compare against the outgoing package's templates,
+-- still on disk at this point, and drop only a file that still matches one.
+if arg[2] and tonumber(arg[2]) == 1 then return end
+
+local function contents(path)
+ local handle = io.open(path, "rb")
+ if not handle then return nil end
+ local data = handle:read("*a")
+ handle:close()
+ return data
+end
+
+local templates = { contents("/etc/%httpconfigdir/conf.orig/xcat.conf.apach24"),
+ contents("/etc/%httpconfigdir/conf.orig/xcat.conf.apach22") }
+
+for _, active in ipairs({ "/etc/httpd/conf.d/xcat.conf",
+ "/etc/apache2/conf.d/xcat.conf" }) do
+ -- A symlink or any other non-regular path is treated as locally managed.
+ if posix.stat(active, "type") == "regular" then
+ local current = contents(active)
+ for _, template in ipairs(templates) do
+ if current and current == template then
+ os.remove(active)
+ break
+ end
+ end
+ end
+end
+%endif
+
%pre
# this is now handled by requiring /usr/sbin/dhcpd
#if [ -e "/etc/SuSE-release" ]; then
@@ -199,8 +237,15 @@ chmod 755 $RPM_BUILD_ROOT/install/postscripts/*
rm LICENSE.html
mkdir -p postscripts/hostkeys
cd -
+# Pick the Apache generation at build time. Selecting it in the post scriptlet
+# instead rewrites a file rpm has already checksummed, defeating noreplace.
+%if 0%{?fedora} || 0%{?rhel} >= 7 || 0%{?suse_version} >= 1200
+cp %{SOURCE7} $RPM_BUILD_ROOT/etc/httpd/conf.d/xcat.conf
+cp %{SOURCE7} $RPM_BUILD_ROOT/etc/apache2/conf.d/xcat.conf
+%else
cp %{SOURCE1} $RPM_BUILD_ROOT/etc/httpd/conf.d/xcat.conf
cp %{SOURCE1} $RPM_BUILD_ROOT/etc/apache2/conf.d/xcat.conf
+%endif
cp %{SOURCE7} $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat.conf.apach24
cp %{SOURCE1} $RPM_BUILD_ROOT/etc/%httpconfigdir/conf.orig/xcat.conf.apach22
cp %{SOURCE5} $RPM_BUILD_ROOT/etc/xCATMN
@@ -211,25 +256,6 @@ cp LICENSE.html $RPM_BUILD_ROOT/%{prefix}/share/doc/packages/xCAT
%post
%ifos linux
-#Apply the correct httpd/apache configuration file according to the httpd/apache version
-if [ -n "$(httpd -v 2>&1 |grep -e '^Server version\s*:.*\/2.4')" ]
-then
- rm -rf /etc/httpd/conf.d/xcat.conf
- cp /etc/%httpconfigdir/conf.orig/xcat.conf.apach24 /etc/httpd/conf.d/xcat.conf
-fi
-
-if [ -n "$(apachectl -v 2>&1 |grep -e '^Server version\s*:.*\/2.4')" ]
-then
- rm -rf /etc/apache2/conf.d/xcat.conf
- cp /etc/%httpconfigdir/conf.orig/xcat.conf.apach24 /etc/apache2/conf.d/xcat.conf
-fi
-
-if [ -n "$(apache2ctl -v 2>&1 |grep -e '^Server version\s*:.*\/2.4')" ]
-then
- rm -rf /etc/apache2/conf.d/xcat.conf
- cp /etc/%httpconfigdir/conf.orig/xcat.conf.apach24 /etc/apache2/conf.d/xcat.conf
-fi
-
# On SUSE apache2, mod_headers is not loaded by default; enable it so the
# security response headers in xcat.conf take effect (a no-op where a2enmod
# is absent, e.g. httpd on EL where mod_headers is already loaded).
@@ -282,10 +308,9 @@ if grep -q "command returned error code" "$xcatupgradeout"; then
fi
rm -f "$xcatupgradeout"
-# The package replaces xcat.conf before this upgrade path runs. Reload an
-# active web server so the new security headers and ServerTokens setting take
-# effect immediately, but do not try to manage services while building an
-# image in a chroot.
+# rpm has either updated a stock xcat.conf or preserved a modified one and left
+# the new file as xcat.conf.rpmnew. Reload an active web server so an updated
+# stock configuration takes effect, but do not manage services in a chroot.
if [ -f "/proc/cmdline" ] && [ "x$(stat -c '%i %d' /)" == "x$(stat -c '%i %d' /proc/1/root/. 2>/dev/null)" ]; then
if [ -e "/etc/redhat-release" ]; then
apachedaemon='httpd'
@@ -311,8 +336,8 @@ exit 0
# one for sles, one for rhel. yes, it's ugly...
/etc/%httpconfigdir/conf.orig/xcat.conf.apach24
/etc/%httpconfigdir/conf.orig/xcat.conf.apach22
-/etc/httpd/conf.d/xcat.conf
-/etc/apache2/conf.d/xcat.conf
+%config(noreplace) /etc/httpd/conf.d/xcat.conf
+%config(noreplace) /etc/apache2/conf.d/xcat.conf
/etc/xCATMN
/install/postscripts
/install/prescripts
diff --git a/xCATsn/xCATsn.spec b/xCATsn/xCATsn.spec
index d9585edc0..7540cd694 100644
--- a/xCATsn/xCATsn.spec
+++ b/xCATsn/xCATsn.spec
@@ -50,7 +50,6 @@ Conflicts: xCAT
# on RHEL7, need to specify it explicitly
Requires: net-tools
Requires: /usr/bin/killall
-Requires: /usr/bin/bc
# yaboot-xcat is pulled in so any SN can manage ppc nodes
Requires: httpd nfs-utils nmap bind
# DHCP backend resolved at INSTALL time (not build time) via an RPM rich
@@ -112,8 +111,15 @@ mkdir -p $RPM_BUILD_ROOT/etc/apache2/conf.d
mkdir -p $RPM_BUILD_ROOT/etc/httpd/conf.d/
mkdir -p $RPM_BUILD_ROOT/%{prefix}/share/xcat/
# cd -
+# Pick the Apache generation at build time. Selecting it in the post scriptlet
+# instead rewrites a file rpm has already checksummed, defeating noreplace.
+%if 0%{?fedora} || 0%{?rhel} >= 7 || 0%{?suse_version} >= 1200
+cp %{SOURCE6} $RPM_BUILD_ROOT/etc/apache2/conf.d/xcat.conf
+cp %{SOURCE6} $RPM_BUILD_ROOT/etc/httpd/conf.d/xcat.conf
+%else
cp %{SOURCE1} $RPM_BUILD_ROOT/etc/apache2/conf.d/xcat.conf
cp %{SOURCE1} $RPM_BUILD_ROOT/etc/httpd/conf.d/xcat.conf
+%endif
cp %{SOURCE3} $RPM_BUILD_ROOT/etc/xCATSN
cp %{SOURCE1} $RPM_BUILD_ROOT/etc/xcat/conf.orig/xcat.conf.apach22
cp %{SOURCE6} $RPM_BUILD_ROOT/etc/xcat/conf.orig/xcat.conf.apach24
@@ -141,6 +147,44 @@ rm templates.tar
%endif
+%ifos linux
+# rpm fixes each config file's fate before %pre runs, so a migration there can
+# delete an active file rpm has already decided to leave as xcat.conf.rpmnew.
+# %pretrans runs before that decision. It is Lua because a pre-transaction
+# scriptlet cannot rely on any dependency being unpacked yet.
+%pretrans -p
+-- Older packages shipped xcat.conf as ordinary payload and then rewrote it
+-- from conf.orig in %post, so rpm's recorded digest cannot tell a stock file
+-- from a customised one. Compare against the outgoing package's templates,
+-- still on disk at this point, and drop only a file that still matches one.
+if arg[2] and tonumber(arg[2]) == 1 then return end
+
+local function contents(path)
+ local handle = io.open(path, "rb")
+ if not handle then return nil end
+ local data = handle:read("*a")
+ handle:close()
+ return data
+end
+
+local templates = { contents("/etc/xcat/conf.orig/xcat.conf.apach24"),
+ contents("/etc/xcat/conf.orig/xcat.conf.apach22") }
+
+for _, active in ipairs({ "/etc/httpd/conf.d/xcat.conf",
+ "/etc/apache2/conf.d/xcat.conf" }) do
+ -- A symlink or any other non-regular path is treated as locally managed.
+ if posix.stat(active, "type") == "regular" then
+ local current = contents(active)
+ for _, template in ipairs(templates) do
+ if current and current == template then
+ os.remove(active)
+ break
+ end
+ end
+ end
+end
+%endif
+
%pre
# only need to check on AIX
%ifnos linux
@@ -155,25 +199,6 @@ fi
%post
%ifos linux
-#Apply the correct httpd/apache configuration file according to the httpd/apache version
-
-compare_version="2.4"
-version=`rpm -qi httpd 2>&1 | grep 'Version' | awk '$NF { print $3 }' | cut -d. -f1,2`
-if [ -n "$version" ]; then
- if (( `bc <<< "$version >= $compare_version"` )); then
- rm -rf /etc/httpd/conf.d/xcat.conf
- cp /etc/xcat/conf.orig/xcat.conf.apach24 /etc/httpd/conf.d/xcat.conf
- fi
-fi
-
-version=`rpm -qi apache2 2>&1 | grep '^Version' | awk '$NF { print $3 }' | cut -d. -f1,2`
-if [ -n "$version" ]; then
- if (( `bc <<< "$version >= $compare_version"` )); then
- rm -rf /etc/apache2/conf.d/xcat.conf
- cp /etc/xcat/conf.orig/xcat.conf.apach24 /etc/apache2/conf.d/xcat.conf
- fi
-fi
-
# On SUSE apache2, mod_headers is not loaded by default; enable it so the
# security response headers in xcat.conf take effect (a no-op where a2enmod
# is absent, e.g. httpd on EL where mod_headers is already loaded).
@@ -293,8 +318,8 @@ fi
%ifos linux
/etc/xcat/conf.orig/xcat.conf.apach24
/etc/xcat/conf.orig/xcat.conf.apach22
-/etc/httpd/conf.d/xcat.conf
-/etc/apache2/conf.d/xcat.conf
+%config(noreplace) /etc/httpd/conf.d/xcat.conf
+%config(noreplace) /etc/apache2/conf.d/xcat.conf
/etc/xcat/logrotate.conf/
/etc/xcat/rsyslog.conf/
%endif