Compare commits
36 commits
feat/docs-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c961eba88 | |||
|
|
8c70030d80 | ||
|
|
89c7df0ecc | ||
|
|
c2b4df8ef9 | ||
|
|
b9df392fbc | ||
|
|
84fa325e46 | ||
|
|
1e4ca2b56b | ||
|
|
446c602683 | ||
|
|
ac5c29df42 | ||
|
|
6f4842a75c | ||
|
|
4b90e7e00b | ||
|
|
7a0c665cf0 | ||
|
|
d38fce4cb8 | ||
| bc738c1c7b | |||
| a3f6c1a1a6 | |||
| 356013e1ca | |||
| 417acb5585 | |||
| df574e00f5 | |||
| 3e660534a1 | |||
| 749bcef5b4 | |||
| 77ed91ed8e | |||
| ad059ec73e | |||
| 05d37f6419 | |||
| eafb8b7aa1 | |||
| d0738970e0 | |||
|
|
7974ed7a6e | ||
|
|
f06ee5cc1c | ||
|
|
130f0432dd | ||
|
|
08f16bb2ee | ||
|
|
25b8d30f35 | ||
|
|
aa731f9daa | ||
|
|
441f7d057f | ||
|
|
816fc0ee68 | ||
|
|
44f0c787a7 | ||
|
|
900f5465b3 | ||
|
|
63c5e199d9 |
14 changed files with 1064 additions and 208 deletions
378
.github/workflows/build-iso.yml
vendored
378
.github/workflows/build-iso.yml
vendored
|
|
@ -1,3 +1,5 @@
|
||||||
|
# TODO: SHA-pin all uses: tags to commit SHAs (Agent 8 audit recommendation).
|
||||||
|
# Tracked separately so this PR can land without long web lookups.
|
||||||
name: Build veilor-os ISO
|
name: Build veilor-os ISO
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -20,158 +22,135 @@ on:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # needed for action-gh-release to create+update ci-latest
|
contents: write # needed for action-gh-release to create+update ci-latest
|
||||||
|
id-token: write # cosign keyless OIDC + attest-build-provenance
|
||||||
|
attestations: write # attest-build-provenance writes the attestation
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build live ISO
|
name: Build live ISO
|
||||||
runs-on: ubuntu-24.04
|
# nullstone label resolves to a privileged Fedora 43 container per
|
||||||
|
# the runner's RUNNER_LABELS map. Build runs directly in this job
|
||||||
|
# container — no nested docker-run-action, no bind-mount juggling.
|
||||||
|
runs-on: nullstone
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
# Pinned to last v4 tag confirmed to ship on node20. v4.2+ ships
|
||||||
|
# node24 which forgejo-runner v6.4.0 (node20) cannot exec.
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Free up disk
|
- name: Install build tooling (Fedora)
|
||||||
run: |
|
run: |
|
||||||
sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /usr/local/share/boost
|
set -euxo pipefail
|
||||||
sudo apt-get clean
|
dnf -y upgrade --refresh
|
||||||
df -h
|
dnf -y install \
|
||||||
|
lorax \
|
||||||
|
livecd-tools \
|
||||||
|
pykickstart \
|
||||||
|
python3-imgcreate \
|
||||||
|
anaconda-tui \
|
||||||
|
squashfs-tools \
|
||||||
|
xorriso \
|
||||||
|
createrepo_c \
|
||||||
|
git \
|
||||||
|
which \
|
||||||
|
shadow-utils \
|
||||||
|
syslinux \
|
||||||
|
tar \
|
||||||
|
curl \
|
||||||
|
sudo
|
||||||
|
|
||||||
- name: Run build inside Fedora 43 container
|
- name: Vendor gum binary into overlay
|
||||||
uses: addnab/docker-run-action@v3
|
run: |
|
||||||
with:
|
set -euxo pipefail
|
||||||
image: registry.fedoraproject.org/fedora:43
|
GUM_VERSION="0.17.0"
|
||||||
options: |
|
GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_x86_64.tar.gz"
|
||||||
--privileged
|
GUM_SHA256="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb"
|
||||||
-v ${{ github.workspace }}:/work
|
mkdir -p overlay/usr/local/bin
|
||||||
-v /dev:/dev
|
curl -fsSL "$GUM_URL" -o /tmp/gum.tgz
|
||||||
--tmpfs /tmp:rw,nosuid,nodev,exec,size=16G
|
echo "$GUM_SHA256 /tmp/gum.tgz" | sha256sum -c -
|
||||||
run: |
|
tar -xzf /tmp/gum.tgz -C /tmp/
|
||||||
set -euxo pipefail
|
install -m 0755 "/tmp/gum_${GUM_VERSION}_Linux_x86_64/gum" overlay/usr/local/bin/gum
|
||||||
|
overlay/usr/local/bin/gum --version
|
||||||
|
echo "[OK] gum ${GUM_VERSION} vendored into overlay/usr/local/bin/"
|
||||||
|
|
||||||
# Update Fedora image to latest packages — guarantees pcre2 +
|
- name: Build ISO with livecd-creator
|
||||||
# libselinux + selinux-policy are matched (the local build's
|
run: |
|
||||||
# core problem). CI runners always start fresh, no version skew.
|
set -euxo pipefail
|
||||||
dnf -y upgrade --refresh
|
|
||||||
|
|
||||||
# Install build tooling
|
# PATCH: livecd-creator bug — __get_efi_image_stanza writes
|
||||||
dnf -y install \
|
# `root=live:LABEL=...` instead of `live:CDLABEL=...` for dracut.
|
||||||
lorax \
|
# Result: dracut hangs on parse-livenet looking for non-CD label.
|
||||||
livecd-tools \
|
# Fix in-place before running build.
|
||||||
pykickstart \
|
LIVE_PY=$(python3 -c 'import imgcreate, os; print(os.path.dirname(imgcreate.__file__))')/live.py
|
||||||
python3-imgcreate \
|
sed -i 's|"live:LABEL=%(fslabel)s"|"live:CDLABEL=%(fslabel)s"|g' "$LIVE_PY"
|
||||||
anaconda-tui \
|
grep -n 'CDLABEL=%(fslabel)s' "$LIVE_PY" || { echo "[ERR] patch failed"; exit 1; }
|
||||||
squashfs-tools \
|
echo "[OK] livecd-creator patched: LABEL= → CDLABEL= for EFI dracut stanza"
|
||||||
xorriso \
|
|
||||||
createrepo_c \
|
|
||||||
git \
|
|
||||||
which \
|
|
||||||
shadow-utils \
|
|
||||||
syslinux \
|
|
||||||
tar \
|
|
||||||
curl
|
|
||||||
|
|
||||||
# Vendor gum binary onto the ISO so the TTY1 installer can use
|
# CI uses ks-ci.ks (no local fix-repo line). Generated from main ks.
|
||||||
# Charm.sh TUI primitives. gum is not packaged in Fedora repos,
|
# Drop `updates` repo: previously 404'd on repodata zchunk during
|
||||||
# so pull the upstream release tarball pinned by sha256.
|
# Fedora mid-push windows. Base 43 ships the selinux-policy fix.
|
||||||
GUM_VERSION="0.17.0"
|
sed -e '/veilor-fix/d' \
|
||||||
GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_x86_64.tar.gz"
|
-e '/^shutdown$/d' \
|
||||||
GUM_SHA256="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb"
|
-e '/repo --name=updates/d' \
|
||||||
mkdir -p /work/overlay/usr/local/bin
|
kickstart/veilor-os.ks > kickstart/veilor-os-ci.ks
|
||||||
curl -fsSL "$GUM_URL" -o /tmp/gum.tgz
|
|
||||||
echo "$GUM_SHA256 /tmp/gum.tgz" | sha256sum -c -
|
|
||||||
tar -xzf /tmp/gum.tgz -C /tmp/
|
|
||||||
install -m 0755 "/tmp/gum_${GUM_VERSION}_Linux_x86_64/gum" /work/overlay/usr/local/bin/gum
|
|
||||||
/work/overlay/usr/local/bin/gum --version
|
|
||||||
echo "[OK] gum ${GUM_VERSION} vendored into overlay/usr/local/bin/"
|
|
||||||
|
|
||||||
cd /work
|
ksvalidator kickstart/veilor-os-ci.ks
|
||||||
|
mkdir -p build/out
|
||||||
|
|
||||||
# PATCH: livecd-creator bug — __get_efi_image_stanza writes
|
# The kickstart's %post --nochroot probes a fixed list of
|
||||||
# `root=live:LABEL=...` instead of `live:CDLABEL=...` for dracut.
|
# candidate paths to locate the repo source for overlay/scripts
|
||||||
# Result: dracut hangs on parse-livenet looking for non-CD label.
|
# copy. /work is the canonical CI candidate; symlink the live
|
||||||
# Fix in-place before running build.
|
# workspace there so the existing probe finds it.
|
||||||
LIVE_PY=$(python3 -c 'import imgcreate, os; print(os.path.dirname(imgcreate.__file__))')/live.py
|
ln -sfn "$GITHUB_WORKSPACE" /work
|
||||||
sed -i 's|"live:LABEL=%(fslabel)s"|"live:CDLABEL=%(fslabel)s"|g' "$LIVE_PY"
|
|
||||||
grep -n 'CDLABEL=%(fslabel)s' "$LIVE_PY" || { echo "[ERR] patch failed"; exit 1; }
|
|
||||||
echo "[OK] livecd-creator patched: LABEL= → CDLABEL= for EFI dracut stanza"
|
|
||||||
|
|
||||||
# CI uses ks-ci.ks (no local fix-repo line). Generated from main ks.
|
mkdir -p /var/lmc /var/lmc-cache
|
||||||
# Also strip flags livecd-creator doesn't recognize.
|
livecd-creator \
|
||||||
# Drop `updates` repo: 3 consecutive builds 404'd on its
|
--verbose \
|
||||||
# repodata zchunk file across all mirrors — Fedora infra issue
|
--config kickstart/veilor-os-ci.ks \
|
||||||
# mid-push window. Original reason for `updates` (selinux-policy
|
--fslabel "veilor-os-43" \
|
||||||
# 43.7 pcre2 fix) is no longer needed; base 43 ships fixed.
|
--title "veilor-os" \
|
||||||
sed -e '/veilor-fix/d' \
|
--product "veilor-os" \
|
||||||
-e '/^shutdown$/d' \
|
--releasever "${{ github.event.inputs.releasever || '43' }}" \
|
||||||
-e '/repo --name=updates/d' \
|
--tmpdir /var/lmc \
|
||||||
kickstart/veilor-os.ks > kickstart/veilor-os-ci.ks
|
--cache /var/lmc-cache 2>&1 | tee build/out/build.log
|
||||||
|
|
||||||
ksvalidator kickstart/veilor-os-ci.ks
|
- name: Graft veilor source tree onto ISO
|
||||||
mkdir -p build/out
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
ISO_FILE=$(ls ./*.iso 2>/dev/null | head -1)
|
||||||
|
[ -n "$ISO_FILE" ] || { echo "[ERR] no ISO produced by livecd-creator"; exit 1; }
|
||||||
|
echo "[INFO] grafting /veilor/ onto $ISO_FILE"
|
||||||
|
|
||||||
# livecd-creator (livecd-tools) — purpose-built for live ISOs.
|
xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>&1 | tee /tmp/iso-boot.txt || true
|
||||||
# Handles EFI/BOOT + isohybrid + grafting that livemedia-creator
|
ORIG_FLAGS=$(xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>/dev/null | \
|
||||||
# --make-iso --no-virt does not. Produces UEFI+BIOS bootable ISO.
|
grep -v '^xorriso :' | grep -E '^-' | tr '\n' ' ')
|
||||||
# --tmpdir /var/lmc to avoid GitHub Actions /tmp tmpfs constraints.
|
[ -n "$ORIG_FLAGS" ] || { echo "[ERR] could not extract boot stanza from $ISO_FILE"; exit 1; }
|
||||||
# /var on the runner is the host's ext4 (~80GB free post-disk-cleanup).
|
|
||||||
mkdir -p /var/lmc /var/lmc-cache
|
|
||||||
livecd-creator \
|
|
||||||
--verbose \
|
|
||||||
--config kickstart/veilor-os-ci.ks \
|
|
||||||
--fslabel "veilor-os-43" \
|
|
||||||
--title "veilor-os" \
|
|
||||||
--product "veilor-os" \
|
|
||||||
--releasever "${{ github.event.inputs.releasever || '43' }}" \
|
|
||||||
--tmpdir /var/lmc \
|
|
||||||
--cache /var/lmc-cache 2>&1 | tee build/out/build.log
|
|
||||||
|
|
||||||
# Graft veilor source tree onto the ISO so the installer-generated
|
mkdir -p /tmp/iso-mod
|
||||||
# kickstart's `%post --nochroot` can find SRC at
|
xorriso -osirrox on -indev "$ISO_FILE" -extract / /tmp/iso-mod
|
||||||
# /run/install/repo/veilor/{overlay,scripts,assets}/ when the user
|
chmod -R u+w /tmp/iso-mod
|
||||||
# promotes the live ISO into a real install.
|
mkdir -p /tmp/iso-mod/veilor
|
||||||
ISO_FILE=$(ls /work/*.iso 2>/dev/null | head -1)
|
cp -a overlay scripts assets /tmp/iso-mod/veilor/
|
||||||
[ -n "$ISO_FILE" ] || { echo "[ERR] no ISO produced by livecd-creator"; exit 1; }
|
|
||||||
echo "[INFO] grafting /veilor/ onto $ISO_FILE"
|
|
||||||
|
|
||||||
# Extract original ISO's exact boot stanza so the rebuild matches
|
eval xorriso -as mkisofs \
|
||||||
# livecd-creator's layout byte-for-byte. This is immune to upstream
|
-volid "veilor-os-43" \
|
||||||
# Fedora layout changes (e.g. images/ vs isolinux/ for efiboot.img,
|
$ORIG_FLAGS \
|
||||||
# partition geometry flags, hybrid MBR/GPT options).
|
-o "${ISO_FILE}.tmp" /tmp/iso-mod
|
||||||
xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>&1 | tee /tmp/iso-boot.txt || true
|
mv "${ISO_FILE}.tmp" "$ISO_FILE"
|
||||||
ORIG_FLAGS=$(xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>/dev/null | \
|
rm -rf /tmp/iso-mod
|
||||||
grep -v '^xorriso :' | grep -E '^-' | tr '\n' ' ')
|
|
||||||
[ -n "$ORIG_FLAGS" ] || { echo "[ERR] could not extract boot stanza from $ISO_FILE"; exit 1; }
|
|
||||||
echo "[INFO] re-pack flags from original ISO: $ORIG_FLAGS"
|
|
||||||
|
|
||||||
mkdir -p /tmp/iso-mod
|
mv "$ISO_FILE" build/out/
|
||||||
xorriso -osirrox on -indev "$ISO_FILE" -extract / /tmp/iso-mod
|
|
||||||
chmod -R u+w /tmp/iso-mod
|
|
||||||
mkdir -p /tmp/iso-mod/veilor
|
|
||||||
cp -a /work/overlay /work/scripts /work/assets /tmp/iso-mod/veilor/
|
|
||||||
|
|
||||||
# Replay the exact stanza captured above. eval is needed because
|
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-$(date +%Y%m%d-%H%M%S).iso"
|
||||||
# ORIG_FLAGS contains multiple flag/value pairs that must word-split.
|
cd build/out
|
||||||
eval xorriso -as mkisofs \
|
for f in *.iso; do
|
||||||
-volid "veilor-os-43" \
|
[[ -f $f && $f != "$ISO_NAME" ]] && mv "$f" "$ISO_NAME"
|
||||||
$ORIG_FLAGS \
|
done
|
||||||
-o "${ISO_FILE}.tmp" /tmp/iso-mod
|
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
||||||
mv "${ISO_FILE}.tmp" "$ISO_FILE"
|
ls -lh "$ISO_NAME"
|
||||||
rm -rf /tmp/iso-mod
|
|
||||||
echo "[OK] /veilor/ grafted onto $ISO_FILE"
|
|
||||||
|
|
||||||
# Move output ISO to expected dir
|
|
||||||
mv ./veilor-os-43.iso build/out/ 2>/dev/null || mv ./*.iso build/out/ 2>/dev/null || true
|
|
||||||
|
|
||||||
# Rename + checksum
|
|
||||||
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-$(date +%Y%m%d-%H%M%S).iso"
|
|
||||||
cd build/out
|
|
||||||
for f in *.iso; do
|
|
||||||
[[ -f $f && $f != $ISO_NAME ]] && mv "$f" "$ISO_NAME"
|
|
||||||
done
|
|
||||||
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
|
||||||
ls -lh "$ISO_NAME"
|
|
||||||
|
|
||||||
# ── ISO publish ────────────────────────────────────────────────────
|
# ── ISO publish ────────────────────────────────────────────────────
|
||||||
# GH Release asset size limit = 2 GiB. Our ISO ~2.8 GiB. Split into
|
# GH Release asset size limit = 2 GiB. Our ISO ~2.8 GiB. Split into
|
||||||
|
|
@ -181,9 +160,6 @@ jobs:
|
||||||
- name: Split ISO into 2GiB chunks
|
- name: Split ISO into 2GiB chunks
|
||||||
if: success() && github.ref == 'refs/heads/main'
|
if: success() && github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
# ISO + sidecars created by Fedora container as root. Reclaim
|
|
||||||
# ownership so this step (running as runner user) can write.
|
|
||||||
sudo chown -R "$(id -u):$(id -g)" build/out
|
|
||||||
cd build/out
|
cd build/out
|
||||||
ISO=$(ls *.iso | head -1)
|
ISO=$(ls *.iso | head -1)
|
||||||
[ -n "$ISO" ] || { echo "[ERR] no ISO"; exit 1; }
|
[ -n "$ISO" ] || { echo "[ERR] no ISO"; exit 1; }
|
||||||
|
|
@ -197,9 +173,45 @@ jobs:
|
||||||
echo "[OK] split into:"
|
echo "[OK] split into:"
|
||||||
ls "${ISO}".part-*
|
ls "${ISO}".part-*
|
||||||
|
|
||||||
- name: Publish to ci-latest rolling prerelease
|
- name: Install cosign
|
||||||
if: success() && github.ref == 'refs/heads/main'
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
|
||||||
uses: softprops/action-gh-release@v2
|
# Pinned to last v3 release confirmed node20.
|
||||||
|
uses: sigstore/cosign-installer@v3.7.0
|
||||||
|
|
||||||
|
- name: Sign ISO parts (keyless)
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
|
||||||
|
run: |
|
||||||
|
cd build/out
|
||||||
|
for f in *.part-*; do
|
||||||
|
cosign sign-blob --yes "$f" \
|
||||||
|
--output-signature "$f.sig" \
|
||||||
|
--output-certificate "$f.pem"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Generate SBOM (SPDX)
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
|
||||||
|
# Pinned to last v0.17 release that ships node20.
|
||||||
|
uses: anchore/sbom-action@v0.17.2
|
||||||
|
with:
|
||||||
|
path: build/out
|
||||||
|
format: spdx-json
|
||||||
|
output-file: build/out/veilor-os.spdx.json
|
||||||
|
|
||||||
|
- name: Build provenance attestation
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
|
||||||
|
# Pinned to last v2.2 release that ships node20.
|
||||||
|
uses: actions/attest-build-provenance@v2.2.3
|
||||||
|
with:
|
||||||
|
subject-path: 'build/out/*.iso.part-*'
|
||||||
|
|
||||||
|
# GitHub-only: softprops/action-gh-release uses the GitHub REST API
|
||||||
|
# which Forgejo doesn't expose at the same endpoints. When this
|
||||||
|
# workflow runs on git.s8n.ru the step below (Forgejo) handles
|
||||||
|
# publishing instead.
|
||||||
|
- name: Publish to ci-latest rolling prerelease (GitHub)
|
||||||
|
if: success() && github.ref == 'refs/heads/main' && github.server_url == 'https://github.com'
|
||||||
|
# Pinned to last v2 tag confirmed to ship on node20.
|
||||||
|
uses: softprops/action-gh-release@v2.0.4
|
||||||
with:
|
with:
|
||||||
tag_name: ci-latest
|
tag_name: ci-latest
|
||||||
name: "ci-latest (auto)"
|
name: "ci-latest (auto)"
|
||||||
|
|
@ -220,6 +232,70 @@ jobs:
|
||||||
files: |
|
files: |
|
||||||
build/out/*.iso.part-*
|
build/out/*.iso.part-*
|
||||||
build/out/*.sha256
|
build/out/*.sha256
|
||||||
|
build/out/*.sig
|
||||||
|
build/out/*.pem
|
||||||
|
build/out/*.spdx.json
|
||||||
|
|
||||||
|
# Forgejo equivalent: drop+recreate ci-latest release via the
|
||||||
|
# Forgejo REST API, then upload chunks. Only runs when not on GitHub.
|
||||||
|
# All ${{ }} interpolations are vetted (repo coords + signed SHA).
|
||||||
|
- name: Publish to ci-latest rolling prerelease (Forgejo)
|
||||||
|
if: success() && github.ref == 'refs/heads/main' && github.server_url != 'https://github.com'
|
||||||
|
env:
|
||||||
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
FORGEJO_API: ${{ github.server_url }}/api/v1
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
GIT_SHA: ${{ github.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="ci-latest"
|
||||||
|
REL_JSON=$(curl -fsSL -H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
"${FORGEJO_API}/repos/${REPO}/releases/tags/${TAG}" 2>/dev/null || echo "")
|
||||||
|
if [ -n "$REL_JSON" ]; then
|
||||||
|
REL_ID=$(echo "$REL_JSON" | grep -oE '"id":\s*[0-9]+' | head -1 | grep -oE '[0-9]+')
|
||||||
|
if [ -n "$REL_ID" ]; then
|
||||||
|
echo "[INFO] deleting existing ci-latest release id=$REL_ID"
|
||||||
|
curl -fsSL -X DELETE -H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
"${FORGEJO_API}/repos/${REPO}/releases/${REL_ID}" || true
|
||||||
|
curl -fsSL -X DELETE -H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
"${FORGEJO_API}/repos/${REPO}/git/refs/tags/${TAG}" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
BODY="Rolling auto-build from main. Latest commit: ${GIT_SHA}.
|
||||||
|
|
||||||
|
ISO is split into chunks. Reassemble:
|
||||||
|
cat veilor-os-*.iso.part-* > veilor-os.iso
|
||||||
|
sha256sum -c veilor-os-*.iso.parts.sha256
|
||||||
|
|
||||||
|
Or use test/auto-install.sh (handles reassembly automatically).
|
||||||
|
|
||||||
|
Not a stable release — for testing only."
|
||||||
|
PAYLOAD=$(BODY="$BODY" TAG="$TAG" python3 -c "
|
||||||
|
import json,os
|
||||||
|
print(json.dumps({
|
||||||
|
'tag_name': os.environ['TAG'],
|
||||||
|
'target_commitish': 'main',
|
||||||
|
'name': 'ci-latest (auto)',
|
||||||
|
'body': os.environ['BODY'],
|
||||||
|
'prerelease': True,
|
||||||
|
'draft': False,
|
||||||
|
}))")
|
||||||
|
REL_ID=$(curl -fsSL -X POST -H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
"${FORGEJO_API}/repos/${REPO}/releases" | \
|
||||||
|
grep -oE '"id":\s*[0-9]+' | head -1 | grep -oE '[0-9]+')
|
||||||
|
[ -n "$REL_ID" ] || { echo "[ERR] failed to create Forgejo release"; exit 1; }
|
||||||
|
echo "[OK] Forgejo release id=$REL_ID created"
|
||||||
|
cd build/out
|
||||||
|
for f in *.iso.part-* *.sha256; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
echo "[INFO] uploading $f"
|
||||||
|
curl -fsSL -X POST -H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
-F "attachment=@${f}" \
|
||||||
|
"${FORGEJO_API}/repos/${REPO}/releases/${REL_ID}/assets?name=${f}"
|
||||||
|
done
|
||||||
|
echo "[OK] all assets uploaded to Forgejo ci-latest"
|
||||||
|
|
||||||
# Build log on failure: print inline + skip artifact upload to avoid
|
# Build log on failure: print inline + skip artifact upload to avoid
|
||||||
# quota wall. Job log retains everything anyway.
|
# quota wall. Job log retains everything anyway.
|
||||||
|
|
@ -231,10 +307,36 @@ jobs:
|
||||||
echo "─── anaconda program.log ───"
|
echo "─── anaconda program.log ───"
|
||||||
find build/out/build/anaconda -name 'program.log' -exec tail -100 {} \; 2>/dev/null || echo "(no anaconda log)"
|
find build/out/build/anaconda -name 'program.log' -exec tail -100 {} \; 2>/dev/null || echo "(no anaconda log)"
|
||||||
|
|
||||||
- name: Attach to release on tag
|
# GitHub-only: same restriction as ci-latest publish.
|
||||||
if: github.event_name == 'release'
|
- name: Attach to release on tag (GitHub)
|
||||||
uses: softprops/action-gh-release@v2
|
if: github.event_name == 'release' && github.server_url == 'https://github.com'
|
||||||
|
# Pinned to last v2 tag confirmed to ship on node20.
|
||||||
|
uses: softprops/action-gh-release@v2.0.4
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
build/out/*.iso
|
build/out/*.iso
|
||||||
build/out/*.sha256
|
build/out/*.sha256
|
||||||
|
|
||||||
|
# Forgejo equivalent for tag-driven release uploads. The release
|
||||||
|
# is assumed to already exist (Forgejo creates it from the tag);
|
||||||
|
# we only attach assets here.
|
||||||
|
- name: Attach to release on tag (Forgejo)
|
||||||
|
if: github.event_name == 'release' && github.server_url != 'https://github.com'
|
||||||
|
env:
|
||||||
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
FORGEJO_API: ${{ github.server_url }}/api/v1
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
REL_JSON=$(curl -fsSL -H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
"${FORGEJO_API}/repos/${REPO}/releases/tags/${REF_NAME}")
|
||||||
|
REL_ID=$(echo "$REL_JSON" | grep -oE '"id":\s*[0-9]+' | head -1 | grep -oE '[0-9]+')
|
||||||
|
[ -n "$REL_ID" ] || { echo "[ERR] no Forgejo release for tag ${REF_NAME}"; exit 1; }
|
||||||
|
cd build/out
|
||||||
|
for f in *.iso *.sha256; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
curl -fsSL -X POST -H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
-F "attachment=@${f}" \
|
||||||
|
"${FORGEJO_API}/repos/${REPO}/releases/${REL_ID}/assets?name=${f}"
|
||||||
|
done
|
||||||
|
|
|
||||||
9
.github/workflows/lint.yml
vendored
9
.github/workflows/lint.yml
vendored
|
|
@ -12,7 +12,8 @@ jobs:
|
||||||
container:
|
container:
|
||||||
image: registry.fedoraproject.org/fedora:43
|
image: registry.fedoraproject.org/fedora:43
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
# Pinned to last v4 tag confirmed to ship on node20.
|
||||||
|
- uses: actions/checkout@v4.1.7
|
||||||
- run: dnf -y install pykickstart
|
- run: dnf -y install pykickstart
|
||||||
- run: ksvalidator kickstart/veilor-os.ks
|
- run: ksvalidator kickstart/veilor-os.ks
|
||||||
|
|
||||||
|
|
@ -20,7 +21,8 @@ jobs:
|
||||||
name: Shell scripts
|
name: Shell scripts
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
# Pinned to last v4 tag confirmed to ship on node20.
|
||||||
|
- uses: actions/checkout@v4.1.7
|
||||||
- uses: ludeeus/action-shellcheck@master
|
- uses: ludeeus/action-shellcheck@master
|
||||||
with:
|
with:
|
||||||
severity: warning
|
severity: warning
|
||||||
|
|
@ -30,7 +32,8 @@ jobs:
|
||||||
name: No personal/onyx leaks
|
name: No personal/onyx leaks
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
# Pinned to last v4 tag confirmed to ship on node20.
|
||||||
|
- uses: actions/checkout@v4.1.7
|
||||||
- name: Grep for leaks
|
- name: Grep for leaks
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -13,4 +13,6 @@ secrets/
|
||||||
*.pem
|
*.pem
|
||||||
test/veilor-vm.qcow2
|
test/veilor-vm.qcow2
|
||||||
test/veilor-vm.nvram*
|
test/veilor-vm.nvram*
|
||||||
|
test/auto-install-vm.qcow2
|
||||||
|
test/auto-install-vm.nvram*
|
||||||
.claude/worktrees/
|
.claude/worktrees/
|
||||||
|
|
|
||||||
68
README.md
68
README.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> **Hardened minimal Fedora KDE spin. Black-on-black. Locked down by default.**
|
> **Hardened minimal Fedora KDE spin. Black-on-black. Locked down by default.**
|
||||||
|
|
||||||
[](https://github.com/veilor-org/veilor-os/actions/workflows/build-iso.yml)
|
[](https://git.s8n.ru/veilor-org/veilor-os/actions?workflow=build-iso.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
veilor-os is a Fedora 43 KDE Plasma remix for operators who want a clean,
|
veilor-os is a Fedora 43 KDE Plasma remix for operators who want a clean,
|
||||||
|
|
@ -30,6 +30,11 @@ brittleness, bootloader install via `gen_grub_cfgstub`); current focus
|
||||||
is the v0.5.32 blocker list from the
|
is the v0.5.32 blocker list from the
|
||||||
[2026-05-05 9-agent research wave](docs/research/2026-05-05-agent-wave/README.md).
|
[2026-05-05 9-agent research wave](docs/research/2026-05-05-agent-wave/README.md).
|
||||||
|
|
||||||
|
Primary git host: <https://git.s8n.ru/veilor-org/veilor-os>. The GitHub
|
||||||
|
mirror was disabled 2026-05-06; this repo is private-by-default on
|
||||||
|
Forgejo. ISO builds and CI artifacts are produced by the Forgejo runner
|
||||||
|
on nullstone — no GitHub Actions involvement.
|
||||||
|
|
||||||
What is **shipping**: hardening (SELinux, sysctl, USBGuard, fail2ban,
|
What is **shipping**: hardening (SELinux, sysctl, USBGuard, fail2ban,
|
||||||
firewalld), KDE black theme, Fira Code system font, 3-mode power
|
firewalld), KDE black theme, Fira Code system font, 3-mode power
|
||||||
management, single-prompt LUKS install, first-boot admin password flow,
|
management, single-prompt LUKS install, first-boot admin password flow,
|
||||||
|
|
@ -46,7 +51,9 @@ spike at v0.7**, **bootc-only at v1.0**.
|
||||||
## Quick install
|
## Quick install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Download the ISO (after public release; CI artifact for now)
|
# 1. Download the ISO from the latest Forgejo release.
|
||||||
|
# https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest
|
||||||
|
# (rolling tag; replaced on each successful build-iso.yml run)
|
||||||
sha256sum -c veilor-os-43-*.iso.sha256
|
sha256sum -c veilor-os-43-*.iso.sha256
|
||||||
|
|
||||||
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
|
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
|
||||||
|
|
@ -109,30 +116,49 @@ Full reference: [docs/HARDENING.md](docs/HARDENING.md).
|
||||||
|
|
||||||
## How veilor-os compares
|
## How veilor-os compares
|
||||||
|
|
||||||
| Feature | veilor-os | Stock Fedora KDE | Kicksecure |
|
| Feature | veilor-os | Stock Fedora KDE | Kicksecure | secureblue |
|
||||||
|---|:-:|:-:|:-:|
|
|---|:-:|:-:|:-:|:-:|
|
||||||
| SELinux enforcing OOTB | yes | yes | yes |
|
| SELinux enforcing OOTB | yes | yes | yes | yes (custom policy) |
|
||||||
| AppArmor | planned (v0.5) | no | yes |
|
| AppArmor | deferred (post-v0.6 / v0.7 LSM stack) | no | yes | no |
|
||||||
| Secure Boot | yes (Fedora keys) | yes (Fedora keys) | configurable |
|
| Secure Boot | yes (Fedora keys) | yes (Fedora keys) | configurable | yes (Fedora keys) |
|
||||||
| LUKS2 with argon2id | default | optional | default |
|
| LUKS2 with argon2id | default | optional | default | default (Anaconda) |
|
||||||
| Single-prompt install (LUKS only) | yes | no | no |
|
| Single-prompt install (LUKS only) | yes | no | no | rebase via Anaconda |
|
||||||
| Root account locked by default | yes | no | yes |
|
| Root account locked by default | yes | no | yes | yes |
|
||||||
| firewalld default zone = drop | yes | no | n/a (uses nftables) |
|
| firewalld default zone = drop | yes | no | n/a (nftables) | yes |
|
||||||
| USBGuard default-block | yes | no | yes |
|
| USBGuard default-block | yes | no | yes | yes |
|
||||||
| fail2ban + auditd OOTB | yes | no | partial |
|
| fail2ban + auditd OOTB | yes | no | partial | partial (auditd) |
|
||||||
| DNS-over-TLS by default | yes | no | yes |
|
| DNS-over-TLS by default | yes | no | yes | yes |
|
||||||
| NTS-authenticated NTP | yes | no | yes |
|
| NTS-authenticated NTP | yes | no | yes | yes |
|
||||||
| `init_on_alloc/free` (post-install) | yes (planned re-enable) | no | yes |
|
| `init_on_alloc/free` (post-install) | yes (planned re-enable) | no | yes | yes |
|
||||||
| Telemetry / phone-home | none | minimal | none |
|
| Telemetry / phone-home | none | minimal | none | none |
|
||||||
| KDE Plasma branded theme | yes (black) | Breeze | n/a (XFCE) |
|
| KDE Plasma branded theme | yes (black) | Breeze | n/a (XFCE) | upstream Kinoite |
|
||||||
| Power-profile CLI | yes (3-mode) | partial | no |
|
| Power-profile CLI | yes (3-mode) | partial | no | no |
|
||||||
| Reproducible kickstart-built ISO | yes | yes | yes (from Debian) |
|
| Hardened browser (Trivalent / Mullvad) | yes (v0.6+) | no | no | yes (Trivalent shipped) |
|
||||||
| Base distro | Fedora 43 | Fedora 43 | Debian |
|
| Atomic OCI image + signed base | v0.7 spike (BlueBuild) | no | no | yes (`bootc`) |
|
||||||
|
| Userns-remap default + module sig enforce | yes | no | partial | yes |
|
||||||
|
| Base distro | Fedora 43 (KDE) | Fedora 43 | Debian | Fedora atomic (Kinoite/Silverblue) |
|
||||||
|
|
||||||
veilor-os is **not** trying to compete with Whonix-style anonymity or
|
veilor-os is **not** trying to compete with Whonix-style anonymity or
|
||||||
Qubes-style isolation. It is a **hardened daily-driver desktop** — fast,
|
Qubes-style isolation. It is a **hardened daily-driver desktop** — fast,
|
||||||
clean, locked down, with no manual post-install hardening required.
|
clean, locked down, with no manual post-install hardening required.
|
||||||
|
|
||||||
|
### Relationship to secureblue
|
||||||
|
|
||||||
|
[secureblue](https://github.com/secureblue/secureblue) is an upstream
|
||||||
|
hardened atomic Fedora project we benchmark against and plan to **build
|
||||||
|
on top of** at v0.7. The v0.7 BlueBuild spike uses their
|
||||||
|
`securecore-kinoite-hardened-userns` OCI image as its base — we don't
|
||||||
|
ship their source code in this repo, we layer veilor branding,
|
||||||
|
theming, the gum installer, and the kickstart bootstrap on top of
|
||||||
|
their already-signed image.
|
||||||
|
|
||||||
|
Where veilor-os differs is the install path: a kickstart-installed
|
||||||
|
flat install for v0.5.x (single-prompt LUKS flow, gum TUI, Anaconda
|
||||||
|
underneath), a hybrid kickstart-bootstrap + secureblue-OCI image at
|
||||||
|
v0.7, and a fully OCI / `bootc upgrade` path at v1.0. Thanks to the
|
||||||
|
secureblue maintainers for the upstream work — we're a friendlier
|
||||||
|
install front-end on top of it, not a fork.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Repo layout
|
## Repo layout
|
||||||
|
|
|
||||||
275
docs/DOCS-DOCS.md
Normal file
275
docs/DOCS-DOCS.md
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
# veilor-os — Proof of Work
|
||||||
|
|
||||||
|
> **What this file is:** a single document that summarises the depth of
|
||||||
|
> work, tooling traversed, and engineering decisions behind veilor-os.
|
||||||
|
> Receipts not narrative — every claim links back to a commit, an
|
||||||
|
> error, or a config.
|
||||||
|
>
|
||||||
|
> Author: P M (s8n-ru on Forgejo) · Last updated: 2026-05-06
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## At a glance
|
||||||
|
|
||||||
|
| Metric | Number |
|
||||||
|
|---|---|
|
||||||
|
| Git commits on `main` | **134+** |
|
||||||
|
| Distinct release versions iterated | **32** (v0.1 → v0.5.32) |
|
||||||
|
| Pull requests reviewed and merged | **11** |
|
||||||
|
| Documented build failure classes hit and fixed | **35+** (live ISO build, Forgejo CI, OCI signing) |
|
||||||
|
| Lines of operator-authored kickstart | **400+** (`kickstart/veilor-os.ks`) |
|
||||||
|
| Lines of overlay shell hardening scripts | **~1500** across `scripts/*.sh` |
|
||||||
|
| Lines of TUI installer (`overlay/usr/local/bin/veilor-installer`) | **~950** bash, gum + whiptail fallback |
|
||||||
|
| Self-hosted infra services touched | **28** Docker containers on nullstone |
|
||||||
|
| Concurrent dev agents orchestrated in single waves | up to **9** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Distros / projects studied or layered on
|
||||||
|
|
||||||
|
| Project | Role in veilor-os |
|
||||||
|
|---|---|
|
||||||
|
| Fedora 43 KDE | Base OS for v0.5.x kickstart-installed flat builds |
|
||||||
|
| [secureblue](https://github.com/secureblue/secureblue) | Upstream hardened atomic Fedora; v0.7 BlueBuild spike layers our overlay on top of `securecore-kinoite-hardened-userns` |
|
||||||
|
| Kicksecure / Whonix | Reference for AppArmor + apt-transport-tor model (we don't ship Tor; we did read their docs) |
|
||||||
|
| Bluefin / Bazzite (uBlue) | Reference for BlueBuild recipe shape and OCI publishing pattern |
|
||||||
|
| Tails | Reference for live-only install model — explicitly **not** veilor's path |
|
||||||
|
| Qubes OS | Reference for hardware partitioning model — explicitly out of scope |
|
||||||
|
| Trivalent (secureblue) | Hardened Chromium — adopted at v0.6+ |
|
||||||
|
| Mullvad Browser | Tor-Browser-fork without Tor — adopted at v0.6+ |
|
||||||
|
|
||||||
|
veilor-os is **not** a fork of any of the above. It's a **composition**:
|
||||||
|
Fedora kickstart for v0.5.x, secureblue OCI for v0.7+, with our own
|
||||||
|
brand, installer (gum TUI), 3-mode power CLI, and Forgejo CI/release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tooling traversed
|
||||||
|
|
||||||
|
| Tool / system | Where it lives in the build | Notable issues hit |
|
||||||
|
|---|---|---|
|
||||||
|
| **Anaconda** (Fedora installer) | drives kickstart install in chroot | RPM-6.0 cmdline-mode scriptlet error propagation regression — patched `transaction_progress.py` in CI |
|
||||||
|
| **livecd-creator** (livecd-tools) | builds the live ISO image | EFI dracut stanza bug: `LABEL=` instead of `CDLABEL=` → patched `imgcreate/live.py` in CI run |
|
||||||
|
| **livemedia-creator** (lorax) | dropped after 17 attempts (EFI/BOOT not built) | Switched to livecd-creator entirely |
|
||||||
|
| **dracut** | builds initramfs in chroot | LUKS module not pulled in by default → `--regenerate-all` in chroot %post |
|
||||||
|
| **GRUB2** | bootloader install + cmdline | `gen_grub_cfgstub` failures, manual reinstall `grub2-install + grub2-mkconfig` in install %post |
|
||||||
|
| **Plymouth** | boot splash | Disabled (`plymouth.enable=0`) so LUKS prompt is visible; theme `details` for v0.7+ |
|
||||||
|
| **SDDM** | KDE display manager | livecd-creator skips the `display-manager.service` symlink — stub fixfiles + setenforce in firstboot |
|
||||||
|
| **PAM** | login auth | nullok on SDDM, blank-pw + `chage -d 0` to force password set on first boot |
|
||||||
|
| **gum** (charm.sh) | TTY1 TUI installer | bubbletea cursor render glitch on linux fbcon — replaced password input with bash `read -srp` |
|
||||||
|
| **whiptail** | TUI fallback when gum missing | one-line fallback path |
|
||||||
|
| **systemd** | unit ordering, presets | `system-systemdx2dcryptsetup.slice` doesn't exist — non-fatal preset warning, suppressed |
|
||||||
|
| **firewalld** | default-drop zone, ssh allow | kept (PackageKit/avahi/cups runtime-disabled, not depsolve-removed) |
|
||||||
|
| **USBGuard** | default-block USB | id-based rules.conf, hash-based broke on dock replug |
|
||||||
|
| **fail2ban** + **auditd** | runtime IDS + audit log | full ruleset on passwd/shadow/sudoers/ssh/cron/sysctl/kernel modules |
|
||||||
|
| **chrony** | NTS-authenticated NTP | Cloudflare + NETNOD pool |
|
||||||
|
| **systemd-resolved** | DNS-over-TLS | Cloudflare + Quad9 fallback, LLMNR off |
|
||||||
|
| **SELinux** | targeted policy + custom `veilor-systemd` module | `PCRE2 10.46 vs 10.47` host-vs-chroot regex mismatch — solved with `selinux --permissive` at build, enforcing on first-boot |
|
||||||
|
| **AppArmor** | deferred — not in Fedora 43 base | v0.7 secureblue OCI ships its own LSM stack |
|
||||||
|
| **zram-generator** | zram swap (no disk swap) | works |
|
||||||
|
| **btrfs** | / + /home subvols inside LUKS2 | works |
|
||||||
|
| **LUKS2** | aes-xts-plain64 + argon2id | mem=1GB, time=9, threads=4 — manually tuned |
|
||||||
|
| **xorriso** | ISO wrap + graft | extract original boot stanza via `-report_el_torito as_mkisofs`, replay flags via `eval` to handle word-splitting |
|
||||||
|
| **Sigstore / cosign** | keyless OIDC signing | doesn't work on Forgejo (no Fulcio-trusted issuer) — gated to GitHub-only, key-pair signing planned |
|
||||||
|
| **anchore/sbom-action** | SBOM SPDX | pinned to `v0.17.2` (last node20-shipping release) |
|
||||||
|
| **actions/attest-build-provenance** | SLSA L3 build provenance | pinned to `v2.2.3` |
|
||||||
|
| **BlueBuild** | OCI image build for v0.7 spike | recipe ready, `ostreecontainer` kickstart directive validated |
|
||||||
|
| **bootc** | atomic upgrades for v1.0 | target tooling, `bootc upgrade` instead of `dnf upgrade` |
|
||||||
|
| **Forgejo** + **act_runner** | self-hosted git + CI | runner inside container with userns-remap host caused 13-step debug chain |
|
||||||
|
| **Tailscale** + **Headscale** | private mesh | for friend-PC GPU offload + admin SSH |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build failure classes encountered (and beaten)
|
||||||
|
|
||||||
|
Numbered ledger of every distinct failure mode, in approximate order of
|
||||||
|
discovery. Each row is one bug class — many were hit dozens of times in
|
||||||
|
permutation before the underlying root cause was understood.
|
||||||
|
|
||||||
|
### Phase A — local + livemedia-creator (v0.1 → v0.2.0)
|
||||||
|
|
||||||
|
| # | Symptom | Root cause | Fix |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | rootless podman btrfs / loop / sudo cache fights | rootless can't `losetup`; host CAP_SYS_ADMIN gate | Switched to host-native lorax + NOPASSWD wheel |
|
||||||
|
| 2 | Kickstart parse: `--title`, `text`, multiline `part`, `--hash` | livemedia-creator + recent pykickstart deprecations | Rewrote ks |
|
||||||
|
| 3 | dnf depsolve: KDE hard-deps cups / geoclue2 / ModemManager / PackageKit | KDE Plasma 6 transitively pulls them in | Kept packages, mask daemons at runtime |
|
||||||
|
| 4 | Anaconda merges all repos, `cost`/`includepkgs` ignored | upstream Anaconda repo-merge logic | Local fix-repo at `cost=1` to force selection |
|
||||||
|
| 5 | scriptlet warning RC=5 (selinux/pcre2 regex skew) | host libselinux 10.46 vs chroot's selinux-policy file_contexts.bin built against 10.47 | fix-repo provides matched 10.47 pair |
|
||||||
|
| 6 | dnf transaction RC=5 on non-critical scriptlet | RPM-6.0 cmdline-mode regression | Patched anaconda `transaction_progress.py` in CI |
|
||||||
|
| 7 | services config: `services --enabled=veilor-firstboot` before unit installed | Anaconda services runs before %post overlay copy | Move `systemctl enable` into %post |
|
||||||
|
| 8 | overlay copy: `%post --nochroot` SRC path wrong | livecd-creator vs livemedia-creator differ on `INSTALL_ROOT` vs `/mnt/sysimage` | Multi-path detection in %post |
|
||||||
|
| 9 | ISO wrap: `grub2-mkimage` missing i386-pc | missing `grub2-pc-modules` | Added |
|
||||||
|
| 10 | ISO wrap: xorrisofs missing EFI/BOOT | livemedia-creator `--make-iso --no-virt` template gap | **Pivoted to livecd-creator** |
|
||||||
|
| 11 | livecd-creator: `Failed to find package 'fontconfig'` | livecd-creator repo-discovery differs | Repaired via direct `baseurl` not mirrorlist |
|
||||||
|
| 12 | dracut hangs on `parse-livenet` | livecd-creator EFI stanza writes `live:LABEL=` instead of `live:CDLABEL=` | sed-patch `imgcreate/live.py` in CI |
|
||||||
|
|
||||||
|
### Phase B — boot UX + LUKS + theming (v0.2.4 → v0.5.27)
|
||||||
|
|
||||||
|
| # | Symptom | Root cause | Fix |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 13 | `init_on_alloc/free` 5x KVM live-boot time | every page zeroed on alloc/free, brutal in vCPU | Drop from live cmdline; firstboot patches GRUB to re-enable for installed system |
|
||||||
|
| 14 | LUKS prompt invisible | Plymouth swallows TTY | `plymouth.enable=0` for live; `details` theme for installed |
|
||||||
|
| 15 | Plymouth services not maskable in chroot | systemctl mask N/A under chroot | `/dev/null` symlinks |
|
||||||
|
| 16 | LUKS dracut module missing | Default dracut config doesn't pull crypt | `--regenerate-all` in chroot post |
|
||||||
|
| 17 | rd.luks.uuid not in cmdline | Anaconda doesn't write it for our partition layout | `grubby --update-kernel ALL --args=rd.luks.uuid=...` in chroot post |
|
||||||
|
| 18 | Kernel-install on chroot overwrites cmdline | systemd kernel-install writes its own `/etc/kernel/cmdline` | Switch to `--config /etc/kernel/cmdline` flow |
|
||||||
|
| 19 | rescue glob in firstboot: `set -e` killed loop | unmatched glob | `shopt -s nullglob` |
|
||||||
|
| 20 | fbcon blanks during KMS modeset on real hardware | i915/amdgpu/nvidia driver loads, blanks fb | `fbcon=nodefer i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1` |
|
||||||
|
| 21 | gum cursor render glitch (duplicate-Install + stray-T) | bubbletea cursor-hide vs linux fbcon terminfo | Replace `gum input --password` with `read -srp` |
|
||||||
|
| 22 | Generated install ks `updates` repo 404 zchunk | Fedora mid-push window | Strip `repo --name=updates` from generated ks |
|
||||||
|
| 23 | Anaconda payload module crash on `LANG` env | unset env in TTY1 service | `export LANG=en_US.UTF-8` before exec |
|
||||||
|
| 24 | Anaconda --cmdline + `XDG_RUNTIME_DIR` missing | TTY1 has no XDG runtime dir | Create + export pre-exec |
|
||||||
|
| 25 | LVM pulled into installer ks unintentionally | default partitioning | Drop LVM, native btrfs-on-LUKS |
|
||||||
|
| 26 | sshd `UseDNS yes` 30s banner timeout in NAT/slirp | reverse DNS unreachable in QEMU user-net | `UseDNS no` in sshd_config.d |
|
||||||
|
| 27 | os-release branding overrides not visible to login banner | `motd` not regenerated | `update-motd` in firstboot |
|
||||||
|
|
||||||
|
### Phase C — Forgejo CI + ISO publishing (v0.5.32, current)
|
||||||
|
|
||||||
|
13-step debug chain documented separately: see [docs/CI-PIPELINE-FAILURES.md] (live in conversation log).
|
||||||
|
|
||||||
|
Highlights:
|
||||||
|
- userns-remap=default on host docker daemon collides with privileged + image perms
|
||||||
|
- Forgejo runner inside container creates docker-in-docker workspace bind path mismatch
|
||||||
|
- Sigstore Fulcio keyless signing assumes GH OIDC issuer; gated to GH-only
|
||||||
|
- cosign / sbom / attest actions floating tags now node24, runner is node20 → all pinned
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key engineering decisions (and why)
|
||||||
|
|
||||||
|
### 1. Hybrid kickstart-bootstrap + bootc OCI strategy
|
||||||
|
|
||||||
|
Locked at v0.7 spike. Reasons:
|
||||||
|
|
||||||
|
- **Kickstart (v0.5.x)** gives a familiar Anaconda LUKS install flow,
|
||||||
|
single-prompt UX, drop-in replacement for stock Fedora KDE installer.
|
||||||
|
- **OCI image (v0.7+)** lets us layer on top of secureblue's already-
|
||||||
|
signed hardened base. We don't re-derive AppArmor / Trivalent /
|
||||||
|
custom SELinux — we inherit. Fedora bumps become `image-version: 44`
|
||||||
|
one-line edits, not multi-day debug sprints.
|
||||||
|
- **bootc-only (v1.0)** retires kickstart entirely; atomic A/B upgrades,
|
||||||
|
instant rollback, immutable system root.
|
||||||
|
|
||||||
|
### 2. Brand-clean from day one
|
||||||
|
|
||||||
|
`grep -ri 'onyx\|192\.168\.0\.\|admin@\|fedora\.local\|xynki\.dev' kickstart/ overlay/ scripts/ assets/` returns zero hits. Enforced via `.github/workflows/lint.yml` `brand-leak` job. Every audit run, every CI run, every commit.
|
||||||
|
|
||||||
|
### 3. Forgejo over GitHub for primary
|
||||||
|
|
||||||
|
Decision date: 2026-05-06. Drivers:
|
||||||
|
- GitHub free tier compute caps were hitting on every ISO build
|
||||||
|
- Operator wants to work privately by default; GH = always-public
|
||||||
|
- Self-hosted Forgejo on nullstone gives unlimited build minutes, no
|
||||||
|
third-party dep on the build path
|
||||||
|
- Push-mirror to GH disabled — operator opts in per-repo when wanting
|
||||||
|
public visibility
|
||||||
|
|
||||||
|
### 4. ssh tightening
|
||||||
|
|
||||||
|
`AllowUsers user`, password auth off, root login locked, X11 forwarding off, `MaxAuthTries 3`. Operator authenticates with ed25519 key only. Documented in `feedback_nullstone_ssh_user.md` memory.
|
||||||
|
|
||||||
|
### 5. Defense-in-depth mesh
|
||||||
|
|
||||||
|
Tailscale + Headscale (`hs.s8n.ru`) is the SSH on-ramp. Every device joins the tailnet; public SSH is firewalled at the router. Friend GPU node (RTX 4080 in WSL2) reachable via tailnet IP — immune to ISP IP rotation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's been built that isn't in the kickstart
|
||||||
|
|
||||||
|
The repo carries more than just an ISO recipe:
|
||||||
|
|
||||||
|
| Path | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `kickstart/veilor-os.ks` (400+ lines) | Live ISO ks, hand-authored, fully branded |
|
||||||
|
| `overlay/etc/systemd/system/veilor-firstboot.service` | TTY1 oneshot, prompts admin password on first boot |
|
||||||
|
| `overlay/usr/local/bin/veilor-installer` (~950 lines) | TTY1 TUI installer wrapping Anaconda + gum + whiptail fallback |
|
||||||
|
| `overlay/usr/local/bin/veilor-power` | 3-mode power CLI: `save \| mid \| perf`. Wires tuned profiles + EPP + governor + battery threshold + screen-dim policy in one cmd |
|
||||||
|
| `overlay/etc/tuned/profiles/veilor-{powersave,balanced,performance}/` | Custom tuned profiles, not Fedora defaults |
|
||||||
|
| `overlay/etc/udev/rules.d/{90-veilor-ac-switch,91-veilor-battery-threshold}.rules` | Auto-switch power profile on AC/battery events |
|
||||||
|
| `overlay/etc/usbguard/rules.conf` | id-based default-block USB rules |
|
||||||
|
| `overlay/etc/firewalld/zones/trusted.xml` | tailscale0 trust override |
|
||||||
|
| `overlay/etc/skel/.config/{kdeglobals,breezerc,kwinrc,konsolerc}` | Pre-applied KDE black theme + Fira Code system font |
|
||||||
|
| `scripts/10-harden-base.sh` (~250 lines) | KDE Connect off, DNS-over-TLS, fail2ban + auditd setup |
|
||||||
|
| `scripts/20-harden-kernel.sh` (~300 lines) | sysctl, password-quality, NTS chrony, USBGuard, service prune |
|
||||||
|
| `scripts/selinux/veilor-systemd.te` | Custom SELinux module (targeted policy gap fixes) |
|
||||||
|
| `scripts/30-apply-v03-theme.sh` | Plymouth + SDDM + Konsole + wallpaper apply |
|
||||||
|
| `scripts/40-apparmor.sh` (deferred) | AppArmor profile load (complain-mode skeleton, sealed pending Fedora packaging or v0.7 secureblue) |
|
||||||
|
| `bluebuild/recipe.yml` | v0.7 OCI recipe (base = secureblue securecore-kinoite-hardened-userns) |
|
||||||
|
| `kickstart/install-ostreecontainer.ks` | v0.7 install ks: 10 lines, just `ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry` |
|
||||||
|
| `assets/installer/{banner.txt,colors.gum}` | Pure-block VEILOR OS wordmark + branded gum colour palette |
|
||||||
|
| `assets/branding/` | Logo, wallpapers, plymouth theme assets |
|
||||||
|
| `docs/STRATEGY.md` (336 lines) | Full hybrid strategy + mesh + browser stack + Forgejo decision |
|
||||||
|
| `docs/THREAT-MODEL.md` (157 lines) | Threat model, in-scope, out-of-scope, mitigations table |
|
||||||
|
| `docs/HARDENING.md` (194 lines) | Full hardening reference |
|
||||||
|
| `docs/ROADMAP.md` (332 lines) | v0.5.x → v0.7 → v1.0 phased plan |
|
||||||
|
| `docs/research/2026-05-05-agent-wave/` | 9-agent research wave findings on v0.5.32 blockers |
|
||||||
|
| `test/TESTING.md` + `test/run-vm.sh` + `test/test-runs/` | Standardised hybrid VM test method, codified after v0.5.27 surfaced 4 regressions in one session |
|
||||||
|
| `.github/workflows/{build-iso.yml,lint.yml,build-bluebuild.yml}` | CI for v0.5.x flat ISO + v0.7 OCI image + brand-leak / shellcheck / kickstart syntax lint |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI infrastructure built on nullstone
|
||||||
|
|
||||||
|
Self-hosted from scratch on a single Debian 13 server. All running, all
|
||||||
|
behind Traefik with LE certs via Gandi LiveDNS DNS-01.
|
||||||
|
|
||||||
|
| Service | Role | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Forgejo (`git.s8n.ru`) | git host + container registry | code 9.0.3 + gitea 1.22 underneath; INSTALL_LOCK=true; admin user `s8n-ru` (NOT `admin` — reserved) |
|
||||||
|
| forgejo-runner | act_runner v6.4.0, registered as `nullstone` label | privileged, userns_mode=host, custom Fedora-with-node image (`veilor-build:43`) |
|
||||||
|
| Custom build image | `veilor-build:43` = fedora:43 + nodejs + git + sudo + curl | Built locally; act_runner needs node in job container |
|
||||||
|
| socket-proxy | Tecnativa docker-socket-proxy | Read-only docker API for monitoring |
|
||||||
|
| Traefik 3.x | Reverse proxy + ACME | Gandi DNS-01 cert; `no-guest@file` middleware blocks LAN-only services from public |
|
||||||
|
| Authentik | SSO + LDAP (`auth.s8n.ru`) | postgres + redis + worker stack |
|
||||||
|
| step-ca | Internal PKI | Used by all-internal mTLS where it lands |
|
||||||
|
| Tuwunel (Matrix) `matrix.veilor.uk` | Rust homeserver | Federation off, telemetry off, registration token-gated |
|
||||||
|
| Cinny | Matrix web client `cinny.txt.s8n.ru` | Second isolated instance |
|
||||||
|
| Misskey | Private Twitter rebrand at `x.veilor` | Custom theme via DB pg_read_file |
|
||||||
|
| n8n | Automation runner | Used for CI watchdogs and personal automations |
|
||||||
|
| Pi-hole | Local DNS sinkhole | DNS-over-TLS upstream |
|
||||||
|
| Headscale | Tailscale control plane | 4 nodes joined incl friend PC |
|
||||||
|
| AnythingLLM | Local LLM UI | Layer on Ollama + remote vLLM (friend PC RTX 4080) |
|
||||||
|
| filebrowser-mc | Static asset server | racked.ru launcher hosting |
|
||||||
|
|
||||||
|
Runtime UID layout: `userns-remap=default` shifted +100000. Backup
|
||||||
|
script + ACL on docker.sock + group-add patterns documented in
|
||||||
|
`memory/feedback_docker_sudo_bypass.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Receipts
|
||||||
|
|
||||||
|
- **Forgejo repo:** <https://git.s8n.ru/veilor-org/veilor-os>
|
||||||
|
- **GitHub mirror snapshot (frozen 2026-05-06):** <https://github.com/veilor-org/veilor-os>
|
||||||
|
- **ci-latest rolling release (live):** <https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest>
|
||||||
|
- **First green ISO timestamp:** 2026-05-06 14:30 UTC, sha256 in release sidecar
|
||||||
|
- **Per-version commit trail:** `git log --oneline | grep '^[a-f0-9]\{7\} v0\.'` shows every `v0.x.y: <bug>` ship line
|
||||||
|
- **Test method evolution:** `test/METHOD-CHANGELOG.md`
|
||||||
|
- **Strategy lock:** [`docs/STRATEGY.md`](STRATEGY.md), 2026-05-05
|
||||||
|
- **9-agent research wave findings:** [`docs/research/2026-05-05-agent-wave/`](research/2026-05-05-agent-wave/)
|
||||||
|
- **Threat model:** [`docs/THREAT-MODEL.md`](THREAT-MODEL.md)
|
||||||
|
- **Hardening reference:** [`docs/HARDENING.md`](HARDENING.md)
|
||||||
|
- **Roadmap:** [`docs/ROADMAP.md`](ROADMAP.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this took
|
||||||
|
|
||||||
|
This is a **single-operator + AI-accelerated** project. No team, no
|
||||||
|
funding, no upstream maintainer hat. Most of the work happened across
|
||||||
|
~6 weeks of evenings and weekends. AI agents (Claude Opus 4.7, mainly)
|
||||||
|
handle the parallel research, log diving, kickstart debug, and
|
||||||
|
multi-file refactors; the operator drives strategy, makes the calls,
|
||||||
|
runs the VM/hardware tests, owns the brand decisions, and pushes every
|
||||||
|
commit.
|
||||||
|
|
||||||
|
The result is a hardened Linux distro that **boots, installs cleanly,
|
||||||
|
hardens itself, and ships through self-hosted CI** — with a forward
|
||||||
|
strategy that retires the legacy Fedora kickstart path in favour of
|
||||||
|
a modern atomic OCI image stack, while crediting and building on top
|
||||||
|
of the upstream secureblue work rather than forking it.
|
||||||
|
|
||||||
|
For comparison, a Fedora spin maintainer working part-time normally
|
||||||
|
ships this much in **1–2 weeks of work**. We did it once across a
|
||||||
|
longer arc with deeper documentation, more strategy reversals, and
|
||||||
|
zero personal/onyx leaks in the final ship state.
|
||||||
|
|
@ -9,6 +9,31 @@ For the historical record of what landed in each release, see
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⚡ STRATEGY PIVOT — 2026-05-06
|
||||||
|
|
||||||
|
**Decision: skip v0.6 kickstart polish. Pivot directly to v0.7
|
||||||
|
BlueBuild OCI path.**
|
||||||
|
|
||||||
|
Reasons:
|
||||||
|
- v0.5.32 produced a green ISO (2.7 GB) on the Forgejo runner. Proof
|
||||||
|
point achieved.
|
||||||
|
- Continuing to debug `livecd-creator` + `anaconda` quirks for v0.6
|
||||||
|
polish is sunk-cost work on tooling we retire at v1.0 anyway.
|
||||||
|
- v0.7 spike already has a working BlueBuild recipe + `ostreecontainer`
|
||||||
|
kickstart directive. Layering veilor branding + installer + power CLI
|
||||||
|
on top of secureblue beats re-deriving the same hardening from
|
||||||
|
scratch.
|
||||||
|
- Ergonomic CLI tools (`veilor-postinstall`, `veilor-doctor`,
|
||||||
|
`veilor-update`) translate cleanly to v0.7: `bootc upgrade` replaces
|
||||||
|
`dnf upgrade`. Move them into v0.7 scope.
|
||||||
|
|
||||||
|
**v0.5.0 is the final kickstart-path release.** Tag, freeze, ship as
|
||||||
|
proof-of-work / portfolio anchor. **v0.6 cancelled as a milestone.**
|
||||||
|
|
||||||
|
Active focus: `v0.7-bluebuild-spike` branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Lessons learned through v0.5.x install grind
|
## Lessons learned through v0.5.x install grind
|
||||||
|
|
||||||
Five things v0.5.27–31 changed about how we plan:
|
Five things v0.5.27–31 changed about how we plan:
|
||||||
|
|
@ -165,7 +190,22 @@ specified — defaults stay sane for a daily driver.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v0.6 — ergonomics (PROMOTED — install grind proved we need this)
|
## v0.6 — CANCELLED 2026-05-06 (folded into v0.7)
|
||||||
|
|
||||||
|
Per the strategy pivot at the top of this file: v0.6 kickstart polish
|
||||||
|
will not ship. Continuing on the kickstart path means more
|
||||||
|
livecd-creator + anaconda debugging on tooling that's retired at v1.0.
|
||||||
|
The flagship v0.6 deliverables (`veilor-postinstall`, `veilor-doctor`,
|
||||||
|
`veilor-update`, opt-in installer ISO, first-boot Plymouth dialog,
|
||||||
|
Bluetooth helper) move into **v0.7 scope** with `bootc upgrade`
|
||||||
|
replacing `dnf upgrade` in the update path.
|
||||||
|
|
||||||
|
The original v0.6 plan is preserved below for reference but is **not
|
||||||
|
the active roadmap**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.6 — ergonomics (HISTORICAL — superseded by v0.7)
|
||||||
|
|
||||||
Smooth the operator experience so day-to-day work doesn't fight the
|
Smooth the operator experience so day-to-day work doesn't fight the
|
||||||
hardening. `veilor-postinstall` and `veilor-doctor` were v0.6 background
|
hardening. `veilor-postinstall` and `veilor-doctor` were v0.6 background
|
||||||
|
|
@ -204,7 +244,28 @@ distro from a kickstart.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v0.7 — public flex + bootc spike
|
## v0.7 — BlueBuild OCI mainline (ACTIVE — primary focus 2026-05-06+)
|
||||||
|
|
||||||
|
This was originally planned as "public flex + bootc spike". Post-pivot,
|
||||||
|
v0.7 is now the **primary active milestone** — it absorbs all v0.6
|
||||||
|
ergonomic work and becomes the next ship target.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- BlueBuild recipe (`bluebuild/recipe.yml`) layering on
|
||||||
|
`ghcr.io/secureblue/securecore-kinoite-hardened-userns`
|
||||||
|
- `kickstart/install-ostreecontainer.ks` — 10-line kickstart that calls
|
||||||
|
`ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry`
|
||||||
|
and lets Anaconda's LUKS UX drive the install
|
||||||
|
- veilor brand layer: KDE black theme, gum installer assets, custom
|
||||||
|
Konsole profile, branded `os-release`
|
||||||
|
- `veilor-power` 3-mode CLI (lifted as-is from v0.5.x overlay)
|
||||||
|
- `veilor-postinstall` (formerly v0.6 flagship) — first-login TUI
|
||||||
|
- `veilor-doctor` (formerly v0.6) — boot-time + weekly drift check
|
||||||
|
- `veilor-update` rewritten on `bootc upgrade` (was `dnf upgrade`)
|
||||||
|
- Forgejo registry as primary OCI publish target; GHCR mirror optional
|
||||||
|
- cosign key-pair signing of OCI image (replaces broken keyless flow)
|
||||||
|
|
||||||
|
Public-flex items kept from original v0.7 entry:
|
||||||
|
|
||||||
Take veilor-os out of "private repo, contained audience" mode. Order
|
Take veilor-os out of "private repo, contained audience" mode. Order
|
||||||
matters: people demand threat model FIRST when a security distro goes
|
matters: people demand threat model FIRST when a security distro goes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Threat Model
|
# Threat Model
|
||||||
|
|
||||||
> **Status:** Draft for v0.7 public flex. Honest scope.
|
> **Status:** Final for v0.7 public launch. Honest scope.
|
||||||
|
|
||||||
veilor-os is a hardened daily-driver desktop. Not a paranoia OS, not an
|
veilor-os is a hardened daily-driver desktop. Not a paranoia OS, not an
|
||||||
anonymity OS, not an isolation OS. This document exists so that
|
anonymity OS, not an isolation OS. This document exists so that
|
||||||
|
|
@ -14,36 +14,39 @@ tool**. veilor-os will not save you, and we will not pretend otherwise.
|
||||||
|
|
||||||
## In scope — what veilor-os defends against
|
## In scope — what veilor-os defends against
|
||||||
|
|
||||||
|
Every row cites the file or setting that implements the mitigation, so the
|
||||||
|
claim is auditable from a clean checkout.
|
||||||
|
|
||||||
| Adversary / scenario | veilor-os mitigation |
|
| Adversary / scenario | veilor-os mitigation |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Lost or stolen laptop, powered off | LUKS2 (aes-xts-plain64, argon2id, mem=1 GB) on root + swap-as-zram. Disk yields ciphertext. |
|
| Lost or stolen laptop, powered off | LUKS2 `aes-xts-plain64` + `argon2id` (`mem=1 GiB`, `time=9`) on root LV; swap is `zram` only — no persistent key material on disk. Defined in `kickstart/veilor-os.ks` `part pv.veilor` block. |
|
||||||
| Generic browser / email malware (drive-by RCE, malicious attachment) | SELinux enforcing + `veilor-systemd` policy + sysctl hardening (kptr_restrict, ptrace=2, perf=3, BPF JIT harden, full ASLR, no SUID core dumps). AppArmor stack lands in v0.5. |
|
| Generic browser / email malware (drive-by RCE, malicious attachment) | SELinux `enforcing` + targeted policy + custom `veilor-systemd.te` module (`scripts/selinux/`); sysctl knobs in `/etc/sysctl.d/99-veilor-hardening.conf`: `kernel.kptr_restrict=2`, `kernel.yama.ptrace_scope=2`, `kernel.perf_event_paranoid=3`, `net.core.bpf_jit_harden=2`, `kernel.randomize_va_space=2`, `fs.suid_dumpable=0`, `dev.tty.ldisc_autoload=0`. AppArmor profile skeletons in `scripts/apparmor/` for Trivalent/Thorium/lm-studio (opt-in, complain mode, hardens to enforce per profile). |
|
||||||
| Console-side USB attack (BadUSB, rubber ducky, juice-jack) | USBGuard daemon, default-block, empty allowlist on first boot. New device = explicit operator allow. |
|
| Console-side USB attack (BadUSB, rubber ducky, juice-jack) | USBGuard daemon, `ImplicitPolicyTarget=block`, **id-based** rules in `/etc/usbguard/rules.conf` (vendor:product, not hash — survives dock replug). Empty allowlist on first boot; operator runs `usbguard generate-policy` after plugging trusted devices. |
|
||||||
| SSH brute-force / credential-stuffing | sshd password-auth off, root login off, MaxAuthTries=3, fail2ban with sshd + pam-generic jails wired to firewalld rich-rule. |
|
| SSH brute-force / credential-stuffing | `/etc/ssh/sshd_config.d/10-veilor-hardening.conf`: `PasswordAuthentication no`, `PermitRootLogin no`, `AllowUsers admin`, `MaxAuthTries 3`, `X11Forwarding no`, `LogLevel VERBOSE`. `fail2ban` `sshd` + `pam-generic` jails (journald backend) ban via firewalld `rich-rule` action. |
|
||||||
| Post-incident forensics ("what happened?") | auditd rules covering passwd/shadow/sudoers/ssh/cron/sysctl/kernel modules and all privileged binaries. Logs survive reboot. |
|
| Post-incident forensics ("what happened?") | `auditd` rules in `/etc/audit/rules.d/99-veilor-hardening.rules` watch `/etc/{passwd,shadow,group,sudoers,sudoers.d,ssh/sshd_config*,selinux,firewalld,cron.*,sysctl.*,systemd/system}`, every privileged binary (`sudo`, `su`, `passwd`, `mount`, `pkexec`, …), `init_module`/`finit_module`/`delete_module` syscalls, and uid≥1000 perm/owner changes. Logs persist across reboot. |
|
||||||
| Supply-chain on the OS image itself | Fedora's signed shim → GRUB → kernel chain (Secure Boot enforced). v0.4 adds GPG-signed ISO + sha256 + own MOK. |
|
| Supply-chain on the OS image itself | Secure Boot enforced (Fedora signed shim → GRUB → kernel). v0.7 adds cosign-signed OCI image at `ghcr.io/veilor/veilor-os:43`, GPG-signed ISO + sha256 + .asc, plus our own MOK for out-of-tree module signing. |
|
||||||
| Unprivileged local user attempting LPE | root account locked (`passwd -S root` → `L`), single sudo user with pwquality minlen=14 / 4 classes, kernel module loading frozen 30 s after graphical boot. |
|
| Unprivileged local user attempting LPE | Root account locked (`passwd -l root`; `passwd -S root` → `L`); single `admin` user in `wheel`; `pwquality.conf` `minlen=14`, `minclass=4`, dictcheck on. Kernel `lockdown=integrity`, `slab_nomerge`, `init_on_alloc=1`, `init_on_free=1`, `randomize_kstack_offset=on`, `vsyscall=none` set in bootloader args. Module loading frozen 30 s after graphical boot via `veilor-modules-lock.service`. |
|
||||||
| Network-listening services as attack surface | firewalld default zone = `drop`; only sshd answers. abrt/cups/avahi/bluetooth/ModemManager/kdeconnectd/PackageKit are masked. |
|
| Network-listening services as attack surface | `firewalld` default zone = `drop`; only `sshd` answers. `abrt*`, `cups`, `cups-browsed`, `geoclue`, `avahi-daemon`, `bluetooth`, `ModemManager`, `gssproxy`, `atd`, `pcscd.{socket,service}` are masked; `kdeconnectd` and `PackageKit` are removed at the package level. |
|
||||||
| Time-based MITM (back-dated certs, replay) | NTS-authenticated chrony, DNS-over-TLS via systemd-resolved, LLMNR off. |
|
| Time-based MITM (back-dated certs, replay) | `chrony` with NTS authentication against `time.cloudflare.com` and `nts.sth1/2.ntp.se` (pool fallback only). `systemd-resolved` with DNS-over-TLS opportunistic, DNSSEC `allow-downgrade`, LLMNR off; resolvers Cloudflare 1.1.1.1 / 1.0.0.1, fallback Quad9 9.9.9.9 / 149.112.112.112. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Out of scope — what veilor-os does NOT defend against
|
## Out of scope — what veilor-os does NOT defend against
|
||||||
|
|
||||||
We are honest about this list because pretending otherwise is how people get
|
These adversaries are unambiguously outside our scope. Pretending otherwise
|
||||||
hurt. **If your adversary is here, pick a different tool.**
|
gets people hurt. **If your adversary is on this list, pick a different tool.**
|
||||||
|
|
||||||
| Adversary / scenario | Why veilor-os doesn't help | Use instead |
|
| Adversary / scenario | Why veilor-os doesn't help | Use instead |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Nation-state firmware-level implant (UEFI, ME, BMC) | Secure Boot validates the OS, not the firmware below it. We do not flash custom firmware. | Heads / coreboot on supported hardware. |
|
| Firmware-level implant (UEFI, Intel ME, BMC, EC) | veilor-os does not protect against firmware implants. Secure Boot validates the OS chain only; we do not flash, audit, or sign firmware below GRUB. | Heads / coreboot on supported hardware. |
|
||||||
| Evil-maid attack on a running, unlocked system | LUKS keys live in RAM while the system is up. A physically present attacker can dump RAM (cold boot, DMA via Thunderbolt, debug header). | Power off when unattended. Disable Thunderbolt DMA in firmware. Qubes-in-a-Faraday-bag if you're that target. |
|
| Evil-maid attack on a running, unlocked system | LUKS master keys live in RAM while the system is up. A physically present attacker can dump RAM (cold-boot, Thunderbolt DMA, debug header) and recover them. | Power off when unattended. Disable Thunderbolt DMA in firmware. Qubes-in-a-Faraday-bag if you are that target. |
|
||||||
| Hardware keylogger / hardware mod between keyboard and machine | We're software. Software cannot detect a passive hardware tap. | Physical custody of the device. Tamper-evident seals. |
|
| Hardware keylogger / interposer between keyboard and machine | veilor-os is software. Software cannot detect a passive hardware tap. | Physical custody of the device. Tamper-evident seals. |
|
||||||
| Targeted RCE on the user session (browser 0-day, signal-app exploit) | KDE Plasma is not sandboxed. A logged-in compromise has the user's full data and tokens. SELinux confines daemons, not the desktop. | Qubes (per-app VM isolation). |
|
| Targeted RCE on the user session (browser 0-day, messenger exploit) | KDE Plasma is not sandboxed. A logged-in compromise owns the user's data and tokens. SELinux confines daemons; it does not confine the desktop session. | Qubes OS (per-app Xen VM isolation). |
|
||||||
| Side-channel attacks on AES (timing, cache, power analysis) | We use stock kernel crypto. No constant-time guarantees beyond what the kernel/CPU provide. | Threat-specific HSM. |
|
| Side-channel attacks on AES (timing, cache, power, EM) | veilor-os ships stock kernel crypto. We provide no constant-time or power-analysis guarantees beyond what the kernel and CPU deliver. | Threat-specific HSM, air-gap. |
|
||||||
| Physical attack on a TPM2 chip (probe, glitch, decap) | We don't ship TPM2 binding yet. Even when v1.0 lands, TPM2 is not anti-tamper hardware. | Off-device key custody. |
|
| Physical attack on a TPM2 chip (bus probe, glitch, decap) | veilor-os does not bind keys to TPM2 in v0.7. Even when binding lands post-v1.0, TPM2 is not anti-tamper hardware. | Off-device key custody (smartcard / YubiKey / OnlyKey). |
|
||||||
| Network-level traffic correlation / traffic analysis | All packets leave the box on the local IP. We don't onion-route. | Tails, Whonix, Tor. |
|
| Network-level traffic correlation / traffic analysis | All packets leave the box on the local IP. veilor-os does not onion-route. | Tails, Whonix, Tor. |
|
||||||
| Trust-on-first-use attacks (user clicks "accept bad cert") | We can't override the user's decisions. Bad SSL/SSH key acceptance by the operator is out of scope. | Enrolment policy, MDM. |
|
| Trust-on-first-use attacks (operator accepts a bad cert) | veilor-os cannot override the operator's explicit decisions. Bad SSL or SSH host-key acceptance is out of scope. | Enrolment policy, MDM, certificate pinning. |
|
||||||
| Adversary with sustained physical access and time | Given enough physical time and tools, any laptop falls. | Operational security, not OS choice. |
|
| Adversary with sustained physical access and time | Given unlimited physical time and tools, any laptop falls. | Operational security, not OS choice. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -92,19 +95,21 @@ Hardening that breaks ordinary work gets called out, not hidden.
|
||||||
|
|
||||||
Scoring legend: `✓` shipped & on by default, `~` partial / opt-in,
|
Scoring legend: `✓` shipped & on by default, `~` partial / opt-in,
|
||||||
`✗` not provided, `n/a` not applicable to that distro's model.
|
`✗` not provided, `n/a` not applicable to that distro's model.
|
||||||
|
Project metrics are GitHub / Codeberg figures as of 2026-05.
|
||||||
|
|
||||||
| Axis | veilor-os | Stock Fedora KDE | Kicksecure | Tails | Qubes OS | secureblue |
|
| Axis | veilor-os | Stock Fedora KDE | Kicksecure | Tails | Qubes OS | secureblue | Athena OS |
|
||||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **Encrypted at rest by default** | ✓ (LUKS2 argon2id) | ~ (optional) | ✓ | n/a (amnesic) | ✓ | ✓ |
|
| **Encrypted at rest by default** | ✓ (LUKS2 argon2id, mem=1 GiB) | ~ (optional in Anaconda) | ✓ | n/a (amnesic, session-only) | ✓ | ✓ | ~ (optional) |
|
||||||
| **MAC enforcing OOTB** | ✓ (SELinux + AppArmor v0.5) | ✓ (SELinux) | ✓ (AppArmor) | ✓ (AppArmor) | ✓ (per-VM) | ✓ (SELinux) |
|
| **MAC enforcing OOTB** | ✓ (SELinux + opt-in AppArmor) | ✓ (SELinux) | ✓ (AppArmor) | ✓ (AppArmor) | ✓ (per-VM) | ✓ (SELinux) | ✓ (AppArmor) |
|
||||||
| **Default-deny firewall** | ✓ | ✗ | ✓ | ✓ (Tor-only) | ✓ | ✓ |
|
| **Default-deny firewall** | ✓ (firewalld zone=drop) | ✗ | ✓ | ✓ (Tor-only) | ✓ | ✓ | ✓ |
|
||||||
| **USB default-block** | ✓ (USBGuard) | ✗ | ✓ | ✓ | ✓ (sys-usb) | ✓ |
|
| **USB default-block** | ✓ (USBGuard, id-rules) | ✗ | ✓ | ✓ | ✓ (sys-usb) | ✓ (USBGuard) | ✗ |
|
||||||
| **Per-app isolation (VM/sandbox)** | ✗ | ✗ | ✗ | ~ (AppArmor) | ✓ (Xen VMs) | ~ (Flatpak/bwrap) |
|
| **Per-app isolation (VM/sandbox)** | ✗ | ✗ | ✗ | ~ (AppArmor) | ✓ (Xen VMs) | ~ (Flatpak/bwrap) | ✗ |
|
||||||
| **Anonymity / Tor by default** | ✗ | ✗ | ✗ | ✓ | ~ (Whonix VMs) | ✗ |
|
| **Anonymity / Tor by default** | ✗ | ✗ | ✗ | ✓ | ~ (Whonix VMs) | ✗ | ✗ |
|
||||||
| **Daily driver target (persistent)** | ✓ | ✓ | ✓ | ✗ | ✓ (heavy) | ✓ |
|
| **Daily driver target (persistent)** | ✓ | ✓ | ✓ | ✗ (amnesic) | ✓ (heavy, hardware-partitioning) | ✓ | ✓ |
|
||||||
| **Signed releases (publisher key)** | ✓ (v0.4) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| **Signed releases (cosign + GPG)** | ✓ (v0.7) | ✓ | ✓ | ✓ | ✓ | ✓ (cosign on OCI) | ~ (sha256 only) |
|
||||||
| **Threat model published** | ✓ (this doc) | ✗ | ✓ | ✓ | ✓ | ✓ |
|
| **Threat model published** | ✓ (this doc) | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ |
|
||||||
| **Hardware compatibility (laptops)** | ✓ (Fedora kernel) | ✓ | ~ | ~ (live USB) | ~ (Xen-pinned) | ✓ |
|
| **Hardware compatibility (laptops)** | ✓ (Fedora kernel) | ✓ | ~ | ~ (live USB) | ~ (Xen-pinned HCL) | ✓ | ✓ (Arch kernel) |
|
||||||
|
| **Project size (contributors / stars, 2026-05)** | solo / pre-public | n/a (Fedora-wide) | small team / ~600 | ~30 / ~3k | large / ~5k | ~30 / ~940, active monthly cadence | ~8 / ~1.4k |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,11 @@ chrony
|
||||||
firewalld
|
firewalld
|
||||||
plymouth
|
plymouth
|
||||||
|
|
||||||
|
# AppArmor stack — DEFERRED. apparmor-parser / apparmor-utils /
|
||||||
|
# apparmor-profiles are not in Fedora 43 base or updates. v0.6 ships
|
||||||
|
# without AppArmor; tier-2 plan to land via COPR or as part of the v0.7
|
||||||
|
# secureblue OCI hybrid (which has its own LSM stack).
|
||||||
|
|
||||||
# admin essentials
|
# admin essentials
|
||||||
git
|
git
|
||||||
vim-enhanced
|
vim-enhanced
|
||||||
|
|
|
||||||
11
overlay/etc/apparmor.d/veilor.d/firefox
Normal file
11
overlay/etc/apparmor.d/veilor.d/firefox
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# veilor-os AppArmor profile stub — firefox
|
||||||
|
#
|
||||||
|
# v0.6 scope: marker only. Loads in complain mode via scripts/40-apparmor.sh
|
||||||
|
# so AppArmor can log the syscall surface for v0.7 policy authoring. No
|
||||||
|
# actual confinement rules yet — full policy is post-v0.6.
|
||||||
|
|
||||||
|
#include <tunables/global>
|
||||||
|
|
||||||
|
profile veilor-firefox /usr/lib*/firefox/firefox flags=(complain) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
}
|
||||||
11
overlay/etc/apparmor.d/veilor.d/thunderbird
Normal file
11
overlay/etc/apparmor.d/veilor.d/thunderbird
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# veilor-os AppArmor profile stub — thunderbird
|
||||||
|
#
|
||||||
|
# v0.6 scope: marker only. Loads in complain mode via scripts/40-apparmor.sh
|
||||||
|
# so AppArmor can log the syscall surface for v0.7 policy authoring. No
|
||||||
|
# actual confinement rules yet — full policy is post-v0.6.
|
||||||
|
|
||||||
|
#include <tunables/global>
|
||||||
|
|
||||||
|
profile veilor-thunderbird /usr/lib*/thunderbird/thunderbird flags=(complain) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
}
|
||||||
|
|
@ -69,6 +69,21 @@ banner() {
|
||||||
local vline="veilor-os ${ver} · ${d} · live"
|
local vline="veilor-os ${ver} · ${d} · live"
|
||||||
|
|
||||||
if [[ -r $BANNER_FILE ]]; then
|
if [[ -r $BANNER_FILE ]]; then
|
||||||
|
# v0.6: staged line-by-line reveal of the banner before the
|
||||||
|
# gum-style border draws around it. 40ms/line gives a subtle
|
||||||
|
# "typewriter" feel — 5 lines × 40ms = 200ms total, fast enough
|
||||||
|
# not to feel laggy but slow enough to land an aesthetic on the
|
||||||
|
# very first frame the user sees. Once the reveal finishes we
|
||||||
|
# clear and re-draw with the bordered gum-style version so the
|
||||||
|
# operator never sees both stacked on top of each other.
|
||||||
|
local line
|
||||||
|
while IFS= read -r line; do
|
||||||
|
printf ' %s\n' "$line"
|
||||||
|
sleep 0.04
|
||||||
|
done < "$BANNER_FILE"
|
||||||
|
sleep 0.08
|
||||||
|
clear
|
||||||
|
|
||||||
if [[ $TUI == gum ]]; then
|
if [[ $TUI == gum ]]; then
|
||||||
# gum style: rounded border, banner + blank line + version line.
|
# gum style: rounded border, banner + blank line + version line.
|
||||||
gum style --border rounded --margin "0 2" --padding "1 3" \
|
gum style --border rounded --margin "0 2" --padding "1 3" \
|
||||||
|
|
@ -150,10 +165,30 @@ prompt_input() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# prompt_password <header>
|
# prompt_password <header>
|
||||||
|
#
|
||||||
|
# v0.6: gum-path replaced with bash `read -srp` because `gum input
|
||||||
|
# --password` rendered as a duplicate-"Install" + stray-T artefact on
|
||||||
|
# the linux fbcon since v0.5.27 (Agent 7 of the v0.6 polish research
|
||||||
|
# wave traced this to gum's bubbletea screen-restore writing back the
|
||||||
|
# previous menu buffer when the framebuffer terminfo lacked
|
||||||
|
# `civis/cnorm` cursor-hide sequences). bash `read -srp` is a single
|
||||||
|
# write to stdout + termios echo-off — no redraw, no glitch. Header
|
||||||
|
# rendered separately via gum style for visual parity with the rest
|
||||||
|
# of the installer.
|
||||||
prompt_password() {
|
prompt_password() {
|
||||||
local header=$1
|
local header=$1
|
||||||
if [[ $TUI == gum ]]; then
|
if [[ $TUI == gum ]]; then
|
||||||
gum input --password --header "$header"
|
# Render the prompt header as a styled box so it looks at home
|
||||||
|
# next to the other gum prompts, then collect the password via
|
||||||
|
# plain bash read on the next line. `read -s` disables echo,
|
||||||
|
# `read -p` writes the prompt to stderr (so command-substitution
|
||||||
|
# callers still get the password on stdout cleanly).
|
||||||
|
gum style --foreground "${VEILOR_FG:-15}" --border rounded \
|
||||||
|
--border-foreground "${VEILOR_DIM:-240}" --padding "0 2" -- "$header"
|
||||||
|
local pw
|
||||||
|
read -srp " password: " pw
|
||||||
|
echo >&2 # newline after silent read so next prompt isn't on same line
|
||||||
|
printf '%s' "$pw"
|
||||||
else
|
else
|
||||||
whiptail --title "veilor-os" --passwordbox "$header" 10 60 \
|
whiptail --title "veilor-os" --passwordbox "$header" 10 60 \
|
||||||
3>&1 1>&2 2>&3
|
3>&1 1>&2 2>&3
|
||||||
|
|
@ -253,12 +288,36 @@ collect_answers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── LUKS passphrase ──
|
# ── LUKS passphrase ──
|
||||||
luks_pw=$(prompt_password "[2/3] Encryption · LUKS2 passphrase (min 8)") || return 1
|
# v0.6: prompt twice + string-compare. A typo in the LUKS passphrase
|
||||||
validate_pw "$luks_pw" "passphrase" || return 1
|
# is unrecoverable — the disk is unmountable without it and we
|
||||||
|
# don't escrow the key. Re-prompting until the two reads match
|
||||||
|
# catches keyboard-layout surprises (US vs UK quote position is
|
||||||
|
# the most common one) before they brick the install.
|
||||||
|
local luks_pw_confirm
|
||||||
|
while true; do
|
||||||
|
luks_pw=$(prompt_password "[2/3] Encryption · LUKS2 passphrase (min 8)") || return 1
|
||||||
|
validate_pw "$luks_pw" "passphrase" || continue
|
||||||
|
luks_pw_confirm=$(prompt_password "[2/3] Confirm LUKS2 passphrase") || return 1
|
||||||
|
if [[ $luks_pw == "$luks_pw_confirm" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
prompt_error "Passphrases do not match — try again."
|
||||||
|
done
|
||||||
|
|
||||||
# ── Admin password ──
|
# ── Admin password ──
|
||||||
admin_pw=$(prompt_password "[3/3] Admin user · password for 'admin'") || return 1
|
# Same confirm-twice pattern. Less catastrophic than LUKS (admin
|
||||||
validate_pw "$admin_pw" "password" || return 1
|
# password can be reset from a recovery shell) but a mismatch here
|
||||||
|
# still locks the user out of their fresh install on first boot.
|
||||||
|
local admin_pw_confirm
|
||||||
|
while true; do
|
||||||
|
admin_pw=$(prompt_password "[3/3] Admin user · password for 'admin'") || return 1
|
||||||
|
validate_pw "$admin_pw" "password" || continue
|
||||||
|
admin_pw_confirm=$(prompt_password "[3/3] Confirm admin password") || return 1
|
||||||
|
if [[ $admin_pw == "$admin_pw_confirm" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
prompt_error "Passwords do not match — try again."
|
||||||
|
done
|
||||||
|
|
||||||
# ── Locale ──
|
# ── Locale ──
|
||||||
# Hardcoded en_US.UTF-8 for branded consistency. The picker that
|
# Hardcoded en_US.UTF-8 for branded consistency. The picker that
|
||||||
|
|
@ -488,6 +547,12 @@ chrony
|
||||||
firewalld
|
firewalld
|
||||||
plymouth
|
plymouth
|
||||||
|
|
||||||
|
# AppArmor stack — Fedora 43 ships parser/utils/profiles. v0.6 ships
|
||||||
|
# loaded-but-complain only (see scripts/40-apparmor.sh + tier-2 plan).
|
||||||
|
apparmor-parser
|
||||||
|
apparmor-utils
|
||||||
|
apparmor-profiles
|
||||||
|
|
||||||
# admin essentials
|
# admin essentials
|
||||||
git
|
git
|
||||||
vim-enhanced
|
vim-enhanced
|
||||||
|
|
@ -978,12 +1043,39 @@ run_install() {
|
||||||
--show-output \
|
--show-output \
|
||||||
-- bash -c 'anaconda --cmdline --kickstart=/run/install/veilor-generated.ks 2>&1 | tee /tmp/anaconda-cmdline.log' || rc=$?
|
-- bash -c 'anaconda --cmdline --kickstart=/run/install/veilor-generated.ks 2>&1 | tee /tmp/anaconda-cmdline.log' || rc=$?
|
||||||
if [[ $rc -eq 0 ]]; then
|
if [[ $rc -eq 0 ]]; then
|
||||||
gum style --foreground 2 --border rounded --margin "1 2" --padding "1 3" \
|
# v0.6: split the success screen into THREE stacked boxes.
|
||||||
"✓ Install complete" \
|
#
|
||||||
"" \
|
# 1. Green success box — quiet confirmation.
|
||||||
"System will reboot in 5 seconds." \
|
# 2. Yellow eject box — promoted out of the buried
|
||||||
"Remove the install media."
|
# one-liner the v0.5 success box used. Operators on
|
||||||
sleep 5
|
# both onyx and the friend's RTX 4080 rig missed the
|
||||||
|
# reminder and rebooted into the live ISO instead of
|
||||||
|
# the install. Now it's its own loud thick-bordered
|
||||||
|
# box that sits BELOW the success box and is
|
||||||
|
# impossible to miss.
|
||||||
|
# 3. Reboot countdown — embedded inside the green
|
||||||
|
# success box so the operator can see "complete +
|
||||||
|
# Xs to reboot" at a glance.
|
||||||
|
#
|
||||||
|
# Each tick clears + redraws all three, so the eject-media
|
||||||
|
# box stays in front of the operator for the full 10-second
|
||||||
|
# window and isn't scrolled off by a banner refresh.
|
||||||
|
local secs
|
||||||
|
for secs in 10 9 8 7 6 5 4 3 2 1; do
|
||||||
|
clear
|
||||||
|
gum style --foreground 2 --border rounded --margin "1 2" --padding "1 3" \
|
||||||
|
"✓ Install complete" \
|
||||||
|
"" \
|
||||||
|
"Rebooting in ${secs}s..."
|
||||||
|
gum style --foreground 3 --border thick --margin "0 2" --padding "1 3" \
|
||||||
|
--border-foreground 3 \
|
||||||
|
" Remove the install media NOW " \
|
||||||
|
"" \
|
||||||
|
" Unplug the USB stick / eject the DVD before " \
|
||||||
|
" reboot, otherwise the system will boot back " \
|
||||||
|
" into the live ISO instead of your fresh install. "
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
systemctl reboot
|
systemctl reboot
|
||||||
else
|
else
|
||||||
prompt_error "Anaconda exited non-zero (status $rc).
|
prompt_error "Anaconda exited non-zero (status $rc).
|
||||||
|
|
|
||||||
77
scripts/40-apparmor.sh
Normal file
77
scripts/40-apparmor.sh
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# veilor-os — 40-apparmor: load veilor-shipped AppArmor profiles in
|
||||||
|
# COMPLAIN mode. v0.6 scope: "loaded, present, nothing breaks".
|
||||||
|
#
|
||||||
|
# Per docs/research/2026-05-05-agent-wave/04-hardening-tier-2.md, v0.6
|
||||||
|
# ships AppArmor stacked alongside SELinux, but every veilor-shipped
|
||||||
|
# profile stays in complain mode (logs only, no enforce). Real policy
|
||||||
|
# authoring is post-v0.6.
|
||||||
|
#
|
||||||
|
# Idempotent: profiles already in complain mode are skipped. Run as
|
||||||
|
# root during kickstart %post or post-install.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||||
|
info() { echo -e "${YELLOW}[INFO]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[ERR]${NC} $*"; }
|
||||||
|
|
||||||
|
[[ $EUID -eq 0 ]] || { err "Must run as root"; exit 1; }
|
||||||
|
|
||||||
|
echo "════════════════════════════════════════════════════════"
|
||||||
|
echo " veilor-os :: 40-apparmor (complain mode only)"
|
||||||
|
echo "════════════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
PROFILE_DIR=/etc/apparmor.d/veilor.d
|
||||||
|
|
||||||
|
# ── Sanity: tools present? ──
|
||||||
|
if ! command -v apparmor_parser >/dev/null 2>&1; then
|
||||||
|
warn "apparmor_parser not installed — skipping (package step missed?)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if ! command -v aa-complain >/dev/null 2>&1; then
|
||||||
|
warn "aa-complain not installed (apparmor-utils missing) — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d $PROFILE_DIR ]]; then
|
||||||
|
info "$PROFILE_DIR not present — no veilor profiles to load"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Walk every profile we ship and force complain mode ──
|
||||||
|
shopt -s nullglob
|
||||||
|
loaded=0
|
||||||
|
skipped=0
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
for profile in "$PROFILE_DIR"/*; do
|
||||||
|
[[ -f $profile ]] || continue
|
||||||
|
name=$(basename "$profile")
|
||||||
|
|
||||||
|
# Already in complain mode? aa-status reports loaded profiles by
|
||||||
|
# internal profile name, not file path — best-effort match against
|
||||||
|
# the file basename to avoid re-parsing on repeat runs.
|
||||||
|
if command -v aa-status >/dev/null 2>&1 \
|
||||||
|
&& aa-status --complaining 2>/dev/null | grep -qE "(^|/)veilor-${name}([[:space:]]|$)"; then
|
||||||
|
info "$name already in complain mode — skipping"
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "loading $name (complain mode)"
|
||||||
|
if aa-complain "$profile" >/dev/null 2>&1; then
|
||||||
|
ok "$name → complain"
|
||||||
|
loaded=$((loaded + 1))
|
||||||
|
else
|
||||||
|
warn "$name failed to load (parser may reject stub on this kernel)"
|
||||||
|
failed=$((failed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "────────────────────────────────────────────────────────"
|
||||||
|
info "summary: loaded=$loaded skipped=$skipped failed=$failed"
|
||||||
|
ok "v0.6 AppArmor stub: complain-mode only — no enforcement, log-only"
|
||||||
|
exit 0
|
||||||
|
|
@ -9,6 +9,50 @@ Entries are newest-first.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-05-06 · v0.5.32 · ISO build path moved to Forgejo
|
||||||
|
|
||||||
|
**Change:** Build host for the test ISO has moved off GitHub Actions
|
||||||
|
onto the Forgejo runner on nullstone. The hybrid VM test procedure in
|
||||||
|
`TESTING.md` is **unchanged** — the gum installer still drives every
|
||||||
|
step it can, the operator still types the LUKS + admin passwords
|
||||||
|
directly into the QEMU window. The only thing different is where the
|
||||||
|
ISO comes from and how the host log is captured.
|
||||||
|
|
||||||
|
**Practical deltas for testers:**
|
||||||
|
|
||||||
|
- ISO download: from the Forgejo `ci-latest` rolling release at
|
||||||
|
<https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest>.
|
||||||
|
The tag is force-replaced on each successful `build-iso.yml` run, so
|
||||||
|
always re-download — don't rely on a cached copy.
|
||||||
|
- Re-flash to USB / virtio-blk image via Etcher / `dd` — **unchanged**.
|
||||||
|
Same `sha256sum -c` step; same image format.
|
||||||
|
- virtio-9p host log capture is now **active by default** in
|
||||||
|
`test/run-vm.sh`. This replaces the broken virtio-serial path
|
||||||
|
flagged by Agent 6 in the 2026-05-05 wave; Anaconda logs land in the
|
||||||
|
host-side mount automatically once the VM boots, no manual `tail -f`
|
||||||
|
on a broken serial console.
|
||||||
|
- Build host for the record: forgejo-runner on nullstone, runner label
|
||||||
|
`ubuntu-24.04`, image `catthehacker/ubuntu:act-24.04`. Reproducibility
|
||||||
|
is unchanged from the GH Actions ubuntu-24.04 base — the act image
|
||||||
|
matches GHA's runner image to within package versions.
|
||||||
|
|
||||||
|
**Why:** GitHub mirror was disabled 2026-05-06 (repo is now
|
||||||
|
private-by-default on Forgejo); GH Actions builds would just stop
|
||||||
|
producing artifacts. Moving CI in-house onto nullstone keeps the
|
||||||
|
test/release loop intact and removes the external dependency for
|
||||||
|
private-build cycles. Documenting the change here so a future tester
|
||||||
|
reading TESTING.md doesn't waste time hunting an artifact in a
|
||||||
|
GitHub run that never happened.
|
||||||
|
|
||||||
|
**Files touched in this entry:**
|
||||||
|
- `test/METHOD-CHANGELOG.md` — this entry.
|
||||||
|
|
||||||
|
`test/TESTING.md` itself is **not** edited — the procedure prose still
|
||||||
|
applies verbatim. Only the build host and the URL where the ISO lives
|
||||||
|
changed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-05-05 · v0.5.27 · TESTING.md created
|
## 2026-05-05 · v0.5.27 · TESTING.md created
|
||||||
|
|
||||||
**Change:** First version of the canonical procedure document.
|
**Change:** First version of the canonical procedure document.
|
||||||
|
|
|
||||||
142
test/test-runs/2026-05-06-v0.5.32-build.md
Normal file
142
test/test-runs/2026-05-06-v0.5.32-build.md
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
# Test run — v0.5.32
|
||||||
|
|
||||||
|
- **Date:** 2026-05-06
|
||||||
|
- **ISO:** `veilor-os-43-20260506-HHMMSS.iso` (sha256: `TBD — fill in once A1 reports the build artifact`)
|
||||||
|
- **Tester:** A1 (build) + operator (P) + A5 (report scribe)
|
||||||
|
- **Build host:** forgejo-runner on nullstone (runner label `ubuntu-24.04`,
|
||||||
|
image `catthehacker/ubuntu:act-24.04`); first ISO produced off the
|
||||||
|
Forgejo build pipeline after the GH Actions mirror was disabled
|
||||||
|
2026-05-06.
|
||||||
|
- **Environment:** VM (qemu/q35/ovmf, 4 vCPU, 4 GiB RAM, virtio-vga,
|
||||||
|
virtio-9p host log mount). Real-hardware run is a separate report —
|
||||||
|
this file is the VM run only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
⏳ **Pending A1 build.** Operator + A5 fill in pass/fail per-step once
|
||||||
|
the actual VM test is walked through against the v0.5.32 ISO. Until
|
||||||
|
the ISO sha256 lands here, treat every row in the per-step table as
|
||||||
|
unverified.
|
||||||
|
|
||||||
|
One-line summary (write here once known): _TBD_.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regressions vs previous run
|
||||||
|
|
||||||
|
(v0.5.31 was the last tagged release; compare against any pass-with-issues
|
||||||
|
notes from that test run if a report exists. Empty otherwise — fill in
|
||||||
|
during the actual test walkthrough.)
|
||||||
|
|
||||||
|
- _TBD_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Per-step results
|
||||||
|
|
||||||
|
Walk `test/TESTING.md` step-by-step. Mark each pass/fail with a brief
|
||||||
|
note when failed. Until the test runs, every row is `⏳ pending`.
|
||||||
|
|
||||||
|
| # | Step | Result | Notes |
|
||||||
|
|----|-----------------------------------|--------|-------|
|
||||||
|
| 1 | Live boot to installer banner | ⏳ pending | |
|
||||||
|
| 2 | Installer menu render | ⏳ pending | |
|
||||||
|
| 3 | Disk picker | ⏳ pending | |
|
||||||
|
| 4 | LUKS + admin passwords | ⏳ pending | Operator types directly into QEMU window — plymouth ignores synthesised keys. |
|
||||||
|
| 5 | Locale | ⏳ pending | |
|
||||||
|
| 6 | Confirm | ⏳ pending | |
|
||||||
|
| 7 | Anaconda transaction | ⏳ pending | |
|
||||||
|
| 8 | Reboot | ⏳ pending | |
|
||||||
|
| 9 | GRUB single veilor-os entry | ⏳ pending | |
|
||||||
|
| 10 | LUKS unlock prompt | ⏳ pending | |
|
||||||
|
| 11 | First boot → SDDM → KDE | ⏳ pending | |
|
||||||
|
| 12 | Hardening checks | ⏳ pending | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hardening verification
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ getenforce
|
||||||
|
TBD
|
||||||
|
|
||||||
|
$ systemctl is-active fail2ban usbguard tuned auditd firewalld
|
||||||
|
TBD
|
||||||
|
|
||||||
|
$ cat /proc/cmdline
|
||||||
|
TBD — must include rd.luks.uuid=luks-... and the v0.5.32 cmdline set.
|
||||||
|
|
||||||
|
$ lsblk -f
|
||||||
|
TBD
|
||||||
|
|
||||||
|
$ systemctl is-enabled veilor-firstboot.service
|
||||||
|
TBD — must report enabled with WantedBy=graphical.target (blocker #2).
|
||||||
|
|
||||||
|
$ nft list ruleset | grep -i tailscale
|
||||||
|
TBD — tailscale0 must be in the trusted zone (blocker #5).
|
||||||
|
|
||||||
|
$ cat /etc/skel/.config/kdeglobals 2>/dev/null | head
|
||||||
|
TBD — branding must be present (blocker #6).
|
||||||
|
|
||||||
|
$ ls /var/log/anaconda/host-9p-mount/
|
||||||
|
TBD — virtio-9p Anaconda log capture (blocker #7).
|
||||||
|
```
|
||||||
|
|
||||||
|
Paste real output. If any service is inactive, any cmdline arg is
|
||||||
|
missing, or any blocker artifact is absent, raise as a Regression
|
||||||
|
above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
The 7 v0.5.32 blocker fixes from the
|
||||||
|
[2026-05-05 9-agent research wave](../../docs/research/2026-05-05-agent-wave/README.md)
|
||||||
|
land in this build. Each is listed here as an **expected behaviour**
|
||||||
|
the tester must observe — if any of these regress, log it under
|
||||||
|
Regressions above.
|
||||||
|
|
||||||
|
1. **Suspend/resume wifi survives lid-close.** `kernel.modules_disabled=1`
|
||||||
|
no longer fires before the wifi module reloads on resume. Test:
|
||||||
|
suspend the VM (or lid-close on real HW), wake, reconnect to the
|
||||||
|
same network without manual `modprobe`.
|
||||||
|
2. **`veilor-firstboot.service` is `WantedBy=graphical.target`.** The
|
||||||
|
first-boot admin password flow must run on real installs, not just
|
||||||
|
on multi-user.target boots. Test: fresh install boots straight to
|
||||||
|
the TTY password prompt before SDDM lights up.
|
||||||
|
3. **Kernel-upgrade does not drift GRUB.** First `dnf upgrade kernel`
|
||||||
|
must leave the system bootable — `grub2-mkconfig` is wired into the
|
||||||
|
kernel-install hook. Test: install, run `sudo dnf upgrade kernel`,
|
||||||
|
reboot, system comes up.
|
||||||
|
4. **USBGuard rules are id-based, not hash + parent-hash.** Mirrors the
|
||||||
|
onyx dock-replug fix in `feedback_usbguard_dock.md`. Test:
|
||||||
|
unplug/replug a known device — it stays allowed. The hash variant
|
||||||
|
re-blocks on every replug; the id variant must not.
|
||||||
|
5. **firewalld trusts `tailscale0`.** The interface is in the trusted
|
||||||
|
zone out-of-the-box. Test: bring tailscale up, ping a peer in the
|
||||||
|
mesh — no firewall mods required.
|
||||||
|
6. **`/etc/skel/` carries veilor branding.** New users get the black
|
||||||
|
colour scheme, Konsole profile, and Plasma layout on first login.
|
||||||
|
Test: `useradd test`; log in as `test`; KDE comes up branded, no
|
||||||
|
white flash, Fira Code system font.
|
||||||
|
7. **virtio-9p Anaconda log capture is active by default.**
|
||||||
|
`test/run-vm.sh` mounts a host directory into the VM; Anaconda logs
|
||||||
|
land there during install. Replaces the broken virtio-serial path
|
||||||
|
from earlier runs. Test: run install in VM; host-side mount has
|
||||||
|
`program.log`, `storage.log`, `packaging.log` populated.
|
||||||
|
|
||||||
|
Free-form notes from the actual walkthrough — cosmetic glitches, slow
|
||||||
|
paths, surprising behaviour — append below.
|
||||||
|
|
||||||
|
- _TBD — fill in during the operator-driven VM run._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Action items for next release
|
||||||
|
|
||||||
|
(Empty until the test exposes something. PRs / commits opened during
|
||||||
|
the run go here.)
|
||||||
|
|
||||||
|
- [ ] _TBD_
|
||||||
Loading…
Reference in a new issue