mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
pfSense is FreeBSD underneath, but the FreeBSD package does not work there, failing in three silent ways. pfSense runs only /usr/local/etc/rc.d/*.sh at boot and re-runs them when WAN gets a new address, so a suffixless rc script never starts; unbound.conf is generated from config.xml with no conf.d, so a drop-in is never read; and on a firewall where the default-on "Allow IPv6" has been turned off, unbound is then generated with do-ip6: no and a responder on ::1 is unreachable. So this ships fips.sh, wires the fips. zone into the DNS Resolver through config.xml, and binds the responder on 127.0.0.1 for robustness against that last case. The rc script is plain sh: what pfSense imposes is the .sh name and that a re-run leave a running daemon alone and exit 0. It identifies the daemon by process name and recovers an orphaned daemon(8) supervisor found via fstat, since a locked empty pidfile makes daemon(8) report pid -1. The DNS setup is a manual step, never run from post-install, and validates the merged options with unbound-checkconf (pfSense's test_unbound_config) before touching config.xml, so a bad merge cannot take DNS from every client behind the firewall. The daemon runs under daemon(8) -H so newsyslog can rotate its log by signalling a reopen. Packages link statically by default: pfSense runs a FreeBSD base that cannot be obtained to link against. A firmware upgrade keeps the package (pfSense-upgrade removes only pfSense-pkg-*; confirmed on a live Plus 26.03.1 -> 26.07 upgrade, aarch64 — the package survived and the daemon restarted at boot. That is a minor, FreeBSD 16 -> 16 change; the cross-major compat case is still only source-reasoned). aarch64 is refused, where a static binary faults at posix_spawn. The mechanics the two builders share — version derivation, the stage layout, the manifest fields, the @sample scripts and pkg create — live in packaging/common/pkg-lib.sh, which both source; the FreeBSD package is byte-identical before and after that extraction. One ABI can serve more than one product: CE 2.9 and Plus 26.x on Intel are both FreeBSD:16:amd64 with a byte-identical artifact, named ...-ce2.9-plus26-amd64.pkg. The pfSense package is built and checked in its own CI job — separate from the FreeBSD package, and not a dependency of the release job, so a pfSense-only failure reds that job alone and is never a release asset. It is kept as a workflow artifact until it has been installed on a real pfSense box. CI produces the CE 2.8.1 (FreeBSD:15:amd64) package; CE 2.9, Plus 26.x Intel and ARM need a FreeBSD 16 build host the CI does not have, and ARM stays build-it-yourself because rustup ships no toolchain for it. testing/check-pfsense-pkg.sh validates a built package on any FreeBSD host and runs in that CI job: contents, modes, a positive boot-script lifecycle against a stub daemon, php -l and a fips_strip_block unit test of the config.xml helper. Installing on a real pfSense box, and the firmware-upgrade behaviour, are covered only by an aarch64 hardware run and pfSense-upgrade's source; the README records what is and is not tested. Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com>
423 lines
17 KiB
YAML
423 lines
17 KiB
YAML
name: FreeBSD Package
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
- maint
|
|
- next
|
|
tags:
|
|
- "v*"
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
|
|
jobs:
|
|
determine-versioning:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
freebsd_package_version: ${{ steps.freebsd_version.outputs.freebsd_package_version }}
|
|
freebsd_pkg_file_version: ${{ steps.freebsd_version.outputs.freebsd_pkg_file_version }}
|
|
steps:
|
|
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Derive FreeBSD package version
|
|
id: freebsd_version
|
|
shell: bash
|
|
run: |
|
|
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
|
|
|
BASE_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
|
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
|
VERSION="${GITHUB_REF_NAME#v}"
|
|
else
|
|
BRANCH=$(echo "$GITHUB_REF_NAME" | sed 's|[^A-Za-z0-9]|.|g; s/\.\{2,\}/./g; s/^\.//; s/\.$//')
|
|
HEIGHT=$(git rev-list --count HEAD)
|
|
HASH=$(git rev-parse --short HEAD)
|
|
if [[ -z "$BRANCH" ]]; then
|
|
BRANCH="ref"
|
|
fi
|
|
VERSION="${BASE_VERSION}+${BRANCH}.${HEIGHT}.${HASH}"
|
|
fi
|
|
|
|
# build-pkg.sh maps '-' and '+' to '.' (neither is allowed in a
|
|
# pkg version); derive the same mapping here so later steps can
|
|
# assert the exact artifact filename.
|
|
PKG_FILE_VERSION=$(printf '%s' "$VERSION" | tr -- '+-' '..')
|
|
|
|
echo "freebsd_package_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "freebsd_pkg_file_version=${PKG_FILE_VERSION}" >> "$GITHUB_OUTPUT"
|
|
|
|
build:
|
|
name: Build FreeBSD package (x86_64)
|
|
# No GitHub-hosted FreeBSD runners exist; build inside a KVM-accelerated
|
|
# FreeBSD VM on the Linux runner. The release must track the .pkg ABI
|
|
# major (FreeBSD:15:amd64) — pkg on other majors refuses the package.
|
|
runs-on: ubuntu-latest
|
|
needs: determine-versioning
|
|
# Successful runs of this job take 8 to 11 minutes. Five consecutive runs
|
|
# in August 2026 instead sat in the VM step for 70, 190, 360, 360 and 360
|
|
# minutes and ended cancelled, the last three at GitHub's own six-hour job
|
|
# ceiling. Nothing here bounded them. This bound is deliberately loose
|
|
# enough that a slow-but-working run still passes, and tight enough that a
|
|
# stall fails in half an hour instead of burning a runner for six.
|
|
timeout-minutes: 30
|
|
|
|
steps:
|
|
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Set SOURCE_DATE_EPOCH from git
|
|
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
|
|
|
- name: Build and smoke-install in FreeBSD VM
|
|
uses: vmactions/freebsd-vm@f0552d3b69211736abd97f02ff3d4674c56b73b1 # v1
|
|
env:
|
|
FREEBSD_PACKAGE_VERSION: ${{ needs.determine-versioning.outputs.freebsd_package_version }}
|
|
with:
|
|
release: "15.1"
|
|
usesh: true
|
|
sync: rsync
|
|
copyback: true
|
|
mem: 6144
|
|
envs: "SOURCE_DATE_EPOCH CARGO_TERM_COLOR FREEBSD_PACKAGE_VERSION"
|
|
prepare: |
|
|
pkg install -y curl
|
|
run: |
|
|
set -e
|
|
|
|
# rustup rather than the ports rust: rust-toolchain.toml pins
|
|
# the toolchain, and rustup honors the pin on first cargo use.
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
|
| sh -s -- -y --default-toolchain none --profile minimal
|
|
. "$HOME/.cargo/env"
|
|
|
|
cargo build --release
|
|
|
|
# The only place the FreeBSD cfg arms' unit tests ever run in
|
|
# CI — the main CI matrix is Linux-only, and a release build
|
|
# compiles no #[cfg(test)] code (AF-prefix strip round-trips,
|
|
# platform module, config path gates).
|
|
#
|
|
# Bounded, because libtest has no per-test deadline and this job
|
|
# runs plain `cargo test` rather than nextest, so one test that
|
|
# blocks on a syscall holds the whole binary open with nothing to
|
|
# end it. Six runs in August 2026 did exactly that. The bound is on
|
|
# the suite rather than on the job so the failure is a named step
|
|
# failure at fifteen minutes instead of the job ceiling at thirty,
|
|
# and `timeout` leaves the test binary's stdout untouched up to the
|
|
# kill: that stdout is what carries libtest's "has been running for
|
|
# over N seconds" lines, which are what name the blocked test.
|
|
#
|
|
# This bounds the damage; it does not fix anything. A test that can
|
|
# block for ever is a defect at the test, and the ones this suite
|
|
# has are bounded where they are written.
|
|
if ! timeout -s KILL 900 cargo test; then
|
|
echo "FAIL: cargo test failed, or did not finish within its 900s bound." >&2
|
|
echo " If the output above stops mid-run, look for libtest's" >&2
|
|
echo " 'has been running for over' lines: they name the test" >&2
|
|
echo " that blocked, and the tests that never reported at all" >&2
|
|
echo " are the rest of the answer." >&2
|
|
exit 1
|
|
fi
|
|
|
|
packaging/freebsd/build-pkg.sh \
|
|
--version "$FREEBSD_PACKAGE_VERSION" \
|
|
--no-build
|
|
|
|
# Smoke-install the package in the VM: files land where the
|
|
# rc.d scripts and DNS integration expect them, and the
|
|
# binaries link against this release's base libraries.
|
|
PKG=$(ls deploy/fips-*-freebsd-*.pkg)
|
|
pkg add "$PKG"
|
|
for bin in fips fipsctl fipstop; do
|
|
test -x "/usr/local/bin/$bin" || { echo "FAIL: missing /usr/local/bin/$bin"; exit 1; }
|
|
if ldd "/usr/local/bin/$bin" | grep "not found"; then
|
|
echo "FAIL: unresolved shared libraries in $bin"; exit 1
|
|
fi
|
|
done
|
|
test -x /usr/local/etc/rc.d/fips
|
|
test -x /usr/local/etc/rc.d/fips_dns
|
|
test -f /usr/local/etc/fips/fips.yaml.sample
|
|
test -f /usr/local/etc/fips/hosts.sample
|
|
# The manifest post-install script must have copied the
|
|
# samples into place (install-if-absent semantics).
|
|
test -f /usr/local/etc/fips/fips.yaml
|
|
test -f /usr/local/etc/fips/hosts
|
|
# fips.yaml may hold a node private key (nsec:); it must not
|
|
# be world-readable — Debian and macOS both install it 0600.
|
|
for f in /usr/local/etc/fips/fips.yaml /usr/local/etc/fips/fips.yaml.sample; do
|
|
mode=$(stat -f %Lp "$f")
|
|
if [ "$mode" != "600" ]; then
|
|
echo "FAIL: $f mode is $mode, expected 600"; exit 1
|
|
fi
|
|
done
|
|
# post-install must create the control-socket access group.
|
|
pw groupshow fips >/dev/null || { echo "FAIL: fips group missing"; exit 1; }
|
|
test -x /usr/local/libexec/fips/fips-dns-setup
|
|
pkg info fips
|
|
echo "==> pkg smoke-install PASSED"
|
|
|
|
# SHA-256 sidecar computed inside the VM; the host verifies the
|
|
# bytes again after the rsync copyback, so corruption across
|
|
# the VM handoff is detected before upload.
|
|
( cd deploy && sha256 -q "$(basename "$PKG")" \
|
|
| { read -r h; printf '%s %s\n' "$h" "$(basename "$PKG")"; } \
|
|
> "$(basename "$PKG").sha256" )
|
|
|
|
# The whole workspace is rsynced back to the host; drop the
|
|
# build tree so the copyback moves megabytes, not gigabytes.
|
|
rm -rf target
|
|
|
|
- name: Resolve FreeBSD asset path
|
|
id: freebsd-assets
|
|
shell: bash
|
|
run: |
|
|
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
|
set -euo pipefail
|
|
|
|
# build-pkg.sh names the package from the derived version and the
|
|
# pkg ABI arch; assert the exact name so a naming regression fails
|
|
# here instead of colliding on the release page.
|
|
EXPECTED="deploy/fips-${{ needs.determine-versioning.outputs.freebsd_pkg_file_version }}-freebsd-amd64.pkg"
|
|
if [[ ! -f "$EXPECTED" ]]; then
|
|
echo "Expected package $EXPECTED was not produced" >&2
|
|
echo "deploy/ contains:" >&2
|
|
ls -la deploy >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
echo "pkg=$EXPECTED" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Verify .pkg integrity across the VM handoff
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
PKG="${{ steps.freebsd-assets.outputs.pkg }}"
|
|
sidecar="${PKG}.sha256"
|
|
if [[ ! -f "$sidecar" ]]; then
|
|
echo "FAIL: missing SHA-256 sidecar for $(basename "$PKG")" >&2
|
|
exit 1
|
|
fi
|
|
expected=$(awk '{print $1}' "$sidecar")
|
|
actual=$(sha256sum "$PKG" | awk '{print $1}')
|
|
if [[ "$expected" != "$actual" ]]; then
|
|
echo "FAIL: $(basename "$PKG") SHA-256 mismatch across the VM copyback" >&2
|
|
echo " expected (FreeBSD VM): $expected" >&2
|
|
echo " actual (host): $actual" >&2
|
|
exit 1
|
|
fi
|
|
echo "PASS: $(basename "$PKG") matches the in-VM SHA-256 ($actual)"
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: fips_${{ needs.determine-versioning.outputs.freebsd_package_version }}_x86_64_freebsd
|
|
path: |
|
|
${{ steps.freebsd-assets.outputs.pkg }}
|
|
${{ steps.freebsd-assets.outputs.pkg }}.sha256
|
|
retention-days: 30
|
|
|
|
- name: Build summary
|
|
run: |
|
|
echo "Build Summary for freebsd/x86_64:"
|
|
echo " Package: ${{ steps.freebsd-assets.outputs.pkg }}"
|
|
|
|
pfsense:
|
|
name: Build and check the pfSense package (x86_64)
|
|
# A job of its own, and deliberately NOT a dependency of `release`. A
|
|
# pfSense-only failure — a new key in the common dns: block, a
|
|
# dependency that stops linking statically, a checker regression — reds
|
|
# this check and nothing else; it can never hide behind the FreeBSD
|
|
# job's result, and it cannot block the FreeBSD release asset. The
|
|
# pfSense package is therefore never a release asset. It is published
|
|
# here as a workflow artifact (30-day retention) for anyone to test,
|
|
# until someone has installed it on a real pfSense box; see
|
|
# packaging/pfsense/README.md.
|
|
#
|
|
# This VM is FreeBSD 15.1, so the package it produces is FreeBSD:15:amd64
|
|
# (pfSense CE 2.8.1). CE 2.9 and Plus 26.x are FreeBSD 16 and need a
|
|
# FreeBSD 16 host this workflow does not have. No aarch64 package is built
|
|
# here or published anywhere: rustup ships no toolchain for
|
|
# aarch64-unknown-freebsd, so such a build cannot honour the
|
|
# rust-toolchain.toml pin every published artifact is built with. ARM is
|
|
# build-it-yourself, per the README.
|
|
runs-on: ubuntu-latest
|
|
needs: determine-versioning
|
|
# Its own full release build in an emulated FreeBSD VM, like the build
|
|
# job; the same generous bound applies.
|
|
timeout-minutes: 45
|
|
|
|
steps:
|
|
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Set SOURCE_DATE_EPOCH from git
|
|
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
|
|
|
- name: Build and check the pfSense package in a FreeBSD VM
|
|
uses: vmactions/freebsd-vm@f0552d3b69211736abd97f02ff3d4674c56b73b1 # v1
|
|
env:
|
|
FREEBSD_PACKAGE_VERSION: ${{ needs.determine-versioning.outputs.freebsd_package_version }}
|
|
with:
|
|
release: "15.1"
|
|
usesh: true
|
|
sync: rsync
|
|
copyback: true
|
|
mem: 6144
|
|
envs: "SOURCE_DATE_EPOCH CARGO_TERM_COLOR FREEBSD_PACKAGE_VERSION"
|
|
prepare: |
|
|
# curl for rustup; bash and php for testing/check-pfsense-pkg.sh
|
|
# (the checker is bash, and it runs php -l plus a fips_strip_block
|
|
# unit test on the config.xml helper).
|
|
pkg install -y curl bash php85
|
|
run: |
|
|
set -e
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none --profile minimal
|
|
. "$HOME/.cargo/env"
|
|
|
|
# Builds the release binaries (static by default) and packages
|
|
# them; on a FreeBSD 15.1 VM this yields the FreeBSD:15:amd64
|
|
# package for pfSense CE 2.8.1.
|
|
packaging/pfsense/build-pkg.sh --version "$FREEBSD_PACKAGE_VERSION"
|
|
|
|
PKG=$(ls deploy/fips-*-pfsense-*.pkg)
|
|
testing/check-pfsense-pkg.sh "$PKG"
|
|
php -l packaging/pfsense/fips-unbound-custom.php
|
|
|
|
( cd deploy && sha256 -q "$(basename "$PKG")" \
|
|
| { read -r h; printf '%s %s\n' "$h" "$(basename "$PKG")"; } \
|
|
> "$(basename "$PKG").sha256" )
|
|
|
|
rm -rf target
|
|
|
|
- name: Resolve pfSense asset path
|
|
id: pfsense-asset
|
|
shell: bash
|
|
run: |
|
|
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
|
set -euo pipefail
|
|
VER="${{ needs.determine-versioning.outputs.freebsd_pkg_file_version }}"
|
|
PKG=$(ls deploy/fips-${VER}-pfsense-*.pkg)
|
|
if [[ ! -f "$PKG" ]]; then
|
|
echo "No pfSense package was produced" >&2
|
|
ls -la deploy >&2 || true
|
|
exit 1
|
|
fi
|
|
echo "pkg=$PKG" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Upload pfSense artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: fips_${{ needs.determine-versioning.outputs.freebsd_package_version }}_x86_64_pfsense
|
|
path: |
|
|
${{ steps.pfsense-asset.outputs.pkg }}
|
|
${{ steps.pfsense-asset.outputs.pkg }}.sha256
|
|
retention-days: 30
|
|
|
|
release:
|
|
name: Publish FreeBSD assets to GitHub Release
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
if: startsWith(github.ref, 'refs/tags/')
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- name: Download FreeBSD artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
with:
|
|
# Only the FreeBSD artifact. Without a pattern this action downloads
|
|
# every artifact in the run — including the pfSense package from the
|
|
# `pfsense` job, which is a workflow artifact by design and must never
|
|
# reach a release. `needs` only orders jobs; it does not scope this.
|
|
pattern: fips_*_x86_64_freebsd
|
|
path: dist
|
|
merge-multiple: true
|
|
|
|
- name: Validate .pkg bytes before publishing
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
cd dist
|
|
|
|
# The pfSense package is never a release asset (see the `pfsense`
|
|
# job). The download step is scoped to the FreeBSD artifact; this
|
|
# keeps the rule if that artifact is ever renamed or a new one added.
|
|
if compgen -G '*-pfsense-*' >/dev/null; then
|
|
echo "FAIL: a pfSense package reached the release stage; it must stay a workflow artifact:" >&2
|
|
ls -la ./*-pfsense-* >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
pkgs=$(find . -maxdepth 1 -type f -name '*.pkg' | LC_ALL=C sort)
|
|
if [[ -z "$pkgs" ]]; then
|
|
echo "FAIL: no .pkg artifacts were downloaded" >&2
|
|
exit 1
|
|
fi
|
|
|
|
fail=0
|
|
while IFS= read -r pkg; do
|
|
base=$(basename "$pkg")
|
|
sidecar="${pkg}.sha256"
|
|
if [[ ! -f "$sidecar" ]]; then
|
|
echo "FAIL: missing SHA-256 sidecar for $base" >&2
|
|
fail=1
|
|
continue
|
|
fi
|
|
expected=$(awk '{print $1}' "$sidecar")
|
|
actual=$(sha256sum "$pkg" | awk '{print $1}')
|
|
if [[ "$expected" != "$actual" ]]; then
|
|
echo "FAIL: $base SHA-256 mismatch on the bytes about to be published" >&2
|
|
echo " expected (FreeBSD VM): $expected" >&2
|
|
echo " actual (downloaded): $actual" >&2
|
|
fail=1
|
|
continue
|
|
fi
|
|
echo "PASS: $base matches the in-VM SHA-256 ($actual)"
|
|
done <<<"$pkgs"
|
|
|
|
if [[ "$fail" -ne 0 ]]; then
|
|
echo "==> pre-publish .pkg verification FAILED; not publishing" >&2
|
|
exit 1
|
|
fi
|
|
echo "==> pre-publish .pkg verification PASSED"
|
|
|
|
- name: Generate FreeBSD release checksums
|
|
run: |
|
|
cd dist
|
|
find . -maxdepth 1 -type f -name '*.pkg' -printf '%P\n' \
|
|
| LC_ALL=C sort \
|
|
| xargs sha256sum \
|
|
> checksums-freebsd.txt
|
|
|
|
- name: Wait for tag release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
for attempt in $(seq 1 20); do
|
|
if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
|
|
exit 0
|
|
fi
|
|
echo "Release ${GITHUB_REF_NAME} not available yet; waiting..."
|
|
sleep 15
|
|
done
|
|
|
|
echo "Timed out waiting for release ${GITHUB_REF_NAME}" >&2
|
|
exit 1
|
|
|
|
- name: Upload FreeBSD assets
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
gh release upload "${GITHUB_REF_NAME}" \
|
|
dist/*.pkg \
|
|
dist/checksums-freebsd.txt \
|
|
--clobber \
|
|
--repo "${GITHUB_REPOSITORY}"
|