mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
Package FIPS for RPM-based distributions
Add an RPM package for Fedora and RHEL. `make -C packaging rpm` builds it from packaging/rpm/fips.spec, and the release workflow attaches it beside the .deb and the systemd tarball. - The package is named `fips-mesh`, because Fedora already ships an unrelated `fips` (a FITS image viewer) that owns /usr/bin/fips. The spec declares `Conflicts: fips`. - It installs the same files, units and fips group as the .deb. fips.service and fips-dns.service are enabled but not started on install; fips-firewall and fips-gateway stay opt-in. - An upgrade queues `systemctl --no-block try-restart` of fips, fips-dns and fips-gateway, and reloads fips-firewall rather than restarting it. - The binaries come from the pinned build image via build-deb-container.sh, so the RPM passes the same glibc floor and dependency checks as the .deb. rpmbuild runs in FIPS_RPM_BUILD_IMAGE, AlmaLinux 9 pinned by digest. - The glibc floor is now 2.34 project-wide, the lowest supported RPM distribution (RHEL 9). testing/check-rpm-floor.sh fails a package that requires a newer glibc. RHEL 8 and openSUSE are not supported. - Erase removes the DNS drop-ins fips-dns-setup wrote and restarts or reloads the resolver whose file it removed, as the Debian postrm does. /etc/fips is kept, since rpm has no purge. - Dev builds are versioned 0.6.0-0.dev.git<date>.<sha>. Tested on Fedora 44 and in AlmaLinux 9 containers with systemd: the package requires GLIBC_2.34 and passes the floor check; install leaves both units enabled and inactive; upgrade returns immediately and the daemon restarts on the new binary; erase removes the units and DNS files and keeps /etc/fips; host-built binaries (GLIBC_2.39) fail the floor check; dnf refuses to install alongside the FITS viewer. The erase branch's resolver restart and reload were exercised in AlmaLinux 9 with systemctl stubbed. No install-test suite covers the RPM yet; it is only built.
This commit is contained in:
committed by
Johnathan Corgan
parent
0d77dbe2de
commit
17ca52e694
@@ -199,6 +199,44 @@ jobs:
|
||||
done
|
||||
rm -rf "$UNPACK"
|
||||
|
||||
# The RPM is packaged from the binaries the .deb shipped, so all three
|
||||
# Linux artifacts carry the same objects and the floor check that has
|
||||
# already passed on the .deb covers them. The script runs rpmbuild in the
|
||||
# rpm image declared in packaging/build-floor.env and checks the glibc
|
||||
# requirement of the package it produced; it is the same script a local
|
||||
# `make rpm` calls, which is what keeps the two identical.
|
||||
- name: Build RPM package
|
||||
id: rpm
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
||||
|
||||
packaging/rpm/build-rpm-container.sh \
|
||||
--no-build \
|
||||
--version "${{ needs.determine-versioning.outputs.linux_package_version }}" \
|
||||
--output-dir deploy \
|
||||
| tee /tmp/build-rpm.log
|
||||
|
||||
# The script prints the package path as its last line of stdout; its
|
||||
# diagnostics go to stderr, as with build-deb-container.sh.
|
||||
RPM_FILE=$(tail -n 1 /tmp/build-rpm.log)
|
||||
if [[ ! -f "$RPM_FILE" ]]; then
|
||||
echo "build-rpm-container.sh did not name a package: '$RPM_FILE'" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$RPM_FILE" in
|
||||
*.${{ matrix.artifact_arch }}.rpm) ;;
|
||||
*)
|
||||
echo "Package $RPM_FILE is not ${{ matrix.artifact_arch }}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Recorded relative to the checkout, like the .deb: upload-artifact
|
||||
# derives its layout from the common ancestor of its paths.
|
||||
echo "rpm=${RPM_FILE#"$PWD"/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build systemd tarball
|
||||
env:
|
||||
STRIP: llvm-strip
|
||||
@@ -240,13 +278,15 @@ jobs:
|
||||
|
||||
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
|
||||
echo "deb=${{ steps.deb.outputs.deb }}" >> "$GITHUB_OUTPUT"
|
||||
echo "rpm=${{ steps.rpm.outputs.rpm }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: SHA-256 hashes
|
||||
run: |
|
||||
echo "==> Linux release assets:"
|
||||
sha256sum \
|
||||
"${{ steps.linux-assets.outputs.tarball }}" \
|
||||
"${{ steps.linux-assets.outputs.deb }}"
|
||||
"${{ steps.linux-assets.outputs.deb }}" \
|
||||
"${{ steps.linux-assets.outputs.rpm }}"
|
||||
|
||||
- name: Upload artifact (GitHub only)
|
||||
if: ${{ env.ACT != 'true' }}
|
||||
@@ -256,6 +296,7 @@ jobs:
|
||||
path: |
|
||||
${{ steps.linux-assets.outputs.tarball }}
|
||||
${{ steps.linux-assets.outputs.deb }}
|
||||
${{ steps.linux-assets.outputs.rpm }}
|
||||
retention-days: 30
|
||||
|
||||
- name: Build Summary
|
||||
@@ -263,6 +304,7 @@ jobs:
|
||||
echo "Build Summary for linux/${{ matrix.artifact_arch }}:"
|
||||
echo " Tarball: ${{ steps.linux-assets.outputs.tarball }}"
|
||||
echo " Debian: ${{ steps.linux-assets.outputs.deb }}"
|
||||
echo " RPM: ${{ steps.linux-assets.outputs.rpm }}"
|
||||
|
||||
release:
|
||||
name: Publish Linux assets to GitHub Release
|
||||
@@ -282,7 +324,7 @@ jobs:
|
||||
- name: Generate Linux release checksums
|
||||
run: |
|
||||
cd dist
|
||||
find . -maxdepth 1 -type f \( -name '*.deb' -o -name '*.tar.gz' \) -printf '%P\n' \
|
||||
find . -maxdepth 1 -type f \( -name '*.deb' -o -name '*.rpm' -o -name '*.tar.gz' \) -printf '%P\n' \
|
||||
| LC_ALL=C sort \
|
||||
| xargs sha256sum \
|
||||
> checksums-linux.txt
|
||||
@@ -308,6 +350,7 @@ jobs:
|
||||
run: |
|
||||
gh release upload "${GITHUB_REF_NAME}" \
|
||||
dist/*.deb \
|
||||
dist/*.rpm \
|
||||
dist/*.tar.gz \
|
||||
dist/checksums-linux.txt \
|
||||
--clobber \
|
||||
|
||||
@@ -176,6 +176,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
#### Packaging
|
||||
|
||||
- An RPM package for Fedora and RHEL (`packaging/rpm/`,
|
||||
`make -C packaging rpm`), built from the same binaries the `.deb` and the
|
||||
systemd tarball carry and attached to each release beside them. The package
|
||||
is named `fips-mesh`, not `fips`: Fedora's namespace already has a `fips` —
|
||||
an unrelated OpenGL FITS image viewer at 3.4.0 — which owns `/usr/bin/fips`,
|
||||
so a `fips` at 0.6.0 is an older release of that one to every RPM tool, and
|
||||
an ordinary `dnf upgrade` replaces a running mesh node with an image viewer.
|
||||
The spec declares `Conflicts: fips`, since both ship `/usr/bin/fips`.
|
||||
`make rpm` compiles nothing on the host: it builds in the pinned image by way
|
||||
of `build-deb-container.sh`, which has already run the glibc floor and
|
||||
Depends checks, and packages what that produced; `rpmbuild` itself runs in
|
||||
`FIPS_RPM_BUILD_IMAGE` (AlmaLinux 9, pinned by digest), which supplies the
|
||||
`systemd-rpm-macros` a build host may lack and writes packages every newer
|
||||
rpm can read. `make rpm-host` remains for iteration, as `deb-host` does.
|
||||
`testing/check-rpm-floor.sh` reads the glibc requirement rpm derived out of
|
||||
the finished package — the table `dnf` enforces at install time — and fails a
|
||||
build above the declared floor. Erase keeps `/etc/fips`: rpm has no purge, so
|
||||
the code that removes a node's identity keys on a dpkg purge would run on an
|
||||
ordinary erase, including the one a distribution upgrade performs.
|
||||
|
||||
- A pfSense package (`packaging/pfsense/`, `gmake pfsense`). pfSense is
|
||||
FreeBSD underneath, but the FreeBSD package fails there in three
|
||||
silent ways: pfSense runs only `/usr/local/etc/rc.d/*.sh` at boot and
|
||||
@@ -328,6 +348,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
binder tearing down and rebinding every second while teardown silently
|
||||
declined to abort anything.
|
||||
|
||||
#### Packaging
|
||||
|
||||
- The glibc floor is 2.34, one step below the 2.35 Ubuntu 22.04 sets. Both
|
||||
families ship the same binaries, so the project-wide floor is the lowest
|
||||
supported member of either, and that is RHEL 9 and its rebuilds.
|
||||
`packaging/build-floor.env` now records the RPM family alongside the Debian
|
||||
one, and records RHEL 8 as a deliberate exclusion: its glibc is 2.28, and
|
||||
reaching it means a second build image and a second floor. `FIPS_BUILD_IMAGE`
|
||||
is still the oldest Debian-family distribution, which is no longer the oldest
|
||||
distribution outright.
|
||||
|
||||
#### Packaging (Debian)
|
||||
|
||||
- An upgrade of the `.deb` now reapplies the firewall ruleset in place. Until
|
||||
|
||||
@@ -132,6 +132,24 @@ default `/etc/fips/fips.yaml` you can edit before starting. The package
|
||||
enables `fips` and `fips-dns` but starts neither, which is why the
|
||||
second command is there.
|
||||
|
||||
On Fedora or RHEL, download `fips-mesh-<version>-<release>.x86_64.rpm` (or
|
||||
`.aarch64.rpm`) and install it:
|
||||
|
||||
```bash
|
||||
sudo dnf install ./fips-mesh-<version>-<release>.x86_64.rpm
|
||||
sudo systemctl start fips fips-dns
|
||||
```
|
||||
|
||||
The package is `fips-mesh` because Fedora's `fips` is an unrelated FITS image
|
||||
viewer that owns `/usr/bin/fips`; the two conflict and dnf will say so.
|
||||
|
||||
It carries the same binaries as the `.deb` — built in the same pinned
|
||||
container, checked against the same glibc floor — and leaves the same
|
||||
post-install state. No install-test suite covers it, and it does not
|
||||
delete `/etc/fips` when removed, because rpm has no purge;
|
||||
[packaging/README.md](packaging/README.md) has the full list of what it
|
||||
does and does not share with the `.deb`.
|
||||
|
||||
For macOS, Windows, FreeBSD (including a pfSense build under
|
||||
`packaging/pfsense/`), OpenWrt, the systemd tarball or a Nix
|
||||
flake, see [docs/getting-started.md](docs/getting-started.md)
|
||||
@@ -200,7 +218,10 @@ and Android. Linux is not one target. Debian, Ubuntu, Arch and NixOS
|
||||
are the same glibc build, and what
|
||||
differs is the packaging: Debian and Ubuntu take the same `.deb`, Arch
|
||||
takes `fips` from the AUR, and NixOS uses the Nix flake described
|
||||
below. **Only the `.deb` is exercised by an install test**, by the
|
||||
below, and Fedora and RHEL take the `.rpm` built from the same binaries by
|
||||
`packaging/rpm/`. RPM-based distributions have no column of
|
||||
their own for the same reason pfSense does not: the build is the glibc
|
||||
one and only the packaging differs. **Only the `.deb` is exercised by an install test**, by the
|
||||
`deb-install` suite across debian12, debian13, ubuntu22, ubuntu24 and
|
||||
ubuntu26; neither the AUR package nor the flake is. That suite runs on
|
||||
every push and pull request, on x86_64, against a `.deb` built by the same
|
||||
|
||||
+12
-7
@@ -59,6 +59,8 @@ The most direct path. The release distribution carries a
|
||||
per-platform installer:
|
||||
|
||||
- Debian/Ubuntu: `.deb` package
|
||||
- Fedora/RHEL: `.rpm` package, named `fips-mesh` (Fedora's `fips` is an
|
||||
unrelated FITS image viewer)
|
||||
- Arch Linux: `fips` AUR package
|
||||
- OpenWrt: `.ipk` and `.apk` packages
|
||||
- macOS: `.pkg` installer
|
||||
@@ -66,13 +68,16 @@ per-platform installer:
|
||||
- Windows: `.zip` with service-install scripts
|
||||
- Generic systemd Linux: `.tar.gz` with an `install.sh` script
|
||||
|
||||
The `.deb` and the systemd tarball support every version of a glibc
|
||||
distribution that its vendor still supports for free: currently Ubuntu
|
||||
22.04, Debian 12, Ubuntu 24.04, Debian 13 and Ubuntu 26.04. Those binaries
|
||||
are built in a container pinned to the oldest of them, so they run on all
|
||||
five, and the glibc floor that follows is declared in
|
||||
`packaging/build-floor.env` and checked by `testing/check-glibc-floor.sh` on
|
||||
what the release workflow produces. Arch and NixOS build from source on your
|
||||
The `.deb`, the `.rpm` and the systemd tarball carry the same binaries and
|
||||
support every version of a glibc distribution that its vendor still supports
|
||||
for free: currently Ubuntu 22.04, Debian 12, Ubuntu 24.04, Debian 13 and
|
||||
Ubuntu 26.04 on the Debian side, and RHEL 9 and later on the RPM side. Those
|
||||
binaries are built in a container pinned to the oldest Debian-family member,
|
||||
and the floor they are held to is the lowest of either family — RHEL 9's glibc
|
||||
2.34 — declared in `packaging/build-floor.env` and checked by
|
||||
`testing/check-glibc-floor.sh` on what the release workflow produces. The
|
||||
`.rpm` also records that floor as an ordinary dependency, so a package built
|
||||
above it is refused rather than installed. Arch and NixOS build from source on your
|
||||
own machine, and OpenWrt is a musl target rather than glibc, so none of them
|
||||
depends on that floor.
|
||||
|
||||
|
||||
+22
-1
@@ -6,6 +6,8 @@
|
||||
# Usage:
|
||||
# make deb Build a Debian/Ubuntu .deb package in the pinned container
|
||||
# make deb-host Build a .deb with the host toolchain (see below)
|
||||
# make rpm Build an .rpm in the pinned container
|
||||
# make rpm-host Build an .rpm with the host toolchain (see below)
|
||||
# make tarball Build a systemd install tarball
|
||||
# make ipk Build an OpenWrt .ipk package (opkg, OpenWrt 24.x and earlier)
|
||||
# make apk Build an OpenWrt .apk package (apk-tools, mandatory on OpenWrt 25+)
|
||||
@@ -21,7 +23,7 @@ SHELL := /bin/bash
|
||||
PACKAGING_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
PROJECT_ROOT := $(abspath $(PACKAGING_DIR)/..)
|
||||
|
||||
.PHONY: all deb deb-host tarball ipk apk aur pkg freebsd pfsense zip clean
|
||||
.PHONY: all deb deb-host rpm rpm-host tarball ipk apk aur pkg freebsd pfsense zip clean
|
||||
|
||||
all: deb tarball
|
||||
|
||||
@@ -40,6 +42,25 @@ deb:
|
||||
deb-host:
|
||||
@bash $(PACKAGING_DIR)/debian/build-deb.sh
|
||||
|
||||
# `rpm` compiles nothing on the host either: it builds the binaries in the same
|
||||
# pinned container the .deb uses, packages those, and then checks the glibc
|
||||
# requirement rpm derived for the finished package against the declared floor.
|
||||
# So the RPM carries the same objects as the .deb and the tarball, and a
|
||||
# package built above the floor fails here rather than at a user's `dnf
|
||||
# install` -- which is what `deb` gets from its container and its Depends
|
||||
# check.
|
||||
rpm:
|
||||
@bash $(PACKAGING_DIR)/rpm/build-rpm-container.sh
|
||||
|
||||
# `rpm-host` packages whatever the host toolchain built, and like `deb-host` it
|
||||
# is for local iteration and NOT for anything anyone else installs. Nothing
|
||||
# checks its floor, deliberately, so the check stays attached to the artifact
|
||||
# that ships. Its failure is at least loud: rpm derives the requirement from
|
||||
# the binaries, so a package built on a host above the floor is refused by dnf
|
||||
# on an older system rather than installed and unable to start.
|
||||
rpm-host:
|
||||
@bash $(PACKAGING_DIR)/rpm/build-rpm.sh
|
||||
|
||||
tarball:
|
||||
@bash $(PACKAGING_DIR)/systemd/build-tarball.sh
|
||||
|
||||
|
||||
+125
-2
@@ -8,6 +8,7 @@ and `make apk` write to `dist/` instead.
|
||||
|
||||
```sh
|
||||
make deb # Debian/Ubuntu .deb (built in the pinned container)
|
||||
make rpm # Fedora/RHEL .rpm, named fips-mesh (built in the pinned container)
|
||||
make tarball # systemd install tarball
|
||||
make ipk # OpenWrt .ipk (opkg, OpenWrt 24.x and earlier)
|
||||
make apk # OpenWrt .apk (apk-tools, mandatory on OpenWrt 25+)
|
||||
@@ -22,9 +23,11 @@ make all # deb + tarball (default)
|
||||
## The two Debian build paths
|
||||
|
||||
`make deb` builds in a container pinned to the oldest supported
|
||||
distribution, named with the glibc floor in
|
||||
Debian-family distribution, named with the glibc floor in
|
||||
[build-floor.env](build-floor.env), and checks the package it produced
|
||||
against that floor before handing it back. Its only host prerequisite is
|
||||
against that floor before handing it back. The floor itself is lower than that
|
||||
image's glibc: RHEL 9 is the lowest supported distribution project-wide, and
|
||||
both families ship these same binaries. Its only host prerequisite is
|
||||
docker: the toolchain and the build dependencies live in the image. This
|
||||
is the path the release workflow, the integration suite and the internal
|
||||
builder all take, so a package that passes locally is built the way the
|
||||
@@ -74,6 +77,8 @@ packaging/
|
||||
common/ Shared assets (default config, hosts file) and pkg-lib.sh,
|
||||
the helpers the FreeBSD and pfSense builders share
|
||||
debian/ Debian/Ubuntu .deb packaging via cargo-deb
|
||||
rpm/ Fedora/RHEL .rpm packaging via rpmbuild, over the binaries
|
||||
the Debian container build produces
|
||||
freebsd/ FreeBSD .pkg packaging via pkg-create(8)
|
||||
pfsense/ pfSense .pkg packaging (FreeBSD-based, but not the same)
|
||||
macos/ macOS .pkg installer via pkgbuild
|
||||
@@ -118,6 +123,124 @@ sudo dpkg -r fips
|
||||
sudo dpkg -P fips
|
||||
```
|
||||
|
||||
### RPM (`.rpm`)
|
||||
|
||||
Built with `rpmbuild` from [rpm/fips.spec](rpm/fips.spec). The same files land
|
||||
in the same places as the `.deb`, the same `fips` system group is created, the
|
||||
same `/etc/fips/fips.yaml` seeding happens, and the same two units are enabled;
|
||||
`fips-firewall` and `fips-gateway` stay opt-in.
|
||||
|
||||
**The package is named `fips-mesh`, not `fips`.** Fedora's namespace already
|
||||
has a `fips` — an unrelated OpenGL FITS image viewer, currently 3.4.0 — which
|
||||
owns `/usr/bin/fips`. Ours at 0.6.0 would be an *older* `fips` to every RPM
|
||||
tool, so a routine `dnf upgrade` replaces a running mesh node with an image
|
||||
viewer and takes the units with it; that is not hypothetical, it happened
|
||||
within the hour on a test machine. The two cannot coexist either, since both
|
||||
ship `/usr/bin/fips`, so the spec declares `Conflicts: fips` and dnf refuses
|
||||
with both names on screen instead of a bare path.
|
||||
|
||||
Like the Debian package, it has two build paths, and for the same reason.
|
||||
|
||||
`make rpm` compiles nothing on the host: it builds the binaries in the image
|
||||
declared in [build-floor.env](build-floor.env), packages those, and checks the
|
||||
glibc requirement of the finished package against the declared floor. So the
|
||||
RPM carries the same objects as the `.deb` and the tarball, and a package built
|
||||
above the floor fails there rather than at a user's `dnf install`. rpmbuild
|
||||
runs in `FIPS_RPM_BUILD_IMAGE` (AlmaLinux 9, pinned by digest), which supplies
|
||||
two things a build host may lack: rpmbuild itself, and systemd-rpm-macros,
|
||||
without which the spec's `%systemd_post` would not expand and the package would
|
||||
ship scriptlets that quietly do nothing. Docker is the only host prerequisite.
|
||||
|
||||
`make rpm-host` packages whatever the host toolchain built. Like `deb-host` it
|
||||
is for local iteration and not for anything anyone else installs; nothing
|
||||
checks its floor.
|
||||
|
||||
The release workflow calls the same container script both matrix legs, with
|
||||
`--no-build`, over the binaries it has already recovered from the `.deb` — one
|
||||
build, three artifacts — and attaches the result to the GitHub Release next to
|
||||
the `.deb` and the tarball.
|
||||
|
||||
```sh
|
||||
# Build (requires docker)
|
||||
make rpm
|
||||
|
||||
# Install
|
||||
sudo dnf install ./deploy/fips-mesh-<version>-<release>.<arch>.rpm
|
||||
|
||||
# Remove (keeps /etc/fips, including identity keys)
|
||||
sudo dnf remove fips-mesh
|
||||
```
|
||||
|
||||
Two firewalls, on the distributions where firewalld owns nftables. They do not
|
||||
conflict — firewalld manages its own tables and `fips-firewall.service` adds
|
||||
`table inet fips`, which returns immediately for anything not arriving on
|
||||
`fips0` — but firewalld is filtering the node whether or not that unit ever
|
||||
runs, and in two places worth knowing:
|
||||
|
||||
- **Inbound peers arrive on your ordinary interface**, on the transport ports
|
||||
(`2121/udp` and `8443/tcp` in the shipped config), and those are in whatever
|
||||
zone that interface belongs to. Fedora Workstation's default zone opens
|
||||
`1025-65535` for both protocols, so it works there untouched; RHEL, CentOS
|
||||
Stream and Fedora Server default to `public`, which allows `ssh`,
|
||||
`dhcpv6-client` and `mdns` and nothing else, so a node there accepts no
|
||||
inbound peers until the ports are opened:
|
||||
|
||||
```sh
|
||||
sudo firewall-cmd --permanent --add-port=2121/udp --add-port=8443/tcp
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
- **`fips0` itself lands in the default zone**, since nothing assigns it one —
|
||||
`firewall-cmd --get-zone-of-interface=fips0` says `no zone`, which means the
|
||||
default. Mesh traffic to local services is then subject to that zone as well
|
||||
as to the fips baseline. Giving the interface its own zone keeps the two
|
||||
decisions apart, and `trusted` leaves the filtering to `/etc/fips/fips.nft`
|
||||
and its drop-ins, which is where it is meant to be:
|
||||
|
||||
```sh
|
||||
sudo firewall-cmd --permanent --zone=trusted --change-interface=fips0
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
Either way the fips table stays invisible to firewalld: `firewall-cmd
|
||||
--list-all` will not show it, and opening a port with `firewall-cmd` does not
|
||||
open it in the fips table. That is what `/etc/fips/fips.d/` is for.
|
||||
|
||||
Note also that a default RHEL, CentOS Stream or AlmaLinux install has no
|
||||
resolver backend `fips-dns-setup` can use: systemd is older than the
|
||||
`dns-delegate` drop-in, systemd-resolved is installed but not enabled, and
|
||||
dnsmasq is not installed. The script falls through to its last branch and
|
||||
prints manual instructions, so `.fips` names do not resolve until a backend is
|
||||
in place. Fedora, which enables systemd-resolved, is configured automatically.
|
||||
|
||||
Three things differ from the Debian package, because the package managers do:
|
||||
|
||||
- **The floor is checked on the package, not against it.** `cargo-deb` writes a
|
||||
dependency floor that can disagree with the binaries, so
|
||||
`testing/check-deb-depends.sh` compares the two. rpm derives the requirement
|
||||
from the ELF files and cannot disagree with them, which moves the risk one
|
||||
step back — to binaries built above the floor in the first place.
|
||||
`testing/check-rpm-floor.sh` reads `libc.so.6(GLIBC_x.y)` out of the finished
|
||||
package, the same table `dnf` enforces at install time, and fails the build
|
||||
above the floor.
|
||||
- **No purge.** dpkg distinguishes remove from purge, and `postrm purge`
|
||||
deletes `/etc/fips` and the `fips` group. rpm has no such distinction, so the
|
||||
equivalent would run on an ordinary erase — and during a distribution upgrade
|
||||
that erases and reinstalls — taking the node's identity keys with it.
|
||||
Configuration and keys therefore survive `dnf remove`; delete `/etc/fips`
|
||||
yourself if you mean it.
|
||||
- **Version vs Release.** A dev build is `0.6.0-0.dev.git<date>.<sha>` rather
|
||||
than the `.deb`'s `0.6.0~dev+git<date>.<sha>-1`. rpm has understood `~` since
|
||||
4.10, so this is a choice rather than a limitation: a Release beginning with
|
||||
`0.` is the convention for pre-release packages in this ecosystem, and it
|
||||
sorts below the `1` a tagged release carries. The Release carries no `%{dist}` tag
|
||||
either: there is one build, the glibc one, and a dist tag would name whichever
|
||||
image happened to run rpmbuild in an artifact that installs on all of them.
|
||||
|
||||
No install-test suite covers the RPM. The `deb-install` suite exercises the
|
||||
`.deb` across five distributions on every push; the RPM is built on every push
|
||||
and installed by nobody but you.
|
||||
|
||||
### systemd Tarball
|
||||
|
||||
A self-contained tarball with binaries and an `install.sh` script for
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# The glibc floor for the Linux release artifacts, and the image that produces it.
|
||||
#
|
||||
# Sourced by packaging/debian/build-deb-container.sh and by
|
||||
# testing/check-glibc-floor.sh. It exists so the floor is a decision written
|
||||
# down in one place rather than a side effect of whichever build host ran last.
|
||||
# Sourced by packaging/debian/build-deb-container.sh,
|
||||
# packaging/rpm/build-rpm-container.sh, testing/check-glibc-floor.sh and
|
||||
# testing/check-rpm-floor.sh. It exists so the floor is a decision written
|
||||
# down in one place rather than a side effect of whichever build host ran
|
||||
# last.
|
||||
#
|
||||
# The rule it encodes: FIPS installs on every version of a supported operating
|
||||
# system that its distributor still supports for free. As of 2026-09-05 that is
|
||||
@@ -13,8 +15,31 @@
|
||||
# Debian 13 glibc 2.41 LTS ends 2030-06-30
|
||||
# Ubuntu 26.04 glibc 2.43
|
||||
#
|
||||
# so the lowest is Ubuntu 22.04 and the floor is its 2.35. Debian 11 left the
|
||||
# set on 2026-08-31 and is deliberately not counted.
|
||||
# and on the RPM side, where the same binaries ship as fips-mesh:
|
||||
#
|
||||
# RHEL 9 and rebuilds glibc 2.34 AlmaLinux/Rocky 9 supported to 2032-05
|
||||
# Fedora (current two) glibc 2.42+ each release supported ~13 months
|
||||
#
|
||||
# so the lowest of both families is RHEL 9 and the floor is its 2.34. Debian 11
|
||||
# left the set on 2026-08-31 and is deliberately not counted. openSUSE is not
|
||||
# in the set yet: nobody has run the package on Leap, and it joins when an
|
||||
# install leg does.
|
||||
#
|
||||
# RHEL 8 and its rebuilds are deliberately NOT in the set. Their glibc is 2.28
|
||||
# and AlmaLinux 8 and Rocky 8 are in free support until 2029-05, so this is a
|
||||
# real exclusion rather than an oversight: reaching 2.28 means building on an
|
||||
# EL8-era toolchain, which is a second build image and a second floor, and no
|
||||
# one has asked for it. If someone does, that is the decision to reopen -- not
|
||||
# this number.
|
||||
#
|
||||
# The RPM family is why the floor is 2.34 rather than Ubuntu 22.04's 2.35: both
|
||||
# families ship the same binaries, so the project-wide floor is the lowest
|
||||
# member of either, and that is RHEL 9. The binaries built in FIPS_BUILD_IMAGE
|
||||
# happen to reference nothing above 2.34 today, which is what lets a package
|
||||
# built there install on RHEL 9 at all; without this line that is luck, and the
|
||||
# first commit to pull in a 2.35 symbol would pass the check and silently drop
|
||||
# every EL9 host. One step tighter costs the Debian side nothing and makes the
|
||||
# EL9 claim enforced.
|
||||
#
|
||||
# Deliberately NOT a GitHub runner label. Runner availability follows GitHub's
|
||||
# rule of supporting the newest two images; the floor follows distributors'
|
||||
@@ -26,10 +51,36 @@
|
||||
# Changing FIPS_GLIBC_FLOOR drops support for every distribution below it. Check
|
||||
# the table above first, and expect check-glibc-floor.sh to hold you to it.
|
||||
|
||||
# Base image for the build. Pinned to the oldest supported distribution.
|
||||
# Base image for the build. The oldest supported *Debian-family* distribution,
|
||||
# which is no longer the oldest supported distribution outright: RHEL 9 sits a
|
||||
# step below it at 2.34, and FIPS_GLIBC_FLOOR rather than this image is what
|
||||
# the binaries are held to.
|
||||
FIPS_BUILD_IMAGE="ubuntu:22.04"
|
||||
|
||||
# Image that runs rpmbuild. It compiles nothing -- the binaries it packages are
|
||||
# built in FIPS_BUILD_IMAGE -- and supplies two things the build host may not
|
||||
# have: rpmbuild itself, and systemd-rpm-macros, without which the spec's
|
||||
# %systemd_post would not expand and the package would ship scriptlets that
|
||||
# quietly do nothing. The oldest rpm in free support (RHEL 9, rpm 4.16), so a
|
||||
# package it writes is readable by every newer rpm, which is this file's glibc
|
||||
# rule applied to the packaging format.
|
||||
#
|
||||
# Pinned by digest, not by tag. `almalinux:9` floats: it moves with every
|
||||
# rebuild, and the systemd-rpm-macros it carries is what expands %systemd_post
|
||||
# into the scriptlets a release artifact ships. A floating input to a release
|
||||
# artifact is the thing this file exists to prevent, and the workflow that
|
||||
# consumes it runs on three branches, every pull request and every tag. The
|
||||
# digest is a multi-arch index, so both matrix legs resolve their own
|
||||
# architecture from it.
|
||||
#
|
||||
# To move it: `docker buildx imagetools inspect almalinux:9 --format
|
||||
# '{{.Manifest.Digest}}'`, and check that the rpm inside is still old enough
|
||||
# for the distributions in the table above.
|
||||
FIPS_RPM_BUILD_IMAGE="almalinux@sha256:3a3fa7f043b142bc8008c8b308d39b47d2c84008addcd52f9f9a7a82d2a90474"
|
||||
|
||||
# Highest glibc symbol version any shipped binary may require. Building on
|
||||
# FIPS_BUILD_IMAGE currently yields 2.34, one step below this, so there is a
|
||||
# little headroom: the check is an upper bound, not an equality.
|
||||
FIPS_GLIBC_FLOOR="2.35"
|
||||
# FIPS_BUILD_IMAGE currently yields exactly this, so there is no headroom left:
|
||||
# the check is an upper bound, and the build sits on it. A change that raises
|
||||
# what the binaries need will fail check-glibc-floor.sh rather than ship a
|
||||
# package RHEL 9 refuses.
|
||||
FIPS_GLIBC_FLOOR="2.34"
|
||||
|
||||
Executable
+265
@@ -0,0 +1,265 @@
|
||||
#!/bin/bash
|
||||
# Build the RPM from binaries compiled in the pinned container, then check the
|
||||
# floor of the package it produced.
|
||||
#
|
||||
# This is the supported path, and the counterpart of
|
||||
# packaging/debian/build-deb-container.sh. Both exist for the same reason: a
|
||||
# package built against the host's C library carries that library's version
|
||||
# floor, and the host is almost never the oldest system the package has to
|
||||
# install on. The Debian package answered that with a pinned build image; this
|
||||
# reuses that image rather than pinning a second one, so the RPM ships the same
|
||||
# objects the .deb and the tarball do.
|
||||
#
|
||||
# rpmbuild itself runs in FIPS_RPM_BUILD_IMAGE, which compiles nothing. It is
|
||||
# there because the build host may have no rpmbuild at all, and -- the part
|
||||
# that would fail quietly -- may have no systemd-rpm-macros, without which the
|
||||
# spec's %systemd_post does not expand and the package ships scriptlets that do
|
||||
# nothing.
|
||||
#
|
||||
# Usage: build-rpm-container.sh [--output-dir DIR] [--version V] [--features L]
|
||||
# [--no-build] [--bin-dir DIR]
|
||||
#
|
||||
# --no-build packages the binaries already under target/release instead of
|
||||
# building any, for a caller that has them: the release workflow recovers them
|
||||
# from the .deb it just built, and building them twice would only be slower.
|
||||
# --bin-dir says where those binaries are, if not target/release.
|
||||
#
|
||||
# --features reaches cargo through the Debian container build and then marks
|
||||
# the Release, so a feature build of a commit is a different package from the
|
||||
# default build of the same commit.
|
||||
#
|
||||
# Requires docker. Nothing else: no rust toolchain, no rpmbuild, no dpkg.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# shellcheck source=../build-floor.env
|
||||
. "$REPO_ROOT/packaging/build-floor.env"
|
||||
# shellcheck source=SCRIPTDIR/../../testing/lib/image-build.sh
|
||||
. "$REPO_ROOT/testing/lib/image-build.sh"
|
||||
|
||||
DEST_DIR="$REPO_ROOT/deploy"
|
||||
VERSION=""
|
||||
FEATURES=""
|
||||
NO_BUILD=0
|
||||
BIN_DIR=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output-dir) DEST_DIR="${2:?missing value for --output-dir}"; shift 2 ;;
|
||||
--version) VERSION="${2:?missing value for --version}"; shift 2 ;;
|
||||
--features) FEATURES="${2:?missing value for --features}"; shift 2 ;;
|
||||
--no-build) NO_BUILD=1; shift ;;
|
||||
--bin-dir) BIN_DIR="${2:?missing value for --bin-dir}"; NO_BUILD=1; shift 2 ;;
|
||||
-h | --help) sed -n '2,28p' "$0"; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
command -v docker >/dev/null 2>&1 || {
|
||||
echo "build-rpm-container: docker is required and was not found." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# The same refusal build-rpm.sh makes, and for the same reason: with no build
|
||||
# of our own the features cannot reach cargo, so the Release marking below
|
||||
# would claim binaries that were compiled by somebody else, with who knows
|
||||
# what. Refused here rather than after the container build that --no-build was
|
||||
# asked to skip.
|
||||
if [ -n "$FEATURES" ] && [ "$NO_BUILD" -eq 1 ]; then
|
||||
echo "build-rpm-container: --features cannot be combined with --no-build or" >&2
|
||||
echo "--bin-dir: the features would not reach the binaries, but the Release" >&2
|
||||
echo "would claim they had." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$DEST_DIR"
|
||||
DEST_ABS="$(cd "$DEST_DIR" && pwd)"
|
||||
|
||||
# Both scratch directories live inside the output directory rather than under
|
||||
# /tmp, which is the shape build-deb-container.sh takes and for the same
|
||||
# reason: a bind-mount source is resolved by the Docker daemon in the host's
|
||||
# mount namespace, so under a private /tmp -- systemd's PrivateTmp=, which the
|
||||
# CI worker sets -- a path from a bare `mktemp -d` exists only in this
|
||||
# process's namespace. The daemon would create its own directory at that path
|
||||
# in the host's /tmp, the container would write there, and this script would
|
||||
# read an empty one. The output directory is already bind-mounted as /out and
|
||||
# so resolves the same way in both namespaces.
|
||||
#
|
||||
# The traps clear them on any ordinary exit but not on a SIGKILL, and the
|
||||
# builder's watch loop group-kills a run that overruns or is superseded, so
|
||||
# sweep siblings old enough that no live run can own them.
|
||||
find "$DEST_ABS" -maxdepth 1 -type d \( -name '.name.*' -o -name '.stage.*' \) \
|
||||
-mmin +120 -exec rm -rf {} + 2>/dev/null || :
|
||||
|
||||
STAGE=""
|
||||
# The body is last, not the test: written as `[ -n "$STAGE" ] && rm -rf ...`,
|
||||
# an unset STAGE makes the test the handler's final command, the handler
|
||||
# returns 1, and from an EXIT trap under `set -e` that becomes the script's
|
||||
# exit status.
|
||||
cleanup() {
|
||||
if [ -n "$STAGE" ]; then
|
||||
rm -rf "$STAGE"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ "$NO_BUILD" -eq 0 ]; then
|
||||
# Build the .deb in the pinned container and package the binaries out of
|
||||
# it. That is one build rather than two, and it is the build that
|
||||
# build-deb-container.sh has already run the glibc floor and Depends checks
|
||||
# on, so the RPM cannot carry objects those checks never saw.
|
||||
STAGE=$(mktemp -d "$DEST_ABS/.stage.XXXXXX") || {
|
||||
echo "build-rpm-container: could not create a staging directory in $DEST_ABS" >&2
|
||||
exit 1
|
||||
}
|
||||
DEB_DIR="$STAGE/deb"
|
||||
BIN_DIR="$STAGE/bin"
|
||||
mkdir -p "$DEB_DIR" "$BIN_DIR"
|
||||
|
||||
deb_args=(--output-dir "$DEB_DIR")
|
||||
[ -n "$VERSION" ] && deb_args+=(--version "$VERSION")
|
||||
[ -n "$FEATURES" ] && deb_args+=(--features "$FEATURES")
|
||||
|
||||
echo "=== Building the binaries in the pinned container ===" >&2
|
||||
DEB=$("$REPO_ROOT/packaging/debian/build-deb-container.sh" "${deb_args[@]}" | tail -n 1)
|
||||
[ -f "$DEB" ] || {
|
||||
echo "build-rpm-container: the Debian build did not produce a package" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# dpkg-deb lives in the build image, not necessarily on a host that wants
|
||||
# an RPM -- a Fedora workstation has no dpkg at all.
|
||||
BUILD_IMAGE=$("$REPO_ROOT/packaging/debian/build-deb-container.sh" --print-image-tag)
|
||||
docker run --rm \
|
||||
-v "$DEB_DIR":/deb:ro \
|
||||
-v "$BIN_DIR":/bin-out \
|
||||
-e "HOST_UID=$(id -u)" -e "HOST_GID=$(id -g)" \
|
||||
"$BUILD_IMAGE" \
|
||||
bash -euo pipefail -c '
|
||||
unpack=$(mktemp -d)
|
||||
dpkg-deb -x /deb/*.deb "$unpack"
|
||||
for binary in fips fipsctl fipstop fips-gateway; do
|
||||
install -m 0755 "$unpack/usr/bin/$binary" "/bin-out/$binary"
|
||||
done
|
||||
chown "$HOST_UID:$HOST_GID" /bin-out/*
|
||||
' >&2
|
||||
fi
|
||||
|
||||
: "${BIN_DIR:=$REPO_ROOT/target/release}"
|
||||
BIN_DIR="$(cd "$BIN_DIR" && pwd)"
|
||||
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "$REPO_ROOT" log -1 --format=%ct)}"
|
||||
|
||||
# The version is derived on the host and passed in, because a worktree's .git
|
||||
# is a file pointing outside the mount and git in the container cannot read it.
|
||||
# Same reasoning as the Debian container build.
|
||||
if [ -z "$VERSION" ]; then
|
||||
CRATE_VERSION=$(awk -F'"' '/^version = /{print $2; exit}' "$REPO_ROOT/Cargo.toml")
|
||||
if [[ "$CRATE_VERSION" == *-dev ]]; then
|
||||
GIT_DATE=$(git -C "$REPO_ROOT" log -1 --format=%cs | tr -d '-')
|
||||
GIT_SHA=$(git -C "$REPO_ROOT" rev-parse --short HEAD)
|
||||
DIRTY=""
|
||||
[ -n "$(git -C "$REPO_ROOT" status --porcelain 2>/dev/null)" ] && DIRTY=".dirty"
|
||||
VERSION="${CRATE_VERSION%-dev}-0.dev.git${GIT_DATE}.${GIT_SHA}${DIRTY}"
|
||||
else
|
||||
VERSION="$CRATE_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# `docker run` pulls the image implicitly on a miss, and that pull is not
|
||||
# retried by anything. It reaches a registry on every runner that has not seen
|
||||
# the digest before, which is every fresh one. retry_build is what the Debian
|
||||
# builder image uses, so a pull that fails and then succeeds leaves a warning
|
||||
# on the run rather than passing silently.
|
||||
if ! docker image inspect "$FIPS_RPM_BUILD_IMAGE" >/dev/null 2>&1; then
|
||||
retry_build "docker pull $FIPS_RPM_BUILD_IMAGE" \
|
||||
docker pull --quiet "$FIPS_RPM_BUILD_IMAGE" >&2
|
||||
fi
|
||||
|
||||
echo "=== Packaging fips $VERSION in $FIPS_RPM_BUILD_IMAGE ===" >&2
|
||||
|
||||
NAME_DIR=$(mktemp -d "$DEST_ABS/.name.XXXXXX") || {
|
||||
echo "build-rpm-container: could not create a name directory in $DEST_ABS" >&2
|
||||
exit 1
|
||||
}
|
||||
trap 'cleanup; rm -rf "$NAME_DIR"' EXIT
|
||||
|
||||
# The features reached cargo in the build above, so the binaries already have
|
||||
# them; what is left is to say so in the Release. build-rpm.sh refuses
|
||||
# --features with --no-build for exactly that reason -- the flag would promise
|
||||
# a build it is not doing -- so the marker is folded into the version here
|
||||
# instead of passed inward.
|
||||
if [ -n "$FEATURES" ]; then
|
||||
MARKER="features.$(printf '%s' "$FEATURES" | tr -c 'a-zA-Z0-9.' '.')"
|
||||
case "$VERSION" in
|
||||
*-*) VERSION="${VERSION}.${MARKER}" ;;
|
||||
*) VERSION="${VERSION}-1.${MARKER}" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
rpm_args=(--no-build --bin-dir /bins --version "$VERSION" --output-dir /out --name-file /name/rpm)
|
||||
|
||||
docker run --rm \
|
||||
-v "$REPO_ROOT":/src:ro \
|
||||
-v "$BIN_DIR":/bins:ro \
|
||||
-v "$DEST_ABS":/out \
|
||||
-v "$NAME_DIR":/name \
|
||||
-e SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \
|
||||
-e "HOST_UID=$(id -u)" -e "HOST_GID=$(id -g)" \
|
||||
-w /src \
|
||||
"$FIPS_RPM_BUILD_IMAGE" \
|
||||
bash -euo pipefail -c "
|
||||
# Retried: this reaches a mirror, and a transient failure here would
|
||||
# fail a release build that has nothing wrong with it. The Debian
|
||||
# builder image gets the same treatment one layer up, in
|
||||
# testing/lib/image-build.sh.
|
||||
for attempt in 1 2 3; do
|
||||
if dnf install -y --setopt=install_weak_deps=False rpm-build systemd-rpm-macros >/dev/null; then
|
||||
break
|
||||
fi
|
||||
if [ \"\$attempt\" -eq 3 ]; then
|
||||
echo 'could not install rpm-build and systemd-rpm-macros' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep \$((attempt * 5))
|
||||
done
|
||||
packaging/rpm/build-rpm.sh ${rpm_args[*]}
|
||||
chown \"\$HOST_UID:\$HOST_GID\" /name/rpm
|
||||
" >&2
|
||||
|
||||
RPM_NAME=""
|
||||
[ -f "$NAME_DIR/rpm" ] && RPM_NAME=$(head -n 1 "$NAME_DIR/rpm")
|
||||
[ -n "$RPM_NAME" ] || {
|
||||
echo "build-rpm-container: the build did not name its package" >&2
|
||||
echo "build-rpm-container: the name travels through $NAME_DIR, bind-mounted as /name." >&2
|
||||
echo "build-rpm-container: if that path is not visible to the Docker daemon -- a private" >&2
|
||||
echo "build-rpm-container: /tmp is the usual cause -- the container wrote the name elsewhere." >&2
|
||||
exit 1
|
||||
}
|
||||
if [[ "$RPM_NAME" == */* || "$RPM_NAME" != fips-mesh-*.rpm ]]; then
|
||||
echo "build-rpm-container: the build named '$RPM_NAME', which is not a package file name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RPM="$DEST_ABS/$RPM_NAME"
|
||||
[ -f "$RPM" ] || {
|
||||
echo "build-rpm-container: the build named $RPM_NAME but $RPM does not exist" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check the artifact, not its inputs. rpm records the requirement it derived
|
||||
# from the binaries, which is the table dnf enforces at install time, so this
|
||||
# reads what a user's package manager will read. It runs in the rpm image
|
||||
# because the host may have no rpm.
|
||||
docker run --rm \
|
||||
-v "$REPO_ROOT":/src:ro \
|
||||
-v "$DEST_ABS":/out:ro \
|
||||
-w /src \
|
||||
"$FIPS_RPM_BUILD_IMAGE" \
|
||||
testing/check-rpm-floor.sh "/out/$RPM_NAME" >&2
|
||||
|
||||
echo "=== Built $RPM ===" >&2
|
||||
printf '%s\n' "$RPM"
|
||||
Executable
+264
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build an .rpm package for FIPS.
|
||||
#
|
||||
# The counterpart of packaging/debian/build-deb.sh, and deliberately the same
|
||||
# shape: the same options, the same dev-version derivation, the same output in
|
||||
# deploy/. cargo-deb builds the binaries itself; here cargo builds them and
|
||||
# rpmbuild packages what it produced, so one cargo invocation stays the only
|
||||
# thing that compiles FIPS.
|
||||
#
|
||||
# Usage: ./build-rpm.sh [--target <triple>] [--version <version>] [--no-build]
|
||||
# [--features <list>] [--output-dir <dir>]
|
||||
# [--name-file <path>] [--bin-dir <dir>]
|
||||
#
|
||||
# Prerequisites: rpm-build and systemd-rpm-macros.
|
||||
# Output: deploy/fips-mesh-<version>-<release>.<arch>.rpm
|
||||
#
|
||||
# "fips-mesh", not "fips": Fedora's namespace has a `fips` package already (an
|
||||
# unrelated FITS image viewer), and ours would look like an old version of it.
|
||||
# See the header of fips.spec.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
SPEC="${SCRIPT_DIR}/fips.spec"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: packaging/rpm/build-rpm.sh [options]
|
||||
|
||||
Options:
|
||||
--target <triple> Rust target triple to build/package
|
||||
--version <version> Override the RPM Version-Release. Accepts either
|
||||
"<version>" or "<version>-<release>".
|
||||
--no-build Package existing binaries without running cargo build
|
||||
--features <list> Cargo features to build with (comma-separated). Marks the
|
||||
auto-derived Release so the package is distinguishable
|
||||
from a default build of the same commit.
|
||||
--output-dir <dir> Where to put the finished .rpm. Defaults to deploy/ under
|
||||
the project root.
|
||||
|
||||
Environment:
|
||||
HOST_UID, HOST_GID Give the finished package to this owner. Set by a
|
||||
container build, whose root would otherwise leave a file
|
||||
on the mounted tree that its owner cannot remove.
|
||||
--name-file <path> Also write the finished package's file name (basename
|
||||
only) to <path>.
|
||||
--bin-dir <dir> Package the binaries in <dir> rather than the ones under
|
||||
target/. Implies --no-build. This is how the container
|
||||
build packages binaries compiled somewhere else.
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
TARGET_TRIPLE=""
|
||||
VERSION_OVERRIDE=""
|
||||
BIN_DIR_OVERRIDE=""
|
||||
NO_BUILD=0
|
||||
FEATURES=""
|
||||
DEST_DIR=""
|
||||
NAME_FILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--target)
|
||||
TARGET_TRIPLE="${2:?missing value for --target}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
VERSION_OVERRIDE="${2:?missing value for --version}"
|
||||
shift 2
|
||||
;;
|
||||
--no-build)
|
||||
NO_BUILD=1
|
||||
shift
|
||||
;;
|
||||
--features)
|
||||
FEATURES="${2:?missing value for --features}"
|
||||
shift 2
|
||||
;;
|
||||
--output-dir)
|
||||
DEST_DIR="${2:?missing value for --output-dir}"
|
||||
shift 2
|
||||
;;
|
||||
--name-file)
|
||||
NAME_FILE="${2:?missing value for --name-file}"
|
||||
shift 2
|
||||
;;
|
||||
--bin-dir)
|
||||
BIN_DIR_OVERRIDE="${2:?missing value for --bin-dir}"
|
||||
NO_BUILD=1
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Same refusal as the Debian build, for the same reason: a feature build that
|
||||
# skips the build step would stamp a feature-marked Release onto whatever
|
||||
# binaries already sit in target/, which is the one outcome the marking exists
|
||||
# to prevent.
|
||||
if [[ -n "${FEATURES}" && "${NO_BUILD}" -eq 1 ]]; then
|
||||
echo "--features cannot be combined with --no-build: the features would not" >&2
|
||||
echo "reach the binaries, but the Release would claim they had." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
if ! command -v rpmbuild &>/dev/null; then
|
||||
echo "rpmbuild not found. Install the rpm-build package." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reproducible builds, as on the Debian side.
|
||||
if [ -z "${SOURCE_DATE_EPOCH:-}" ]; then
|
||||
SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
|
||||
export SOURCE_DATE_EPOCH
|
||||
fi
|
||||
|
||||
CRATE_VERSION=$(awk -F'"' '/^version = /{print $2; exit}' Cargo.toml)
|
||||
|
||||
if [[ -n "${VERSION_OVERRIDE}" ]]; then
|
||||
# Accept "<version>" or "<version>-<release>".
|
||||
RPM_VERSION="${VERSION_OVERRIDE%%-*}"
|
||||
if [[ "${VERSION_OVERRIDE}" == *-* ]]; then
|
||||
RPM_RELEASE="${VERSION_OVERRIDE#*-}"
|
||||
else
|
||||
RPM_RELEASE="1"
|
||||
fi
|
||||
elif [[ "${CRATE_VERSION}" == *-dev ]]; then
|
||||
# A dev build gets a Release that sorts BELOW the eventual tagged release
|
||||
# and differs between commits, so `dnf upgrade` on one dev package
|
||||
# installed over another is not a silent no-op. rpm has understood "~"
|
||||
# since 4.10 and the Debian version uses it; a Release beginning with 0. is
|
||||
# the convention for pre-release packages here and is what this picks.
|
||||
RPM_VERSION="${CRATE_VERSION%-dev}"
|
||||
GIT_DATE=$(git log -1 --format=%cs | tr -d '-')
|
||||
GIT_SHA=$(git rev-parse --short HEAD)
|
||||
RPM_RELEASE="0.dev.git${GIT_DATE}.${GIT_SHA}"
|
||||
if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
|
||||
RPM_RELEASE="${RPM_RELEASE}.dirty"
|
||||
fi
|
||||
# A feature build of a commit is a different package from the default
|
||||
# build of the same commit, and nothing else in the version says so. The
|
||||
# suffix sorts above the unsuffixed build, so installing a feature build
|
||||
# is an upgrade and going back is a downgrade — which dnf refuses unless
|
||||
# told; use `dnf downgrade` or `rpm -U --oldpackage`.
|
||||
if [[ -n "${FEATURES}" ]]; then
|
||||
RPM_RELEASE="${RPM_RELEASE}.features.$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')"
|
||||
fi
|
||||
echo "Auto-derived dev Version-Release: ${RPM_VERSION}-${RPM_RELEASE}"
|
||||
else
|
||||
RPM_VERSION="${CRATE_VERSION}"
|
||||
RPM_RELEASE="1"
|
||||
fi
|
||||
|
||||
# Build the binaries, unless we were told they are already there.
|
||||
if [[ "${NO_BUILD}" -eq 0 ]]; then
|
||||
cargo_args=(build --release)
|
||||
[[ -n "${TARGET_TRIPLE}" ]] && cargo_args+=(--target "${TARGET_TRIPLE}")
|
||||
[[ -n "${FEATURES}" ]] && cargo_args+=(--features "${FEATURES}")
|
||||
echo "Building binaries..."
|
||||
cargo "${cargo_args[@]}"
|
||||
fi
|
||||
|
||||
if [[ -n "${BIN_DIR_OVERRIDE}" ]]; then
|
||||
BIN_DIR="$(cd "${BIN_DIR_OVERRIDE}" && pwd)"
|
||||
RPM_ARCH="$(rpm --eval '%{_target_cpu}')"
|
||||
elif [[ -n "${TARGET_TRIPLE}" ]]; then
|
||||
BIN_DIR="${PROJECT_ROOT}/target/${TARGET_TRIPLE}/release"
|
||||
# rpm names architectures its own way; map the ones we cross-build for.
|
||||
case "${TARGET_TRIPLE}" in
|
||||
x86_64-*) RPM_ARCH="x86_64" ;;
|
||||
aarch64-*) RPM_ARCH="aarch64" ;;
|
||||
armv7-*) RPM_ARCH="armv7hl" ;;
|
||||
riscv64-*) RPM_ARCH="riscv64" ;;
|
||||
*)
|
||||
echo "Unknown target triple for rpm: ${TARGET_TRIPLE}" >&2
|
||||
echo "Add it to the case in $(basename "$0")." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
BIN_DIR="${PROJECT_ROOT}/target/release"
|
||||
RPM_ARCH="$(rpm --eval '%{_target_cpu}')"
|
||||
fi
|
||||
|
||||
for binary in fips fipsctl fipstop fips-gateway; do
|
||||
if [[ ! -x "${BIN_DIR}/${binary}" ]]; then
|
||||
echo "Missing ${BIN_DIR}/${binary}." >&2
|
||||
[[ "${NO_BUILD}" -eq 1 ]] && echo "Drop --no-build, or build first." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
TOP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TOP_DIR}"' EXIT
|
||||
mkdir -p "${TOP_DIR}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
echo "Building .rpm package..."
|
||||
# The Release carries no %{dist} tag. A dist tag says which distribution's
|
||||
# build of a package this is, and there is one build: the same glibc binaries
|
||||
# the .deb and the tarball ship, packaged. Leaving it in would name whichever
|
||||
# image or host happened to run rpmbuild (.el9 from the release build, .fc44
|
||||
# from a developer's) in an artifact that installs on all of them.
|
||||
rpmbuild -bb "${SPEC}" \
|
||||
--target "${RPM_ARCH}" \
|
||||
--define "dist %{nil}" \
|
||||
--define "_topdir ${TOP_DIR}" \
|
||||
--define "_sourcedir ${PROJECT_ROOT}" \
|
||||
--define "fips_srcdir ${PROJECT_ROOT}" \
|
||||
--define "fips_bindir ${BIN_DIR}" \
|
||||
--define "fips_version ${RPM_VERSION}" \
|
||||
--define "fips_release ${RPM_RELEASE}" \
|
||||
--quiet
|
||||
|
||||
: "${DEST_DIR:=deploy}"
|
||||
mkdir -p "${DEST_DIR}"
|
||||
RPM_FILE=$(find "${TOP_DIR}/RPMS" -name '*.rpm' -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)
|
||||
|
||||
if [ -z "${RPM_FILE}" ]; then
|
||||
echo "Error: No .rpm file found under ${TOP_DIR}/RPMS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "${RPM_FILE}" "${DEST_DIR}/"
|
||||
BASENAME=$(basename "${RPM_FILE}")
|
||||
|
||||
# A container build runs as root on a mounted source tree, which would leave a
|
||||
# package its owner cannot delete without sudo. HOST_UID/HOST_GID say who asked
|
||||
# for it; unset (the ordinary case, building as yourself) changes nothing.
|
||||
if [[ -n "${HOST_UID:-}" && -n "${HOST_GID:-}" ]]; then
|
||||
chown "${HOST_UID}:${HOST_GID}" "${DEST_DIR}/${BASENAME}"
|
||||
fi
|
||||
if [[ -n "${NAME_FILE}" ]]; then
|
||||
printf '%s\n' "${BASENAME}" > "${NAME_FILE}"
|
||||
fi
|
||||
|
||||
# dnf needs a path it can tell from a package name, so a relative one gets a
|
||||
# "./" and an absolute one is already unambiguous.
|
||||
OUT_PATH="${DEST_DIR}/${BASENAME}"
|
||||
case "${OUT_PATH}" in
|
||||
/*) INSTALL_PATH="${OUT_PATH}" ;;
|
||||
*) INSTALL_PATH="./${OUT_PATH}" ;;
|
||||
esac
|
||||
|
||||
echo "Package built: ${OUT_PATH}"
|
||||
echo ""
|
||||
echo "Install with: sudo dnf install ${INSTALL_PATH}"
|
||||
echo "Remove with: sudo dnf remove fips-mesh (keeps /etc/fips and its identity keys)"
|
||||
|
||||
# The last line of stdout is the package path, and nothing may follow it: the
|
||||
# release workflow reads it to learn which package this run produced, exactly
|
||||
# as it does with build-deb-container.sh.
|
||||
echo "${OUT_PATH}"
|
||||
@@ -0,0 +1,312 @@
|
||||
# FIPS RPM packaging.
|
||||
#
|
||||
# The counterpart of packaging/debian: the same files land in the same places,
|
||||
# the same group is created, the same config is seeded, and the same two units
|
||||
# are enabled. Where the two package managers differ, the differences are
|
||||
# marked below rather than smoothed over.
|
||||
#
|
||||
# The binaries are built before rpmbuild runs, by packaging/rpm/build-rpm.sh,
|
||||
# and this spec packages them. That is how cargo-deb works on the Debian side,
|
||||
# and it keeps one cargo invocation — with its target directory, features and
|
||||
# cross-compilation flags — as the only thing that compiles FIPS. So there is
|
||||
# no %%prep and no %%build here, and `fips_bindir` says where the binaries are.
|
||||
#
|
||||
# Dependencies are not listed: rpmbuild derives them from the ELF files, down
|
||||
# to the glibc and libdbus symbol versions, which is what the Debian side gets
|
||||
# from "$auto" plus testing/check-deb-depends.sh. An RPM built on a host newer
|
||||
# than the target therefore *refuses to install* there rather than installing
|
||||
# and failing to start. rpm derives the glibc requirement from the binaries, so
|
||||
# what needs checking is the binaries themselves: testing/check-rpm-floor.sh
|
||||
# reads that requirement out of the finished package and compares it with
|
||||
# FIPS_GLIBC_FLOOR — see packaging/README.md.
|
||||
|
||||
%global fips_group fips
|
||||
%global fips_libdir %{_prefix}/lib/fips
|
||||
|
||||
# Where build-rpm.sh leaves the binaries and where the source tree is. Both are
|
||||
# passed with --define; the defaults only exist so `rpmspec -q` can parse this
|
||||
# file without them.
|
||||
%{!?fips_bindir: %global fips_bindir %{_sourcedir}/target/release}
|
||||
%{!?fips_srcdir: %global fips_srcdir %{_sourcedir}}
|
||||
|
||||
# NOT "fips". Fedora's namespace already has a package by that name -- an
|
||||
# OpenGL FITS image viewer (github.com/matwey/fips3), currently 3.4.0 -- and it
|
||||
# owns /usr/bin/fips. A package named `fips` at 0.6.0 is therefore an *older*
|
||||
# `fips` to every RPM tool there is, so a routine `dnf upgrade` replaces a
|
||||
# running mesh node with an image viewer and takes the units with it. That is
|
||||
# not hypothetical: it happened on a test machine within the hour, silently.
|
||||
#
|
||||
# The Debian side has no such collision, which is why only this name differs.
|
||||
Name: fips-mesh
|
||||
Version: %{?fips_version}%{!?fips_version:0.6.0}
|
||||
Release: %{?fips_release}%{!?fips_release:1}%{?dist}
|
||||
Summary: Free Internetworking Peering System mesh network daemon
|
||||
|
||||
License: MIT
|
||||
URL: https://github.com/jmcorgan/fips
|
||||
# The source is the working tree, not a tarball: see the header.
|
||||
Source0: %{name}-%{version}.tar.gz
|
||||
|
||||
# Shipped by systemd, needed by the scriptlets below.
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
Requires: systemd
|
||||
# The FITS viewer owns /usr/bin/fips, so the two cannot both be installed. rpm
|
||||
# would refuse on the file conflict anyway; saying so here makes the refusal
|
||||
# name the problem instead of naming a path.
|
||||
Conflicts: fips
|
||||
# groupadd, used by %%post. openSUSE calls the package `shadow`; the rich
|
||||
# dependency satisfies both without naming a distribution.
|
||||
Requires(post): (shadow-utils or shadow)
|
||||
# Bluetooth (BLE) transport at runtime; the daemon runs without it.
|
||||
Recommends: bluez
|
||||
# fips-firewall.service runs nft(8). Not required: the unit is opt-in and the
|
||||
# daemon does not need it.
|
||||
Recommends: nftables
|
||||
|
||||
%description
|
||||
FIPS is a distributed, decentralized network routing protocol for mesh nodes
|
||||
connecting over arbitrary transports including UDP, TCP, Ethernet, Tor, and
|
||||
Bluetooth (BLE). It provides encrypted peer-to-peer connectivity with automatic
|
||||
key management, TUN-based virtual networking, and .fips DNS resolution.
|
||||
|
||||
%prep
|
||||
# Nothing to unpack: the binaries and the data files come from the working tree.
|
||||
|
||||
%build
|
||||
# Nothing to build: see the header.
|
||||
|
||||
%install
|
||||
install -D -m 0755 %{fips_bindir}/fips %{buildroot}%{_bindir}/fips
|
||||
install -D -m 0755 %{fips_bindir}/fipsctl %{buildroot}%{_bindir}/fipsctl
|
||||
install -D -m 0755 %{fips_bindir}/fipstop %{buildroot}%{_bindir}/fipstop
|
||||
install -D -m 0755 %{fips_bindir}/fips-gateway %{buildroot}%{_bindir}/fips-gateway
|
||||
|
||||
install -D -m 0755 %{fips_srcdir}/packaging/common/fips-dns-setup \
|
||||
%{buildroot}%{fips_libdir}/fips-dns-setup
|
||||
install -D -m 0755 %{fips_srcdir}/packaging/common/fips-dns-teardown \
|
||||
%{buildroot}%{fips_libdir}/fips-dns-teardown
|
||||
|
||||
# The units are the Debian package's, unmodified. Both packages install the
|
||||
# binaries to %%{_bindir} and target the same systemd, so a second copy of four
|
||||
# unit files would only be a second thing to keep in step.
|
||||
install -D -m 0644 %{fips_srcdir}/packaging/debian/fips.service \
|
||||
%{buildroot}%{_unitdir}/fips.service
|
||||
install -D -m 0644 %{fips_srcdir}/packaging/debian/fips-dns.service \
|
||||
%{buildroot}%{_unitdir}/fips-dns.service
|
||||
install -D -m 0644 %{fips_srcdir}/packaging/debian/fips-firewall.service \
|
||||
%{buildroot}%{_unitdir}/fips-firewall.service
|
||||
install -D -m 0644 %{fips_srcdir}/packaging/debian/fips-gateway.service \
|
||||
%{buildroot}%{_unitdir}/fips-gateway.service
|
||||
|
||||
install -D -m 0644 %{fips_srcdir}/packaging/debian/fips.tmpfiles \
|
||||
%{buildroot}%{_tmpfilesdir}/fips.conf
|
||||
|
||||
# The example config is read by %%post, so it is not under %%{_docdir}: minimal
|
||||
# and container installs path-exclude that directory. Same reasoning as the
|
||||
# Debian package.
|
||||
install -D -m 0644 %{fips_srcdir}/packaging/common/fips.yaml \
|
||||
%{buildroot}%{_datadir}/fips/fips.yaml.example
|
||||
|
||||
install -D -m 0644 %{fips_srcdir}/packaging/common/hosts \
|
||||
%{buildroot}%{_sysconfdir}/fips/hosts
|
||||
install -D -m 0644 %{fips_srcdir}/packaging/common/fips.nft \
|
||||
%{buildroot}%{_sysconfdir}/fips/fips.nft
|
||||
|
||||
# Drop-in directory for operator nftables rules included by /etc/fips/fips.nft.
|
||||
# Empty by default; the include glob matches nothing cleanly out of the box.
|
||||
install -d -m 0755 %{buildroot}%{_sysconfdir}/fips/fips.d
|
||||
|
||||
install -D -m 0644 %{fips_srcdir}/docs/design/fips-security.md \
|
||||
%{buildroot}%{_docdir}/fips/fips-security.md
|
||||
install -D -m 0644 %{fips_srcdir}/LICENSE %{buildroot}%{_licensedir}/fips/LICENSE
|
||||
|
||||
%post
|
||||
# Control-socket access is by group membership, so the group exists before the
|
||||
# daemon can create the socket.
|
||||
getent group %{fips_group} >/dev/null || groupadd --system %{fips_group}
|
||||
|
||||
# Seed /etc/fips/fips.yaml from the shipped example only if it does not already
|
||||
# exist. The live config is deliberately not a packaged config file: this
|
||||
# copy-if-absent yields to any operator- or configuration-management-rendered
|
||||
# file and never clobbers it, and never leaves a .rpmnew beside it either.
|
||||
if [ ! -e %{_sysconfdir}/fips/fips.yaml ]; then
|
||||
install -m 0600 -o root -g root \
|
||||
%{_datadir}/fips/fips.yaml.example \
|
||||
%{_sysconfdir}/fips/fips.yaml
|
||||
fi
|
||||
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemd-tmpfiles --create %{_tmpfilesdir}/fips.conf >/dev/null 2>&1 || :
|
||||
fi
|
||||
|
||||
# Presets first: a distribution preset that disables these units is applied
|
||||
# here, though the explicit enable below then overrides it, so the presets have
|
||||
# the last word only on units this package does not name.
|
||||
%systemd_post fips.service fips-dns.service
|
||||
|
||||
# Then enable the two units the package considers its own: installing FIPS is
|
||||
# how an operator asks for a mesh node, and a node that is installed but not
|
||||
# enabled is not one. fips-firewall.service and fips-gateway.service are
|
||||
# deliberately left alone — both are opt-in, see
|
||||
# %%{_docdir}/fips/fips-security.md.
|
||||
#
|
||||
# On first install only, which is narrower than the Debian postinst: that one
|
||||
# enables on every configure, so it re-enables a unit an operator has disabled.
|
||||
# Here a later `systemctl disable fips` survives an upgrade, which is the
|
||||
# behaviour an operator who disabled it would expect.
|
||||
#
|
||||
# Enabled, not started. The Debian postinst starts units only under
|
||||
# `[ -n "$2" ]`, which holds on an upgrade and never on a fresh install, so a
|
||||
# first install there leaves the node to the next boot or to the operator.
|
||||
# Starting here would also mean fips-dns.service — Type=oneshot running
|
||||
# fips-dns-setup — rewriting the host resolver inside the install transaction,
|
||||
# against a fips.yaml seeded from the example seconds earlier. An upgrade
|
||||
# queues a restart of whatever was running, in the try-restart in %%postun
|
||||
# below.
|
||||
#
|
||||
# (A package submitted to Fedora proper would drop the enables too and let the
|
||||
# distribution's presets decide, which is the policy there. This package is
|
||||
# built upstream and installed deliberately, so it matches the .deb instead.)
|
||||
if [ $1 -eq 1 ] && [ -d /run/systemd/system ]; then
|
||||
systemctl enable fips.service >/dev/null 2>&1 || :
|
||||
systemctl enable fips-dns.service >/dev/null 2>&1 || :
|
||||
fi
|
||||
|
||||
# On upgrade, reapply the firewall ruleset in place, before the daemon is
|
||||
# restarted by %%systemd_postun_with_restart below -- %%post of the new package
|
||||
# runs ahead of %%postun of the old one, which is the ordering the Debian
|
||||
# postinst has. "try" leaves an inactive unit alone, so this never opts a host
|
||||
# in, and it must be a reload rather than a restart: that unit's ExecStop
|
||||
# deletes the table, and a restart would leave the mesh interface unfiltered
|
||||
# in between. A reload that fails leaves the previous ruleset in force, so it
|
||||
# is reported and the upgrade goes on.
|
||||
if [ $1 -ge 2 ] && [ -d /run/systemd/system ]; then
|
||||
# The unit files this upgrade installed are not loaded yet -- the reload
|
||||
# systemd runs from a file trigger comes at the end of the transaction --
|
||||
# so without this the reload below would act on the pre-upgrade unit.
|
||||
systemctl daemon-reload >/dev/null 2>&1 || :
|
||||
if ! systemctl try-reload-or-restart fips-firewall.service >/dev/null 2>&1; then
|
||||
echo "fips: reloading fips-firewall.service failed; the ruleset loaded before the upgrade stays in force" >&2
|
||||
echo "fips: check /etc/fips/fips.nft and the rules in /etc/fips/fips.d/" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
%preun
|
||||
# Stops and disables only on the last erase, not on an upgrade.
|
||||
%systemd_preun fips.service fips-dns.service fips-gateway.service fips-firewall.service
|
||||
|
||||
%postun
|
||||
# Restarts what was running, on upgrade only. fips-gateway.service is in the
|
||||
# list because a host that opted it in would otherwise keep running the old
|
||||
# binary; try-restart leaves an inactive unit alone, so listing it opts nobody
|
||||
# in. fips-firewall.service is not: it is reloaded in %%post above, because
|
||||
# restarting it would run its ExecStop and delete the table.
|
||||
#
|
||||
# Spelled out rather than left to %%systemd_postun_with_restart. That macro is
|
||||
# expanded at build time, in the image this package is built in, and the EL9
|
||||
# expansion only *marks* the units -- `systemd-update-helper
|
||||
# mark-restart-system-units` -- for a file trigger in that distribution's
|
||||
# systemd package to act on. On a distribution without that trigger the mark is
|
||||
# written and nothing ever reads it, so an upgrade silently leaves the old
|
||||
# binary running. try-restart is portable, and is what the macro would have
|
||||
# reached in the end anyway.
|
||||
#
|
||||
# Queued, not waited on. `systemctl try-restart` without --no-block returns
|
||||
# when the jobs finish, and this scriptlet runs inside the rpm transaction,
|
||||
# holding dnf's lock: fips-dns.service is Type=oneshot and fips-dns-setup waits
|
||||
# up to 30 s for fips0, so a daemon that comes back slowly -- or not at all --
|
||||
# would hold the whole upgrade there. The restart is a request; whether it
|
||||
# succeeds is the daemon's business and the journal's, not the package
|
||||
# manager's.
|
||||
#
|
||||
# This is also where the RPM deliberately parts from the Debian postinst,
|
||||
# which waits for each unit it starts with a bounded poll. That bound exists
|
||||
# because a blocking `systemctl start` under dpkg held apt, and every package
|
||||
# operation queued behind it, for ever. Queuing the restart avoids the problem
|
||||
# the bound was written to contain, rather than reimplementing the bound.
|
||||
if [ $1 -ge 1 ] && [ -d /run/systemd/system ]; then
|
||||
systemctl --no-block try-restart fips.service fips-dns.service fips-gateway.service >/dev/null 2>&1 || :
|
||||
fi
|
||||
|
||||
if [ $1 -eq 0 ]; then
|
||||
# The runtime directory is not packaged, so nothing else removes it.
|
||||
rm -rf /run/fips
|
||||
|
||||
# DNS configuration fips-dns-setup may have written outside the package,
|
||||
# one file per backend it picks between. fips-dns-teardown runs on
|
||||
# ExecStop and removes the file of the backend recorded in its state file,
|
||||
# or all four when the state file is missing; %%systemd_preun stops the unit
|
||||
# before rpm gets here, so this is what catches a host where the unit was
|
||||
# not running. Each resolver whose file is removed is told to drop it, as
|
||||
# the Debian postrm does: otherwise a host erased while fips-dns was stopped
|
||||
# keeps sending .fips queries to the daemon's resolver port until that
|
||||
# resolver next restarts. rpm has no purge, so this erase branch is the only
|
||||
# cleanup that will ever run.
|
||||
restart_resolved=0
|
||||
if [ -f %{_sysconfdir}/systemd/dns-delegate.d/fips.dns-delegate ]; then
|
||||
rm -f %{_sysconfdir}/systemd/dns-delegate.d/fips.dns-delegate
|
||||
restart_resolved=1
|
||||
fi
|
||||
if [ -f %{_sysconfdir}/systemd/resolved.conf.d/fips.conf ]; then
|
||||
rm -f %{_sysconfdir}/systemd/resolved.conf.d/fips.conf
|
||||
restart_resolved=1
|
||||
fi
|
||||
if [ "$restart_resolved" = 1 ] && [ -d /run/systemd/system ] \
|
||||
&& systemctl is-active --quiet systemd-resolved.service; then
|
||||
systemctl restart systemd-resolved \
|
||||
|| echo "fips: warning: could not restart systemd-resolved; restart it to drop the .fips route"
|
||||
fi
|
||||
if [ -f %{_sysconfdir}/dnsmasq.d/fips.conf ]; then
|
||||
rm -f %{_sysconfdir}/dnsmasq.d/fips.conf
|
||||
if [ -d /run/systemd/system ] \
|
||||
&& systemctl is-active --quiet dnsmasq.service; then
|
||||
systemctl reload dnsmasq \
|
||||
|| echo "fips: warning: could not reload dnsmasq; reload it to drop the .fips route"
|
||||
fi
|
||||
fi
|
||||
if [ -f %{_sysconfdir}/NetworkManager/dnsmasq.d/fips.conf ]; then
|
||||
rm -f %{_sysconfdir}/NetworkManager/dnsmasq.d/fips.conf
|
||||
if [ -d /run/systemd/system ] \
|
||||
&& systemctl is-active --quiet NetworkManager.service \
|
||||
&& command -v nmcli >/dev/null 2>&1; then
|
||||
nmcli general reload \
|
||||
|| echo "fips: warning: could not reload NetworkManager; reload it to drop the .fips route"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Note what is *not* here. The Debian postrm removes /etc/fips and the fips
|
||||
# group on purge — an explicit, separate operator action. rpm has no purge, so
|
||||
# the equivalent code would run on an ordinary erase and take the node's
|
||||
# identity keys with it, including during a distribution upgrade that erases
|
||||
# and reinstalls. Config and keys therefore survive an erase; remove
|
||||
# /etc/fips yourself if you mean it.
|
||||
|
||||
%files
|
||||
%dir %{_licensedir}/fips
|
||||
%license %{_licensedir}/fips/LICENSE
|
||||
%dir %{_docdir}/fips
|
||||
%doc %{_docdir}/fips/fips-security.md
|
||||
%{_bindir}/fips
|
||||
%{_bindir}/fipsctl
|
||||
%{_bindir}/fipstop
|
||||
%{_bindir}/fips-gateway
|
||||
%dir %{fips_libdir}
|
||||
%{fips_libdir}/fips-dns-setup
|
||||
%{fips_libdir}/fips-dns-teardown
|
||||
%{_unitdir}/fips.service
|
||||
%{_unitdir}/fips-dns.service
|
||||
%{_unitdir}/fips-firewall.service
|
||||
%{_unitdir}/fips-gateway.service
|
||||
%{_tmpfilesdir}/fips.conf
|
||||
%dir %{_datadir}/fips
|
||||
%{_datadir}/fips/fips.yaml.example
|
||||
%dir %{_sysconfdir}/fips
|
||||
%dir %{_sysconfdir}/fips/fips.d
|
||||
%config(noreplace) %{_sysconfdir}/fips/hosts
|
||||
%config(noreplace) %{_sysconfdir}/fips/fips.nft
|
||||
|
||||
%changelog
|
||||
* Sat Sep 19 2026 Johnathan Corgan <johnathan@corganlabs.com>
|
||||
- Packaging for RPM-based distributions, translated from the Debian recipe.
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
# Fail when an RPM records a glibc requirement above the declared floor.
|
||||
#
|
||||
# The counterpart of check-deb-depends.sh, for the other package format and for
|
||||
# a different failure. On the Debian side the package's Depends are written by
|
||||
# hand and can disagree with what the binaries need, so that check compares the
|
||||
# two. rpm derives the requirement from the ELF files and cannot disagree with
|
||||
# them -- which moves the risk one step back: the binaries themselves may have
|
||||
# been built somewhere above the floor, and the package that results installs
|
||||
# nowhere older, silently, until someone tries.
|
||||
#
|
||||
# This reads the requirement out of the finished package, which is the artifact
|
||||
# that ships and the same table dnf enforces at install time.
|
||||
#
|
||||
# Usage: check-rpm-floor.sh <package.rpm>...
|
||||
#
|
||||
# Reads the floor from packaging/build-floor.env unless FIPS_GLIBC_FLOOR is set.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
if [ -z "${FIPS_GLIBC_FLOOR:-}" ]; then
|
||||
# shellcheck source=../packaging/build-floor.env
|
||||
. "$REPO_ROOT/packaging/build-floor.env"
|
||||
fi
|
||||
FLOOR="${FIPS_GLIBC_FLOOR:?no floor declared}"
|
||||
|
||||
command -v rpm >/dev/null 2>&1 || {
|
||||
echo "check-rpm-floor: rpm is not installed; cannot check anything." >&2
|
||||
echo " Refusing to report a pass I did not establish." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ $# -gt 0 ] || {
|
||||
echo "usage: check-rpm-floor.sh <package.rpm>..." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Sorts versions the way rpm does, so 2.10 is above 2.9 rather than below it.
|
||||
version_gt() {
|
||||
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ] && [ "$1" != "$2" ]
|
||||
}
|
||||
|
||||
FAILED=0
|
||||
CHECKED=0
|
||||
|
||||
for pkg in "$@"; do
|
||||
if [ ! -f "$pkg" ]; then
|
||||
echo " ERROR $pkg does not exist" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Every libc.so.6(GLIBC_x.y) entry rpm derived from the packaged binaries.
|
||||
# The highest one is the floor the package will be held to.
|
||||
need=$(rpm -qp --requires "$pkg" 2>/dev/null \
|
||||
| grep -oE 'GLIBC_[0-9.]+' \
|
||||
| sed 's/GLIBC_//' \
|
||||
| sort -V \
|
||||
| tail -1) || true
|
||||
|
||||
if [ -z "$need" ]; then
|
||||
# No requirement at all means the package holds no dynamically linked
|
||||
# binary, which for this package means the file list moved. Not a pass.
|
||||
echo " ERROR $(basename "$pkg") records no glibc requirement" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
CHECKED=$((CHECKED + 1))
|
||||
if version_gt "$need" "$FLOOR"; then
|
||||
echo " FAIL $(basename "$pkg") requires glibc $need, above the declared floor $FLOOR" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
else
|
||||
echo " ok $(basename "$pkg") requires glibc $need"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
echo "check-rpm-floor: $FAILED check(s) failed against floor $FLOOR." >&2
|
||||
echo " The package was built from binaries compiled above the floor. Build" >&2
|
||||
echo " them in the pinned container: packaging/rpm/build-rpm-container.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CHECKED" -eq 0 ]; then
|
||||
echo "check-rpm-floor: nothing was checked; refusing to report a pass." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "=== RPM glibc floor check passed ($CHECKED package(s)) ==="
|
||||
Reference in New Issue
Block a user