Compare commits
61 commits
main
...
backup/pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e99a32084 | ||
|
|
e71ccaf198 | ||
|
|
cfd2eb69fd | ||
|
|
a06f54dd79 | ||
|
|
e6aa2d14a8 | ||
|
|
5910989f12 | ||
|
|
9a087ae0da | ||
|
|
266090ea0d | ||
|
|
b4c0feb30d | ||
|
|
c152953089 | ||
|
|
4966a65e37 | ||
|
|
2c197796e3 | ||
|
|
237968bfac | ||
|
|
f50f427ff8 | ||
|
|
ded80c6e15 | ||
|
|
48ccabe914 | ||
|
|
756b03aa5c | ||
|
|
1e70cc5461 | ||
|
|
9ee2cec20e | ||
|
|
8926894ceb | ||
|
|
6d8164c199 | ||
|
|
bbdafbce94 | ||
|
|
6391b1104b | ||
|
|
4d53d76442 | ||
|
|
606806f82f | ||
|
|
61fec5e1a9 | ||
|
|
5e94a61ea0 | ||
|
|
d48e59f05b | ||
|
|
ecd374ab1a | ||
|
|
e17c04007d | ||
|
|
97939d76f8 | ||
|
|
abaff9d3c3 | ||
|
|
29a6677d54 | ||
|
|
b3572565e2 | ||
|
|
9bf063a178 | ||
|
|
3f138e7435 | ||
|
|
7d6054311b | ||
|
|
6b0828d692 | ||
| a59f1f026a | |||
|
|
beef32a77c | ||
|
|
0a70eea950 | ||
|
|
877ad91096 | ||
|
|
a3b3d29b38 | ||
|
|
55221a6af2 | ||
| d76597c57a | |||
|
|
631e7bd040 | ||
|
|
9158532c9d | ||
|
|
e93ef644e1 | ||
|
|
21f2b4da9a | ||
| 91d5d26473 | |||
|
|
c7c0a0bcc8 | ||
| 9011fd2dbf | |||
| 20b3541d38 | |||
|
|
1fa45c3749 | ||
|
|
d9b206e46b | ||
|
|
89949dc8f2 | ||
| 0b568b016b | |||
|
|
e50c9a3b43 | ||
| 9dc2846316 | |||
| f2e36bfead | |||
|
|
3c247bc601 |
42 changed files with 3002 additions and 291 deletions
265
.github/workflows/build-bluebuild.yml
vendored
Normal file
265
.github/workflows/build-bluebuild.yml
vendored
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
name: Build veilor-os OCI (BlueBuild)
|
||||||
|
|
||||||
|
# v0.7 spike — builds the bootable OCI image used by the bootstrap
|
||||||
|
# kickstart's `ostreecontainer` directive. Runs on the Forgejo
|
||||||
|
# self-hosted runner (label `nullstone`); GitHub-side cosign/SBOM/
|
||||||
|
# attest steps are gated off because Forgejo has no Sigstore Fulcio-
|
||||||
|
# trusted OIDC issuer (see docs/PROOF-OF-WORK.md, build-iso.yml fix).
|
||||||
|
#
|
||||||
|
# Reference: https://blue-build.org/how-to/setup-build-action/
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [v0.7-bluebuild-spike]
|
||||||
|
paths:
|
||||||
|
- 'bluebuild/**'
|
||||||
|
- 'overlay/**'
|
||||||
|
- 'assets/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- '.github/workflows/build-bluebuild.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [main, v0.7-bluebuild-spike]
|
||||||
|
schedule:
|
||||||
|
# Rebuild weekly so we pick up upstream secureblue + Fedora updates.
|
||||||
|
- cron: '0 6 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build + push OCI
|
||||||
|
# nullstone label resolves to veilor-build:43 (fedora43 + nodejs)
|
||||||
|
# via runner config. Privileged + userns=host + sock pass-through
|
||||||
|
# already wired in the runner config (see infra/forgejo/).
|
||||||
|
runs-on: nullstone
|
||||||
|
timeout-minutes: 60
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write # for GH-only cosign keyless (skipped on Forgejo)
|
||||||
|
attestations: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Forgejo container registry path. PAT in FORGEJO_REGISTRY_TOKEN
|
||||||
|
# secret has package:write on veilor-org.
|
||||||
|
FORGEJO_REGISTRY: git.s8n.ru
|
||||||
|
FORGEJO_IMAGE: git.s8n.ru/veilor-org/veilor-os
|
||||||
|
OCI_TAG: "43"
|
||||||
|
# GH parallel target — only used when run on github.com.
|
||||||
|
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/veilor-os
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
# Pinned to last v4 tag confirmed to ship on node20.
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Fix sudo perms (userns=host artefact)
|
||||||
|
run: |
|
||||||
|
# Daemon has userns-remap=default; the act job container is
|
||||||
|
# launched with --userns=host. The image was pulled under
|
||||||
|
# remap so /etc/sudo.conf + /etc/sudoers ship as uid 100000.
|
||||||
|
# sudo refuses to read either unless owned by uid 0. Restore.
|
||||||
|
chown -R 0:0 /etc/sudo.conf /etc/sudoers /etc/sudoers.d 2>/dev/null || true
|
||||||
|
ls -la /etc/sudo.conf /etc/sudoers 2>&1 | head -5
|
||||||
|
|
||||||
|
- name: Install build tooling (Fedora)
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
dnf -y upgrade --refresh
|
||||||
|
# veilor-build:43 already ships git, curl, tar, sudo, nodejs.
|
||||||
|
# cosign is not packaged in Fedora 43; we install it from the
|
||||||
|
# upstream release tarball below in a separate step.
|
||||||
|
dnf -y install --skip-unavailable \
|
||||||
|
podman \
|
||||||
|
buildah \
|
||||||
|
skopeo \
|
||||||
|
jq
|
||||||
|
# blue-build/github-action shells out to `docker`; Fedora ships
|
||||||
|
# podman. Symlink so the action finds the CLI.
|
||||||
|
if ! command -v docker >/dev/null; then
|
||||||
|
ln -sf "$(command -v podman)" /usr/local/bin/docker
|
||||||
|
docker --version
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install cosign binary (upstream release)
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
# Fedora 43 has no cosign rpm. Pull static x86_64 binary
|
||||||
|
# from sigstore/cosign GitHub releases. Pinned to v2.4.1.
|
||||||
|
COSIGN_VERSION="2.4.1"
|
||||||
|
curl -fsSL \
|
||||||
|
"https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" \
|
||||||
|
-o /usr/local/bin/cosign
|
||||||
|
chmod +x /usr/local/bin/cosign
|
||||||
|
cosign version
|
||||||
|
|
||||||
|
- name: Pre-pull secureblue base image
|
||||||
|
env:
|
||||||
|
GHCR_PULL_TOKEN: ${{ secrets.GHCR_PULL_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
# GHCR rate-limits anonymous CI pulls (403 on bearer-token).
|
||||||
|
# Login with a read-only PAT (forgejo secret GHCR_PULL_TOKEN)
|
||||||
|
# so bluebuild's buildah inside the CLI container also sees a
|
||||||
|
# valid auth.json via shared storage bind-mount below.
|
||||||
|
if [ -n "${GHCR_PULL_TOKEN:-}" ]; then
|
||||||
|
echo "$GHCR_PULL_TOKEN" | podman login \
|
||||||
|
--username s8n-ru \
|
||||||
|
--password-stdin ghcr.io
|
||||||
|
else
|
||||||
|
echo "[WARN] GHCR_PULL_TOKEN secret empty; trying anonymous pull"
|
||||||
|
fi
|
||||||
|
podman pull ghcr.io/secureblue/kinoite-main-hardened:latest
|
||||||
|
|
||||||
|
- name: Stage cosign private key for signing module
|
||||||
|
env:
|
||||||
|
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${COSIGN_PRIVATE_KEY:-}" ]; then
|
||||||
|
echo "[ERR] COSIGN_PRIVATE_KEY secret missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# bluebuild signing module reads from this env var when
|
||||||
|
# building the cosign.key bind stage. Also write to bluebuild/
|
||||||
|
# so it sits next to cosign.pub for local reproducible runs.
|
||||||
|
mkdir -p bluebuild
|
||||||
|
printf '%s' "$COSIGN_PRIVATE_KEY" > bluebuild/cosign.key
|
||||||
|
chmod 600 bluebuild/cosign.key
|
||||||
|
# bluebuild's generated Containerfile uses `FROM scratch as
|
||||||
|
# stage-keys; COPY cosign.pub /keys/`. Buildah's build context
|
||||||
|
# is the cwd ($PWD) — symlink the keys to repo root so COPY
|
||||||
|
# finds them there too.
|
||||||
|
ln -sf bluebuild/cosign.pub cosign.pub
|
||||||
|
ln -sf bluebuild/cosign.key cosign.key
|
||||||
|
ls -la cosign.pub cosign.key 2>&1 | head -4
|
||||||
|
|
||||||
|
- name: Build OCI image with BlueBuild CLI container
|
||||||
|
id: bluebuild
|
||||||
|
# blue-build/github-action requires docker buildx which podman
|
||||||
|
# doesn't ship. Run the official BlueBuild CLI container with
|
||||||
|
# buildah driver instead — works against rootless or rootful
|
||||||
|
# podman, no docker dependency.
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
# Pull cli image; pinned to v0.9.x at action time.
|
||||||
|
podman pull ghcr.io/blue-build/cli:latest
|
||||||
|
# Mount the repo + podman socket; build with buildah driver.
|
||||||
|
# Bind host /var/lib/containers/storage into the bluebuild
|
||||||
|
# CLI container so buildah inside it can see the pre-pulled
|
||||||
|
# secureblue base layer (avoids GHCR auth round-trip during
|
||||||
|
# templating).
|
||||||
|
# podman login writes to $XDG_RUNTIME_DIR/containers/auth.json
|
||||||
|
# by default, which is volatile. Find it + copy to a stable
|
||||||
|
# path that we then bind into the bluebuild container.
|
||||||
|
AUTH_SRC=""
|
||||||
|
for cand in \
|
||||||
|
"${XDG_RUNTIME_DIR:-/run/user/0}/containers/auth.json" \
|
||||||
|
"/run/containers/0/auth.json" \
|
||||||
|
"/root/.config/containers/auth.json" \
|
||||||
|
"/root/.docker/config.json"; do
|
||||||
|
if [ -f "$cand" ]; then AUTH_SRC="$cand"; break; fi
|
||||||
|
done
|
||||||
|
if [ -z "$AUTH_SRC" ]; then
|
||||||
|
echo "[ERR] no podman/docker auth.json found post-login"
|
||||||
|
find / -name auth.json -o -name 'config.json' 2>/dev/null | head -10
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p /root/.config/containers
|
||||||
|
cp "$AUTH_SRC" /root/.config/containers/auth.json
|
||||||
|
ls -la /root/.config/containers/auth.json
|
||||||
|
|
||||||
|
# Diagnostic: confirm the keypair landed where bluebuild expects.
|
||||||
|
ls -la bluebuild/
|
||||||
|
head -1 bluebuild/cosign.pub
|
||||||
|
head -1 bluebuild/cosign.key | cut -c1-30
|
||||||
|
|
||||||
|
podman run --rm \
|
||||||
|
--privileged \
|
||||||
|
--security-opt label=disable \
|
||||||
|
--security-opt seccomp=unconfined \
|
||||||
|
--entrypoint /usr/bin/bluebuild \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-v /var/lib/containers/storage:/var/lib/containers/storage \
|
||||||
|
-v /root/.config/containers/auth.json:/root/.config/containers/auth.json:ro \
|
||||||
|
-w /work \
|
||||||
|
-e BB_BUILD_DRIVER=buildah \
|
||||||
|
ghcr.io/blue-build/cli:latest \
|
||||||
|
build \
|
||||||
|
--build-driver buildah \
|
||||||
|
-vv \
|
||||||
|
bluebuild/recipe.yml
|
||||||
|
# bluebuild CLI tags as <recipe-name>:<tag> in local podman
|
||||||
|
# storage. List + verify, then re-tag for the registries.
|
||||||
|
podman images
|
||||||
|
podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:${OCI_TAG}" || true
|
||||||
|
podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:latest" || true
|
||||||
|
|
||||||
|
- name: Push to Forgejo registry (primary)
|
||||||
|
if: success() && github.event_name != 'pull_request' && github.server_url != 'https://github.com'
|
||||||
|
env:
|
||||||
|
FORGEJO_REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
|
||||||
|
FORGEJO_REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${FORGEJO_REGISTRY_TOKEN:-}" ]; then
|
||||||
|
echo "[WARN] FORGEJO_REGISTRY_TOKEN secret is empty; skipping push"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "$FORGEJO_REGISTRY_TOKEN" | podman login \
|
||||||
|
--username "${FORGEJO_REGISTRY_USER:-veilor-org}" \
|
||||||
|
--password-stdin "$FORGEJO_REGISTRY"
|
||||||
|
podman push "${FORGEJO_IMAGE}:${OCI_TAG}"
|
||||||
|
podman push "${FORGEJO_IMAGE}:latest"
|
||||||
|
echo "[OK] pushed ${FORGEJO_IMAGE}:{${OCI_TAG},latest}"
|
||||||
|
|
||||||
|
- name: Push to GHCR (mirror, GitHub-only)
|
||||||
|
if: success() && github.event_name != 'pull_request' && github.server_url == 'https://github.com'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
podman tag localhost/veilor-os:latest "${GHCR_IMAGE}:${OCI_TAG}"
|
||||||
|
podman tag localhost/veilor-os:latest "${GHCR_IMAGE}:latest"
|
||||||
|
echo "${{ secrets.GITHUB_TOKEN }}" | podman login \
|
||||||
|
--username "${{ github.repository_owner }}" \
|
||||||
|
--password-stdin ghcr.io
|
||||||
|
podman push "${GHCR_IMAGE}:${OCI_TAG}"
|
||||||
|
podman push "${GHCR_IMAGE}:latest"
|
||||||
|
|
||||||
|
- name: Smoke-test OCI image
|
||||||
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
podman run --rm "localhost/veilor-os:latest" /bin/bash -c '
|
||||||
|
set -e
|
||||||
|
echo "-- os-release"
|
||||||
|
head -5 /etc/os-release
|
||||||
|
echo "-- sudo present"; which sudo
|
||||||
|
echo "-- mullvad-browser path"; rpm -q mullvad-browser || echo "not installed"
|
||||||
|
echo "-- yggdrasil"; rpm -q yggdrasil || echo "not installed"
|
||||||
|
echo "-- tailscale"; rpm -q tailscale || echo "not installed"
|
||||||
|
echo "-- veilor-firstboot unit"; ls -la /etc/systemd/system/veilor-firstboot.service 2>&1 || true
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── GitHub-only signing/SBOM/attest ────────────────────────────
|
||||||
|
# cosign keyless needs Sigstore Fulcio-trusted OIDC. Forgejo
|
||||||
|
# has none, so these are GH-only. v0.7+ TODO: cosign key-pair
|
||||||
|
# signing for Forgejo using a stored secret.
|
||||||
|
|
||||||
|
- name: SBOM (SPDX, GitHub-only)
|
||||||
|
if: github.event_name == 'push' && github.server_url == 'https://github.com'
|
||||||
|
# Pinned to last v0.17 release that ships node20.
|
||||||
|
uses: anchore/sbom-action@v0.17.2
|
||||||
|
with:
|
||||||
|
image: ${{ env.GHCR_IMAGE }}:${{ env.OCI_TAG }}
|
||||||
|
format: spdx-json
|
||||||
|
output-file: veilor-os-oci.spdx.json
|
||||||
|
|
||||||
|
- name: Build provenance attestation (GitHub-only)
|
||||||
|
if: github.event_name == 'push' && 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-name: ${{ env.GHCR_IMAGE }}
|
||||||
|
subject-digest: ${{ steps.bluebuild.outputs.digest }}
|
||||||
167
.github/workflows/build-installer-iso.yml
vendored
Normal file
167
.github/workflows/build-installer-iso.yml
vendored
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
name: Build veilor-os Installer ISO
|
||||||
|
|
||||||
|
# v0.7+ — produces a small Anaconda installer ISO that consumes
|
||||||
|
# kickstart/install-ostreecontainer-installer.ks. The ISO boots
|
||||||
|
# Anaconda, asks for LUKS pw + admin pw interactively, then
|
||||||
|
# `ostreecontainer` populates / from the v0.7 OCI image at
|
||||||
|
# ghcr.io/veilor-org/veilor-os:43.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [v0.7-bluebuild-spike]
|
||||||
|
paths:
|
||||||
|
- 'kickstart/install-ostreecontainer.ks'
|
||||||
|
- 'kickstart/install-ostreecontainer-installer.ks'
|
||||||
|
- 'bluebuild/recipe.yml'
|
||||||
|
- '.github/workflows/build-installer-iso.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
releasever:
|
||||||
|
description: 'Fedora release version'
|
||||||
|
required: false
|
||||||
|
default: '43'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # needed to create+update installer-latest release
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build installer ISO
|
||||||
|
runs-on: nullstone
|
||||||
|
timeout-minutes: 120
|
||||||
|
|
||||||
|
env:
|
||||||
|
RELEASEVER: ${{ github.event.inputs.releasever || '43' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Install build tooling (Fedora)
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
dnf -y upgrade --refresh
|
||||||
|
dnf -y install --skip-unavailable \
|
||||||
|
lorax \
|
||||||
|
pykickstart \
|
||||||
|
anaconda-tui \
|
||||||
|
syslinux \
|
||||||
|
xorriso \
|
||||||
|
grub2-efi-x64 \
|
||||||
|
grub2-efi-x64-modules \
|
||||||
|
grub2-pc \
|
||||||
|
grub2-pc-modules \
|
||||||
|
shim-x64 \
|
||||||
|
efibootmgr
|
||||||
|
|
||||||
|
- name: Validate installer kickstart
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
ksvalidator kickstart/install-ostreecontainer-installer.ks
|
||||||
|
|
||||||
|
- name: Build installer ISO with livemedia-creator
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
# livemedia-creator refuses an existing non-empty resultdir.
|
||||||
|
rm -rf build/out
|
||||||
|
mkdir -p /var/lmc
|
||||||
|
ln -sfn "$GITHUB_WORKSPACE" /work
|
||||||
|
# livemedia-creator does NOT support --title (that's livecd-creator).
|
||||||
|
# --volid replaces it for the ISO volume label.
|
||||||
|
livemedia-creator \
|
||||||
|
--make-iso \
|
||||||
|
--no-virt \
|
||||||
|
--ks kickstart/install-ostreecontainer-installer.ks \
|
||||||
|
--resultdir build/out \
|
||||||
|
--tmp /var/lmc \
|
||||||
|
--volid "veilor-os-installer-${RELEASEVER}" \
|
||||||
|
--project "veilor-os" \
|
||||||
|
--releasever "$RELEASEVER" \
|
||||||
|
--logfile build/out/build.log \
|
||||||
|
2>&1 | tee -a build/out/build.log
|
||||||
|
|
||||||
|
- name: Rename ISO + sha256
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
ISO_FILE=$(ls build/out/*.iso 2>/dev/null | head -1)
|
||||||
|
[ -n "$ISO_FILE" ] || { echo "[ERR] no ISO produced"; exit 1; }
|
||||||
|
ISO_NAME="veilor-os-installer-${RELEASEVER}-$(date +%Y%m%d-%H%M%S).iso"
|
||||||
|
mv "$ISO_FILE" "build/out/$ISO_NAME"
|
||||||
|
cd build/out
|
||||||
|
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
||||||
|
ls -lh "$ISO_NAME"
|
||||||
|
|
||||||
|
- name: Split ISO into 1900M chunks
|
||||||
|
if: success() && github.ref == 'refs/heads/v0.7-bluebuild-spike'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cd build/out
|
||||||
|
ISO=$(ls *.iso | head -1)
|
||||||
|
[ -n "$ISO" ] || { echo "[ERR] no ISO"; exit 1; }
|
||||||
|
split -b 1900M -d --suffix-length=2 "$ISO" "${ISO}.part-"
|
||||||
|
rm -f "$ISO"
|
||||||
|
sha256sum *.part-* > "${ISO}.parts.sha256"
|
||||||
|
ls "${ISO}".part-*
|
||||||
|
|
||||||
|
- name: Publish to installer-latest rolling prerelease (Forgejo)
|
||||||
|
if: success() && github.ref == 'refs/heads/v0.7-bluebuild-spike' && 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="installer-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
|
||||||
|
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 v0.7-bluebuild-spike. Latest commit: ${GIT_SHA}.
|
||||||
|
|
||||||
|
Installer ISO — boots Anaconda, prompts for LUKS pw + admin pw,
|
||||||
|
then ostreecontainer-pulls / from ghcr.io/veilor-org/veilor-os:43.
|
||||||
|
|
||||||
|
Reassemble:
|
||||||
|
cat veilor-os-installer-*.iso.part-* > veilor-os-installer.iso
|
||||||
|
sha256sum -c veilor-os-installer-*.iso.parts.sha256
|
||||||
|
|
||||||
|
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': 'v0.7-bluebuild-spike',
|
||||||
|
'name': 'installer-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 release"; exit 1; }
|
||||||
|
cd build/out
|
||||||
|
for f in *.iso.part-* *.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
|
||||||
|
|
||||||
|
- name: Print build log on failure
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
echo "─── build/out/build.log ───"
|
||||||
|
tail -200 build/out/build.log 2>/dev/null || echo "(no build.log)"
|
||||||
|
find build/out -name 'program.log' -exec tail -100 {} \; 2>/dev/null || true
|
||||||
|
find /var/lmc -name '*.log' -exec tail -50 {} \; 2>/dev/null || true
|
||||||
238
.github/workflows/build-iso.yml
vendored
238
.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:
|
||||||
|
|
@ -21,41 +23,28 @@ on:
|
||||||
|
|
||||||
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: |
|
|
||||||
sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /usr/local/share/boost
|
|
||||||
sudo apt-get clean
|
|
||||||
df -h
|
|
||||||
|
|
||||||
- name: Run build inside Fedora 43 container
|
|
||||||
uses: addnab/docker-run-action@v3
|
|
||||||
with:
|
|
||||||
image: registry.fedoraproject.org/fedora:43
|
|
||||||
options: |
|
|
||||||
--privileged
|
|
||||||
-v ${{ github.workspace }}:/work
|
|
||||||
-v /dev:/dev
|
|
||||||
--tmpfs /tmp:rw,nosuid,nodev,exec,size=16G
|
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
# Update Fedora image to latest packages — guarantees pcre2 +
|
|
||||||
# libselinux + selinux-policy are matched (the local build's
|
|
||||||
# core problem). CI runners always start fresh, no version skew.
|
|
||||||
dnf -y upgrade --refresh
|
dnf -y upgrade --refresh
|
||||||
|
|
||||||
# Install build tooling
|
|
||||||
dnf -y install \
|
dnf -y install \
|
||||||
lorax \
|
lorax \
|
||||||
livecd-tools \
|
livecd-tools \
|
||||||
|
|
@ -70,23 +59,26 @@ jobs:
|
||||||
shadow-utils \
|
shadow-utils \
|
||||||
syslinux \
|
syslinux \
|
||||||
tar \
|
tar \
|
||||||
curl
|
curl \
|
||||||
|
sudo
|
||||||
|
|
||||||
# Vendor gum binary onto the ISO so the TTY1 installer can use
|
- name: Vendor gum binary into overlay
|
||||||
# Charm.sh TUI primitives. gum is not packaged in Fedora repos,
|
run: |
|
||||||
# so pull the upstream release tarball pinned by sha256.
|
set -euxo pipefail
|
||||||
GUM_VERSION="0.17.0"
|
GUM_VERSION="0.17.0"
|
||||||
GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_x86_64.tar.gz"
|
GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_x86_64.tar.gz"
|
||||||
GUM_SHA256="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb"
|
GUM_SHA256="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb"
|
||||||
mkdir -p /work/overlay/usr/local/bin
|
mkdir -p overlay/usr/local/bin
|
||||||
curl -fsSL "$GUM_URL" -o /tmp/gum.tgz
|
curl -fsSL "$GUM_URL" -o /tmp/gum.tgz
|
||||||
echo "$GUM_SHA256 /tmp/gum.tgz" | sha256sum -c -
|
echo "$GUM_SHA256 /tmp/gum.tgz" | sha256sum -c -
|
||||||
tar -xzf /tmp/gum.tgz -C /tmp/
|
tar -xzf /tmp/gum.tgz -C /tmp/
|
||||||
install -m 0755 "/tmp/gum_${GUM_VERSION}_Linux_x86_64/gum" /work/overlay/usr/local/bin/gum
|
install -m 0755 "/tmp/gum_${GUM_VERSION}_Linux_x86_64/gum" overlay/usr/local/bin/gum
|
||||||
/work/overlay/usr/local/bin/gum --version
|
overlay/usr/local/bin/gum --version
|
||||||
echo "[OK] gum ${GUM_VERSION} vendored into overlay/usr/local/bin/"
|
echo "[OK] gum ${GUM_VERSION} vendored into overlay/usr/local/bin/"
|
||||||
|
|
||||||
cd /work
|
- name: Build ISO with livecd-creator
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
# PATCH: livecd-creator bug — __get_efi_image_stanza writes
|
# PATCH: livecd-creator bug — __get_efi_image_stanza writes
|
||||||
# `root=live:LABEL=...` instead of `live:CDLABEL=...` for dracut.
|
# `root=live:LABEL=...` instead of `live:CDLABEL=...` for dracut.
|
||||||
|
|
@ -98,11 +90,8 @@ jobs:
|
||||||
echo "[OK] livecd-creator patched: LABEL= → CDLABEL= for EFI dracut stanza"
|
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.
|
# CI uses ks-ci.ks (no local fix-repo line). Generated from main ks.
|
||||||
# Also strip flags livecd-creator doesn't recognize.
|
# Drop `updates` repo: previously 404'd on repodata zchunk during
|
||||||
# Drop `updates` repo: 3 consecutive builds 404'd on its
|
# Fedora mid-push windows. Base 43 ships the selinux-policy fix.
|
||||||
# repodata zchunk file across all mirrors — Fedora infra issue
|
|
||||||
# mid-push window. Original reason for `updates` (selinux-policy
|
|
||||||
# 43.7 pcre2 fix) is no longer needed; base 43 ships fixed.
|
|
||||||
sed -e '/veilor-fix/d' \
|
sed -e '/veilor-fix/d' \
|
||||||
-e '/^shutdown$/d' \
|
-e '/^shutdown$/d' \
|
||||||
-e '/repo --name=updates/d' \
|
-e '/repo --name=updates/d' \
|
||||||
|
|
@ -111,11 +100,12 @@ jobs:
|
||||||
ksvalidator kickstart/veilor-os-ci.ks
|
ksvalidator kickstart/veilor-os-ci.ks
|
||||||
mkdir -p build/out
|
mkdir -p build/out
|
||||||
|
|
||||||
# livecd-creator (livecd-tools) — purpose-built for live ISOs.
|
# The kickstart's %post --nochroot probes a fixed list of
|
||||||
# Handles EFI/BOOT + isohybrid + grafting that livemedia-creator
|
# candidate paths to locate the repo source for overlay/scripts
|
||||||
# --make-iso --no-virt does not. Produces UEFI+BIOS bootable ISO.
|
# copy. /work is the canonical CI candidate; symlink the live
|
||||||
# --tmpdir /var/lmc to avoid GitHub Actions /tmp tmpfs constraints.
|
# workspace there so the existing probe finds it.
|
||||||
# /var on the runner is the host's ext4 (~80GB free post-disk-cleanup).
|
ln -sfn "$GITHUB_WORKSPACE" /work
|
||||||
|
|
||||||
mkdir -p /var/lmc /var/lmc-cache
|
mkdir -p /var/lmc /var/lmc-cache
|
||||||
livecd-creator \
|
livecd-creator \
|
||||||
--verbose \
|
--verbose \
|
||||||
|
|
@ -127,48 +117,37 @@ jobs:
|
||||||
--tmpdir /var/lmc \
|
--tmpdir /var/lmc \
|
||||||
--cache /var/lmc-cache 2>&1 | tee build/out/build.log
|
--cache /var/lmc-cache 2>&1 | tee build/out/build.log
|
||||||
|
|
||||||
# Graft veilor source tree onto the ISO so the installer-generated
|
- name: Graft veilor source tree onto ISO
|
||||||
# kickstart's `%post --nochroot` can find SRC at
|
run: |
|
||||||
# /run/install/repo/veilor/{overlay,scripts,assets}/ when the user
|
set -euxo pipefail
|
||||||
# promotes the live ISO into a real install.
|
ISO_FILE=$(ls ./*.iso 2>/dev/null | head -1)
|
||||||
ISO_FILE=$(ls /work/*.iso 2>/dev/null | head -1)
|
|
||||||
[ -n "$ISO_FILE" ] || { echo "[ERR] no ISO produced by livecd-creator"; exit 1; }
|
[ -n "$ISO_FILE" ] || { echo "[ERR] no ISO produced by livecd-creator"; exit 1; }
|
||||||
echo "[INFO] grafting /veilor/ onto $ISO_FILE"
|
echo "[INFO] grafting /veilor/ onto $ISO_FILE"
|
||||||
|
|
||||||
# Extract original ISO's exact boot stanza so the rebuild matches
|
|
||||||
# livecd-creator's layout byte-for-byte. This is immune to upstream
|
|
||||||
# Fedora layout changes (e.g. images/ vs isolinux/ for efiboot.img,
|
|
||||||
# partition geometry flags, hybrid MBR/GPT options).
|
|
||||||
xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>&1 | tee /tmp/iso-boot.txt || true
|
xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>&1 | tee /tmp/iso-boot.txt || true
|
||||||
ORIG_FLAGS=$(xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>/dev/null | \
|
ORIG_FLAGS=$(xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>/dev/null | \
|
||||||
grep -v '^xorriso :' | grep -E '^-' | tr '\n' ' ')
|
grep -v '^xorriso :' | grep -E '^-' | tr '\n' ' ')
|
||||||
[ -n "$ORIG_FLAGS" ] || { echo "[ERR] could not extract boot stanza from $ISO_FILE"; exit 1; }
|
[ -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
|
mkdir -p /tmp/iso-mod
|
||||||
xorriso -osirrox on -indev "$ISO_FILE" -extract / /tmp/iso-mod
|
xorriso -osirrox on -indev "$ISO_FILE" -extract / /tmp/iso-mod
|
||||||
chmod -R u+w /tmp/iso-mod
|
chmod -R u+w /tmp/iso-mod
|
||||||
mkdir -p /tmp/iso-mod/veilor
|
mkdir -p /tmp/iso-mod/veilor
|
||||||
cp -a /work/overlay /work/scripts /work/assets /tmp/iso-mod/veilor/
|
cp -a overlay scripts assets /tmp/iso-mod/veilor/
|
||||||
|
|
||||||
# Replay the exact stanza captured above. eval is needed because
|
|
||||||
# ORIG_FLAGS contains multiple flag/value pairs that must word-split.
|
|
||||||
eval xorriso -as mkisofs \
|
eval xorriso -as mkisofs \
|
||||||
-volid "veilor-os-43" \
|
-volid "veilor-os-43" \
|
||||||
$ORIG_FLAGS \
|
$ORIG_FLAGS \
|
||||||
-o "${ISO_FILE}.tmp" /tmp/iso-mod
|
-o "${ISO_FILE}.tmp" /tmp/iso-mod
|
||||||
mv "${ISO_FILE}.tmp" "$ISO_FILE"
|
mv "${ISO_FILE}.tmp" "$ISO_FILE"
|
||||||
rm -rf /tmp/iso-mod
|
rm -rf /tmp/iso-mod
|
||||||
echo "[OK] /veilor/ grafted onto $ISO_FILE"
|
|
||||||
|
|
||||||
# Move output ISO to expected dir
|
mv "$ISO_FILE" build/out/
|
||||||
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"
|
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-$(date +%Y%m%d-%H%M%S).iso"
|
||||||
cd build/out
|
cd build/out
|
||||||
for f in *.iso; do
|
for f in *.iso; do
|
||||||
[[ -f $f && $f != $ISO_NAME ]] && mv "$f" "$ISO_NAME"
|
[[ -f $f && $f != "$ISO_NAME" ]] && mv "$f" "$ISO_NAME"
|
||||||
done
|
done
|
||||||
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
||||||
ls -lh "$ISO_NAME"
|
ls -lh "$ISO_NAME"
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
122
.github/workflows/smoke-test-oci.yml
vendored
Normal file
122
.github/workflows/smoke-test-oci.yml
vendored
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
name: Smoke-test veilor-os OCI
|
||||||
|
|
||||||
|
# Pulls git.s8n.ru/veilor-org/veilor-os:43 and asserts that the image
|
||||||
|
# contains the veilor brand + the v0.5.x hardening overlay + the v0.7
|
||||||
|
# CLI tools, and that cosign verifies it against bluebuild/cosign.pub.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Build veilor-os OCI (BlueBuild)"]
|
||||||
|
types: [completed]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
smoke:
|
||||||
|
name: OCI smoke test
|
||||||
|
runs-on: nullstone
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event_name == 'workflow_run' &&
|
||||||
|
github.event.workflow_run.conclusion == 'success')
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: git.s8n.ru/veilor-org/veilor-os:43
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Fix sudo perms
|
||||||
|
run: chown -R 0:0 /etc/sudo.conf /etc/sudoers /etc/sudoers.d 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Install podman + cosign
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
command -v podman >/dev/null || dnf -y install --skip-unavailable podman
|
||||||
|
if ! command -v cosign >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64" \
|
||||||
|
-o /usr/local/bin/cosign
|
||||||
|
chmod +x /usr/local/bin/cosign
|
||||||
|
fi
|
||||||
|
podman --version
|
||||||
|
cosign version
|
||||||
|
|
||||||
|
- name: Login + pull OCI image
|
||||||
|
env:
|
||||||
|
FORGEJO_REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
|
||||||
|
FORGEJO_REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
if [ -n "${FORGEJO_REGISTRY_TOKEN:-}" ]; then
|
||||||
|
echo "$FORGEJO_REGISTRY_TOKEN" | podman login \
|
||||||
|
--username "${FORGEJO_REGISTRY_USER:-veilor-org}" \
|
||||||
|
--password-stdin git.s8n.ru
|
||||||
|
fi
|
||||||
|
podman pull "${IMAGE}"
|
||||||
|
|
||||||
|
- name: Verify cosign signature
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
[ -f bluebuild/cosign.pub ] || { echo "[ERR] bluebuild/cosign.pub missing"; exit 1; }
|
||||||
|
cosign verify --key bluebuild/cosign.pub "${IMAGE}" 2>&1 | tail -10
|
||||||
|
|
||||||
|
- name: Run OCI assertions
|
||||||
|
run: |
|
||||||
|
set -uo pipefail
|
||||||
|
PASS=0; FAIL=0; ERRORS=""
|
||||||
|
pass() { echo "[PASS] $1"; PASS=$((PASS+1)); }
|
||||||
|
fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); ERRORS="${ERRORS} - $1\n"; }
|
||||||
|
img() { podman run --rm "${IMAGE}" /bin/bash -c "$1" 2>/dev/null; }
|
||||||
|
|
||||||
|
OS=$(img 'cat /etc/os-release 2>/dev/null')
|
||||||
|
echo "$OS" | grep -q 'ID=veilor' && pass "ID=veilor" || fail "ID=veilor missing"
|
||||||
|
echo "$OS" | grep -q 'NAME="veilor-os"' && pass 'NAME="veilor-os"' || fail 'NAME="veilor-os" missing'
|
||||||
|
|
||||||
|
img 'which sudo' >/dev/null && pass "sudo present" || fail "sudo missing"
|
||||||
|
img 'rpm -q mullvad-browser' >/dev/null && pass "mullvad-browser present" || fail "mullvad-browser missing"
|
||||||
|
img 'rpm -q tailscale' >/dev/null && pass "tailscale present" || fail "tailscale missing"
|
||||||
|
img 'rpm -q yggdrasil' >/dev/null && pass "yggdrasil present" || fail "yggdrasil missing"
|
||||||
|
|
||||||
|
if img 'grep -qi "^SELINUX=enforcing" /etc/selinux/config'; then
|
||||||
|
pass "SELinux config = enforcing"
|
||||||
|
else
|
||||||
|
fail "SELinux not enforcing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
img 'test -e /etc/systemd/system/multi-user.target.wants/veilor-firstboot.service \
|
||||||
|
-o -e /etc/systemd/system/graphical.target.wants/veilor-firstboot.service' >/dev/null \
|
||||||
|
&& pass "veilor-firstboot enabled" || fail "veilor-firstboot not enabled"
|
||||||
|
img 'test -e /etc/systemd/system/multi-user.target.wants/veilor-postinstall.service \
|
||||||
|
-o -e /etc/systemd/system/graphical.target.wants/veilor-postinstall.service' >/dev/null \
|
||||||
|
&& pass "veilor-postinstall enabled" || fail "veilor-postinstall not enabled"
|
||||||
|
|
||||||
|
for b in veilor-power veilor-update veilor-doctor veilor-postinstall; do
|
||||||
|
img "test -x /usr/local/bin/${b}" >/dev/null \
|
||||||
|
&& pass "${b} executable" || fail "${b} missing"
|
||||||
|
done
|
||||||
|
|
||||||
|
if img 'ls /usr/share/veilor-os/scripts/ 2>/dev/null' | grep -qE '(10-harden|20-harden|30-apply)'; then
|
||||||
|
pass "/usr/share/veilor-os/scripts populated"
|
||||||
|
else
|
||||||
|
fail "/usr/share/veilor-os/scripts missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
LEAKS=$(img "grep -rIni 'onyx\|192\.168\.0\.\|fedora\.local\|xynki\.dev' /etc/veilor* /usr/share/veilor-os 2>/dev/null")
|
||||||
|
if [ -z "$LEAKS" ]; then
|
||||||
|
pass "no brand leaks"
|
||||||
|
else
|
||||||
|
fail "brand leaks found"
|
||||||
|
echo "$LEAKS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "═══ ${PASS} passed, ${FAIL} failed ═══"
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
printf "%b" "$ERRORS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ veilor-os:43 smoke test passed"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -13,4 +13,7 @@ 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/
|
||||||
|
**/cosign.key
|
||||||
|
|
|
||||||
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -11,6 +11,18 @@ future maintainers can see why a change exists, not just what it changes.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### v0.7 BlueBuild OCI spike (active)
|
||||||
|
|
||||||
|
- Promote `v0.7-bluebuild-spike` to active mainline; v0.6 cancelled.
|
||||||
|
- Port `build-bluebuild.yml` to the Forgejo runner (`runs-on: nullstone`):
|
||||||
|
install BlueBuild CLI in-job, push to `git.s8n.ru/veilor-org/veilor-os`,
|
||||||
|
gate cosign keyless / SBOM / attest steps to GitHub-only.
|
||||||
|
- Atomic CLI tools: `veilor-update` rewritten on `bootc upgrade`,
|
||||||
|
new `veilor-postinstall` first-login TUI, `veilor-doctor` learns
|
||||||
|
`bootc status --json` while keeping the legacy dnf path.
|
||||||
|
- Docs: `docs/INSTALL-V07.md`, `docs/STRATEGY.md` PIVOT EXECUTION
|
||||||
|
section, README quick-install rewritten for v0.7.
|
||||||
|
|
||||||
### Planned
|
### Planned
|
||||||
|
|
||||||
- v0.3 polish — Plymouth black theme, SDDM theme, Konsole profile,
|
- v0.3 polish — Plymouth black theme, SDDM theme, Konsole profile,
|
||||||
|
|
|
||||||
104
README.md
104
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,
|
||||||
|
|
@ -43,24 +48,46 @@ spike at v0.7**, **bootc-only at v1.0**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick install
|
## Quick install — v0.7+ (recommended, atomic / OCI)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Download the ISO (after public release; CI artifact for now)
|
# 1. Download the bootstrap installer ISO from Forgejo.
|
||||||
|
# https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest
|
||||||
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 — triple-check.
|
||||||
sudo dd if=veilor-os-43-*.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
sudo dd if=veilor-os-43-*.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
||||||
sync
|
sync
|
||||||
|
|
||||||
# 3. Boot from USB, pick "Install veilor-os" from the menu.
|
# 3. Boot from USB. Anaconda asks for LUKS passphrase + admin password.
|
||||||
# 4. Set a strong LUKS passphrase — the only prompt during install.
|
# Anaconda then runs `ostreecontainer --url=git.s8n.ru/veilor-org/veilor-os:43`
|
||||||
# 5. Reboot, remove USB.
|
# which populates / from the signed BlueBuild OCI image.
|
||||||
# 6. On first boot: TTY prompts for an admin password (≥14 chars, mixed case,
|
|
||||||
# digit, symbol). Once accepted, SDDM starts. Log in as `admin`.
|
# 4. Reboot. Log in as `admin`. The first-login TUI (veilor-postinstall)
|
||||||
|
# asks for the small set of decisions we defer from install:
|
||||||
|
# keyboard, locale, hostname, GPU drivers, package presets,
|
||||||
|
# bluetooth, USBGuard policy snapshot. Each step skippable.
|
||||||
|
|
||||||
|
# 5. Day-to-day: `sudo veilor-update` (atomic, A/B, instant rollback).
|
||||||
```
|
```
|
||||||
|
|
||||||
Full install + first-boot walkthrough: [docs/INSTALL.md](docs/INSTALL.md).
|
Full v0.7 walkthrough: [docs/INSTALL-V07.md](docs/INSTALL-V07.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Legacy v0.5.0 install (kickstart-flat path)
|
||||||
|
|
||||||
|
The kickstart-installed v0.5.0 ISO ships as a frozen proof-of-work
|
||||||
|
release. Same hardening, no bootc/rpm-ostree atomic layer. Updates
|
||||||
|
go through `dnf upgrade` instead of `bootc upgrade`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Same flash + boot, then pick "Install veilor-os".
|
||||||
|
# Single LUKS passphrase prompt during install; admin password set
|
||||||
|
# on first boot via TTY.
|
||||||
|
```
|
||||||
|
|
||||||
|
Walkthrough: [docs/INSTALL.md](docs/INSTALL.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -109,30 +136,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
|
||||||
|
`kinoite-main-hardened` 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
|
||||||
|
|
|
||||||
96
bluebuild/README.md
Normal file
96
bluebuild/README.md
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
# bluebuild/ — v0.7 spike
|
||||||
|
|
||||||
|
This directory contains the BlueBuild recipe + supporting config that
|
||||||
|
builds the veilor-os bootable OCI image. **Active on the
|
||||||
|
`v0.7-bluebuild-spike` branch only.** Does NOT land in v0.5.x main
|
||||||
|
until the spike passes its success criteria (see
|
||||||
|
`docs/STRATEGY.md`).
|
||||||
|
|
||||||
|
## What's here
|
||||||
|
|
||||||
|
```
|
||||||
|
bluebuild/
|
||||||
|
├── recipe.yml # primary BlueBuild recipe
|
||||||
|
├── config/
|
||||||
|
│ └── just/
|
||||||
|
│ └── 60-veilor.just # ujust recipes for opt-in components
|
||||||
|
└── README.md # this file
|
||||||
|
```
|
||||||
|
|
||||||
|
The recipe extends
|
||||||
|
`ghcr.io/secureblue/kinoite-main-hardened:latest`. We
|
||||||
|
inherit secureblue's hardening (sysctl + kargs + custom SELinux
|
||||||
|
policy + USBGuard + hardened-malloc + Unbound DoT + chronyd NTS +
|
||||||
|
Trivalent browser + cosign-signed image chain). On top, we layer:
|
||||||
|
|
||||||
|
- veilor branding (overlay/, theme, plymouth, sddm, os-release)
|
||||||
|
- mullvad-browser (anti-fingerprint companion to Trivalent)
|
||||||
|
- xorg-x11-server-Xwayland (re-enable; secureblue disables it)
|
||||||
|
- sudo (re-enable; secureblue replaces with run0)
|
||||||
|
- tailscale + yggdrasil (mesh stack layer 1 + 2)
|
||||||
|
- ujust recipes for Reticulum (mesh layer 3) + Thorium (opt-in browser)
|
||||||
|
|
||||||
|
Trivalent stays as the default browser (correcting an earlier draft).
|
||||||
|
|
||||||
|
## Build locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requires bluebuild CLI:
|
||||||
|
# curl -fsSL https://raw.githubusercontent.com/blue-build/cli/main/install.sh | sh
|
||||||
|
cd bluebuild
|
||||||
|
bluebuild build recipe.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `localhost/veilor-os:43` in podman storage. Push to GHCR
|
||||||
|
via the workflow.
|
||||||
|
|
||||||
|
## Test the OCI image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Smoke-test (boots into the rootfs; no kernel, no init):
|
||||||
|
podman run --rm -it ghcr.io/veilor-org/veilor-os:43 /bin/bash
|
||||||
|
|
||||||
|
# Inside, sanity:
|
||||||
|
cat /etc/os-release # PRETTY_NAME=veilor-os
|
||||||
|
which sudo # /usr/bin/sudo (re-enabled)
|
||||||
|
which trivalent # secureblue's COPR (default browser)
|
||||||
|
which mullvad-browser # /usr/bin/mullvad-browser
|
||||||
|
systemctl is-enabled yggdrasil # enabled (idle)
|
||||||
|
systemctl is-enabled tailscaled # disabled (awaits ujust veilor-mesh-join)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test the installer ISO
|
||||||
|
|
||||||
|
The installer ISO is built separately by livecd-creator (current path)
|
||||||
|
or bootc-image-builder (v1.0+). Its kickstart's `%packages` block is
|
||||||
|
replaced with:
|
||||||
|
|
||||||
|
```
|
||||||
|
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
|
||||||
|
```
|
||||||
|
|
||||||
|
That populates the target's `/` directly from this OCI image during
|
||||||
|
the install pass. No first-boot rebase. No transition window.
|
||||||
|
|
||||||
|
## Spike success criteria (1 day)
|
||||||
|
|
||||||
|
- [ ] `bluebuild build recipe.yml` exits 0
|
||||||
|
- [ ] `bootc container lint` exits 0 on the resulting image
|
||||||
|
- [ ] `podman run` smoke-test (commands above) all pass
|
||||||
|
- [ ] `.github/workflows/build-bluebuild.yml` builds + cosign-signs +
|
||||||
|
pushes to `ghcr.io/veilor-org/veilor-os:43`
|
||||||
|
- [ ] An installer ISO using `ostreecontainer` against this OCI
|
||||||
|
reaches SDDM with admin login on first boot
|
||||||
|
|
||||||
|
If all five land, merge `v0.7-bluebuild-spike` → `main` as v0.7.0.
|
||||||
|
If any fail in ways that aren't trivially fixable, file each as a GH
|
||||||
|
issue + return to v0.5.x kickstart path.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `docs/STRATEGY.md` — the strategic decision + override list
|
||||||
|
- `docs/ROADMAP.md` v0.7 — full schedule
|
||||||
|
- `docs/THREAT-MODEL.md` — what we publish before launch
|
||||||
|
- secureblue: <https://github.com/secureblue/secureblue>
|
||||||
|
- BlueBuild: <https://blue-build.org>
|
||||||
|
- bootc / ostreecontainer: <https://docs.fedoraproject.org/en-US/bootc/>
|
||||||
73
bluebuild/config/just/60-veilor.just
Normal file
73
bluebuild/config/just/60-veilor.just
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# veilor-os ujust recipes — opt-in components
|
||||||
|
# Loaded into /usr/share/ublue-os/just/ at image build time;
|
||||||
|
# `ujust install-X` discovers + dispatches.
|
||||||
|
|
||||||
|
# install Reticulum / RetiNet AGPL fork + Sideband (mesh layer 3)
|
||||||
|
install-reticulum:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "═══ Reticulum (RetiNet AGPL fork) install ═══"
|
||||||
|
echo
|
||||||
|
echo "Installs RetiNet (AGPL fork — NOT upstream RNS due to anti-AI"
|
||||||
|
echo "license) plus Sideband messenger. Default config: AutoInterface"
|
||||||
|
echo "(LAN multicast) + 1-2 TCP backbone peers. RNode hardware (LoRa"
|
||||||
|
echo "transceiver) is a separate install."
|
||||||
|
echo
|
||||||
|
read -p "Proceed? [y/N]: " confirm
|
||||||
|
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
|
||||||
|
rpm-ostree install python3-pip
|
||||||
|
pip install --user retinet sideband-cli
|
||||||
|
echo
|
||||||
|
echo "Done. To attach an RNode (LoRa transceiver), run:"
|
||||||
|
echo " ujust install-reticulum-rnode"
|
||||||
|
|
||||||
|
# install Reticulum RNode hardware support (LoRa transceiver)
|
||||||
|
install-reticulum-rnode:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "═══ RNode (LoRa transceiver) hardware install ═══"
|
||||||
|
echo
|
||||||
|
echo "Adds RNode firmware-update tooling + udev rules for the LoRa"
|
||||||
|
echo "USB hardware. Required only if you have an RNode device."
|
||||||
|
echo
|
||||||
|
read -p "Proceed? [y/N]: " confirm
|
||||||
|
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
|
||||||
|
pip install --user rnodeconf
|
||||||
|
echo "Done. Plug in your RNode via USB; it will appear as a serial device."
|
||||||
|
|
||||||
|
# install Thorium browser (OPT-IN, with explicit CVE-lag warning)
|
||||||
|
install-thorium:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "═══ Thorium browser install ═══"
|
||||||
|
echo
|
||||||
|
echo "WARNING: Thorium is a perf/media-focused fork of Chromium that"
|
||||||
|
echo "uses LTS Chromium as its base. As of 2026-05 it lags upstream"
|
||||||
|
echo "stable by ~9 milestones (months of CVE backlog)."
|
||||||
|
echo
|
||||||
|
echo "veilor-os ships Trivalent (secureblue's hardened Chromium fork,"
|
||||||
|
echo "tracking upstream M147+ within hours) as the default browser."
|
||||||
|
echo "Thorium is provided as an OPT-IN profile for users who"
|
||||||
|
echo "explicitly need its perf characteristics (e.g. WebGL games,"
|
||||||
|
echo "media decode profiles)."
|
||||||
|
echo
|
||||||
|
echo "DO NOT use Thorium as your daily-driver browser. Use Trivalent"
|
||||||
|
echo "or Mullvad Browser for that."
|
||||||
|
echo
|
||||||
|
read -p "Acknowledge CVE-lag risk and continue? [y/N]: " confirm
|
||||||
|
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
|
||||||
|
flatpak install --user -y org.thorium.Thorium 2>/dev/null || \
|
||||||
|
rpm-ostree install thorium-browser
|
||||||
|
echo "Done. Launch via Plasma menu or `flatpak run org.thorium.Thorium`."
|
||||||
|
|
||||||
|
# join the veilor mesh (Tailscale via Headscale)
|
||||||
|
veilor-mesh-join:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "═══ Join veilor mesh (Tailscale via Headscale) ═══"
|
||||||
|
echo
|
||||||
|
echo "Pre-auth keys are minted by the Misskey signup page at"
|
||||||
|
echo "x.veilor (TTL 24h, single-use). You can paste the hex key"
|
||||||
|
echo "directly OR scan the QR code shown after signup."
|
||||||
|
echo
|
||||||
|
read -p "Hex key (paste): " preauth
|
||||||
|
if [[ -z "$preauth" ]]; then echo "Empty key. Cancelled."; exit 0; fi
|
||||||
|
sudo systemctl enable --now tailscaled
|
||||||
|
sudo tailscale up --login-server=https://hs.s8n.ru --auth-key="$preauth"
|
||||||
|
echo "Done. Status: $(sudo tailscale status | head -1)"
|
||||||
4
bluebuild/cosign.pub
Normal file
4
bluebuild/cosign.pub
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5xQcyP7FHNSiG7+VLsN2ViWlvvIB
|
||||||
|
FYmu2XmPah7/VBlmuQ88H0ZbqCqqnS2u9x5+P1OMaMK+//k89V0Blrx65Q==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
155
bluebuild/recipe.yml
Normal file
155
bluebuild/recipe.yml
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
# veilor-os — BlueBuild recipe (v0.7 spike, 1-day target)
|
||||||
|
#
|
||||||
|
# Extends secureblue's hardened Kinoite OCI image with veilor branding,
|
||||||
|
# threat-model-driven UX choices, and the three-layer mesh stack
|
||||||
|
# (Tailscale + Yggdrasil + opt-in Reticulum). This is the OCI image
|
||||||
|
# that the v0.7+ kickstart's `ostreecontainer` directive pulls into
|
||||||
|
# the target root during the install pass.
|
||||||
|
#
|
||||||
|
# Build: bluebuild build recipe.yml
|
||||||
|
# Test: podman run --rm -it ghcr.io/veilor-org/veilor-os:43 /bin/bash
|
||||||
|
# CI: .github/workflows/build-bluebuild.yml signs + pushes to GHCR.
|
||||||
|
#
|
||||||
|
# Reference: https://blue-build.org/reference/recipe/
|
||||||
|
---
|
||||||
|
name: veilor-os
|
||||||
|
description: Hardened security-branded Fedora KDE on top of secureblue.
|
||||||
|
|
||||||
|
# Base image: secureblue's hardened Kinoite variant with userns sandboxing.
|
||||||
|
# That brings in: sysctl + kargs + custom SELinux policy + USBGuard +
|
||||||
|
# hardened-malloc + Unbound DoT + chronyd NTS + Trivalent browser.
|
||||||
|
base-image: ghcr.io/secureblue/kinoite-main-hardened
|
||||||
|
image-version: latest
|
||||||
|
|
||||||
|
modules:
|
||||||
|
# ── 1. veilor branding overlay ──────────────────────────────────
|
||||||
|
# `type: copy` is a low-level direct COPY (no chmod, no script).
|
||||||
|
# `type: files` was failing with `chmod: Operation not permitted` on
|
||||||
|
# the BlueBuild-shipped /tmp/modules/files/files.sh under buildah +
|
||||||
|
# podman privileged in our runner — the script tries to make itself
|
||||||
|
# executable inside its own bind-mounted layer.
|
||||||
|
- type: copy
|
||||||
|
source: ../overlay
|
||||||
|
destination: /
|
||||||
|
|
||||||
|
- type: copy
|
||||||
|
source: ../assets
|
||||||
|
destination: /usr/share/veilor-os/assets
|
||||||
|
|
||||||
|
- type: copy
|
||||||
|
source: ../scripts
|
||||||
|
destination: /usr/share/veilor-os/scripts
|
||||||
|
|
||||||
|
# ── 2. Branding overrides at build time ─────────────────────────
|
||||||
|
- type: script
|
||||||
|
snippets:
|
||||||
|
- |
|
||||||
|
# os-release brand
|
||||||
|
sed -i \
|
||||||
|
-e 's|^GRUB_DISTRIBUTOR=.*|GRUB_DISTRIBUTOR="veilor-os"|' \
|
||||||
|
/etc/default/grub 2>/dev/null || true
|
||||||
|
# Apply our kde-theme + plymouth in build
|
||||||
|
bash /usr/share/veilor-os/scripts/kde-theme-apply.sh || true
|
||||||
|
bash /usr/share/veilor-os/scripts/30-apply-v03-theme.sh 2>/dev/null || true
|
||||||
|
plymouth-set-default-theme details 2>/dev/null || true
|
||||||
|
# Mark all our shipped scripts + CLIs executable. cp -a from the
|
||||||
|
# repo preserves perms but BlueBuild's `type: files` sometimes
|
||||||
|
# drops the +x bit on the way through; belt-and-braces here.
|
||||||
|
chmod +x /usr/share/veilor-os/scripts/*.sh \
|
||||||
|
/usr/share/veilor-os/scripts/selinux/*.sh \
|
||||||
|
/usr/local/bin/veilor-* 2>/dev/null || true
|
||||||
|
# Refresh fontconfig cache so Fira Code is picked up by KDE
|
||||||
|
fc-cache -f 2>/dev/null || true
|
||||||
|
# os-release brand override (atomic /etc is r/w; safe to overwrite)
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
sed -i \
|
||||||
|
-e 's|^NAME=.*|NAME="veilor-os"|' \
|
||||||
|
-e 's|^PRETTY_NAME=.*|PRETTY_NAME="veilor-os 0.7 (atomic)"|' \
|
||||||
|
-e 's|^ID=.*|ID=veilor|' \
|
||||||
|
-e 's|^ID_LIKE=.*|ID_LIKE="fedora kinoite"|' \
|
||||||
|
/etc/os-release || true
|
||||||
|
fi
|
||||||
|
# Sanity: brand-leak check, fail build if any onyx/personal data slipped in
|
||||||
|
if grep -rqi 'onyx\|192\.168\.0\.\|fedora\.local\|xynki\.dev' \
|
||||||
|
/etc/veilor* /etc/tuned/profiles/veilor-* /usr/share/veilor-os 2>/dev/null; then
|
||||||
|
echo "[ERR] brand leak detected in shipped state"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Override secureblue's run0-only — restore sudo ───────────
|
||||||
|
# secureblue removes sudo + replaces with run0. Too disruptive for
|
||||||
|
# daily-driver workflows. Restore sudo, keep run0 available.
|
||||||
|
- type: rpm-ostree
|
||||||
|
install:
|
||||||
|
- sudo
|
||||||
|
|
||||||
|
# ── 4. Re-enable Xwayland ───────────────────────────────────────
|
||||||
|
# secureblue disables Xwayland for attack-surface reduction. Some
|
||||||
|
# apps (Element, Slack-likes, older Qt5 tools) still need it.
|
||||||
|
# User who wants it removed back can `rpm-ostree override remove`.
|
||||||
|
- type: rpm-ostree
|
||||||
|
install:
|
||||||
|
- xorg-x11-server-Xwayland
|
||||||
|
|
||||||
|
# ── 5. Mullvad Browser as anti-fingerprint companion ────────────
|
||||||
|
# Layered alongside Trivalent (kept as default per STRATEGY.md).
|
||||||
|
# Trivalent for daily browsing, Mullvad for pseudonymous browsing.
|
||||||
|
# Thorium remains opt-in only via `ujust install-thorium` — see
|
||||||
|
# config/thorium.just for the warning + install logic.
|
||||||
|
- type: rpm-ostree
|
||||||
|
install:
|
||||||
|
- mullvad-browser
|
||||||
|
|
||||||
|
# ── 6. Mesh stack packages ──────────────────────────────────────
|
||||||
|
# Layer 1 (Day 1 daily driver, service pre-disabled): Tailscale
|
||||||
|
# Layer 2 (Day 1 idle warm-fallback): Yggdrasil-go
|
||||||
|
# Layer 3 (opt-in via ujust): Reticulum / RetiNet — handled in just/
|
||||||
|
- type: rpm-ostree
|
||||||
|
install:
|
||||||
|
- tailscale
|
||||||
|
- yggdrasil
|
||||||
|
|
||||||
|
# ── 6b. Memory hygiene + ergonomic deps ─────────────────────────
|
||||||
|
# zram-generator gives us zram swap (no disk swap, no cold-boot
|
||||||
|
# leak). gum is the TUI primitive used by veilor-postinstall +
|
||||||
|
# veilor-update + veilor-doctor — vendor binary at build time so
|
||||||
|
# post-install layering doesn't need it.
|
||||||
|
- type: rpm-ostree
|
||||||
|
install:
|
||||||
|
- zram-generator
|
||||||
|
- jq
|
||||||
|
- vim-enhanced
|
||||||
|
- tmux
|
||||||
|
- htop
|
||||||
|
|
||||||
|
# ── 7. ujust recipes for opt-in components ──────────────────────
|
||||||
|
- type: copy
|
||||||
|
source: config/just
|
||||||
|
destination: /usr/share/ublue-os/just
|
||||||
|
|
||||||
|
# ── 8. Service tuning: tailscale pre-disabled, yggdrasil idle ───
|
||||||
|
- type: systemd
|
||||||
|
system:
|
||||||
|
enabled:
|
||||||
|
- yggdrasil.service # idle warm-fallback (config = empty Listen[])
|
||||||
|
disabled:
|
||||||
|
- tailscaled.service # awaits first-boot prompt for join
|
||||||
|
# secureblue parents already enable: sshd, fail2ban, usbguard,
|
||||||
|
# auditd, firewalld, chronyd, sddm — no re-enable needed.
|
||||||
|
|
||||||
|
# ── 9. veilor-os specific systemd units ─────────────────────────
|
||||||
|
# All veilor-* units come in via overlay/etc/systemd/system/ —
|
||||||
|
# explicit enable here since they aren't part of secureblue's set.
|
||||||
|
- type: systemd
|
||||||
|
system:
|
||||||
|
enabled:
|
||||||
|
- veilor-firstboot.service
|
||||||
|
- veilor-modules-lock.service
|
||||||
|
- veilor-postinstall.service
|
||||||
|
- veilor-doctor.timer
|
||||||
|
|
||||||
|
# ── 10. signing config ──────────────────────────────────────────
|
||||||
|
# cosign.pub committed alongside this recipe; cosign.key kept off
|
||||||
|
# repo and provided to CI as Forgejo secret COSIGN_PRIVATE_KEY.
|
||||||
|
# The action exports it to /tmp at build time.
|
||||||
|
- type: signing
|
||||||
138
docs/INSTALL-V07.md
Normal file
138
docs/INSTALL-V07.md
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
# Installing veilor-os (v0.7+)
|
||||||
|
|
||||||
|
> v0.7 is the first OCI / atomic release. The kickstart-installed
|
||||||
|
> v0.5.x path still ships as legacy — if you want that flow, see
|
||||||
|
> [INSTALL.md](INSTALL.md). Both paths produce a hardened veilor-os
|
||||||
|
> system; the v0.7 path is what we recommend going forward.
|
||||||
|
|
||||||
|
## What's different from v0.5
|
||||||
|
|
||||||
|
| Topic | v0.5.x (kickstart) | v0.7+ (BlueBuild OCI) |
|
||||||
|
|---|---|---|
|
||||||
|
| Root filesystem | mutable, `/usr` writable | atomic / immutable, layered via `rpm-ostree` |
|
||||||
|
| Updates | `sudo dnf upgrade` | `sudo bootc upgrade` (atomic A/B, instant rollback) |
|
||||||
|
| Adding a package | `sudo dnf install foo` | `sudo rpm-ostree install foo` (layered into next deployment) |
|
||||||
|
| Base hardening | re-derived in our `%post` scripts | inherited from secureblue OCI image |
|
||||||
|
| Build artefact | `~2.7 GB` live ISO | small bootstrap ISO + signed OCI image at registry |
|
||||||
|
|
||||||
|
## Step-by-step
|
||||||
|
|
||||||
|
### 1. Download the bootstrap installer ISO
|
||||||
|
|
||||||
|
The bootstrap ISO is a tiny Anaconda-driven installer. It does
|
||||||
|
nothing more than collect a LUKS passphrase + admin password and
|
||||||
|
then call `ostreecontainer --url=...:43 --transport=registry` to
|
||||||
|
populate `/` from the pre-built signed OCI image.
|
||||||
|
|
||||||
|
Download from the Forgejo release:
|
||||||
|
|
||||||
|
<https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest>
|
||||||
|
|
||||||
|
Reassemble the chunked ISO if needed (legacy artefact format):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cat veilor-os-*.iso.part-* > veilor-os.iso
|
||||||
|
sha256sum -c veilor-os-*.iso.parts.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Verify the OCI image signature (optional, recommended)
|
||||||
|
|
||||||
|
The OCI image is cosign-signed at build time. If you have `cosign`
|
||||||
|
installed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cosign verify --key cosign.pub git.s8n.ru/veilor-org/veilor-os:43
|
||||||
|
```
|
||||||
|
|
||||||
|
The public key `cosign.pub` ships with the bootstrap ISO and is also
|
||||||
|
on the Forgejo release page.
|
||||||
|
|
||||||
|
### 3. Flash to USB
|
||||||
|
|
||||||
|
Replace `/dev/sdX` with your USB device — triple-check the path.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo dd if=veilor-os.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
||||||
|
sync
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Boot from USB
|
||||||
|
|
||||||
|
Pick **Install veilor-os** from the boot menu. Anaconda starts and
|
||||||
|
asks two things, no more:
|
||||||
|
|
||||||
|
- **LUKS passphrase** for the encrypted root
|
||||||
|
- **admin password** (≥14 chars, mixed case, digit, symbol)
|
||||||
|
|
||||||
|
Anaconda then runs the `ostreecontainer` directive — pulls the
|
||||||
|
signed OCI image, writes it to disk, configures bootloader.
|
||||||
|
|
||||||
|
### 5. Reboot, remove USB
|
||||||
|
|
||||||
|
The first boot lands on SDDM with `admin` pre-filled. Log in.
|
||||||
|
|
||||||
|
### 6. First-login TUI
|
||||||
|
|
||||||
|
`veilor-postinstall` runs once, asks for the small set of things we
|
||||||
|
defer from install time:
|
||||||
|
|
||||||
|
- Keyboard / locale (defaults are fine for most operators)
|
||||||
|
- Hostname (default `veilor`)
|
||||||
|
- GPU drivers (NVIDIA layered via `rpm-ostree install`; mesa = no-op)
|
||||||
|
- Package presets (`dev` / `media` / `homelab`, all opt-in)
|
||||||
|
- Bluetooth (opt-in)
|
||||||
|
- USBGuard snapshot (plug in trusted devices first)
|
||||||
|
- `veilor-doctor` first run
|
||||||
|
|
||||||
|
Each step is skippable. The TUI writes a marker file and disables
|
||||||
|
itself; it never runs again.
|
||||||
|
|
||||||
|
If you need to re-run it: `sudo veilor-postinstall --force`.
|
||||||
|
|
||||||
|
### 7. Day-to-day
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# update (atomic, A/B, instant rollback)
|
||||||
|
sudo veilor-update
|
||||||
|
|
||||||
|
# layer a package (takes effect after reboot)
|
||||||
|
sudo rpm-ostree install foo
|
||||||
|
|
||||||
|
# remove a layered package
|
||||||
|
sudo rpm-ostree uninstall foo
|
||||||
|
|
||||||
|
# health check + drift report
|
||||||
|
veilor-doctor
|
||||||
|
|
||||||
|
# rollback to previous deployment
|
||||||
|
sudo bootc rollback
|
||||||
|
|
||||||
|
# inspect current and staged deployments
|
||||||
|
bootc status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Try |
|
||||||
|
|---|---|
|
||||||
|
| `veilor-update` says "no rollback target" | First boot — bootc only has rollback after the first successful upgrade. Normal. |
|
||||||
|
| Network down inside Anaconda | Bootstrap ISO uses NetworkManager defaults; plug in ethernet for the first install. WiFi support post-first-boot. |
|
||||||
|
| `rpm-ostree install foo` fails | Run `bootc status` — if a staged deployment exists, reboot first, then re-try. rpm-ostree won't layer onto a staged tree. |
|
||||||
|
| First-login TUI didn't appear | Marker check: `ls /var/lib/veilor/postinstall-complete`. If present, run `sudo veilor-postinstall --force`. |
|
||||||
|
| GPU is black after NVIDIA layer + reboot | `bootc rollback` and try mesa first; check `journalctl -b -1 -u sddm` from the previous boot. |
|
||||||
|
|
||||||
|
### Where the OCI image comes from
|
||||||
|
|
||||||
|
The image is built by `.github/workflows/build-bluebuild.yml` on the
|
||||||
|
self-hosted Forgejo runner (label `nullstone`). Build inputs:
|
||||||
|
|
||||||
|
- Base: `ghcr.io/secureblue/kinoite-main-hardened`
|
||||||
|
- Recipe: [`bluebuild/recipe.yml`](../bluebuild/recipe.yml)
|
||||||
|
- Veilor overlay: stamped via BlueBuild `type: files` modules
|
||||||
|
- Layered RPMs: `sudo`, `xorg-x11-server-Xwayland`, `mullvad-browser`,
|
||||||
|
`tailscale`, `yggdrasil`
|
||||||
|
- Output: `git.s8n.ru/veilor-org/veilor-os:{43,latest}`
|
||||||
|
|
||||||
|
The build is cosign-signed (key-pair on Forgejo, keyless on GitHub
|
||||||
|
parallel mirror). See [`bluebuild/README.md`](../bluebuild/README.md)
|
||||||
|
for the recipe walk-through.
|
||||||
275
docs/PROOF-OF-WORK.md
Normal file
275
docs/PROOF-OF-WORK.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 `kinoite-main-hardened` |
|
||||||
|
| 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 kinoite-main-hardened) |
|
||||||
|
| `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,43 @@ 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/kinoite-main-hardened`
|
||||||
|
- `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)
|
||||||
|
- **Installer logs persisted to USB stick by default** (debug mode):
|
||||||
|
the bootstrap ISO writes `/var/log/anaconda/*` + the resolved
|
||||||
|
kickstart + ostreecontainer pull log + dmesg back onto the USB
|
||||||
|
install medium (mounted rw at `/run/install/repo` during install)
|
||||||
|
into a `veilor-install-logs/<timestamp>/` folder. Toggleable via
|
||||||
|
kernel cmdline `inst.veilor.savelogs=0` for opt-out, or
|
||||||
|
`inst.veilor.savelogs=1` (default). Stays **ON by default through
|
||||||
|
v0.7+v0.8+v0.9; flips OFF for v1.0 final release**. Why: any failed
|
||||||
|
install, the operator boots back to a working OS, plugs the USB,
|
||||||
|
reads the logs offline — no need to take screenshots of dracut on a
|
||||||
|
bricked machine. Implementation: `%post --nochroot` block in
|
||||||
|
`kickstart/install-ostreecontainer.ks` that detects the install
|
||||||
|
medium via `/run/install/repo` rw remount, copies the log set,
|
||||||
|
syncs, then unmounts. If the medium is read-only (DVD), skip
|
||||||
|
silently with a `journalctl` warning.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -231,7 +307,7 @@ spike on `quay.io/fedora/fedora-bootc:43`. Research on 2026-05-05
|
||||||
`docs/research/2026-05-05-agent-wave/`), then a parent-operator
|
`docs/research/2026-05-05-agent-wave/`), then a parent-operator
|
||||||
refinement same day, locked the path: **layer veilor's branding +
|
refinement same day, locked the path: **layer veilor's branding +
|
||||||
threat model + UX on top of secureblue's already-shipping
|
threat model + UX on top of secureblue's already-shipping
|
||||||
`securecore-kinoite-hardened-userns` OCI image** via a BlueBuild
|
`kinoite-main-hardened` OCI image** via a BlueBuild
|
||||||
recipe, and install it directly during the Anaconda pass via the
|
recipe, and install it directly during the Anaconda pass via the
|
||||||
`ostreecontainer` kickstart directive (no first-boot rebase).
|
`ostreecontainer` kickstart directive (no first-boot rebase).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Locked at: **v0.5.31 → v0.7 spike → v1.0**
|
||||||
works).
|
works).
|
||||||
- Anaconda's `ostreecontainer` directive populates the root filesystem
|
- Anaconda's `ostreecontainer` directive populates the root filesystem
|
||||||
directly from a **veilor-os OCI image** (built via BlueBuild on top
|
directly from a **veilor-os OCI image** (built via BlueBuild on top
|
||||||
of secureblue's `securecore-kinoite-hardened-userns`) **during the
|
of secureblue's `kinoite-main-hardened`) **during the
|
||||||
install pass — no first-boot rebase, no mutable→atomic transition**.
|
install pass — no first-boot rebase, no mutable→atomic transition**.
|
||||||
- All future updates flow through `bootc upgrade` — atomic A/B,
|
- All future updates flow through `bootc upgrade` — atomic A/B,
|
||||||
instant rollback, cosign-signed.
|
instant rollback, cosign-signed.
|
||||||
|
|
@ -236,7 +236,7 @@ distro: **honest, scoped, public threat model**.
|
||||||
The Containerfile-from-scratch spike plan (Agent 3 of 2026-05-05
|
The Containerfile-from-scratch spike plan (Agent 3 of 2026-05-05
|
||||||
wave) is **superseded** by this hybrid: don't build a Containerfile
|
wave) is **superseded** by this hybrid: don't build a Containerfile
|
||||||
from scratch on `fedora-bootc:43`. Instead, write a BlueBuild recipe
|
from scratch on `fedora-bootc:43`. Instead, write a BlueBuild recipe
|
||||||
on `securecore-kinoite-hardened-userns`. With `ostreecontainer`
|
on `kinoite-main-hardened`. With `ostreecontainer`
|
||||||
swap, spike compresses 1 week → 1 day.
|
swap, spike compresses 1 week → 1 day.
|
||||||
|
|
||||||
## Next concrete steps
|
## Next concrete steps
|
||||||
|
|
@ -254,7 +254,7 @@ in the v0.7 spike branch only.
|
||||||
### v0.7-spike (1 day, separate branch)
|
### v0.7-spike (1 day, separate branch)
|
||||||
|
|
||||||
1. New repo dir: `bluebuild/recipe.yml`.
|
1. New repo dir: `bluebuild/recipe.yml`.
|
||||||
2. `from`: `ghcr.io/secureblue/securecore-kinoite-hardened-userns:latest`.
|
2. `from`: `ghcr.io/secureblue/kinoite-main-hardened:latest`.
|
||||||
3. Override modules:
|
3. Override modules:
|
||||||
- `type: files` — stamp our `overlay/*` tree (branding, themes,
|
- `type: files` — stamp our `overlay/*` tree (branding, themes,
|
||||||
veilor scripts, sddm theme, plymouth theme).
|
veilor scripts, sddm theme, plymouth theme).
|
||||||
|
|
@ -301,6 +301,26 @@ in the v0.7 spike branch only.
|
||||||
`v4.9` on F44). If we follow, we get F44 for free at the same time
|
`v4.9` on F44). If we follow, we get F44 for free at the same time
|
||||||
upstream does.
|
upstream does.
|
||||||
|
|
||||||
|
## Self-hosted git + CI (locked 2026-05-05)
|
||||||
|
|
||||||
|
Primary git host moved off github.com. **Forgejo** runs on nullstone
|
||||||
|
at `git.s8n.ru`, with **forgejo-runner** doing the build work. GH free-
|
||||||
|
tier minute quota was hammering veilor-os iteration; we self-host now.
|
||||||
|
|
||||||
|
- Primary remote: `ssh://git@192.168.0.100:222/veilor-org/veilor-os.git`
|
||||||
|
(Forgejo, LAN-only until router port-forward 222 → nullstone:222
|
||||||
|
added — TODO; or use tailnet hostname once tailscale logged in).
|
||||||
|
- Public mirror: `https://github.com/veilor-org/veilor-os.git`. Forgejo
|
||||||
|
push-mirrors every commit + every 8h, so GH stays in sync without
|
||||||
|
consuming GH minutes.
|
||||||
|
- Runner labels: `ubuntu-24.04` (catthehacker image — works for our
|
||||||
|
current build-iso.yml unmodified) and `nullstone` (privileged Fedora
|
||||||
|
43 container — opt-in via `runs-on: nullstone`).
|
||||||
|
- Build cost: 0 GH minutes. Disk: ~80 GB workspace on /home/docker.
|
||||||
|
|
||||||
|
Deploy artifacts: `~/ai-lab/nullstone-server/forgejo/`. Runbook in same
|
||||||
|
dir.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
- `docs/THREAT-MODEL.md` — drafted, needs publish for v0.7
|
- `docs/THREAT-MODEL.md` — drafted, needs publish for v0.7
|
||||||
|
|
@ -314,3 +334,29 @@ in the v0.7 spike branch only.
|
||||||
- Yggdrasil: <https://github.com/yggdrasil-network/yggdrasil-go>
|
- Yggdrasil: <https://github.com/yggdrasil-network/yggdrasil-go>
|
||||||
- Reticulum manual: <https://reticulum.network/manual/>
|
- Reticulum manual: <https://reticulum.network/manual/>
|
||||||
- Iroh blobs design: <https://github.com/n0-computer/iroh-blobs/blob/main/DESIGN.md>
|
- Iroh blobs design: <https://github.com/n0-computer/iroh-blobs/blob/main/DESIGN.md>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PIVOT EXECUTION — 2026-05-06
|
||||||
|
|
||||||
|
The hybrid strategy locked at v0.5 is now in execution.
|
||||||
|
|
||||||
|
- **v0.5.0 shipped** as the proof-of-work / portfolio release of the
|
||||||
|
kickstart-flat path. Self-hosted Forgejo CI green-built a 2.7 GB
|
||||||
|
ISO; tag pushed; download lives at the ci-latest release.
|
||||||
|
- **v0.6 milestone cancelled.** Continuing to debug
|
||||||
|
`livecd-creator + anaconda` quirks for v0.6 polish would be sunk-
|
||||||
|
cost work on tooling we retire at v1.0. Original v0.6 plan kept in
|
||||||
|
ROADMAP.md as historical reference.
|
||||||
|
- **v0.7 BlueBuild OCI is the active mainline.** The
|
||||||
|
`v0.7-bluebuild-spike` branch carries the BlueBuild recipe layered
|
||||||
|
on `ghcr.io/secureblue/kinoite-main-hardened`, the
|
||||||
|
`ostreecontainer` kickstart bootstrap, and the new `bootc upgrade`-
|
||||||
|
driven update channel.
|
||||||
|
- **v0.6 ergonomic CLIs ported, not rewritten.** `veilor-update`
|
||||||
|
rewrites onto `bootc upgrade`; `veilor-postinstall` becomes the
|
||||||
|
first-login TUI on the atomic system; `veilor-doctor` learns
|
||||||
|
`bootc status --json` while keeping the legacy dnf path for v0.5.x.
|
||||||
|
- **v1.0 retires the kickstart entirely.** Only `kickstart/install-
|
||||||
|
ostreecontainer.ks` (10 lines) ships forward — bootstrap installer
|
||||||
|
for ostreecontainer pulls.
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
47
kickstart/install-ostreecontainer-installer.ks
Normal file
47
kickstart/install-ostreecontainer-installer.ks
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# veilor-os installer kickstart — v0.7 CI build variant
|
||||||
|
#
|
||||||
|
# Derived from kickstart/install-ostreecontainer.ks by stripping all
|
||||||
|
# __PLACEHOLDER__ tokens that the runtime gum TUI substitutes at install
|
||||||
|
# time. Anaconda's interactive TUI handles disk selection, LUKS passphrase,
|
||||||
|
# and user account creation in their place.
|
||||||
|
#
|
||||||
|
# Consumed by livemedia-creator --make-iso to produce
|
||||||
|
# veilor-os-installer-43-*.iso. Do NOT add __PLACEHOLDER__ tokens here —
|
||||||
|
# they cannot be filled at build time. See install-ostreecontainer.ks
|
||||||
|
# for the runtime template the gum TUI fills in.
|
||||||
|
|
||||||
|
# ── Locale / keyboard / time ──
|
||||||
|
keyboard --xlayouts='us'
|
||||||
|
lang en_US.UTF-8
|
||||||
|
timezone Europe/London --utc
|
||||||
|
|
||||||
|
# ── Install mode ──
|
||||||
|
text
|
||||||
|
firstboot --disable
|
||||||
|
eula --agreed
|
||||||
|
selinux --enforcing
|
||||||
|
|
||||||
|
# ── Network ──
|
||||||
|
network --bootproto=dhcp --device=link --activate --hostname=veilor-install
|
||||||
|
firewall --enabled --service=ssh
|
||||||
|
|
||||||
|
# ── Identity ──
|
||||||
|
# rootpw --lock only. No user directive — Anaconda's user spoke handles
|
||||||
|
# admin account creation interactively. Runtime ks substitutes
|
||||||
|
# --password=__ADMIN_PW__ for unattended installs.
|
||||||
|
rootpw --lock
|
||||||
|
|
||||||
|
# ── Disk / partitioning ──
|
||||||
|
# Intentionally absent. Anaconda's disk spoke presents interactive
|
||||||
|
# disk + LUKS + btrfs selection. Runtime ks (gum TUI) provides the
|
||||||
|
# full partition spec at real-install time.
|
||||||
|
|
||||||
|
# ── ostreecontainer: populate / from veilor-os OCI ──
|
||||||
|
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
|
||||||
|
|
||||||
|
# ── %post (chroot) ──
|
||||||
|
%post
|
||||||
|
set -uo pipefail
|
||||||
|
echo veilor-install > /etc/hostname
|
||||||
|
chage -d 0 admin 2>/dev/null || true
|
||||||
|
%end
|
||||||
80
kickstart/install-ostreecontainer.ks
Normal file
80
kickstart/install-ostreecontainer.ks
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# veilor-os install kickstart — v0.7 spike (ostreecontainer path)
|
||||||
|
#
|
||||||
|
# This is the install-time kickstart for the v0.7 hybrid path. The live
|
||||||
|
# ISO boots; the gum TUI collects user answers (disk, LUKS pw, admin pw);
|
||||||
|
# this template gets the answers substituted in and is fed to anaconda.
|
||||||
|
#
|
||||||
|
# Anaconda partitions the disk + creates LUKS + btrfs subvols + mounts
|
||||||
|
# /boot/efi + /boot, then `ostreecontainer` populates `/` directly from
|
||||||
|
# the cosign-signed veilor-os OCI image at `ghcr.io/veilor-org/veilor-os:43`.
|
||||||
|
#
|
||||||
|
# No `%packages` block. No first-boot rebase. No
|
||||||
|
# `veilor-firstboot-rebase.service`. The ostreecontainer install pass is
|
||||||
|
# the entire transition from "Fedora live ISO" to "veilor-os on disk".
|
||||||
|
#
|
||||||
|
# Reference: pykickstart docs ostreecontainer command;
|
||||||
|
# https://docs.fedoraproject.org/en-US/bootc/anaconda-install/
|
||||||
|
|
||||||
|
# ── Locale / keyboard / time ──
|
||||||
|
keyboard --xlayouts='us'
|
||||||
|
lang en_US.UTF-8
|
||||||
|
timezone Europe/London --utc
|
||||||
|
|
||||||
|
# ── Install mode / behaviour ──
|
||||||
|
firstboot --disable
|
||||||
|
eula --agreed
|
||||||
|
# SELinux state inherited from the OCI image; --enforcing is implicit
|
||||||
|
# since secureblue's image ships /etc/selinux/config = enforcing.
|
||||||
|
selinux --enforcing
|
||||||
|
|
||||||
|
# ── Network / hostname ──
|
||||||
|
network --bootproto=dhcp --device=link --activate --hostname=__HOSTNAME__
|
||||||
|
firewall --enabled --service=ssh
|
||||||
|
|
||||||
|
# ── Identity (single LUKS prompt asked at install via gum TUI) ──
|
||||||
|
rootpw --lock
|
||||||
|
user --name=admin --groups=wheel --gecos="veilor admin" --password=__ADMIN_PW__ --plaintext
|
||||||
|
|
||||||
|
# ── Bootloader ──
|
||||||
|
# fbcon=nodefer for laptop KMS handoff (real-hardware audit, agent 9 of
|
||||||
|
# 2026-05-05 wave). rd.luks.options=tries=5,timeout=0 for UX.
|
||||||
|
# rd.luks.uuid is auto-injected by anaconda based on the encrypted
|
||||||
|
# part directive below.
|
||||||
|
#
|
||||||
|
# All other hardening kargs (lockdown=integrity, slab_nomerge, etc.)
|
||||||
|
# come from /usr/lib/bootc/kargs.d/ inside the OCI image — bootc
|
||||||
|
# applies them at install time. We only add what the OCI image can't
|
||||||
|
# know (laptop-specific KMS flag).
|
||||||
|
bootloader --append="fbcon=nodefer"
|
||||||
|
|
||||||
|
# ── Disk: LUKS2 (argon2id) + btrfs subvols ──
|
||||||
|
zerombr
|
||||||
|
clearpart --all --initlabel --drives=__DISK_BASENAME__
|
||||||
|
part /boot/efi --fstype=efi --size=600
|
||||||
|
part /boot --fstype=ext4 --size=1024
|
||||||
|
part btrfs.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id --passphrase=__LUKS_PW__
|
||||||
|
btrfs none --label=veilor btrfs.veilor
|
||||||
|
btrfs / --subvol --name=root LABEL=veilor
|
||||||
|
btrfs /home --subvol --name=home LABEL=veilor
|
||||||
|
|
||||||
|
# ── ostreecontainer: populate / from the veilor-os OCI image ──
|
||||||
|
# `--transport=registry` pulls from ghcr.io directly. Authentication
|
||||||
|
# token can be supplied via /etc/ostree/auth.json baked into the live
|
||||||
|
# rootfs OR via a kickstart `--remote-token` if the registry is private.
|
||||||
|
# At v0.7 spike the OCI image is public, so no auth needed.
|
||||||
|
#
|
||||||
|
# DO NOT migrate to the new `bootc` kickstart command until v1.0 — it
|
||||||
|
# blocks multi-disk and authenticated registries (per parent-operator
|
||||||
|
# handoff 2026-05-05).
|
||||||
|
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
|
||||||
|
|
||||||
|
# ── %post (chroot) — minimal; OCI image already has everything ──
|
||||||
|
# What we keep:
|
||||||
|
# - chage -d 0 admin so first SDDM login forces password change
|
||||||
|
# - hostname write (anaconda's --hostname doesn't always survive)
|
||||||
|
# - veilor-firstboot.service is enabled in the OCI image already
|
||||||
|
%post
|
||||||
|
set -uo pipefail
|
||||||
|
echo veilor > /etc/hostname
|
||||||
|
chage -d 0 admin || true
|
||||||
|
%end
|
||||||
|
|
@ -58,7 +58,7 @@ user --name=admin --groups=wheel --gecos="veilor admin" --password="" --plaintex
|
||||||
# framebuffer — symptom: black screen with blinking cursor for ~30s
|
# framebuffer — symptom: black screen with blinking cursor for ~30s
|
||||||
# while the menu IS in fact rendered, just not painted. virtio-vga in
|
# while the menu IS in fact rendered, just not painted. virtio-vga in
|
||||||
# QEMU doesn't trigger this so it never reproed in VM.
|
# QEMU doesn't trigger this so it never reproed in VM.
|
||||||
bootloader --location=mbr --append="lockdown=integrity slab_nomerge randomize_kstack_offset=on vsyscall=none plymouth.enable=0 fbcon=nodefer"
|
bootloader --location=mbr --append="lockdown=integrity module.sig_enforce=1 slab_nomerge randomize_kstack_offset=on vsyscall=none plymouth.enable=0 fbcon=nodefer i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1 rd.vconsole.keymap=us"
|
||||||
|
|
||||||
# ── Live ISO partitioning (flat — for live rootfs build only) ──
|
# ── Live ISO partitioning (flat — for live rootfs build only) ──
|
||||||
# NOTE: This is the *live* image kickstart. Final installed system uses
|
# NOTE: This is the *live* image kickstart. Final installed system uses
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
12
overlay/etc/firewalld/zones/trusted.xml
Normal file
12
overlay/etc/firewalld/zones/trusted.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- veilor-os: trusted zone with tailscale0 pre-bound.
|
||||||
|
Default zone stays drop (per 10-harden-base.sh). Tailscale's
|
||||||
|
interface is added here so `tailscale up` traffic isn't dropped.
|
||||||
|
Without this entry the firewalld drop zone blocks the tailnet
|
||||||
|
traffic and the user sees: "tailscale up succeeded, but I can't
|
||||||
|
reach hs.s8n.ru". (Agent 9, 2026-05-05 wave.) -->
|
||||||
|
<zone target="ACCEPT">
|
||||||
|
<short>Trusted</short>
|
||||||
|
<description>All network connections are accepted. veilor-os pre-binds tailscale0 here so the mesh layer-1 (Tailscale via Headscale) works out-of-box without manual firewalld zone juggling.</description>
|
||||||
|
<interface name="tailscale0"/>
|
||||||
|
</zone>
|
||||||
60
overlay/etc/skel/.config/breezerc
Normal file
60
overlay/etc/skel/.config/breezerc
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# veilor-os — Breeze window decoration override
|
||||||
|
# Tighter borders, solid black title bar, minimal buttons, smallest border.
|
||||||
|
# Merged into /etc/xdg/breezerc (system default) by 30-apply-v03-theme.sh.
|
||||||
|
|
||||||
|
[Common]
|
||||||
|
# Tighter outline; subtle separator only when active.
|
||||||
|
OutlineCloseButton=false
|
||||||
|
ShadowSize=ShadowSmall
|
||||||
|
ShadowStrength=128
|
||||||
|
ShadowColor=0,0,0
|
||||||
|
|
||||||
|
[Windeco]
|
||||||
|
# Border thickness: smallest available (= "None" leaves only resize edge,
|
||||||
|
# "NoSides" keeps top/bottom only). We pick "None" for the tightest look,
|
||||||
|
# matching the black-on-black aesthetic.
|
||||||
|
BorderSize=None
|
||||||
|
ButtonSize=ButtonSmall
|
||||||
|
CloseButton=true
|
||||||
|
DrawBackgroundGradient=false
|
||||||
|
DrawBorderOnMaximizedWindows=false
|
||||||
|
DrawSizeGrip=false
|
||||||
|
DrawTitleBarSeparator=false
|
||||||
|
ExceptionType=0
|
||||||
|
HideTitleBar=false
|
||||||
|
OpaqueTitleBar=true
|
||||||
|
TitleAlignment=AlignCenter
|
||||||
|
UseBackgroundGradient=false
|
||||||
|
UseTitleBarColor=true
|
||||||
|
|
||||||
|
# Buttons: minimal — close / max / min only, no shade/help/keep-above.
|
||||||
|
ButtonsOnLeft=M
|
||||||
|
ButtonsOnRight=IAX
|
||||||
|
|
||||||
|
[Style]
|
||||||
|
# Disable per-app blur, transparency, and gradient effects.
|
||||||
|
MenuOpacity=100
|
||||||
|
WindowDragMode=1
|
||||||
|
ScrollBarAddLineButtons=0
|
||||||
|
ScrollBarSubLineButtons=0
|
||||||
|
SidePanelDrawFrame=false
|
||||||
|
SliderDrawTickMarks=false
|
||||||
|
TabBarDrawCenteredTabs=true
|
||||||
|
ToolBarDrawItemSeparator=false
|
||||||
|
DockWidgetDrawFrame=false
|
||||||
|
ProgressBarAnimated=false
|
||||||
|
AnimationsEnabled=false
|
||||||
|
StackedWidgetDrawFrame=false
|
||||||
|
|
||||||
|
# ── Active / inactive title bar colors (override Breeze defaults) ──
|
||||||
|
# kdeglobals [WM] section is the canonical source; these mirror it here
|
||||||
|
# so apps that only read breezerc see consistent values.
|
||||||
|
[Windeco][Active]
|
||||||
|
TitleBarColor=0,0,0
|
||||||
|
TitleBarTextColor=216,216,216
|
||||||
|
TitleBarBorderColor=104,107,111
|
||||||
|
|
||||||
|
[Windeco][Inactive]
|
||||||
|
TitleBarColor=15,17,18
|
||||||
|
TitleBarTextColor=161,169,177
|
||||||
|
TitleBarBorderColor=42,46,50
|
||||||
29
overlay/etc/skel/.config/kdeglobals
Normal file
29
overlay/etc/skel/.config/kdeglobals
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
[General]
|
||||||
|
ColorScheme=veilor-black
|
||||||
|
Name=veilor black
|
||||||
|
AccentColor=104,107,111
|
||||||
|
LastUsedCustomAccentColor=104,107,111
|
||||||
|
font=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
|
||||||
|
fixed=Fira Code,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
|
||||||
|
menuFont=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
|
||||||
|
smallestReadableFont=Fira Code,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
|
||||||
|
toolBarFont=Fira Code,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Theme=breeze-dark
|
||||||
|
|
||||||
|
[KDE]
|
||||||
|
LookAndFeelPackage=org.kde.breezedark.desktop
|
||||||
|
SingleClick=false
|
||||||
|
contrast=4
|
||||||
|
widgetStyle=Breeze
|
||||||
|
|
||||||
|
[Mouse]
|
||||||
|
cursorTheme=Breeze_Light
|
||||||
|
cursorSize=24
|
||||||
|
|
||||||
|
[KDecoration]
|
||||||
|
theme=Breeze
|
||||||
|
ButtonsOnLeft=
|
||||||
|
ButtonsOnRight=IAX
|
||||||
|
BorderSize=None
|
||||||
10
overlay/etc/skel/.config/konsolerc
Normal file
10
overlay/etc/skel/.config/konsolerc
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# veilor-os Konsole default — branded profile pre-selected so first
|
||||||
|
# Konsole launch on a fresh user account opens the veilor profile,
|
||||||
|
# not Fedora's default white-bg Breeze.
|
||||||
|
|
||||||
|
[Desktop Entry]
|
||||||
|
DefaultProfile=Veilor.profile
|
||||||
|
|
||||||
|
[KonsoleWindow]
|
||||||
|
ShowMenuBarByDefault=false
|
||||||
|
RememberWindowSize=true
|
||||||
29
overlay/etc/skel/.config/kwinrc
Normal file
29
overlay/etc/skel/.config/kwinrc
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# veilor-os Plasma 6 kwin defaults — seeded into /etc/skel so first-login
|
||||||
|
# users inherit deliberate windowing behaviour rather than default Breeze
|
||||||
|
# animations. Per-user; user can override post-login.
|
||||||
|
|
||||||
|
[Compositing]
|
||||||
|
# OpenGL backend = standard for hardware accel; AnimationSpeed=0 = no
|
||||||
|
# slow window animations on every focus change.
|
||||||
|
Backend=OpenGL
|
||||||
|
AnimationSpeed=0
|
||||||
|
HiddenPreviews=5
|
||||||
|
LatencyPolicy=Low
|
||||||
|
WindowsBlockCompositing=true
|
||||||
|
|
||||||
|
[Plugins]
|
||||||
|
# Disable visual fluff that isn't security-relevant + costs perf.
|
||||||
|
blurEnabled=false
|
||||||
|
contrastEnabled=false
|
||||||
|
slideEnabled=false
|
||||||
|
slidingpopupsEnabled=false
|
||||||
|
fadeEnabled=false
|
||||||
|
zoomEnabled=true
|
||||||
|
|
||||||
|
[Windows]
|
||||||
|
FocusPolicy=ClickToFocus
|
||||||
|
RollOverDesktops=false
|
||||||
|
TitlebarDoubleClickCommand=Maximize
|
||||||
|
|
||||||
|
[Wayland]
|
||||||
|
InputMethod=
|
||||||
104
overlay/etc/skel/.local/share/konsole/Veilor.colorscheme
Normal file
104
overlay/etc/skel/.local/share/konsole/Veilor.colorscheme
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
[General]
|
||||||
|
Anchor=0.5,0.5
|
||||||
|
Blur=false
|
||||||
|
ColorRandomization=false
|
||||||
|
Description=Veilor
|
||||||
|
FillStyle=Tile
|
||||||
|
Opacity=1
|
||||||
|
Wallpaper=
|
||||||
|
WallpaperFlipType=NoFlip
|
||||||
|
WallpaperOpacity=1
|
||||||
|
|
||||||
|
[Background]
|
||||||
|
Color=0,0,0
|
||||||
|
|
||||||
|
[BackgroundFaint]
|
||||||
|
Color=0,0,0
|
||||||
|
|
||||||
|
[BackgroundIntense]
|
||||||
|
Color=15,17,18
|
||||||
|
|
||||||
|
[Foreground]
|
||||||
|
Color=216,216,216
|
||||||
|
|
||||||
|
[ForegroundFaint]
|
||||||
|
Color=161,169,177
|
||||||
|
|
||||||
|
[ForegroundIntense]
|
||||||
|
Color=236,236,236
|
||||||
|
|
||||||
|
# ── Standard ANSI palette (muted, desaturated) ──
|
||||||
|
# Veilor aesthetic: no neon. Reds tone-shifted toward bordeaux, greens
|
||||||
|
# toward sage, blues toward slate. Greys lifted to remain readable.
|
||||||
|
|
||||||
|
[Color0]
|
||||||
|
Color=27,27,27
|
||||||
|
|
||||||
|
[Color0Faint]
|
||||||
|
Color=20,20,20
|
||||||
|
|
||||||
|
[Color0Intense]
|
||||||
|
Color=58,58,58
|
||||||
|
|
||||||
|
[Color1]
|
||||||
|
Color=176,55,69
|
||||||
|
|
||||||
|
[Color1Faint]
|
||||||
|
Color=130,40,52
|
||||||
|
|
||||||
|
[Color1Intense]
|
||||||
|
Color=205,87,99
|
||||||
|
|
||||||
|
[Color2]
|
||||||
|
Color=102,138,90
|
||||||
|
|
||||||
|
[Color2Faint]
|
||||||
|
Color=78,107,68
|
||||||
|
|
||||||
|
[Color2Intense]
|
||||||
|
Color=141,176,128
|
||||||
|
|
||||||
|
[Color3]
|
||||||
|
Color=185,158,98
|
||||||
|
|
||||||
|
[Color3Faint]
|
||||||
|
Color=140,118,72
|
||||||
|
|
||||||
|
[Color3Intense]
|
||||||
|
Color=216,193,134
|
||||||
|
|
||||||
|
[Color4]
|
||||||
|
Color=92,116,143
|
||||||
|
|
||||||
|
[Color4Faint]
|
||||||
|
Color=68,87,107
|
||||||
|
|
||||||
|
[Color4Intense]
|
||||||
|
Color=131,154,182
|
||||||
|
|
||||||
|
[Color5]
|
||||||
|
Color=141,113,150
|
||||||
|
|
||||||
|
[Color5Faint]
|
||||||
|
Color=104,84,112
|
||||||
|
|
||||||
|
[Color5Intense]
|
||||||
|
Color=176,148,186
|
||||||
|
|
||||||
|
[Color6]
|
||||||
|
Color=99,144,148
|
||||||
|
|
||||||
|
[Color6Faint]
|
||||||
|
Color=72,107,110
|
||||||
|
|
||||||
|
[Color6Intense]
|
||||||
|
Color=139,180,184
|
||||||
|
|
||||||
|
[Color7]
|
||||||
|
Color=200,200,200
|
||||||
|
|
||||||
|
[Color7Faint]
|
||||||
|
Color=161,169,177
|
||||||
|
|
||||||
|
[Color7Intense]
|
||||||
|
Color=236,236,236
|
||||||
55
overlay/etc/skel/.local/share/konsole/Veilor.profile
Normal file
55
overlay/etc/skel/.local/share/konsole/Veilor.profile
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
[General]
|
||||||
|
Name=Veilor
|
||||||
|
Parent=FALLBACK/
|
||||||
|
Command=/bin/bash
|
||||||
|
Directory=
|
||||||
|
Icon=utilities-terminal
|
||||||
|
LocalTabTitleFormat=%w
|
||||||
|
RemoteTabTitleFormat=(%u) %h
|
||||||
|
ShowTerminalSizeHint=false
|
||||||
|
StartInCurrentSessionDir=true
|
||||||
|
TerminalCenter=false
|
||||||
|
TerminalMargin=4
|
||||||
|
|
||||||
|
[Appearance]
|
||||||
|
ColorScheme=Veilor
|
||||||
|
Font=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
|
||||||
|
LineSpacing=1
|
||||||
|
UseFontLineCharacters=true
|
||||||
|
|
||||||
|
[Cursor Options]
|
||||||
|
CursorShape=0
|
||||||
|
UseCustomCursorColor=true
|
||||||
|
CustomCursorColor=104,107,111
|
||||||
|
CustomCursorTextColor=216,216,216
|
||||||
|
|
||||||
|
[Scrolling]
|
||||||
|
HistoryMode=2
|
||||||
|
HistorySize=10000
|
||||||
|
ScrollBarPosition=2
|
||||||
|
HighlightScrolledLines=false
|
||||||
|
|
||||||
|
[Terminal Features]
|
||||||
|
BellMode=3
|
||||||
|
BlinkingCursorEnabled=false
|
||||||
|
BlinkingTextEnabled=false
|
||||||
|
FlowControlEnabled=false
|
||||||
|
UrlHintsModifiers=67108864
|
||||||
|
ReverseUrlHints=false
|
||||||
|
VerticalLine=false
|
||||||
|
|
||||||
|
[Interaction Options]
|
||||||
|
AutoCopySelectedText=false
|
||||||
|
CopyTextAsHTML=false
|
||||||
|
TrimLeadingSpacesInSelectedText=false
|
||||||
|
TrimTrailingSpacesInSelectedText=true
|
||||||
|
UnderlineFilesEnabled=true
|
||||||
|
UnderlineLinksEnabled=true
|
||||||
|
OpenLinksByDirectClickEnabled=false
|
||||||
|
WordCharacters=:@-./_~?&=%+#
|
||||||
|
|
||||||
|
[Encoding Options]
|
||||||
|
DefaultEncoding=UTF-8
|
||||||
|
|
||||||
|
[Keyboard]
|
||||||
|
KeyBindings=default
|
||||||
7
overlay/etc/systemd/system/veilor-doctor.service
Normal file
7
overlay/etc/systemd/system/veilor-doctor.service
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[Unit]
|
||||||
|
Description=veilor-doctor — system health + drift check
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/veilor-doctor --quiet
|
||||||
10
overlay/etc/systemd/system/veilor-doctor.timer
Normal file
10
overlay/etc/systemd/system/veilor-doctor.timer
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Unit]
|
||||||
|
Description=veilor-doctor weekly drift check
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=weekly
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=30m
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
@ -18,4 +18,9 @@ TTYReset=yes
|
||||||
TTYVHangup=yes
|
TTYVHangup=yes
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
# Real installs default to graphical.target. Without this entry the unit
|
||||||
|
# never runs on installed systems — admin pw stays at install-time value
|
||||||
|
# + chage -d 0 expired, SDDM PAM bounces user to a chauthtok screen
|
||||||
|
# (recoverable but ugly). Live ISO + multi-user.target installs both
|
||||||
|
# resolve via this list. (Agent 2, 2026-05-05 wave.)
|
||||||
|
WantedBy=graphical.target multi-user.target
|
||||||
|
|
|
||||||
17
overlay/etc/systemd/system/veilor-postinstall.service
Normal file
17
overlay/etc/systemd/system/veilor-postinstall.service
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[Unit]
|
||||||
|
Description=veilor-os one-time post-install TUI (first login)
|
||||||
|
After=graphical.target
|
||||||
|
ConditionPathExists=!/var/lib/veilor/postinstall-complete
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/veilor-postinstall
|
||||||
|
StandardInput=tty
|
||||||
|
StandardOutput=tty
|
||||||
|
StandardError=journal
|
||||||
|
TTYPath=/dev/tty1
|
||||||
|
TTYReset=yes
|
||||||
|
TTYVHangup=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical.target multi-user.target
|
||||||
43
overlay/etc/usbguard/rules.conf
Normal file
43
overlay/etc/usbguard/rules.conf
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# veilor-os USBGuard baseline rules
|
||||||
|
#
|
||||||
|
# Default policy is `block` (set in usbguard-daemon.conf via the
|
||||||
|
# overlay). Without any allow rule, every USB device — including the
|
||||||
|
# user's keyboard — is blocked at boot. That includes the desktop
|
||||||
|
# user with a USB keyboard at SDDM.
|
||||||
|
#
|
||||||
|
# This file allows HID-class interfaces (keyboard, mouse, touchpad,
|
||||||
|
# fingerprint reader, NFC, gamepad) without pinning to specific
|
||||||
|
# vendor:product/serial/hash. id-based rules survive dock replug and
|
||||||
|
# vendor-bump kernel changes, where hash+parent-hash rules don't —
|
||||||
|
# verified pain on onyx (memory: feedback_usbguard_dock.md). Same fix.
|
||||||
|
#
|
||||||
|
# After first login, the user runs:
|
||||||
|
# ujust veilor-usbguard-enroll
|
||||||
|
# (or `usbguard generate-policy --with-hash=false > rules.conf`)
|
||||||
|
# to add their own keyboard's id-rule and tighten the policy further.
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# - usbguard-rules.conf(5)
|
||||||
|
# - https://usbguard.github.io/documentation/rule-language.html
|
||||||
|
# - veilor-os agent 9 audit, 2026-05-05
|
||||||
|
|
||||||
|
# HID class — keyboards, mice, pointers, gamepads, fingerprint, NFC.
|
||||||
|
# Interface descriptor 03:NN:NN where 03=HID. We accept any HID
|
||||||
|
# subclass + protocol so the rule is robust to future HID variants.
|
||||||
|
allow with-interface match-all { 03:*:* }
|
||||||
|
|
||||||
|
# Mass-storage prompt: ask the user before mounting a new flash drive.
|
||||||
|
# Reject blanket-allow (would silently allow USB Rubber Ducky).
|
||||||
|
# Accept only after user confirms via the gnome/plasma USB dialog.
|
||||||
|
# (USBGuard has no native "ask" verb; we leave mass-storage devices
|
||||||
|
# implicit-block here, the user runs `usbguard allow-device <id>`
|
||||||
|
# from a Plasma applet OR the firstboot wizard documents this flow.)
|
||||||
|
|
||||||
|
# Block known-bad. USB Killer signature shows up as a generic-HID
|
||||||
|
# composite descriptor + power draw out of spec. We can't reliably
|
||||||
|
# detect that from descriptors alone — relying on default-block
|
||||||
|
# semantics for now.
|
||||||
|
|
||||||
|
# DO NOT pin to specific id=, serial=, hash=, or parent-hash= here.
|
||||||
|
# That's the user's job post-firstboot for their actual hardware.
|
||||||
|
# Pre-shipped pinned rules break on every dock replug + kernel bump.
|
||||||
|
|
@ -147,12 +147,30 @@ PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || echo "")
|
||||||
|| check Network public_ip fail "lookup timed out"
|
|| check Network public_ip fail "lookup timed out"
|
||||||
|
|
||||||
# ── 5. Updates ──────────────────────────────────────────────────────
|
# ── 5. Updates ──────────────────────────────────────────────────────
|
||||||
|
# v0.7+ atomic — bootc replaces dnf as the update channel. Parse
|
||||||
|
# `bootc status --json` for the booted deployment + staged/cached image
|
||||||
|
# age. Fall back to dnf history if bootc not present (legacy v0.5.x).
|
||||||
|
if have bootc; then
|
||||||
|
BOOTC_JSON=$(sudo -n bootc status --json 2>/dev/null || echo "")
|
||||||
|
if [[ -n $BOOTC_JSON ]] && have jq; then
|
||||||
|
BOOTED_IMG=$(jq -r '.status.booted.image.image.image // "unknown"' <<<"$BOOTC_JSON")
|
||||||
|
BOOTED_DIGEST=$(jq -r '.status.booted.image.imageDigest // ""' <<<"$BOOTC_JSON")
|
||||||
|
check Updates booted_image pass "${BOOTED_IMG}@${BOOTED_DIGEST:0:12}"
|
||||||
|
STAGED=$(jq -r '.status.staged.image.image.image // ""' <<<"$BOOTC_JSON")
|
||||||
|
if [[ -n $STAGED ]]; then
|
||||||
|
check Updates staged_image fail "staged: $STAGED — reboot to apply"
|
||||||
|
else
|
||||||
|
check Updates staged_image pass "no staged update"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
check Updates bootc_state pass "bootc present (jq missing — install for richer detail)"
|
||||||
|
fi
|
||||||
|
elif have dnf; then
|
||||||
|
# Legacy v0.5.x kickstart-installed system.
|
||||||
LAST_DNF=$(sudo -n dnf history list 2>/dev/null \
|
LAST_DNF=$(sudo -n dnf history list 2>/dev/null \
|
||||||
| awk 'NR==4 {for(i=4;i<NF;i++)printf "%s ", $i; print $NF; exit}')
|
| awk 'NR==4 {for(i=4;i<NF;i++)printf "%s ", $i; print $NF; exit}')
|
||||||
[[ -n $LAST_DNF ]] && check Updates last_dnf pass "$LAST_DNF" \
|
[[ -n $LAST_DNF ]] && check Updates last_dnf pass "$LAST_DNF" \
|
||||||
|| check Updates last_dnf pass "(unknown — try \`sudo dnf history\`)"
|
|| check Updates last_dnf pass "(unknown — try \`sudo dnf history\`)"
|
||||||
|
|
||||||
# `dnf check-update` exits 100 if updates available, 0 if not.
|
|
||||||
sudo -n dnf check-update -q >/dev/null 2>&1
|
sudo -n dnf check-update -q >/dev/null 2>&1
|
||||||
RC=$?
|
RC=$?
|
||||||
case $RC in
|
case $RC in
|
||||||
|
|
@ -164,6 +182,9 @@ case $RC in
|
||||||
;;
|
;;
|
||||||
*) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
|
*) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
|
||||||
esac
|
esac
|
||||||
|
else
|
||||||
|
check Updates channel fail "neither bootc nor dnf available"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── 6. veilor services ──────────────────────────────────────────────
|
# ── 6. veilor services ──────────────────────────────────────────────
|
||||||
for unit in veilor-firstboot.service veilor-modules-lock.service; do
|
for unit in veilor-firstboot.service veilor-modules-lock.service; do
|
||||||
|
|
|
||||||
|
|
@ -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 ──
|
||||||
|
# v0.6: prompt twice + string-compare. A typo in the LUKS passphrase
|
||||||
|
# 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
|
luks_pw=$(prompt_password "[2/3] Encryption · LUKS2 passphrase (min 8)") || return 1
|
||||||
validate_pw "$luks_pw" "passphrase" || 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 ──
|
||||||
|
# Same confirm-twice pattern. Less catastrophic than LUKS (admin
|
||||||
|
# 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
|
admin_pw=$(prompt_password "[3/3] Admin user · password for 'admin'") || return 1
|
||||||
validate_pw "$admin_pw" "password" || 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
|
||||||
|
|
@ -427,7 +486,7 @@ __SSHKEY_DIRECTIVE__
|
||||||
# re-runs kernel-install per kernel. That's the canonical Fedora 43 path
|
# re-runs kernel-install per kernel. That's the canonical Fedora 43 path
|
||||||
# for landing args in BLS entries — kernel-install reads /etc/kernel/cmdline
|
# for landing args in BLS entries — kernel-install reads /etc/kernel/cmdline
|
||||||
# (90-loaderentry.install:84-95) when generating BLS option lines.
|
# (90-loaderentry.install:84-95) when generating BLS option lines.
|
||||||
bootloader --append="lockdown=integrity slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none fbcon=nodefer"
|
bootloader --append="lockdown=integrity module.sig_enforce=1 slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none fbcon=nodefer i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1 rd.vconsole.keymap=us"
|
||||||
|
|
||||||
# Disk: zero, LUKS2 (argon2id), btrfs subvolumes (no LVM intermediary).
|
# Disk: zero, LUKS2 (argon2id), btrfs subvolumes (no LVM intermediary).
|
||||||
# Native btrfs-on-LUKS matches Fedora KDE Spin defaults; LVM+btrfs combo
|
# Native btrfs-on-LUKS matches Fedora KDE Spin defaults; LVM+btrfs combo
|
||||||
|
|
@ -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
|
||||||
|
|
@ -927,7 +992,33 @@ KSEOF
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_dump_logs_to_host() {
|
||||||
|
# If a virtio-9p share tagged "hostlogs" was attached by run-vm.sh,
|
||||||
|
# mount it and dump every anaconda + installer log into it. Runs on
|
||||||
|
# success AND failure (called via trap). No-op on real hardware where
|
||||||
|
# the 9p tag doesn't exist.
|
||||||
|
if ! grep -qw 9p /proc/filesystems 2>/dev/null; then return 0; fi
|
||||||
|
local m=/mnt/hostlogs
|
||||||
|
mkdir -p "$m"
|
||||||
|
mount -t 9p -o trans=virtio,version=9p2000.L hostlogs "$m" 2>/dev/null || return 0
|
||||||
|
cp -af /tmp/anaconda.log "$m/" 2>/dev/null || true
|
||||||
|
cp -af /tmp/program.log "$m/" 2>/dev/null || true
|
||||||
|
cp -af /tmp/storage.log "$m/" 2>/dev/null || true
|
||||||
|
cp -af /tmp/packaging.log "$m/" 2>/dev/null || true
|
||||||
|
cp -af /tmp/dnf.log "$m/" 2>/dev/null || true
|
||||||
|
cp -af /tmp/dnf.librepo.log "$m/" 2>/dev/null || true
|
||||||
|
cp -af /tmp/anaconda-cmdline.log "$m/" 2>/dev/null || true
|
||||||
|
cp -af /var/log/veilor-installer.log "$m/" 2>/dev/null || true
|
||||||
|
cp -af /run/install/veilor-generated.ks "$m/" 2>/dev/null || true
|
||||||
|
dmesg > "$m/dmesg.log" 2>/dev/null || true
|
||||||
|
journalctl -b > "$m/journal.log" 2>/dev/null || true
|
||||||
|
sync
|
||||||
|
umount "$m" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
run_install() {
|
run_install() {
|
||||||
|
# Capture logs to host on every exit path (success, failure, ^C).
|
||||||
|
trap _dump_logs_to_host EXIT
|
||||||
# Anaconda env setup (see comments below).
|
# Anaconda env setup (see comments below).
|
||||||
export LANG="${SEL_LOCALE:-en_GB.UTF-8}"
|
export LANG="${SEL_LOCALE:-en_GB.UTF-8}"
|
||||||
export LC_ALL="$LANG"
|
export LC_ALL="$LANG"
|
||||||
|
|
@ -952,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
|
||||||
|
# v0.6: split the success screen into THREE stacked boxes.
|
||||||
|
#
|
||||||
|
# 1. Green success box — quiet confirmation.
|
||||||
|
# 2. Yellow eject box — promoted out of the buried
|
||||||
|
# one-liner the v0.5 success box used. Operators on
|
||||||
|
# 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" \
|
gum style --foreground 2 --border rounded --margin "1 2" --padding "1 3" \
|
||||||
"✓ Install complete" \
|
"✓ Install complete" \
|
||||||
"" \
|
"" \
|
||||||
"System will reboot in 5 seconds." \
|
"Rebooting in ${secs}s..."
|
||||||
"Remove the install media."
|
gum style --foreground 3 --border thick --margin "0 2" --padding "1 3" \
|
||||||
sleep 5
|
--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).
|
||||||
|
|
|
||||||
178
overlay/usr/local/bin/veilor-postinstall
Executable file
178
overlay/usr/local/bin/veilor-postinstall
Executable file
|
|
@ -0,0 +1,178 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
# veilor-postinstall — first-login TUI on v0.7+ atomic systems.
|
||||||
|
#
|
||||||
|
# Runs ONCE on first SDDM login via the user-mode systemd unit
|
||||||
|
# `veilor-postinstall.service`. Asks the operator for the small set
|
||||||
|
# of decisions we deliberately defer from install time:
|
||||||
|
# - keyboard / locale
|
||||||
|
# - hostname override
|
||||||
|
# - GPU drivers (NVIDIA layered via rpm-ostree, mesa = no-op)
|
||||||
|
# - package preset (dev / media / homelab — additive, opt-out)
|
||||||
|
# - bluetooth opt-in
|
||||||
|
# - USBGuard policy snapshot
|
||||||
|
# - veilor-doctor first run
|
||||||
|
# Writes /var/lib/veilor/postinstall-complete on success and disables
|
||||||
|
# its own autostart unit. Idempotent: safe to re-run.
|
||||||
|
#
|
||||||
|
# Style: gum if present, plain bash read fallback. No decorative ASCII.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
export TERM="${TERM:-linux}"
|
||||||
|
|
||||||
|
STATE_DIR=/var/lib/veilor
|
||||||
|
DONE_MARKER="$STATE_DIR/postinstall-complete"
|
||||||
|
LOG=/var/log/veilor-postinstall.log
|
||||||
|
|
||||||
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
GUM=$(have gum && echo gum || echo "")
|
||||||
|
|
||||||
|
# Always log + tee to stdout for live progress.
|
||||||
|
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
||||||
|
exec > >(tee -a "$LOG") 2>&1
|
||||||
|
|
||||||
|
if [[ -e $DONE_MARKER && ${1:-} != "--force" ]]; then
|
||||||
|
echo "veilor-postinstall already ran (marker: $DONE_MARKER). Pass --force to re-run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Wrappers ────────────────────────────────────────────────────────
|
||||||
|
choose() {
|
||||||
|
local header=$1; shift
|
||||||
|
if [[ -n $GUM ]]; then
|
||||||
|
gum choose --header "$header" "$@"
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo "$header"
|
||||||
|
local i=1
|
||||||
|
for opt in "$@"; do printf ' %d) %s\n' "$i" "$opt"; ((i++)); done
|
||||||
|
local n
|
||||||
|
read -rp " choice (1-$#): " n
|
||||||
|
[[ $n -ge 1 && $n -le $# ]] || return 1
|
||||||
|
eval "echo \${$n}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ask() {
|
||||||
|
local prompt=$1 default=${2:-}
|
||||||
|
if [[ -n $GUM ]]; then
|
||||||
|
gum input --header "$prompt" --value "$default"
|
||||||
|
else
|
||||||
|
local v
|
||||||
|
read -rp "$prompt [$default] " v
|
||||||
|
echo "${v:-$default}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
local prompt=$1
|
||||||
|
if [[ -n $GUM ]]; then
|
||||||
|
gum confirm "$prompt" && return 0 || return 1
|
||||||
|
else
|
||||||
|
read -rp "$prompt [y/N] " y
|
||||||
|
[[ ${y,,} == y* ]]
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
say() {
|
||||||
|
if [[ -n $GUM ]]; then
|
||||||
|
gum style --foreground 212 --bold "$1"
|
||||||
|
else
|
||||||
|
printf '\n=== %s ===\n' "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Need root for several actions; re-exec under sudo if not root.
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
say "veilor-postinstall: sudo required"
|
||||||
|
exec sudo -E bash "$0" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
say "veilor-postinstall — one-time setup"
|
||||||
|
echo " This runs once. Each step is skippable. Defaults are sane."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── 1. Keyboard layout ──────────────────────────────────────────────
|
||||||
|
KB=$(choose "Keyboard layout" us gb de fr es ru "skip") || KB=skip
|
||||||
|
if [[ $KB != skip ]]; then
|
||||||
|
localectl set-keymap "$KB" 2>/dev/null || true
|
||||||
|
echo " [OK] keymap = $KB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. Locale ───────────────────────────────────────────────────────
|
||||||
|
LOC=$(choose "Locale" en_US.UTF-8 en_GB.UTF-8 de_DE.UTF-8 fr_FR.UTF-8 "skip") || LOC=skip
|
||||||
|
if [[ $LOC != skip ]]; then
|
||||||
|
localectl set-locale LANG="$LOC" 2>/dev/null || true
|
||||||
|
echo " [OK] locale = $LOC"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Hostname ─────────────────────────────────────────────────────
|
||||||
|
HN=$(ask "Hostname" "veilor")
|
||||||
|
if [[ -n $HN && $HN != $(hostnamectl --static 2>/dev/null) ]]; then
|
||||||
|
hostnamectl set-hostname "$HN"
|
||||||
|
echo " [OK] hostname = $HN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. GPU drivers ──────────────────────────────────────────────────
|
||||||
|
GPU=$(choose "GPU drivers" "Skip (use mesa defaults)" "NVIDIA proprietary (akmod-nvidia)" "Intel/AMD mesa (no-op)") || GPU=skip
|
||||||
|
case "$GPU" in
|
||||||
|
*NVIDIA*)
|
||||||
|
say "Layering NVIDIA driver — this takes a few minutes"
|
||||||
|
rpm-ostree install --idempotent akmod-nvidia xorg-x11-drv-nvidia-cuda \
|
||||||
|
&& echo " [OK] NVIDIA driver layered (reboot to use)" \
|
||||||
|
|| echo " [WARN] NVIDIA layer failed; check rpm-ostree status"
|
||||||
|
;;
|
||||||
|
*) echo " (skipped GPU layering)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── 5. Package presets (multi-select) ───────────────────────────────
|
||||||
|
say "Package presets — pick any combination (skip = none)"
|
||||||
|
PRESET_DEV="git tmux vim-enhanced htop podman skopeo"
|
||||||
|
PRESET_MEDIA="vlc obs-studio"
|
||||||
|
PRESET_HOMELAB="wireguard-tools jq yq tmux"
|
||||||
|
|
||||||
|
PICKED=()
|
||||||
|
confirm "Install dev preset? ($PRESET_DEV)" && PICKED+=($PRESET_DEV) || true
|
||||||
|
confirm "Install media preset? ($PRESET_MEDIA)" && PICKED+=($PRESET_MEDIA) || true
|
||||||
|
confirm "Install homelab preset? ($PRESET_HOMELAB)" && PICKED+=($PRESET_HOMELAB) || true
|
||||||
|
if (( ${#PICKED[@]} > 0 )); then
|
||||||
|
# de-dupe
|
||||||
|
UNIQ=$(printf '%s\n' "${PICKED[@]}" | sort -u | tr '\n' ' ')
|
||||||
|
say "Layering: $UNIQ"
|
||||||
|
rpm-ostree install --idempotent $UNIQ \
|
||||||
|
&& echo " [OK] preset packages layered (reboot to use)" \
|
||||||
|
|| echo " [WARN] preset layer failed; check rpm-ostree status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6. Bluetooth ────────────────────────────────────────────────────
|
||||||
|
if confirm "Enable Bluetooth?"; then
|
||||||
|
systemctl enable --now bluetooth.service 2>/dev/null || true
|
||||||
|
echo " [OK] bluetooth enabled"
|
||||||
|
else
|
||||||
|
echo " (skipped bluetooth)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 7. USBGuard snapshot ────────────────────────────────────────────
|
||||||
|
say "USBGuard policy snapshot"
|
||||||
|
echo " Plug in EVERY USB device you trust right now (keyboard,"
|
||||||
|
echo " mouse, dock, yubikey, etc.) before continuing."
|
||||||
|
if confirm "Snapshot current USB devices into the allowlist?"; then
|
||||||
|
usbguard generate-policy > /etc/usbguard/rules.conf \
|
||||||
|
&& echo " [OK] policy written to /etc/usbguard/rules.conf" \
|
||||||
|
|| echo " [WARN] generate-policy failed"
|
||||||
|
systemctl restart usbguard 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 8. veilor-doctor ────────────────────────────────────────────────
|
||||||
|
if confirm "Run veilor-doctor now?"; then
|
||||||
|
veilor-doctor || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Done ────────────────────────────────────────────────────────────
|
||||||
|
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$DONE_MARKER"
|
||||||
|
say "veilor-postinstall complete"
|
||||||
|
echo " Marker written: $DONE_MARKER"
|
||||||
|
echo " Disabling autostart unit so this never runs again."
|
||||||
|
systemctl --user --global disable veilor-postinstall.service 2>/dev/null || true
|
||||||
|
systemctl disable veilor-postinstall.service 2>/dev/null || true
|
||||||
|
echo
|
||||||
|
echo " If you layered any packages or drivers, reboot to activate."
|
||||||
|
|
@ -1,25 +1,22 @@
|
||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
# veilor-update — system update wrapper.
|
# veilor-update — atomic update wrapper for v0.7+ (bootc + rpm-ostree).
|
||||||
# Wraps `dnf upgrade --refresh` + `flatpak update` behind a single command.
|
#
|
||||||
# User-facing CLI shipped in /usr/local/bin/. v0.6 ergonomic tooling.
|
# Wraps `bootc upgrade` + flatpak update behind a single command.
|
||||||
|
# Pre-checks rollback availability, pauses auditd while staging the
|
||||||
|
# new image, prints a clear post-state summary, and offers reboot.
|
||||||
#
|
#
|
||||||
# Exit codes:
|
# Exit codes:
|
||||||
# 0 success
|
# 0 success (with or without pending reboot)
|
||||||
# 1 dnf failed
|
# 1 bootc upgrade failed
|
||||||
# 2 flatpak failed (dnf still ran successfully)
|
# 2 flatpak failed (bootc still ran successfully)
|
||||||
# 3 no network
|
# 3 no network
|
||||||
#
|
|
||||||
# Uses `gum` for spinner output if present, falls back to plain stdout.
|
|
||||||
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
# ── Helpers ─────────────────────────────────────────────────────────
|
|
||||||
have() { command -v "$1" >/dev/null 2>&1; }
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
GUM=$(have gum && echo gum || echo "")
|
GUM=$(have gum && echo gum || echo "")
|
||||||
|
|
||||||
say() {
|
say() {
|
||||||
# Print a status line. Coloured if gum present, else plain.
|
|
||||||
if [[ -n $GUM ]]; then
|
if [[ -n $GUM ]]; then
|
||||||
gum style --foreground 212 --bold "$1"
|
gum style --foreground 212 --bold "$1"
|
||||||
else
|
else
|
||||||
|
|
@ -27,46 +24,50 @@ say() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
run_with_spinner() {
|
confirm() {
|
||||||
local title=$1; shift
|
local prompt=$1
|
||||||
if [[ -n $GUM ]]; then
|
if [[ -n $GUM ]]; then
|
||||||
gum spin --spinner dot --title "$title" -- "$@"
|
gum confirm "$prompt"
|
||||||
else
|
else
|
||||||
echo "[+] $title"
|
read -r -p "$prompt [y/N] " yn
|
||||||
"$@"
|
[[ ${yn,,} == y* ]]
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Pre-flight: network check ───────────────────────────────────────
|
# ── Pre-flight: network ─────────────────────────────────────────────
|
||||||
say "veilor-update: checking network"
|
say "veilor-update: checking network"
|
||||||
if ! ping -c 1 -W 2 mirrors.fedoraproject.org >/dev/null 2>&1; then
|
if ! ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then
|
||||||
echo
|
echo " No network. Connect and re-run \`veilor-update\`."
|
||||||
echo " No route to mirrors.fedoraproject.org."
|
|
||||||
echo " Connect to a network and re-run \`veilor-update\`."
|
|
||||||
exit 3
|
exit 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Snapshot kernel before upgrade so we can warn about reboot need ─
|
# ── Pre-flight: rollback target available ───────────────────────────
|
||||||
KERNEL_BEFORE=$(uname -r)
|
# bootc has two deployments by design (booted + rollback). If
|
||||||
|
# something's wrong we want the user to see it before staging more.
|
||||||
# ── DNF upgrade ─────────────────────────────────────────────────────
|
if have bootc; then
|
||||||
say "veilor-update: refreshing DNF metadata + applying updates"
|
say "veilor-update: bootc status"
|
||||||
# Capture upgrade output so we can count packages afterwards. Tee to
|
bootc status || true
|
||||||
# stdout for live progress; swallow into a tempfile for the count.
|
else
|
||||||
LOG=$(mktemp -t veilor-update.XXXXXX)
|
echo " bootc not present — this CLI targets v0.7+ atomic systems."
|
||||||
trap 'rm -f "$LOG"' EXIT
|
|
||||||
|
|
||||||
if ! sudo dnf upgrade --refresh -y 2>&1 | tee "$LOG"; then
|
|
||||||
echo
|
|
||||||
echo " dnf upgrade failed. See output above."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Count packages updated ──────────────────────────────────────────
|
# ── Pause auditd while staging ──────────────────────────────────────
|
||||||
# DNF prints "Upgraded: N", "Installed: N", "Removed: N" at end.
|
# Reduces audit log noise during the heavy fs writes; resume after.
|
||||||
# Sum the upgrade/install lines for the user-visible total.
|
AUDIT_PAUSED=0
|
||||||
UPDATED=$(grep -E '^(Upgraded|Installed)\b' "$LOG" 2>/dev/null \
|
if systemctl is-active auditd >/dev/null 2>&1; then
|
||||||
| awk -F: '{ gsub(/[^0-9]/,"",$2); s+=$2 } END { print s+0 }')
|
if sudo systemctl stop auditd 2>/dev/null; then
|
||||||
|
AUDIT_PAUSED=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
trap '[[ $AUDIT_PAUSED == 1 ]] && sudo systemctl start auditd 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
# ── bootc upgrade ───────────────────────────────────────────────────
|
||||||
|
say "veilor-update: bootc upgrade"
|
||||||
|
if ! sudo bootc upgrade; then
|
||||||
|
echo " bootc upgrade failed. See output above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Flatpak (best-effort) ───────────────────────────────────────────
|
# ── Flatpak (best-effort) ───────────────────────────────────────────
|
||||||
FLATPAK_RC=0
|
FLATPAK_RC=0
|
||||||
|
|
@ -74,21 +75,20 @@ if have flatpak; then
|
||||||
say "veilor-update: updating flatpaks"
|
say "veilor-update: updating flatpaks"
|
||||||
if ! flatpak update -y; then
|
if ! flatpak update -y; then
|
||||||
FLATPAK_RC=2
|
FLATPAK_RC=2
|
||||||
echo " flatpak update failed; continuing anyway."
|
echo " flatpak update failed; continuing."
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
echo " (flatpak not installed — skipping)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Post-update: reboot hint if kernel changed ──────────────────────
|
# ── Post-update summary ─────────────────────────────────────────────
|
||||||
KERNEL_AFTER_LATEST=$(rpm -q kernel --last 2>/dev/null \
|
|
||||||
| awk 'NR==1 { sub(/^kernel-/,"",$1); print $1 }')
|
|
||||||
|
|
||||||
say "veilor-update: complete"
|
say "veilor-update: complete"
|
||||||
printf ' Packages updated : %s\n' "${UPDATED:-0}"
|
bootc status 2>/dev/null | head -20 || true
|
||||||
printf ' Running kernel : %s\n' "$KERNEL_BEFORE"
|
|
||||||
if [[ -n ${KERNEL_AFTER_LATEST:-} && $KERNEL_AFTER_LATEST != "$KERNEL_BEFORE" ]]; then
|
# ── Reboot prompt ───────────────────────────────────────────────────
|
||||||
printf ' Newest kernel : %s (reboot suggested)\n' "$KERNEL_AFTER_LATEST"
|
# bootc always writes the new image into the staged deployment; reboot
|
||||||
|
# is required for it to become the running root.
|
||||||
|
if confirm " Reboot now to activate the new image?"; then
|
||||||
|
say "veilor-update: rebooting"
|
||||||
|
sudo systemctl reboot
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit $FLATPAK_RC
|
exit $FLATPAK_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.
|
||||||
|
|
|
||||||
|
|
@ -208,12 +208,20 @@ echo " Seed : ${SEED_ISO:-<none>}"
|
||||||
# We've lost logs three times in a row to anaconda failures + tmpfs
|
# We've lost logs three times in a row to anaconda failures + tmpfs
|
||||||
# reboots. Wiring this up so future failures auto-capture.
|
# reboots. Wiring this up so future failures auto-capture.
|
||||||
ANACONDA_LOG="$TEST_DIR/anaconda-vm-$(date +%Y%m%d-%H%M%S).log"
|
ANACONDA_LOG="$TEST_DIR/anaconda-vm-$(date +%Y%m%d-%H%M%S).log"
|
||||||
|
ANACONDA_LOG_DIR="$TEST_DIR/test-runs/$(date +%Y%m%d-%H%M%S)"
|
||||||
|
mkdir -p "$ANACONDA_LOG_DIR"
|
||||||
ANACONDA_LOG_ARGS=(
|
ANACONDA_LOG_ARGS=(
|
||||||
|
# Belt: virtio-serial (anaconda's setupVirtio rsyslog forward, fragile —
|
||||||
|
# depends on rsyslog being installed in the live ISO).
|
||||||
-chardev "file,id=anaclog,path=$ANACONDA_LOG"
|
-chardev "file,id=anaclog,path=$ANACONDA_LOG"
|
||||||
-device virtio-serial-pci,id=vs1
|
-device virtio-serial-pci,id=vs1
|
||||||
-device "virtserialport,chardev=anaclog,bus=vs1.0,name=org.fedoraproject.anaconda.log.0"
|
-device "virtserialport,chardev=anaclog,bus=vs1.0,name=org.fedoraproject.anaconda.log.0"
|
||||||
|
# Braces: virtio-9p host directory share. veilor-installer mounts this
|
||||||
|
# at /mnt/hostlogs and rsyncs /tmp/*.log there post-anaconda.
|
||||||
|
-virtfs "local,path=$ANACONDA_LOG_DIR,mount_tag=hostlogs,security_model=mapped-xattr,id=hostlogs"
|
||||||
)
|
)
|
||||||
echo " AnaLog : $ANACONDA_LOG"
|
echo " AnaLog : $ANACONDA_LOG"
|
||||||
|
echo " HostFS : $ANACONDA_LOG_DIR (9p tag: hostlogs)"
|
||||||
|
|
||||||
echo " Mode : ${SECBOOT:+secboot}${SECBOOT:-stock UEFI}"
|
echo " Mode : ${SECBOOT:+secboot}${SECBOOT:-stock UEFI}"
|
||||||
echo " Inject: ${HOST_PUBKEY:+yes}${HOST_PUBKEY:-no (no host pubkey)}"
|
echo " Inject: ${HOST_PUBKEY:+yes}${HOST_PUBKEY:-no (no host pubkey)}"
|
||||||
|
|
|
||||||
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