This is the operator runbook for getting ZFW into the IceWhaleTech
Mod-Store so it appears in the ZimaOS web UI's Module Store with a
one-click install. The submission itself is manual because it requires
a GitHub remote on this repo and a forked PR against
IceWhaleTech/Mod-Store — both outside the daemon's scope.
The daemon side of v0.5 is complete as of v0.4.0:
- arm64 build (v0.3.7) — reproducible per-arch tarballs.
- rules.json migration helper (v0.3.8) — schema bumps are safe.
zpkgself-update check (v0.3.9) — the Versions tab can announce new releases the moment a manifest URL exists.- multi-host rule sync (v0.3.10) — a fleet of ZimaOS hosts can stay
in lockstep without manual
scp.
Once the Mod-Store entry is live, set ZFW_UPDATE_URL to its raw
manifest URL on every host so v0.3.9's update banner starts firing.
-
GitHub remote. This repo currently has no remote. Create the public repo:
gh repo create chicohaager/zfw --public --source=. --remote=origin --push
The committed CI workflow (
.github/workflows/ci.yml) goes green on the first push by construction — lint + test + reproducible build + arm64 smoke. -
Cut the release so that the tag reproduces the assets. Two inputs besides the source decide the bytes, and both must be settled before the build that produces the published assets:
SOURCE_DATE_EPOCH(build.sh takes it from the last commit's committer date) and the nearest git tag (cyclonedx-gomod stamps the SBOM's main component with it;sbom.jsontravels inside the tarball). The checksums, in turn, live inmod-store/zfw.yaml, which is part of the release commit — so the commit is created with a chosen date, tagged, built, amended with the checksums under the same date, re-tagged, and rebuilt once more as the positive control. Done this way for v1.0.25:X=$(date +%s) # the release instant # …bump VERSION, README block, cache-busters, openapi, Dockerfile, yaml version… GIT_AUTHOR_DATE=@$X GIT_COMMITTER_DATE=@$X git commit -a -m "release: vX.Y.Z …" git tag -a "v$(cat VERSION)" -m "ZFW v$(cat VERSION)" sh build.sh # epoch = $X, SBOM version = the tag # copy dist/*.tar.gz.sha256 into mod-store/zfw.yaml, then: GIT_AUTHOR_DATE=@$X GIT_COMMITTER_DATE=@$X git commit -a --amend --no-edit git tag -d "v$(cat VERSION)"; git tag -a "v$(cat VERSION)" -m "ZFW v$(cat VERSION)" rm -rf dist; sh build.sh # must reproduce every .sha256
Have
cyclonedx-gomodonPATHfor this, installed exactly as CI does (go install -trimpath …@v1.12.0): the SBOM records the tool's own binary hash, and without-trimpaththat hash carries the install host's paths. build.sh takes care of the other host-dependent input (file modes via umask) itself.The tarballs do not contain
mod-store/zfw.yaml, so the amend changes no shipped byte — the rebuild is the proof. Then push branch and tag, let CI build the tag (its artifact must carry the same checksums), and publish:VERSION=$(cat VERSION) git push origin "release/v${VERSION}" "v${VERSION}" gh release create "v${VERSION}" \ dist/zfw-${VERSION}-amd64.tar.gz \ dist/zfw-${VERSION}-amd64.tar.gz.sha256 \ dist/zfw-${VERSION}-arm64.tar.gz \ dist/zfw-${VERSION}-arm64.tar.gz.sha256 \ dist/sbom.json \ --title "ZFW v${VERSION}" \ --notes-file <(git log -1 --format=%B "v${VERSION}")
The release notes come straight from the body of the release commit — keep the commit message tight and reader-facing so
gh release createships a coherent change-summary without a second authoring pass. A release must never be re-cut from a later commit (the epoch moves, every byte moves); rebuild from the tag. -
mod-store/zfw.yamlcarries the checksums pinned in step 2. The manifest stays in this repo as source-of-truth between releases; the submitted copy inIceWhaleTech/Mod-Storeis generated from it. -
Capture screenshots. Mod-Store renders the screenshots at ~1600×1000 in the listing card. Capture five PNGs and place them in
mod-store/screenshots/:firewall-tab.pngrules-tab.pngexposure-tab.pngevents-tab.pngaudit-tab.png
Use the light theme (default since v0.2.14) and a representative rule set (the v0.2.9 starter defaults work well). Avoid leaking any host IP or hostname that identifies a real production host.
After each release tag (see step 2 above), open a PR against the Mod-Store fork:
gh repo fork IceWhaleTech/Mod-Store --clone
cd Mod-Store
git checkout -b zfw-v${VERSION}
# Copy the manifest + screenshots into the Mod-Store directory layout.
# The exact path depends on Mod-Store's category convention; at time
# of writing it is Apps/<category>/<id>/.
mkdir -p Apps/Network/zfw/screenshots
cp /path/to/zfw/mod-store/zfw.yaml Apps/Network/zfw/
cp /path/to/zfw/mod-store/screenshots/*.png Apps/Network/zfw/screenshots/
git add Apps/Network/zfw
git commit -m "Add ZFW v${VERSION} — host firewall for ZimaOS"
git push -u origin "zfw-v${VERSION}"
gh pr create \
--repo IceWhaleTech/Mod-Store \
--title "Add ZFW v${VERSION} — host firewall for ZimaOS" \
--body "$(cat <<'EOF'
ZFW is a standalone ZimaOS module that adds a host firewall — the one
thing the OS does not ship out of the box. Stock ZimaOS has every
iptables chain at ACCEPT, no nftables ruleset, and Samba / NFS /
rpcbind / the built-in VM VNC console / every Docker-published port
reachable from the whole LAN by default.
ZFW closes that gap with:
- default-drop allowlist on ZFW-IN (host services)
- blocklist on DOCKER-USER (container ports — never reach INPUT)
- IPv6 first-class (ZFW-IN6 default-drop with full bypass list)
- Safe-Apply with 120 s dead-man auto-revert
- five-tab UI (firewall / rules / exposure / events / audit)
- reproducible builds + optional CycloneDX SBOM
- JWT-authed API (ZimaOS session) + CSRF same-origin guard
External-tester sign-off baked into the release: Gelbuilding's
2026-05-24 ZimaBoard validation of v0.2.20, full install +
Safe-Apply + Confirm + reboot-persistence cycle.
Three-round internal security review on file (SECURITY-REPORT.md).
Both amd64 and arm64 artifacts shipped; SHAs in the manifest are
generated by the reproducible build pipeline.
EOF
)"Once the Mod-Store PR merges and ZFW is reachable via a stable URL,
set ZFW_UPDATE_URL to the raw manifest path so the Versions-tab
banner starts firing on every host. The manifest format is a tiny
JSON document:
{ "version": "0.5.0", "notes": "first v0.6 batch — top-sources widget" }A natural source-of-truth: a dist/latest.json file maintained on the
GitHub repo's main branch, regenerated on every release. Sample
release-cut step:
echo "{\"version\":\"${VERSION}\",\"notes\":\"$(git log -1 --pretty=%s)\"}" > dist/latest.json
git add dist/latest.json
git commit -m "release: bump latest.json to ${VERSION}"
git pushThen each ZimaOS host sets:
sudo systemctl edit zfw-ui.service
# [Service]
# Environment=ZFW_UPDATE_URL=https://raw.githubusercontent.com/chicohaager/zfw/main/dist/latest.json
sudo systemctl restart zfw-ui.service(Or set it once in the daemon's systemd unit before publishing v0.5.0 so it ships pre-wired.)
Before pushing a release tag:
-
VERSIONbumped - Cache-buster bumped (
?v=onstyles.css+app.jsinindex.html) -
docs/openapi.yamlinfo.versionbumped -
README.md"Current release" badge bumped -
mod-store/zfw.yamlversionbumped + new SHAs filled in -
go test ./...green -
Dockerfileusage comment + README "Install from Docker Hub" tag bumped -
bash build.shproduces both arches reproducibly (re-run + comparedist/*.tar.gz.sha256across runs) — from the tagged release commit, see step 2 above - Docker Hub image pushed and its payload verified against
dist/zfw-<arch>.raw.sha256per arch (README, "Building the installer image") - Browser-test the UI changes the release introduces
That list is encoded in the project's memory entry
pre-tarball-checklist for any future Claude session that picks
up the release-cut work.