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