-
-
Notifications
You must be signed in to change notification settings - Fork 27
Migration v4.02.8 to v4.03.2
Status: Version-specific Documentation baseline: v4.03.2 Published at: 2026-08-25T14:15:49Z
This runbook covers the one-time migration of an existing Debian 13 host from the historical public SysWarden v4.02.8 package to v4.03.2. The historical v4.02.8 binary predates the signed updater protocol. The first hop to v4.03.2 must use a separately downloaded and checksum-verified Linux package. This procedure also requires provenance verification of the selected DEB.
This procedure does not authorize a production change by itself. Complete it first on a restored snapshot or disposable clone that matches the target. Keep provider console access and a second verified SSH session throughout the live change.
Keep verified local console or SSH recovery access throughout the migration.
Important
TARGET VALIDATION REQUIRED: No command in this page is claimed to have run successfully on the intended VPS until its output is captured in the migration evidence. Replace no placeholder with an inferred value.
The supported source and destination for this page are exact:
| Property | Required value |
|---|---|
| Source operating system | Debian 13 |
| Source package |
syswarden version 4.02.8
|
| Destination package |
syswarden version 4.03.2
|
| Destination architecture | amd64 |
| Service manager | Active systemd on the live VPS |
| Recovery method | Provider or hypervisor snapshot, with console access |
The v4.03.2 package migration is not a generic host rollback facility.
syswarden uninstall is destructive removal and must never be used as an
upgrade rollback. Restoring the preferred pre-upgrade snapshot is a separate,
whole-host recovery action. It can revert unrelated workload data and therefore
requires an approved consistency and reconciliation plan.
SysWarden is a host firewall orchestrator and out-of-band security-log analysis toolkit. WAAP reads logs after an application writes them. It is not an inline HTTP proxy or a replacement for an inline WAF.
Stop before installing the candidate if any condition below is true:
- Provider console access is unavailable, or a second independent SSH session cannot be kept open.
- A complete pre-upgrade snapshot has not been created and its restoration has not been tested on a clone. Stop as well when the snapshot includes mutable application data without an approved quiescence, external durability, replay or reconciliation plan for the possible whole-host restore. The protected file backup created in section 4 must also have a verified encrypted off-host copy because it is created after the snapshot.
-
/etc/os-releasedoes not identify Debian version 13. -
dpkg-querydoes not reportsyswardenasinstall ok installed, version4.02.8, ordpkg --print-architectureis not exactlyamd64. -
dpkg --auditreports an unfinished transaction before the change. - An
apt,apt-get,dpkgor unattended-upgrade process is already active. Never delete package-manager lock files. - The public v4.03.2 Release is missing, draft, prerelease, or has an asset inventory different from the exact ten-file inventory in this page.
- The remote
v4.03.2tag, provenance attestation, approved SHA-256 value, release checksum record and local package digest do not all agree. - DEB package name, version, architecture, dependencies or control-script inventory differs from this page.
- A required path is a symlink, has unexpected ownership, or represents a custom installation that cannot be attributed to the historical package.
-
/etc/syswarden/config/.migration-in-progressexists before the planned dry run. It represents an earlier incomplete migration and must be handled as recovery, not as a fresh upgrade. - Either
/opt/syswarden/syswarden-auto.conf.migration_backupor/opt/syswarden/syswarden-auto.conf.migration_backup.migratedexists. These are interrupted-transaction states and must be handled as recovery, not as a fresh upgrade. - Both a legacy source that still needs migration and
/opt/syswarden/syswarden-auto.conf.bakexist. The package correctly refuses to overwrite the existing archive. - The candidate migration dry run fails, reports an unsupported key, or produces a firewall, WireGuard, SSH, HA or port choice that has not been reviewed.
- WireGuard is enabled while the migrated firewall backend is not
nftables. - Firewall state is ambiguous, an iptables service is active or enabled, or the selected backend is incompatible with the active frontend.
- The effective SSH port and current administrative source address have not been captured from the live SSH transport. Stop as well unless that source is present in the persistent absolute whitelist and, for a SysWarden-owned nftables policy, in the effective kernel set before the snapshot is taken.
- The package simulation proposes a removal, downgrade, architecture change or unrelated package change.
Do not work around a fail-closed error by deleting a marker, moving a backup,
replacing a symlink, disabling validation, or adding || true.
Important
TARGET VALIDATION REQUIRED: Run these observations on the target before the snapshot. Store their output in a root-only evidence location. Do not paste configuration contents, HA tokens, webhook URLs, SSH keys or other secrets into an issue or public log.
Record the platform and installed package:
set -euo pipefail
date --iso-8601=seconds
uname -a
cat /etc/os-release
dpkg --print-architecture
dpkg-query --show --showformat='${Status}\n${Version}\n${Architecture}\n' syswarden
DPKG_AUDIT="$(sudo dpkg --audit)"
if [[ -n "${DPKG_AUDIT}" ]]; then
printf '%s\n' "${DPKG_AUDIT}" >&2
printf '%s\n' 'dpkg reports unfinished or inconsistent package state.' >&2
exit 1
fi
sudo /opt/syswarden/bin/syswarden-cliThe required package output is install ok installed, 4.02.8, and amd64.
The output of dpkg --print-architecture must also be exactly amd64.
Record service, listener, firewall and SSH state without changing it:
observe_status() {
printf '\nObservation:'
printf ' %q' "$@"
printf '\n'
if "$@"; then
OBSERVATION_STATUS=0
else
OBSERVATION_STATUS=$?
fi
printf 'Observation exit status: %d\n' "${OBSERVATION_STATUS}"
}
observe_status sudo systemctl show \
syswarden-core.service syswarden-firewall.service \
--property=Id,LoadState,ActiveState,SubState,UnitFileState,FragmentPath,DropInPaths
observe_status sudo systemctl show ssh.service \
--property=Id,LoadState,ActiveState,SubState,UnitFileState,FragmentPath,DropInPaths
observe_status sudo systemctl show \
nftables.service firewalld.service ufw.service iptables.service \
ip6tables.service netfilter-persistent.service \
--property=Id,LoadState,ActiveState,SubState,UnitFileState,FragmentPath,DropInPaths
sudo ss -lntupH
sudo nft --handle list ruleset
sudo sshd -t
sudo sshd -TCapture the live administrative transport before opening a login root shell.
The four SSH_CONNECTION fields are the client address and port followed by
the server address and port. Do not infer these values from DNS or shell
history:
set -euo pipefail
export LC_ALL=C
test -n "${SSH_CONNECTION:-}"
read -r ADMIN_SOURCE ADMIN_SOURCE_PORT SSH_DESTINATION SSH_SESSION_PORT SSH_EXTRA \
<<< "${SSH_CONNECTION}"
test -n "${ADMIN_SOURCE}" && test -n "${SSH_DESTINATION}"
test -z "${SSH_EXTRA:-}"
case "${ADMIN_SOURCE_PORT}" in ''|*[!0-9]*) exit 1 ;; esac
case "${SSH_SESSION_PORT}" in ''|*[!0-9]*) exit 1 ;; esac
test "${ADMIN_SOURCE_PORT}" -ge 1 && test "${ADMIN_SOURCE_PORT}" -le 65535
test "${SSH_SESSION_PORT}" -ge 1 && test "${SSH_SESSION_PORT}" -le 65535
SSHD_EFFECTIVE="$(sudo sshd -T)"
mapfile -t EFFECTIVE_SSH_PORTS < <(
awk '$1 == "port" { print $2 }' <<< "${SSHD_EFFECTIVE}"
)
if (( ${#EFFECTIVE_SSH_PORTS[@]} != 1 )) ||
[[ "${EFFECTIVE_SSH_PORTS[0]:-}" != "${SSH_SESSION_PORT}" ]]; then
printf '%s\n' \
'The server must expose exactly one effective SSH port matching the live session.' >&2
exit 1
fi
sudo ss -ltnH "sport = :${SSH_SESSION_PORT}" | grep -q .
printf 'Administrative source: %s\nEffective SSH port: %s\n' \
"${ADMIN_SOURCE}" "${SSH_SESSION_PORT}"Keep the address in protected change evidence. It can identify an operator or network and must not be copied into a public issue.
An inactive or absent frontend command can return nonzero. Record each result
individually. Accept a nonzero result only when the output explicitly identifies
an inactive, disabled or absent unit. A privilege, D-Bus or systemctl
execution error is a stop condition. Do not join these observations with a
fallback that hides the first result.
Record file identities and hashes without printing secrets:
for path in \
/opt/syswarden \
/opt/syswarden/syswarden-auto.conf \
/etc/syswarden \
/var/lib/syswarden \
/etc/systemd/system/syswarden-core.service \
/etc/systemd/system/syswarden-firewall.service; do
if sudo test -e "${path}" || sudo test -L "${path}"; then
sudo stat --format='%n %F %U:%G %a %d:%i %s' "${path}"
else
printf 'ABSENT: %s\n' "${path}"
fi
done
sudo sha256sum \
/opt/syswarden/bin/syswarden-cli \
/opt/syswarden/bin/syswarden-core \
/opt/syswarden/bin/syswarden-tui
sudo find /etc/syswarden -xdev -type f -exec sha256sum -- '{}' +
CRONTAB_HASH_FILE="$(mktemp /tmp/syswarden-root-crontab-hash.XXXXXXXX)"
chmod 0600 "${CRONTAB_HASH_FILE}"
if sudo crontab -l | sha256sum > "${CRONTAB_HASH_FILE}"; then
cat "${CRONTAB_HASH_FILE}"
else
CRONTAB_PIPE_STATUS=("${PIPESTATUS[@]}")
if (( CRONTAB_PIPE_STATUS[0] == 1 && CRONTAB_PIPE_STATUS[1] == 0 )); then
printf '%s\n' 'ABSENT: no root crontab is installed.'
else
printf '%s\n' 'Failed to hash the root crontab.' >&2
rm -f -- "${CRONTAB_HASH_FILE}"
exit 1
fi
fi
rm -f -- "${CRONTAB_HASH_FILE}"If an optional path is absent, record that fact. If a required directory or legacy configuration path is a symlink, stop.
Also capture, in protected evidence:
-
journalctl --bootoutput forsyswarden-core,syswarden-firewall,ssh,rsyslogand the active firewall frontend; - current CPU and memory observations for both SysWarden units;
- the current root crontab and
/etc/cron.d/syswarden, if present; - hashes and metadata for HA certificates, WireGuard configuration, persistent lists and rsyslog bridge files;
- the identity of any process using TCP 62027. SysWarden v4.03.2 must not own a listener or generated firewall permission on that port.
Before taking the snapshot, make the current administrative source durable in the historical v4.02.8 absolute whitelist. Exercise this exact mutation on the restored clone first. Run it from the live SSH session, not from a detached root shell:
set -euo pipefail
export LC_ALL=C
test -n "${SSH_CONNECTION:-}"
read -r ADMIN_SOURCE ADMIN_SOURCE_PORT SSH_DESTINATION SSH_SESSION_PORT SSH_EXTRA \
<<< "${SSH_CONNECTION}"
test -n "${ADMIN_SOURCE}" && test -z "${SSH_EXTRA:-}"
case "${SSH_SESSION_PORT}" in ''|*[!0-9]*) exit 1 ;; esac
case "${ADMIN_SOURCE}" in
*:*)
ADMIN_LIST=/etc/syswarden/lists/syswarden_whitelist.ipv6
ADMIN_BLOCKLIST=/etc/syswarden/lists/syswarden_blacklist.ipv6
ADMIN_SET=syswarden_whitelist6
;;
*)
ADMIN_LIST=/etc/syswarden/lists/syswarden_whitelist.ipv4
ADMIN_BLOCKLIST=/etc/syswarden/lists/syswarden_blacklist.ipv4
ADMIN_SET=syswarden_whitelist
;;
esac
assert_safe_legacy_directory() {
local path="$1" identity uid gid mode links
if sudo test -L "${path}"; then
printf 'Refusing symlink directory: %s\n' "${path}" >&2
return 1
fi
identity="$(sudo stat --format='%u|%g|%a|%h' -- "${path}")"
IFS='|' read -r uid gid mode links <<< "${identity}"
if ! sudo test -d "${path}" || [[ "${uid}" != 0 || "${gid}" != 0 ]] ||
(( (8#${mode} & 8#022) != 0 )); then
printf 'Unsafe legacy directory identity: %s %s\n' \
"${path}" "${identity}" >&2
return 1
fi
}
assert_safe_legacy_file() {
local path="$1" identity uid gid mode links
if sudo test -L "${path}"; then
printf 'Refusing symlink file: %s\n' "${path}" >&2
return 1
fi
if ! sudo test -e "${path}"; then
return 0
fi
identity="$(sudo stat --format='%u|%g|%a|%h' -- "${path}")"
IFS='|' read -r uid gid mode links <<< "${identity}"
if ! sudo test -f "${path}" || [[ "${uid}" != 0 || "${gid}" != 0 ||
"${links}" != 1 ]] || (( (8#${mode} & 8#022) != 0 )); then
printf 'Unsafe legacy file identity: %s %s\n' \
"${path}" "${identity}" >&2
return 1
fi
}
assert_safe_legacy_directory /etc/syswarden
assert_safe_legacy_directory /etc/syswarden/lists
sudo find -P /etc/syswarden/lists -xdev -mindepth 1 -maxdepth 1 -print0 |
while IFS= read -r -d '' LEGACY_LIST_PATH; do
assert_safe_legacy_file "${LEGACY_LIST_PATH}"
done
assert_safe_legacy_file "${ADMIN_LIST}"
assert_safe_legacy_file "${ADMIN_BLOCKLIST}"
assert_safe_legacy_file /etc/syswarden/syswarden.nft
if sudo nft list table inet syswarden >/dev/null 2>&1 &&
sudo nft list table netdev syswarden_hw_drop >/dev/null 2>&1; then
if ! sudo grep -Fxq -- "${ADMIN_SOURCE}" "${ADMIN_LIST}"; then
sudo /opt/syswarden/bin/syswarden-cli whitelist "${ADMIN_SOURCE}"
fi
sudo grep -Fx -- "${ADMIN_SOURCE}" "${ADMIN_LIST}"
sudo nft get element inet syswarden "${ADMIN_SET}" \
\{ "${ADMIN_SOURCE}" \}
sudo nft get element netdev syswarden_hw_drop "${ADMIN_SET}" \
\{ "${ADMIN_SOURCE}" \}
else
printf '%s\n' \
'Both SysWarden nftables tables are required for this helper path.' >&2
printf '%s\n' \
'Stop here. Use the recovery console to prove an exact source-and-port rule in the retained operator-managed frontend before following its separately approved path.' >&2
exit 1
fiThe safe path, persistent registry check and successful element lookups in both
inet syswarden and netdev syswarden_hw_drop are mandatory. If either table
is absent or either lookup fails, the block exits nonzero. For a retained
operator-managed frontend, do not run the historical helper. Attach exact
native source-and-port rule evidence from the recovery console and obtain
separate approval for that frontend-specific path.
Do not continue merely because the original established connection still
works: an established flow does not prove that a new connection will survive
policy replacement.
Create a provider or hypervisor snapshot only after this exception is proven, while the recovery console remains available. Record its immutable identifier, creation time and restoration test. The restoration test must boot a clone and prove SSH, package, service and firewall state before the production change begins.
Record the snapshot scope and application recovery point. Prefer a system-volume snapshot when mutable application data is stored and protected separately. If the snapshot includes mutable workload data, quiesce its writers or use the provider-approved application-consistent mechanism, and retain the external backup, transaction log or replay plan required to reconcile changes made after the snapshot. Do not use whole-host restoration as the rollback plan when that boundary cannot be proved.
Back up /etc/syswarden, required list files and HA trust material. The
protected archive below also captures the remaining attributed SysWarden state
needed for forensic comparison.
Create a separate root-only file backup for forensic comparison. This does not replace the snapshot:
set -euo pipefail
BACKUP_ROOT="$(sudo mktemp -d /root/syswarden-v4028-backup.XXXXXXXX)"
sudo chmod 0700 "${BACKUP_ROOT}"
for path in \
/etc/syswarden \
/opt/syswarden \
/var/lib/syswarden \
/var/log/syswarden \
/etc/systemd/system/syswarden-core.service \
/etc/systemd/system/syswarden-firewall.service \
/etc/systemd/system/multi-user.target.wants/syswarden-core.service \
/etc/systemd/system/multi-user.target.wants/syswarden-firewall.service \
/etc/rsyslog.d/99-syswarden-waf-bridge.conf \
/etc/cron.d/syswarden \
/etc/ssh \
/etc/wireguard; do
if sudo test -e "${path}" || sudo test -L "${path}"; then
printf '%s\0' "${path#/}"
fi
done | sudo tee "${BACKUP_ROOT}/paths.nul" >/dev/null
sudo tar --acls --xattrs --numeric-owner \
--directory=/ --null --files-from="${BACKUP_ROOT}/paths.nul" \
--create --file="${BACKUP_ROOT}/rootfs.tar"
sudo chmod 0600 "${BACKUP_ROOT}/paths.nul" "${BACKUP_ROOT}/rootfs.tar"
sudo nft --handle list ruleset | sudo tee "${BACKUP_ROOT}/nftables-before.txt" >/dev/null
sudo sshd -T | sudo tee "${BACKUP_ROOT}/sshd-effective-before.txt" >/dev/null
if sudo crontab -l | sudo tee "${BACKUP_ROOT}/root-crontab-before.txt" >/dev/null; then
:
else
CRONTAB_PIPE_STATUS=("${PIPESTATUS[@]}")
if (( CRONTAB_PIPE_STATUS[0] != 1 || CRONTAB_PIPE_STATUS[1] != 0 )); then
printf '%s\n' 'Failed to capture the root crontab.' >&2
exit 1
fi
printf '%s\n' '# ABSENT: no root crontab was installed before migration.' | \
sudo tee "${BACKUP_ROOT}/root-crontab-before.txt" >/dev/null
fi
sudo chmod 0600 \
"${BACKUP_ROOT}/nftables-before.txt" \
"${BACKUP_ROOT}/sshd-effective-before.txt" \
"${BACKUP_ROOT}/root-crontab-before.txt"
ROOTFS_SHA256="$(sudo sha256sum "${BACKUP_ROOT}/rootfs.tar" | awk '{print $1}')"
test "$(printf '%s' "${ROOTFS_SHA256}" | grep -Ec '^[0-9a-f]{64}$')" -eq 1
printf '%s rootfs.tar\n' "${ROOTFS_SHA256}" | \
sudo tee "${BACKUP_ROOT}/SHA256SUMS" >/dev/null
sudo chmod 0600 "${BACKUP_ROOT}/SHA256SUMS"
printf 'Protected backup: %s\n' "${BACKUP_ROOT}"The backup can contain SSH private keys, HA tokens, WireGuard keys and webhook
credentials. Keep it root-only and never attach it to a public ticket. Before
installation, copy the complete backup directory through an approved encrypted
channel to storage outside the target and outside the rollback snapshot. In the
copied directory, run sha256sum --check --strict SHA256SUMS; the manifest uses
the relative name rootfs.tar so this verifies the off-host copy rather than
the original target path. Record the protected storage identity and prove it
can be retrieved. Stop if this verified off-host copy is not available.
Perform this section on a trusted control workstation before transferring the
package to the target. GitHub CLI, Git, jq and sha256sum are required.
set -euo pipefail
REPOSITORY=duggytuxy/syswarden
TAG=v4.03.2
TAG_ROWS="$(git ls-remote --exit-code \
"https://github.com/${REPOSITORY}.git" \
"refs/tags/${TAG}" "refs/tags/${TAG}^{}")"
printf '%s\n' "${TAG_ROWS}"
DIRECT_SHA="$(printf '%s\n' "${TAG_ROWS}" | \
awk -v ref="refs/tags/${TAG}" '$2 == ref {print $1}')"
PEELED_SHA="$(printf '%s\n' "${TAG_ROWS}" | \
awk -v ref="refs/tags/${TAG}^{}" '$2 == ref {print $1}')"
test "$(printf '%s\n' "${DIRECT_SHA}" | grep -Ec '^[0-9a-f]{40}$')" -eq 1
if test -n "${PEELED_SHA}"; then
test "$(printf '%s\n' "${PEELED_SHA}" | grep -Ec '^[0-9a-f]{40}$')" -eq 1
fi
RELEASE_SHA="${PEELED_SHA:-${DIRECT_SHA}}"
printf 'Resolved release commit: %s\n' "${RELEASE_SHA}"Record RELEASE_SHA in the approved change evidence. Do not accept a shortened
or mixed-case commit ID.
The v4.03.2 Release is published. Bind the annotated signed tag object to the exact qualified commit before accepting any package:
set -euo pipefail
umask 077
: "${REPOSITORY:?Run section 5.1 in this shell first}"
: "${TAG:?Run section 5.1 in this shell first}"
: "${DIRECT_SHA:?Run section 5.1 in this shell first}"
: "${PEELED_SHA:?Run section 5.1 in this shell first}"
: "${RELEASE_SHA:?Run section 5.1 in this shell first}"
EXPECTED_TAG_OBJECT_SHA=f29eea8b05d5a82b851d4ea864f997ae906ba628
EXPECTED_RELEASE_SHA=2eae757bbdee510fdd1058ba7770f2c5564ecb23
test "${DIRECT_SHA}" = "${EXPECTED_TAG_OBJECT_SHA}"
test "${PEELED_SHA}" = "${EXPECTED_RELEASE_SHA}"
test "${RELEASE_SHA}" = "${EXPECTED_RELEASE_SHA}"
SIGNED_TAG_DIR="$(mktemp -d /tmp/syswarden-v4032-tag.XXXXXXXX)"
chmod 0700 "${SIGNED_TAG_DIR}"
TAG_OBJECT_JSON="${SIGNED_TAG_DIR}/tag-object.json"
gh api "repos/${REPOSITORY}/git/tags/${DIRECT_SHA}" > "${TAG_OBJECT_JSON}"
jq -e \
--arg tag "${TAG}" \
--arg tag_object "${EXPECTED_TAG_OBJECT_SHA}" \
--arg release "${EXPECTED_RELEASE_SHA}" '
(.sha == $tag_object) and
(.tag == $tag) and
(.object.type == "commit") and
(.object.sha == $release) and
(.verification.verified == true)
' "${TAG_OBJECT_JSON}" >/dev/nullThese object identifiers are immutable qualification inputs. A different tag object or commit requires a separately versioned and qualified release.
set -euo pipefail
umask 077
RELEASE_DIR="$(mktemp -d /tmp/syswarden-v4032-release.XXXXXXXX)"
chmod 0700 "${RELEASE_DIR}"
gh release view v4.03.2 --repo duggytuxy/syswarden \
--json tagName,isDraft,isPrerelease,targetCommitish,databaseId,assets \
> "${RELEASE_DIR}/release.json"
jq -e '
.tagName == "v4.03.2" and
.isDraft == false and
.isPrerelease == false and
(.databaseId | type == "number") and
.databaseId > 0
' \
"${RELEASE_DIR}/release.json" >/dev/null
jq -r '"Public Release database ID: \(.databaseId)"' \
"${RELEASE_DIR}/release.json"
jq -r '.assets[].name' "${RELEASE_DIR}/release.json" | LC_ALL=C sort \
> "${RELEASE_DIR}/actual-assets.txt"
printf '%s\n' \
RELEASE_SHA256SUMS.txt \
SHA256SUMS.txt \
plumber-report.zip \
syswarden-4.03.2-1.x86_64.rpm \
syswarden-release.tar.gz \
syswarden-sbom.spdx.json \
syswarden-update-manifest-v1.json \
syswarden-update-manifest-v1.json.sig \
syswarden_4.03.2_amd64.deb \
syswarden_4.03.2_x86_64.apk \
> "${RELEASE_DIR}/expected-assets.txt"
diff --unified \
"${RELEASE_DIR}/expected-assets.txt" \
"${RELEASE_DIR}/actual-assets.txt"The fail-closed diff must produce no output. It compares the public Release against exactly these ten entries, in this order, with no extras or duplicates:
RELEASE_SHA256SUMS.txtSHA256SUMS.txtplumber-report.zipsyswarden-4.03.2-1.x86_64.rpmsyswarden-release.tar.gzsyswarden-sbom.spdx.jsonsyswarden-update-manifest-v1.jsonsyswarden-update-manifest-v1.json.sigsyswarden_4.03.2_amd64.debsyswarden_4.03.2_x86_64.apk
This runbook supports only the Debian amd64 package. Bind the package
selection explicitly:
TARGET_ARCH=amd64
test "${TARGET_ARCH}" = amd64
DEB_NAME="syswarden_4.03.2_${TARGET_ARCH}.deb"
gh release download v4.03.2 --repo duggytuxy/syswarden \
--dir "${RELEASE_DIR}" \
--pattern "${DEB_NAME}" \
--pattern SHA256SUMS.txt \
--pattern RELEASE_SHA256SUMS.txt \
--pattern syswarden-update-manifest-v1.json \
--pattern syswarden-update-manifest-v1.json.sig
cd "${RELEASE_DIR}"The independently approved digest for the published amd64 DEB is pinned below. Do not replace it with a value calculated from a fresh download:
EXPECTED_DEB_SHA256=e499370fbed0e40968a6377f4e3cd9a8718993352deccc181a4fb43333289019
test "$(printf '%s' "${EXPECTED_DEB_SHA256}" | grep -Ec '^[0-9a-f]{64}$')" -eq 1
MANIFEST_SHA256="$(awk -v name="${DEB_NAME}" \
'$2 == name {count++; digest=$1} END {if (count != 1) exit 2; print digest}' \
SHA256SUMS.txt)"
test "$(printf '%s' "${MANIFEST_SHA256}" | grep -Ec '^[0-9a-f]{64}$')" -eq 1
test "${MANIFEST_SHA256}" = "${EXPECTED_DEB_SHA256}"
printf '%s %s\n' "${EXPECTED_DEB_SHA256}" "${DEB_NAME}" | \
sha256sum --check --strict -
RELEASE_MANIFEST_SHA256="$(awk -v name="${DEB_NAME}" \
'$2 == name {count++; digest=$1} END {if (count != 1) exit 2; print digest}' \
RELEASE_SHA256SUMS.txt)"
test "${RELEASE_MANIFEST_SHA256}" = "${EXPECTED_DEB_SHA256}"Verify GitHub build provenance for the exact file, release workflow and source commit:
gh attestation verify "${DEB_NAME}" \
--repo duggytuxy/syswarden \
--signer-workflow duggytuxy/syswarden/.github/workflows/release-manager.yml \
--source-digest "${RELEASE_SHA}" \
--deny-self-hosted-runnersThe DEB is not claimed to carry a separate embedded dpkg signature. The
production authenticity chain is the resolved Release tag commit, GitHub build
provenance, an independently approved SHA-256 value, and the exact Release
manifests. The detached Ed25519 signature authenticates the updater manifest
used by signed updates after this first manual hop.
Transfer the verified package, both checksum files, the updater manifest and
its signature through an authenticated channel to a new root-only directory on
the target. Recompute the package SHA-256 after transfer and compare it with
EXPECTED_DEB_SHA256 again.
Create /root/syswarden-v4032-stage with mode 0700, place only the five
verified files there, then open a dedicated trusted root shell with sudo -i.
Run the remaining target snippets from that root shell so access to the
root-only staging and evidence directories is unambiguous.
Important
TARGET VALIDATION REQUIRED: The commands below must pass on the actual Debian 13 host. Do not install if any assertion fails.
set -euo pipefail
cd /root/syswarden-v4032-stage
TARGET_ARCH="$(dpkg --print-architecture)"
test "${TARGET_ARCH}" = amd64
DEB_NAME="syswarden_4.03.2_${TARGET_ARCH}.deb"
EXPECTED_DEB_SHA256=e499370fbed0e40968a6377f4e3cd9a8718993352deccc181a4fb43333289019
test "$(printf '%s' "${EXPECTED_DEB_SHA256}" | grep -Ec '^[0-9a-f]{64}$')" -eq 1
printf '%s %s\n' "${EXPECTED_DEB_SHA256}" "${DEB_NAME}" | \
sha256sum --check --strict -
test "$(dpkg-deb --field "${DEB_NAME}" Package)" = syswarden
test "$(dpkg-deb --field "${DEB_NAME}" Version)" = 4.03.2
test "$(dpkg-deb --field "${DEB_NAME}" Architecture)" = "${TARGET_ARCH}"The Depends field must be exactly:
nftables, ipset, curl, wget, rsyslog, cron, bash-completion, wireguard-tools, qrencode, jq, unattended-upgrades, apt-listchanges, procps, e2fsprogs
Assert it before installation:
EXPECTED_DEPENDS='nftables, ipset, curl, wget, rsyslog, cron, bash-completion, wireguard-tools, qrencode, jq, unattended-upgrades, apt-listchanges, procps, e2fsprogs'
test "$(dpkg-deb --field "${DEB_NAME}" Depends)" = "${EXPECTED_DEPENDS}"The control archive must contain one each of control, md5sums, preinst,
postinst, prerm and postrm:
diff --unified \
<(printf '%s\n' ./ ./control ./md5sums ./postinst ./postrm ./preinst ./prerm | LC_ALL=C sort) \
<(dpkg-deb --ctrl-tarfile "${DEB_NAME}" | tar --list --file=- | LC_ALL=C sort)
dpkg-deb --info "${DEB_NAME}"Require the exact payload inventory attested for the v4.03.2 Debian amd64 package:
diff --unified \
<(printf '%s\n' \
./ \
./opt/ \
./opt/syswarden/ \
./opt/syswarden/bin/ \
./opt/syswarden/bin/syswarden-cli \
./opt/syswarden/bin/syswarden-core \
./opt/syswarden/bin/syswarden-tui \
./opt/syswarden/signatures.json \
./usr/ \
./usr/local/ \
./usr/local/bin/ \
./usr/local/bin/syswarden \
./usr/local/bin/syswarden-tui \
./usr/share/ \
./usr/share/doc/ \
./usr/share/doc/syswarden/ \
./usr/share/doc/syswarden/changelog.gz | LC_ALL=C sort) \
<(dpkg-deb --fsys-tarfile "${DEB_NAME}" | \
tar --list --file=- | LC_ALL=C sort)
PAYLOAD_ROOT="$(mktemp -d /root/syswarden-v4032-payload.XXXXXXXX)"
chmod 0700 "${PAYLOAD_ROOT}"
dpkg-deb --extract "${DEB_NAME}" "${PAYLOAD_ROOT}"
test -x "${PAYLOAD_ROOT}/opt/syswarden/bin/syswarden-cli"
test -x "${PAYLOAD_ROOT}/opt/syswarden/bin/syswarden-core"
test -x "${PAYLOAD_ROOT}/opt/syswarden/bin/syswarden-tui"
test -f "${PAYLOAD_ROOT}/opt/syswarden/signatures.json"
test -f "${PAYLOAD_ROOT}/usr/share/doc/syswarden/changelog.gz"
test -L "${PAYLOAD_ROOT}/usr/local/bin/syswarden"
test -L "${PAYLOAD_ROOT}/usr/local/bin/syswarden-tui"
test "$(readlink "${PAYLOAD_ROOT}/usr/local/bin/syswarden")" = \
/opt/syswarden/bin/syswarden-cli
test "$(readlink "${PAYLOAD_ROOT}/usr/local/bin/syswarden-tui")" = \
/opt/syswarden/bin/syswarden-tuiServices and host configuration are generated by the runtime installation pipeline, not shipped as opaque DEB payload files.
The installed historical v4.02.8 updater must not be used for this first hop. Extract the already verified v4.03.2 DEB into a new protected directory and run its read-only migration validation:
set -euo pipefail
LEGACY_SOURCE=/opt/syswarden/syswarden-auto.conf
MIGRATION_BACKUP=/opt/syswarden/syswarden-auto.conf.migration_backup
MIGRATED_BACKUP=/opt/syswarden/syswarden-auto.conf.migration_backup.migrated
LEGACY_ARCHIVE=/opt/syswarden/syswarden-auto.conf.bak
MIGRATION_MARKER=/etc/syswarden/config/.migration-in-progress
for path in \
"${LEGACY_SOURCE}" \
"${MIGRATION_BACKUP}" \
"${MIGRATED_BACKUP}" \
"${LEGACY_ARCHIVE}" \
"${MIGRATION_MARKER}"; do
if sudo test -e "${path}" || sudo test -L "${path}"; then
sudo stat --format='%n %F %U:%G %a %d:%i %s' "${path}"
else
printf 'ABSENT: %s\n' "${path}"
fi
done
sudo test ! -e "${MIGRATION_MARKER}" && sudo test ! -L "${MIGRATION_MARKER}"
sudo test ! -e "${MIGRATION_BACKUP}" && sudo test ! -L "${MIGRATION_BACKUP}"
sudo test ! -e "${MIGRATED_BACKUP}" && sudo test ! -L "${MIGRATED_BACKUP}"
if sudo test -f "${LEGACY_SOURCE}" && sudo test ! -L "${LEGACY_SOURCE}"; then
sudo test ! -e "${LEGACY_ARCHIVE}" && sudo test ! -L "${LEGACY_ARCHIVE}"
MIGRATION_MODE=legacy
else
sudo test ! -e "${LEGACY_SOURCE}" && sudo test ! -L "${LEGACY_SOURCE}"
sudo test -f /etc/syswarden/config/config.toml
MIGRATION_MODE=modular
fi
PREFLIGHT_ROOT="$(sudo mktemp -d /root/syswarden-v4032-preflight.XXXXXXXX)"
sudo chmod 0700 "${PREFLIGHT_ROOT}"
sudo dpkg-deb --extract "/root/syswarden-v4032-stage/${DEB_NAME}" "${PREFLIGHT_ROOT}"
CANDIDATE_CLI="${PREFLIGHT_ROOT}/opt/syswarden/bin/syswarden-cli"
sudo test -x "${CANDIDATE_CLI}"
sudo test ! -e /etc/syswarden/config/.migration-in-progress
capture_config_state() {
local root=$1
local output=$2
if sudo test ! -e "${root}" && sudo test ! -L "${root}"; then
printf 'ABSENT %s\n' "${root}" > "${output}"
return
fi
{
sudo find -P "${root}" -xdev \
-printf 'META %p %y %m %U %G %s %T@\n'
sudo find -P "${root}" -xdev -type f -exec sha256sum -- '{}' +
sudo find -P "${root}" -xdev -type l -printf 'LINK %p %l\n'
} | LC_ALL=C sort > "${output}"
}
show_toml_value() {
local file=$1
local section=$2
local key=$3
sudo awk -v section="${section}" -v key="${key}" '
/^[[:space:]]*\[/ {
current = $0
gsub(/^[[:space:]]*\[|\][[:space:]]*$/, "", current)
}
current == section && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" {
count++
print section "." $0
}
END { exit(count == 1 ? 0 : 1) }
' "${file}"
}
SOURCE_STATE_BEFORE="${PREFLIGHT_ROOT}/production-source.before"
CONFIG_STATE_BEFORE="${PREFLIGHT_ROOT}/production-config.before"
SOURCE_STATE_AFTER="${PREFLIGHT_ROOT}/production-source.after"
CONFIG_STATE_AFTER="${PREFLIGHT_ROOT}/production-config.after"
capture_config_state "${LEGACY_SOURCE}" "${SOURCE_STATE_BEFORE}"
capture_config_state /etc/syswarden/config "${CONFIG_STATE_BEFORE}"
if test "${MIGRATION_MODE}" = legacy; then
sudo env -i HOME=/root PATH=/usr/sbin:/usr/bin:/sbin:/bin \
"${CANDIDATE_CLI}" config migrate \
--source /opt/syswarden/syswarden-auto.conf \
--output /etc/syswarden/config \
--dry-run
else
sudo env -i HOME=/root PATH=/usr/sbin:/usr/bin:/sbin:/bin \
"${CANDIDATE_CLI}" config validate --path /etc/syswarden/config
fi
capture_config_state "${LEGACY_SOURCE}" "${SOURCE_STATE_AFTER}"
capture_config_state /etc/syswarden/config "${CONFIG_STATE_AFTER}"
diff --unified "${SOURCE_STATE_BEFORE}" "${SOURCE_STATE_AFTER}"
diff --unified "${CONFIG_STATE_BEFORE}" "${CONFIG_STATE_AFTER}"--dry-run performs no source or destination writes. It also refuses to resume
an existing transaction marker. The two fail-closed diffs must produce no
output. They bind that promise to the production source and destination rather
than only to the explicit migration code path. Review every error instead of
modifying the source to force success.
The dry run validates semantics but intentionally does not print the migrated choices. For a legacy source, migrate a protected copy into a disposable output directory, validate it with the candidate, and print only the non-secret values that require operator approval. Continue in the same trusted root shell so the protected variables and helper functions defined above remain in scope:
set -euo pipefail
test "${MIGRATION_MODE}" = legacy
REVIEW_ROOT="$(sudo mktemp -d /root/syswarden-v4032-review.XXXXXXXX)"
sudo chmod 0700 "${REVIEW_ROOT}"
REVIEW_SOURCE="${REVIEW_ROOT}/syswarden-auto.conf"
REVIEW_OUTPUT="${REVIEW_ROOT}/config"
sudo install --mode=0600 --owner=root --group=root \
"${LEGACY_SOURCE}" "${REVIEW_SOURCE}"
sudo env -i HOME=/root PATH=/usr/sbin:/usr/bin:/sbin:/bin \
"${CANDIDATE_CLI}" config migrate \
--source "${REVIEW_SOURCE}" \
--output "${REVIEW_OUTPUT}"
sudo env -i HOME=/root PATH=/usr/sbin:/usr/bin:/sbin:/bin \
"${CANDIDATE_CLI}" config validate --path "${REVIEW_OUTPUT}"
show_toml_value "${REVIEW_OUTPUT}/modules/00-core.toml" \
core firewall_backend
show_toml_value "${REVIEW_OUTPUT}/modules/00-core.toml" \
core ssh_port
show_toml_value "${REVIEW_OUTPUT}/modules/10-network.toml" \
network.wireguard enabled
show_toml_value "${REVIEW_OUTPUT}/modules/10-network.toml" \
network.wireguard port
show_toml_value "${REVIEW_OUTPUT}/modules/10-network.toml" \
network.wireguard subnet
show_toml_value "${REVIEW_OUTPUT}/modules/40-integrations.toml" \
integrations.ha enabled
show_toml_value "${REVIEW_OUTPUT}/modules/40-integrations.toml" \
integrations.ha peer_ips
show_toml_value "${REVIEW_OUTPUT}/modules/40-integrations.toml" \
integrations.ha peer_port
capture_config_state "${LEGACY_SOURCE}" "${SOURCE_STATE_AFTER}"
capture_config_state /etc/syswarden/config "${CONFIG_STATE_AFTER}"
diff --unified "${SOURCE_STATE_BEFORE}" "${SOURCE_STATE_AFTER}"
diff --unified "${CONFIG_STATE_BEFORE}" "${CONFIG_STATE_AFTER}"The migration of the copy can rename or securely wipe only REVIEW_SOURCE;
that is why it must live below the disposable root. The repeated production
diffs must remain empty. Store the printed network values in protected evidence
and do not publish internal addresses. Stop unless the firewall backend, actual
SSH port behavior, WireGuard state and port or subnet, and HA state, peers and
port are explicitly approved.
If the host already has a complete modular configuration, validate it with the
candidate instead, print the same non-secret decision keys with
show_toml_value, and prove the exact seven-file inventory exists:
sudo env -i HOME=/root PATH=/usr/sbin:/usr/bin:/sbin:/bin \
"${CANDIDATE_CLI}" config validate --path /etc/syswarden/config
diff --unified \
<(printf '%s\n' \
'config.toml f' \
'modules d' \
'modules/00-core.toml f' \
'modules/10-network.toml f' \
'modules/20-security.toml f' \
'modules/30-waap.toml f' \
'modules/40-integrations.toml f' \
'modules/99-user.toml f' | LC_ALL=C sort) \
<(sudo find /etc/syswarden/config -xdev -mindepth 1 \
-printf '%P %y\n' | LC_ALL=C sort)
sudo stat /etc/syswarden/config/config.toml \
/etc/syswarden/config/modules/00-core.toml \
/etc/syswarden/config/modules/10-network.toml \
/etc/syswarden/config/modules/20-security.toml \
/etc/syswarden/config/modules/30-waap.toml \
/etc/syswarden/config/modules/40-integrations.toml \
/etc/syswarden/config/modules/99-user.toml
show_toml_value /etc/syswarden/config/modules/00-core.toml \
core firewall_backend
show_toml_value /etc/syswarden/config/modules/00-core.toml \
core ssh_port
show_toml_value /etc/syswarden/config/modules/10-network.toml \
network.wireguard enabled
show_toml_value /etc/syswarden/config/modules/10-network.toml \
network.wireguard port
show_toml_value /etc/syswarden/config/modules/10-network.toml \
network.wireguard subnet
show_toml_value /etc/syswarden/config/modules/40-integrations.toml \
integrations.ha enabled
show_toml_value /etc/syswarden/config/modules/40-integrations.toml \
integrations.ha peer_ips
show_toml_value /etc/syswarden/config/modules/40-integrations.toml \
integrations.ha peer_portMigration maps a historical firewalld backend to keep, which preserves the
operator-managed frontend without automatically changing services. A migrated
nftables backend requires active and enabled nftables.service with
firewalld, UFW, iptables services and netfilter-persistent inactive and
disabled. The legacy iptables value remains parseable but is rejected by
v4.03.2 operational policy mutation. Enabled WireGuard requires nftables.
First prove that no package manager is already active. This is a live pre-install gate, separate from interrupted-transaction recovery:
set -euo pipefail
if ps -eo pid=,comm=,args= | awk '
$2 ~ /^(apt|apt-get|dpkg|unattended-upgr)$/ { print; found = 1 }
END { exit(found ? 0 : 1) }
'; then
printf '%s\n' 'A package-manager process is active; stop the migration.' >&2
exit 1
else
PROCESS_PIPE_STATUS=("${PIPESTATUS[@]}")
if (( PROCESS_PIPE_STATUS[0] != 0 || PROCESS_PIPE_STATUS[1] != 1 )); then
printf 'Package-manager probe failed: ps=%d awk=%d\n' \
"${PROCESS_PIPE_STATUS[0]}" "${PROCESS_PIPE_STATUS[1]}" >&2
exit 1
fi
fi
DPKG_AUDIT="$(sudo dpkg --audit)"
if [[ -n "${DPKG_AUDIT}" ]]; then
printf '%s\n' "${DPKG_AUDIT}" >&2
printf '%s\n' 'dpkg reports unfinished or inconsistent package state.' >&2
exit 1
fiSimulation does not execute maintainer scripts:
cd /root/syswarden-v4032-stage
sudo apt-get --simulate install "./${DEB_NAME}"Stop if the plan removes or downgrades anything, changes architecture, selects an unexpected repository, or changes unrelated packages. Resolve missing dependencies under normal Debian change control before continuing.
During apt-get install, v4.03.2 performs these ordered operations:
-
preinstexports the package-install boundary, creates or tightens the private SysWarden directories, rejects unsafe symlinks and retires only byte-attributed historical Web TUI state. - If
/opt/syswarden/syswarden-auto.confexists and neither migration backup state exists,preinstrenames it to/opt/syswarden/syswarden-auto.conf.migration_backup. -
dpkgunpacks the new binaries and launcher links. -
postinstinvokes the new v4.03.2migrate-configimplementation before the host installation pipeline. A transaction marker at/etc/syswarden/config/.migration-in-progressmakes a retry resume instead of silently starting over. - Migration validates and atomically publishes
schema_version = 1, the master file and all six modules. It preserves a validated operator-owned99-user.tomlinstead of overwriting it. - When
secure_wipe_conf = false, the migrated source is retained and post-install moves it to/opt/syswarden/syswarden-auto.conf.bakwithout overwriting an existing archive. When secure wipe is explicitly enabled, the source is securely finalized and the protected pre-upgrade backup is the recovery copy. - On an active systemd host,
postinstrunssyswarden-cli install. This pipeline validates configuration and firewall state, then configures dependencies, SSH, threat feeds, firewall policy, integrations, hardening, WireGuard or HA, systemd services and scheduled jobs. - The package enables and starts
syswarden-firewall.service, then enables, starts and restartssyswarden-core.service.
Every migration and main host-installation failure propagates to dpkg. The
optional Bash-completion rendering step can be skipped by the package hook, so
its output must be checked separately after installation. There is no fail-open
migration path and no package-script rollback implementation.
Caution
TARGET VALIDATION REQUIRED: Keep the recovery console and the original SSH session open. Start a separate terminal that continuously observes the package log and service journal.
Install only the already verified local file:
set -euo pipefail
cd /root/syswarden-v4032-stage
if ps -eo pid=,comm=,args= | awk '
$2 ~ /^(apt|apt-get|dpkg|unattended-upgr)$/ { print; found = 1 }
END { exit(found ? 0 : 1) }
'; then
printf '%s\n' 'A package-manager process became active; stop the migration.' >&2
exit 1
else
PROCESS_PIPE_STATUS=("${PIPESTATUS[@]}")
if (( PROCESS_PIPE_STATUS[0] != 0 || PROCESS_PIPE_STATUS[1] != 1 )); then
printf 'Package-manager probe failed: ps=%d awk=%d\n' \
"${PROCESS_PIPE_STATUS[0]}" "${PROCESS_PIPE_STATUS[1]}" >&2
exit 1
fi
fi
DPKG_AUDIT="$(sudo dpkg --audit)"
if [[ -n "${DPKG_AUDIT}" ]]; then
printf '%s\n' "${DPKG_AUDIT}" >&2
printf '%s\n' 'dpkg reports unfinished or inconsistent package state.' >&2
exit 1
fi
sudo env DEBIAN_FRONTEND=noninteractive \
apt-get --no-remove install -y "./${DEB_NAME}"Do not close the recovery session when apt-get returns. A successful package
exit is necessary but is not sufficient for acceptance.
set -euo pipefail
test "$(dpkg-query --show --showformat='${Status}' syswarden)" = 'install ok installed'
test "$(dpkg-query --show --showformat='${Version}' syswarden)" = '4.03.2'
test "$(dpkg-query --show --showformat='${Architecture}' syswarden)" = \
"$(dpkg --print-architecture)"
DPKG_AUDIT="$(sudo dpkg --audit)"
if [[ -n "${DPKG_AUDIT}" ]]; then
printf '%s\n' "${DPKG_AUDIT}" >&2
printf '%s\n' 'dpkg reports unfinished or inconsistent package state.' >&2
exit 1
fi
sudo /opt/syswarden/bin/syswarden-cli
sudo syswarden config validate --path /etc/syswarden/config
sudo test ! -e /etc/syswarden/config/.migration-in-progressConfirm the complete modular inventory and record hashes:
diff --unified \
<(printf '%s\n' \
'config.toml f' \
'modules d' \
'modules/00-core.toml f' \
'modules/10-network.toml f' \
'modules/20-security.toml f' \
'modules/30-waap.toml f' \
'modules/40-integrations.toml f' \
'modules/99-user.toml f' | LC_ALL=C sort) \
<(sudo find /etc/syswarden/config -xdev -mindepth 1 \
-printf '%P %y\n' | LC_ALL=C sort)
sudo stat --format='%n %F %U:%G %a %d:%i %s' \
/etc/syswarden/config/config.toml \
/etc/syswarden/config/modules/00-core.toml \
/etc/syswarden/config/modules/10-network.toml \
/etc/syswarden/config/modules/20-security.toml \
/etc/syswarden/config/modules/30-waap.toml \
/etc/syswarden/config/modules/40-integrations.toml \
/etc/syswarden/config/modules/99-user.toml
sudo find /etc/syswarden/config -xdev -type f -exec sha256sum -- '{}' +
sudo test -s /etc/bash_completion.d/syswarden
sudo syswarden config-get core.firewall_backend
sudo syswarden config-get core.ssh_port
sudo syswarden config-get network.wireguard.enabled
sudo syswarden config-get waap.enforcement_mode
sudo syswarden config-get integrations.ha.enabledReview all unknown and deprecated-key diagnostics even when structural validation succeeds. Do not publish or paste values that contain secrets.
If secure wipe was disabled, confirm the historical archive exists as a real, root-controlled regular file:
sudo stat --format='%n %F %U:%G %a %d:%i %s' \
/opt/syswarden/syswarden-auto.conf.bakDo not expect that archive when secure wipe was explicitly enabled. In either case, retain the protected snapshot and backup until final acceptance.
Important
TARGET VALIDATION REQUIRED: Capture every command and exit status.
sudo systemctl is-enabled syswarden-firewall.service
sudo systemctl is-active syswarden-firewall.service
sudo systemctl is-enabled syswarden-core.service
sudo systemctl is-active syswarden-core.service
sudo systemctl show syswarden-firewall.service syswarden-core.service \
--property=Id,LoadState,ActiveState,SubState,UnitFileState,FragmentPath,DropInPaths,ExecMainStatus,MemoryCurrent,CPUUsageNSec
sudo systemd-analyze verify \
/etc/systemd/system/syswarden-firewall.service \
/etc/systemd/system/syswarden-core.service
sudo journalctl --boot --no-pager \
--unit=syswarden-firewall.service --unit=syswarden-core.serviceValidate the effective firewall without changing lists:
sudo syswarden config-get core.firewall_backend
sudo nft --handle list table inet syswarden
sudo nft --handle list table netdev syswarden_hw_drop
sudo syswarden list
sudo syswarden auditsyswarden audit is a bounded local operational diagnostic, not a compliance
certificate and not a complete kernel-state proof. Compare the entire ruleset
and frontend state against the protected before-state evidence. Confirm that
the administrative source, effective SSH port, HA peer port and any required
WireGuard state remain reachable and non-conflicting.
Validate SSH before closing the original session:
sudo sshd -t
sudo sshd -T
sudo systemctl is-active ssh.service
sudo ss -lntpHOpen a new SSH connection through the effective port from an approved administrative source. Keep the original session open until the new session has run a privileged read-only command successfully. If the new connection fails, use the provider console and invoke the snapshot rollback decision.
These labels describe capability areas, not a certification claim. Separate read-only production smoke checks from end-to-end mutation tests.
sudo syswarden audit
sudo journalctl --boot --no-pager --unit=syswarden-core.service
sudo stat --format='%n %F %U:%G %a %s' \
/run/syswarden.sock \
/var/log/syswarden/waf.json \
/var/lib/syswarden/ui/data.json
sudo syswarden config-get waap.enforcement_mode
sudo syswarden config-get waap.bruteforce_logs
sudo syswarden config-get waap.modsec_logsFrom an interactive trusted terminal, run sudo syswarden alerts, verify that
existing kernel and WAAP events render without parser errors, then exit with
Ctrl+C. This observes available telemetry but does not prove an end-to-end
detection or prevention response.
Confirm all of the following in protected evidence:
- the core consumes the configured real log paths and follows rotations;
- the WAAP socket and JSON telemetry file have expected ownership and modes;
- ambiguous, malformed or hostless records do not create a firewall target;
- the authoritative SysWarden nftables tables are loaded;
- persistent blocklist, whitelist and SSH-exception registries are intact;
- no new high-severity service, parser, firewall or kernel errors appear.
Do not generate attack traffic or change production policy merely to satisfy a smoke test. On a disposable clone restored from the pre-upgrade snapshot:
-
Exercise one manual block with the documentation address
192.0.2.25:sudo syswarden block 192.0.2.25 sudo syswarden check 192.0.2.25 sudo nft --handle list table inet syswarden sudo syswarden unblock 192.0.2.25
-
Follow Observe one web log in WAAP audit mode with a known compatible application log. Require simulated telemetry in audit mode before any enforcing-mode test.
-
Exercise a representative HIDS event from an isolated test source and prove the event appears in the journal and
syswarden alertswithout exposing raw secrets or copying attacker-controlled text into an operator command. -
If enforcing mode is in scope, prove the durable list change, matching nftables state, expiry or explicit cleanup, service health and unrelated traffic preservation.
-
Restore the clone snapshot after collecting evidence.
Run the native dashboard from the trusted terminal:
sudo syswarden tuiVerify that the dashboard renders local telemetry and, when configured,
authenticated HA status. Exit with q or Q. TUI rendering is observability,
not kernel-state proof.
Confirm SysWarden owns no browser terminal, HTTPS or WebSocket terminal bridge, remote PTY route, token-management command, listener or generated firewall permission on TCP 62027:
if sudo ss -lntpH | awk '$4 ~ /:62027$/ {print}'; then
:
else
SOCKET_PIPE_STATUS=("${PIPESTATUS[@]}")
printf 'Listener probe failed: ss=%d awk=%d\n' \
"${SOCKET_PIPE_STATUS[0]}" "${SOCKET_PIPE_STATUS[1]}" >&2
exit 1
fi
if sudo nft --handle list ruleset | grep -F '62027'; then
printf '%s\n' 'Review and attribute every matching rule before acceptance.'
else
NFT_PIPE_STATUS=("${PIPESTATUS[@]}")
if (( NFT_PIPE_STATUS[0] == 0 && NFT_PIPE_STATUS[1] == 1 )); then
printf '%s\n' 'No nftables rule contains TCP port 62027.'
else
printf 'Firewall probe failed: nft=%d grep=%d\n' \
"${NFT_PIPE_STATUS[0]}" "${NFT_PIPE_STATUS[1]}" >&2
exit 1
fi
fiNo listener or firewall-rule match is expected for SysWarden-owned state. The probe status messages are expected. If an unrelated process or rule was recorded before the upgrade, prove it was preserved and remains unattributed to SysWarden.
The v4.03.2 CLI includes a fail-closed signed updater:
sudo syswarden update --helpThe command has no check-only or version-pin mode. It installs a newer public release when one exists, so do not run it as a production smoke test. Query the public release API only to record the latest tag at that observation time:
curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \
--connect-timeout 15 --max-time 60 --retry 3 \
https://api.github.com/repos/duggytuxy/syswarden/releases/latest | \
jq -r '.tag_name'Do not follow that observation with sudo syswarden update on the production
host: a newer release can appear between the API query and the updater lookup,
causing an unplanned installation. Run the mutating updater test only on a
disposable restored clone under separate change control. Full Ed25519 manifest,
platform, size, digest and installation behavior requires that clone evidence,
a newer qualified release or the sealed release qualification evidence.
Use this section only when installation was interrupted or dpkg reports the
package unpacked but not configured. Do not delete locks, the migration marker,
the migration backup, the retained archive or any temporary migration file.
First preserve evidence and confirm that no package manager is still running:
sudo dpkg --audit
dpkg-query --show --showformat='${db:Status-Abbrev} ${Status} ${Version} ${Architecture}\n' syswarden
if ps -eo pid,ppid,user,lstart,args | \
grep -E '[a]pt|[d]pkg|[u]nattended-upgrade'; then
printf '%s\n' 'A package-manager process is still active; stop recovery.' >&2
exit 1
else
PROCESS_PIPE_STATUS=("${PIPESTATUS[@]}")
if (( PROCESS_PIPE_STATUS[0] != 0 || PROCESS_PIPE_STATUS[1] != 1 )); then
printf 'Process probe failed: ps=%d grep=%d\n' \
"${PROCESS_PIPE_STATUS[0]}" "${PROCESS_PIPE_STATUS[1]}" >&2
exit 1
fi
fi
for path in \
/etc/syswarden/config/.migration-in-progress \
/opt/syswarden/syswarden-auto.conf.migration_backup \
/opt/syswarden/syswarden-auto.conf.migration_backup.migrated \
/opt/syswarden/syswarden-auto.conf.bak; do
if sudo test -e "${path}" || sudo test -L "${path}"; then
sudo stat --format='%n %F %U:%G %a %d:%i %s' "${path}"
else
printf 'ABSENT: %s\n' "${path}"
fi
done
sudo journalctl --boot --no-pager \
--unit=syswarden-firewall.service --unit=syswarden-core.service
sudo tail -n 200 /var/log/dpkg.logOptional paths can be absent. Record that fact. If no apt, dpkg or
unattended-upgrade process is active, resume configuration once:
sudo dpkg --configure -aThe v4.03.2 post-install state machine is designed to resume the exact transaction marker and retained source. A dry run cannot resume an in-progress transaction and is not the recovery command.
If dependency errors remain, inspect the proposed repair without applying it:
sudo apt-get --simulate --fix-broken installApply apt-get --fix-broken install only after a human confirms that the plan
installs the declared dependencies and configures the verified v4.03.2 package
without removal, downgrade, architecture change or unrelated package changes.
Then repeat the complete post-upgrade validation.
Restore the snapshot instead of improvising if recovery reports any of these:
- archive collision or changed migration-source identity;
- unsafe symlink, owner, mode or directory identity;
- unexpected or changed transaction marker;
- incomplete or invalid modular configuration;
- ambiguous firewall or service-manager state;
- failed SSH recovery path;
- persistent package, service or kernel-policy inconsistency.
Rollback is a change-control decision. Trigger it when acceptance cannot be completed safely, administrative access is at risk, or recovery would require overriding a fail-closed guard.
SysWarden package rollback is an explicit package and configuration recovery procedure, not a general host-state reversal. The preferred provider or hypervisor snapshot restoration below is different: it is a whole-host state reversal and can roll back unrelated application data.
- Stop new operator and workload changes, preserve the current console, package, migration, service and firewall evidence, and capture any forward application data required by the approved reconciliation plan.
- Confirm the recorded snapshot scope, application recovery point and approval for unrelated state reversal. Do not restore while unquiesced writers can create an inconsistent snapshot or overwrite newer durable data.
- Use the provider or hypervisor console to restore the complete pre-upgrade snapshot. Do not attempt to reconstruct historical v4.02.8 state by copying old binaries onto migrated v4.03.2 configuration.
- Boot the restored snapshot in an isolated or controlled network context, then apply the approved application-data replay or reconciliation procedure.
- Prove Debian 13, package version
4.02.8, SSH access, service state, firewall rules, persistent lists, WAAP inputs, scheduled jobs and HA or WireGuard state match the recorded before-state evidence. - Keep the candidate package, logs and protected backup for root-cause analysis. Do not resume the campaign until the cause and corrected procedure are independently reviewed.
A direct package downgrade was exercised in disposable lifecycle qualification, but it is not the preferred production rollback. Package hooks contain no general host rollback implementation, and an older binary must not be assumed compatible with already migrated configuration or retired network surfaces.
The migration is complete only when every applicable item is attached to the internal change record and contains no exposed secret:
- Change identifier, operator, UTC start and end times.
- Target identity recorded internally without publishing it.
- Debian 13, kernel and architecture before and after.
- Snapshot identifier and successful restore-clone evidence.
- Root-only backup path, archive SHA-256, verified encrypted off-host copy and protected-storage location.
- Source package status, version, architecture and binary hashes.
- Public Release ID, tag, resolved 40-character commit SHA and exact asset inventory.
- Successful GitHub provenance attestation for the selected DEB, exact signer workflow and exact source SHA.
- Independently approved package digest, package-manifest digest, release-manifest digest and post-transfer digest.
- Exact DEB package, version, architecture, dependency and control-script metadata.
- Successful candidate migration dry run.
-
apt-get --simulateoutput with no removal, downgrade or unrelated change. - Complete package installation output and relevant
dpkg.logand APT history. - Modular schema validation, seven-file inventory and protected hashes.
- Migration marker absent and expected archive or secure-wipe outcome recorded.
- Firewall backend, complete ruleset comparison and persistent registry validation.
- Both SysWarden systemd units enabled and active with clean journals.
- Effective SSH validation and successful independent new SSH session.
- HIDS, HIPS and WAAP-oriented read-only smoke evidence.
- Snapshot-clone end-to-end firewall and WAAP audit-mode evidence.
- Native TUI renders and exits cleanly.
- No SysWarden network-terminal state or TCP 62027 ownership.
- Signed-update command surface and current public latest tag recorded.
- CPU, memory and service stability compared with the before-state.
- Final production go or rollback decision with reviewer sign-off.
Keep the snapshot and protected backup through the agreed observation period. Only the backup owner may authorize later disposal under the organization's retention policy.