Compare commits
No commits in common. "main" and "feat/a1-forgejo-ci-adapt" have entirely different histories.
main
...
feat/a1-fo
5 changed files with 168 additions and 490 deletions
252
.github/workflows/build-iso.yml
vendored
252
.github/workflows/build-iso.yml
vendored
|
|
@ -29,10 +29,7 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build live ISO
|
name: Build live ISO
|
||||||
# nullstone label resolves to a privileged Fedora 43 container per
|
runs-on: ubuntu-24.04
|
||||||
# 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:
|
||||||
|
|
@ -41,116 +38,151 @@ jobs:
|
||||||
# node24 which forgejo-runner v6.4.0 (node20) cannot exec.
|
# node24 which forgejo-runner v6.4.0 (node20) cannot exec.
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Install build tooling (Fedora)
|
- name: Free up disk
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /usr/local/share/boost
|
||||||
dnf -y upgrade --refresh
|
sudo apt-get clean
|
||||||
dnf -y install \
|
df -h
|
||||||
lorax \
|
|
||||||
livecd-tools \
|
|
||||||
pykickstart \
|
|
||||||
python3-imgcreate \
|
|
||||||
anaconda-tui \
|
|
||||||
squashfs-tools \
|
|
||||||
xorriso \
|
|
||||||
createrepo_c \
|
|
||||||
git \
|
|
||||||
which \
|
|
||||||
shadow-utils \
|
|
||||||
syslinux \
|
|
||||||
tar \
|
|
||||||
curl \
|
|
||||||
sudo
|
|
||||||
|
|
||||||
- name: Vendor gum binary into overlay
|
- name: Run build inside Fedora 43 container
|
||||||
run: |
|
# v3 is composite/docker-based — no node runtime in the action
|
||||||
set -euxo pipefail
|
# itself. Safe under node20 forgejo-runner. TODO(infra): consider
|
||||||
GUM_VERSION="0.17.0"
|
# SHA pinning in a follow-up sweep.
|
||||||
GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_x86_64.tar.gz"
|
uses: addnab/docker-run-action@v3
|
||||||
GUM_SHA256="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb"
|
with:
|
||||||
mkdir -p overlay/usr/local/bin
|
# Pinned to digest from `skopeo inspect --raw` on 2026-05-06.
|
||||||
curl -fsSL "$GUM_URL" -o /tmp/gum.tgz
|
# Refresh by re-running skopeo against fedora:43 and bumping.
|
||||||
echo "$GUM_SHA256 /tmp/gum.tgz" | sha256sum -c -
|
image: registry.fedoraproject.org/fedora:43@sha256:72e874e771b953c6357c7a5823c6fc1e3e3253b90121e795febe01380e32269b
|
||||||
tar -xzf /tmp/gum.tgz -C /tmp/
|
options: |
|
||||||
install -m 0755 "/tmp/gum_${GUM_VERSION}_Linux_x86_64/gum" overlay/usr/local/bin/gum
|
--privileged
|
||||||
overlay/usr/local/bin/gum --version
|
-v ${{ github.workspace }}:/work
|
||||||
echo "[OK] gum ${GUM_VERSION} vendored into overlay/usr/local/bin/"
|
-v /dev:/dev
|
||||||
|
--tmpfs /tmp:rw,nosuid,nodev,exec,size=16G
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
- name: Build ISO with livecd-creator
|
# Update Fedora image to latest packages — guarantees pcre2 +
|
||||||
run: |
|
# libselinux + selinux-policy are matched (the local build's
|
||||||
set -euxo pipefail
|
# core problem). CI runners always start fresh, no version skew.
|
||||||
|
dnf -y upgrade --refresh
|
||||||
|
|
||||||
# PATCH: livecd-creator bug — __get_efi_image_stanza writes
|
# Install build tooling
|
||||||
# `root=live:LABEL=...` instead of `live:CDLABEL=...` for dracut.
|
dnf -y install \
|
||||||
# Result: dracut hangs on parse-livenet looking for non-CD label.
|
lorax \
|
||||||
# Fix in-place before running build.
|
livecd-tools \
|
||||||
LIVE_PY=$(python3 -c 'import imgcreate, os; print(os.path.dirname(imgcreate.__file__))')/live.py
|
pykickstart \
|
||||||
sed -i 's|"live:LABEL=%(fslabel)s"|"live:CDLABEL=%(fslabel)s"|g' "$LIVE_PY"
|
python3-imgcreate \
|
||||||
grep -n 'CDLABEL=%(fslabel)s' "$LIVE_PY" || { echo "[ERR] patch failed"; exit 1; }
|
anaconda-tui \
|
||||||
echo "[OK] livecd-creator patched: LABEL= → CDLABEL= for EFI dracut stanza"
|
squashfs-tools \
|
||||||
|
xorriso \
|
||||||
|
createrepo_c \
|
||||||
|
git \
|
||||||
|
which \
|
||||||
|
shadow-utils \
|
||||||
|
syslinux \
|
||||||
|
tar \
|
||||||
|
curl
|
||||||
|
|
||||||
# CI uses ks-ci.ks (no local fix-repo line). Generated from main ks.
|
# Vendor gum binary onto the ISO so the TTY1 installer can use
|
||||||
# Drop `updates` repo: previously 404'd on repodata zchunk during
|
# Charm.sh TUI primitives. gum is not packaged in Fedora repos,
|
||||||
# Fedora mid-push windows. Base 43 ships the selinux-policy fix.
|
# so pull the upstream release tarball pinned by sha256.
|
||||||
sed -e '/veilor-fix/d' \
|
GUM_VERSION="0.17.0"
|
||||||
-e '/^shutdown$/d' \
|
GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_x86_64.tar.gz"
|
||||||
-e '/repo --name=updates/d' \
|
GUM_SHA256="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb"
|
||||||
kickstart/veilor-os.ks > kickstart/veilor-os-ci.ks
|
mkdir -p /work/overlay/usr/local/bin
|
||||||
|
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/"
|
||||||
|
|
||||||
ksvalidator kickstart/veilor-os-ci.ks
|
cd /work
|
||||||
mkdir -p build/out
|
|
||||||
|
|
||||||
# The kickstart's %post --nochroot probes a fixed list of
|
# PATCH: livecd-creator bug — __get_efi_image_stanza writes
|
||||||
# candidate paths to locate the repo source for overlay/scripts
|
# `root=live:LABEL=...` instead of `live:CDLABEL=...` for dracut.
|
||||||
# copy. /work is the canonical CI candidate; symlink the live
|
# Result: dracut hangs on parse-livenet looking for non-CD label.
|
||||||
# workspace there so the existing probe finds it.
|
# Fix in-place before running build.
|
||||||
ln -sfn "$GITHUB_WORKSPACE" /work
|
LIVE_PY=$(python3 -c 'import imgcreate, os; print(os.path.dirname(imgcreate.__file__))')/live.py
|
||||||
|
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"
|
||||||
|
|
||||||
mkdir -p /var/lmc /var/lmc-cache
|
# CI uses ks-ci.ks (no local fix-repo line). Generated from main ks.
|
||||||
livecd-creator \
|
# Also strip flags livecd-creator doesn't recognize.
|
||||||
--verbose \
|
# Drop `updates` repo: 3 consecutive builds 404'd on its
|
||||||
--config kickstart/veilor-os-ci.ks \
|
# repodata zchunk file across all mirrors — Fedora infra issue
|
||||||
--fslabel "veilor-os-43" \
|
# mid-push window. Original reason for `updates` (selinux-policy
|
||||||
--title "veilor-os" \
|
# 43.7 pcre2 fix) is no longer needed; base 43 ships fixed.
|
||||||
--product "veilor-os" \
|
sed -e '/veilor-fix/d' \
|
||||||
--releasever "${{ github.event.inputs.releasever || '43' }}" \
|
-e '/^shutdown$/d' \
|
||||||
--tmpdir /var/lmc \
|
-e '/repo --name=updates/d' \
|
||||||
--cache /var/lmc-cache 2>&1 | tee build/out/build.log
|
kickstart/veilor-os.ks > kickstart/veilor-os-ci.ks
|
||||||
|
|
||||||
- name: Graft veilor source tree onto ISO
|
ksvalidator kickstart/veilor-os-ci.ks
|
||||||
run: |
|
mkdir -p build/out
|
||||||
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"
|
|
||||||
|
|
||||||
xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>&1 | tee /tmp/iso-boot.txt || true
|
# livecd-creator (livecd-tools) — purpose-built for live ISOs.
|
||||||
ORIG_FLAGS=$(xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>/dev/null | \
|
# Handles EFI/BOOT + isohybrid + grafting that livemedia-creator
|
||||||
grep -v '^xorriso :' | grep -E '^-' | tr '\n' ' ')
|
# --make-iso --no-virt does not. Produces UEFI+BIOS bootable ISO.
|
||||||
[ -n "$ORIG_FLAGS" ] || { echo "[ERR] could not extract boot stanza from $ISO_FILE"; exit 1; }
|
# --tmpdir /var/lmc to avoid GitHub Actions /tmp tmpfs constraints.
|
||||||
|
# /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
|
||||||
|
|
||||||
mkdir -p /tmp/iso-mod
|
# Graft veilor source tree onto the ISO so the installer-generated
|
||||||
xorriso -osirrox on -indev "$ISO_FILE" -extract / /tmp/iso-mod
|
# kickstart's `%post --nochroot` can find SRC at
|
||||||
chmod -R u+w /tmp/iso-mod
|
# /run/install/repo/veilor/{overlay,scripts,assets}/ when the user
|
||||||
mkdir -p /tmp/iso-mod/veilor
|
# promotes the live ISO into a real install.
|
||||||
cp -a overlay scripts assets /tmp/iso-mod/veilor/
|
ISO_FILE=$(ls /work/*.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"
|
||||||
|
|
||||||
eval xorriso -as mkisofs \
|
# Extract original ISO's exact boot stanza so the rebuild matches
|
||||||
-volid "veilor-os-43" \
|
# livecd-creator's layout byte-for-byte. This is immune to upstream
|
||||||
$ORIG_FLAGS \
|
# Fedora layout changes (e.g. images/ vs isolinux/ for efiboot.img,
|
||||||
-o "${ISO_FILE}.tmp" /tmp/iso-mod
|
# partition geometry flags, hybrid MBR/GPT options).
|
||||||
mv "${ISO_FILE}.tmp" "$ISO_FILE"
|
xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>&1 | tee /tmp/iso-boot.txt || true
|
||||||
rm -rf /tmp/iso-mod
|
ORIG_FLAGS=$(xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>/dev/null | \
|
||||||
|
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"
|
||||||
|
|
||||||
mv "$ISO_FILE" build/out/
|
mkdir -p /tmp/iso-mod
|
||||||
|
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/
|
||||||
|
|
||||||
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-$(date +%Y%m%d-%H%M%S).iso"
|
# Replay the exact stanza captured above. eval is needed because
|
||||||
cd build/out
|
# ORIG_FLAGS contains multiple flag/value pairs that must word-split.
|
||||||
for f in *.iso; do
|
eval xorriso -as mkisofs \
|
||||||
[[ -f $f && $f != "$ISO_NAME" ]] && mv "$f" "$ISO_NAME"
|
-volid "veilor-os-43" \
|
||||||
done
|
$ORIG_FLAGS \
|
||||||
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
-o "${ISO_FILE}.tmp" /tmp/iso-mod
|
||||||
ls -lh "$ISO_NAME"
|
mv "${ISO_FILE}.tmp" "$ISO_FILE"
|
||||||
|
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
|
||||||
|
|
@ -160,6 +192,9 @@ 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; }
|
||||||
|
|
@ -174,12 +209,11 @@ jobs:
|
||||||
ls "${ISO}".part-*
|
ls "${ISO}".part-*
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
# Pinned to last v3 release confirmed node20.
|
uses: sigstore/cosign-installer@v3
|
||||||
uses: sigstore/cosign-installer@v3.7.0
|
|
||||||
|
|
||||||
- name: Sign ISO parts (keyless)
|
- name: Sign ISO parts (keyless)
|
||||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
run: |
|
run: |
|
||||||
cd build/out
|
cd build/out
|
||||||
for f in *.part-*; do
|
for f in *.part-*; do
|
||||||
|
|
@ -189,18 +223,16 @@ jobs:
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Generate SBOM (SPDX)
|
- name: Generate SBOM (SPDX)
|
||||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
# Pinned to last v0.17 release that ships node20.
|
uses: anchore/sbom-action@v0
|
||||||
uses: anchore/sbom-action@v0.17.2
|
|
||||||
with:
|
with:
|
||||||
path: build/out
|
path: build/out
|
||||||
format: spdx-json
|
format: spdx-json
|
||||||
output-file: build/out/veilor-os.spdx.json
|
output-file: build/out/veilor-os.spdx.json
|
||||||
|
|
||||||
- name: Build provenance attestation
|
- name: Build provenance attestation
|
||||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
# Pinned to last v2.2 release that ships node20.
|
uses: actions/attest-build-provenance@v2
|
||||||
uses: actions/attest-build-provenance@v2.2.3
|
|
||||||
with:
|
with:
|
||||||
subject-path: 'build/out/*.iso.part-*'
|
subject-path: 'build/out/*.iso.part-*'
|
||||||
|
|
||||||
|
|
|
||||||
57
README.md
57
README.md
|
|
@ -116,49 +116,30 @@ Full reference: [docs/HARDENING.md](docs/HARDENING.md).
|
||||||
|
|
||||||
## How veilor-os compares
|
## How veilor-os compares
|
||||||
|
|
||||||
| Feature | veilor-os | Stock Fedora KDE | Kicksecure | secureblue |
|
| Feature | veilor-os | Stock Fedora KDE | Kicksecure |
|
||||||
|---|:-:|:-:|:-:|:-:|
|
|---|:-:|:-:|:-:|
|
||||||
| SELinux enforcing OOTB | yes | yes | yes | yes (custom policy) |
|
| SELinux enforcing OOTB | yes | yes | yes |
|
||||||
| AppArmor | deferred (post-v0.6 / v0.7 LSM stack) | no | yes | no |
|
| AppArmor | planned (v0.5) | no | yes |
|
||||||
| Secure Boot | yes (Fedora keys) | yes (Fedora keys) | configurable | yes (Fedora keys) |
|
| Secure Boot | yes (Fedora keys) | yes (Fedora keys) | configurable |
|
||||||
| LUKS2 with argon2id | default | optional | default | default (Anaconda) |
|
| LUKS2 with argon2id | default | optional | default |
|
||||||
| Single-prompt install (LUKS only) | yes | no | no | rebase via Anaconda |
|
| Single-prompt install (LUKS only) | yes | no | no |
|
||||||
| Root account locked by default | yes | no | yes | yes |
|
| Root account locked by default | yes | no | yes |
|
||||||
| firewalld default zone = drop | yes | no | n/a (nftables) | yes |
|
| firewalld default zone = drop | yes | no | n/a (uses nftables) |
|
||||||
| USBGuard default-block | yes | no | yes | yes |
|
| USBGuard default-block | yes | no | yes |
|
||||||
| fail2ban + auditd OOTB | yes | no | partial | partial (auditd) |
|
| fail2ban + auditd OOTB | yes | no | partial |
|
||||||
| DNS-over-TLS by default | yes | no | yes | yes |
|
| DNS-over-TLS by default | yes | no | yes |
|
||||||
| NTS-authenticated NTP | yes | no | yes | yes |
|
| NTS-authenticated NTP | yes | no | yes |
|
||||||
| `init_on_alloc/free` (post-install) | yes (planned re-enable) | no | yes | yes |
|
| `init_on_alloc/free` (post-install) | yes (planned re-enable) | no | yes |
|
||||||
| Telemetry / phone-home | none | minimal | none | none |
|
| Telemetry / phone-home | none | minimal | none |
|
||||||
| KDE Plasma branded theme | yes (black) | Breeze | n/a (XFCE) | upstream Kinoite |
|
| KDE Plasma branded theme | yes (black) | Breeze | n/a (XFCE) |
|
||||||
| Power-profile CLI | yes (3-mode) | partial | no | no |
|
| Power-profile CLI | yes (3-mode) | partial | no |
|
||||||
| Hardened browser (Trivalent / Mullvad) | yes (v0.6+) | no | no | yes (Trivalent shipped) |
|
| Reproducible kickstart-built ISO | yes | yes | yes (from Debian) |
|
||||||
| Atomic OCI image + signed base | v0.7 spike (BlueBuild) | no | no | yes (`bootc`) |
|
| Base distro | Fedora 43 | Fedora 43 | Debian |
|
||||||
| 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
|
||||||
|
|
|
||||||
|
|
@ -1,275 +0,0 @@
|
||||||
# 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,31 +9,6 @@ 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:
|
||||||
|
|
@ -190,22 +165,7 @@ specified — defaults stay sane for a daily driver.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v0.6 — CANCELLED 2026-05-06 (folded into v0.7)
|
## v0.6 — ergonomics (PROMOTED — install grind proved we need this)
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -244,28 +204,7 @@ distro from a kickstart.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v0.7 — BlueBuild OCI mainline (ACTIVE — primary focus 2026-05-06+)
|
## v0.7 — public flex + bootc spike
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -119,10 +119,11 @@ chrony
|
||||||
firewalld
|
firewalld
|
||||||
plymouth
|
plymouth
|
||||||
|
|
||||||
# AppArmor stack — DEFERRED. apparmor-parser / apparmor-utils /
|
# AppArmor stack — Fedora 43 ships parser/utils/profiles. v0.6 ships
|
||||||
# apparmor-profiles are not in Fedora 43 base or updates. v0.6 ships
|
# loaded-but-complain only (see scripts/40-apparmor.sh + tier-2 plan).
|
||||||
# without AppArmor; tier-2 plan to land via COPR or as part of the v0.7
|
apparmor-parser
|
||||||
# secureblue OCI hybrid (which has its own LSM stack).
|
apparmor-utils
|
||||||
|
apparmor-profiles
|
||||||
|
|
||||||
# admin essentials
|
# admin essentials
|
||||||
git
|
git
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue