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 |
22 changed files with 1539 additions and 85 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
|
||||||
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"
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,3 +16,4 @@ test/veilor-vm.nvram*
|
||||||
test/auto-install-vm.qcow2
|
test/auto-install-vm.qcow2
|
||||||
test/auto-install-vm.nvram*
|
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,
|
||||||
|
|
|
||||||
42
README.md
42
README.md
|
|
@ -48,26 +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 from the latest Forgejo release.
|
# 1. Download the bootstrap installer ISO from Forgejo.
|
||||||
# https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest
|
# https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest
|
||||||
# (rolling tag; replaced on each successful build-iso.yml run)
|
|
||||||
sha256sum -c veilor-os-43-*.iso.sha256
|
sha256sum -c veilor-os-43-*.iso.sha256
|
||||||
|
|
||||||
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
|
# 2. Flash to USB. Replace /dev/sdX — 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -147,7 +167,7 @@ clean, locked down, with no manual post-install hardening required.
|
||||||
[secureblue](https://github.com/secureblue/secureblue) is an upstream
|
[secureblue](https://github.com/secureblue/secureblue) is an upstream
|
||||||
hardened atomic Fedora project we benchmark against and plan to **build
|
hardened atomic Fedora project we benchmark against and plan to **build
|
||||||
on top of** at v0.7. The v0.7 BlueBuild spike uses their
|
on top of** at v0.7. The v0.7 BlueBuild spike uses their
|
||||||
`securecore-kinoite-hardened-userns` OCI image as its base — we don't
|
`kinoite-main-hardened` OCI image as its base — we don't
|
||||||
ship their source code in this repo, we layer veilor branding,
|
ship their source code in this repo, we layer veilor branding,
|
||||||
theming, the gum installer, and the kickstart bootstrap on top of
|
theming, the gum installer, and the kickstart bootstrap on top of
|
||||||
their already-signed image.
|
their already-signed image.
|
||||||
|
|
|
||||||
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.
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
| Project | Role in veilor-os |
|
| Project | Role in veilor-os |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Fedora 43 KDE | Base OS for v0.5.x kickstart-installed flat builds |
|
| Fedora 43 KDE | Base OS for v0.5.x kickstart-installed flat builds |
|
||||||
| [secureblue](https://github.com/secureblue/secureblue) | Upstream hardened atomic Fedora; v0.7 BlueBuild spike layers our overlay on top of `securecore-kinoite-hardened-userns` |
|
| [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) |
|
| 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 |
|
| Bluefin / Bazzite (uBlue) | Reference for BlueBuild recipe shape and OCI publishing pattern |
|
||||||
| Tails | Reference for live-only install model — explicitly **not** veilor's path |
|
| Tails | Reference for live-only install model — explicitly **not** veilor's path |
|
||||||
|
|
@ -194,7 +194,7 @@ The repo carries more than just an ISO recipe:
|
||||||
| `scripts/selinux/veilor-systemd.te` | Custom SELinux module (targeted policy gap fixes) |
|
| `scripts/selinux/veilor-systemd.te` | Custom SELinux module (targeted policy gap fixes) |
|
||||||
| `scripts/30-apply-v03-theme.sh` | Plymouth + SDDM + Konsole + wallpaper apply |
|
| `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) |
|
| `scripts/40-apparmor.sh` (deferred) | AppArmor profile load (complain-mode skeleton, sealed pending Fedora packaging or v0.7 secureblue) |
|
||||||
| `bluebuild/recipe.yml` | v0.7 OCI recipe (base = secureblue securecore-kinoite-hardened-userns) |
|
| `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` |
|
| `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/installer/{banner.txt,colors.gum}` | Pure-block VEILOR OS wordmark + branded gum colour palette |
|
||||||
| `assets/branding/` | Logo, wallpapers, plymouth theme assets |
|
| `assets/branding/` | Logo, wallpapers, plymouth theme assets |
|
||||||
|
|
@ -252,7 +252,7 @@ ergonomic work and becomes the next ship target.
|
||||||
|
|
||||||
Scope:
|
Scope:
|
||||||
- BlueBuild recipe (`bluebuild/recipe.yml`) layering on
|
- BlueBuild recipe (`bluebuild/recipe.yml`) layering on
|
||||||
`ghcr.io/secureblue/securecore-kinoite-hardened-userns`
|
`ghcr.io/secureblue/kinoite-main-hardened`
|
||||||
- `kickstart/install-ostreecontainer.ks` — 10-line kickstart that calls
|
- `kickstart/install-ostreecontainer.ks` — 10-line kickstart that calls
|
||||||
`ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry`
|
`ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry`
|
||||||
and lets Anaconda's LUKS UX drive the install
|
and lets Anaconda's LUKS UX drive the install
|
||||||
|
|
@ -264,6 +264,21 @@ Scope:
|
||||||
- `veilor-update` rewritten on `bootc upgrade` (was `dnf upgrade`)
|
- `veilor-update` rewritten on `bootc upgrade` (was `dnf upgrade`)
|
||||||
- Forgejo registry as primary OCI publish target; GHCR mirror optional
|
- Forgejo registry as primary OCI publish target; GHCR mirror optional
|
||||||
- cosign key-pair signing of OCI image (replaces broken keyless flow)
|
- 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:
|
Public-flex items kept from original v0.7 entry:
|
||||||
|
|
||||||
|
|
@ -292,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).
|
||||||
|
|
@ -334,3 +334,29 @@ dir.
|
||||||
- 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.
|
||||||
|
|
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
|
|
@ -147,23 +147,44 @@ 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 ──────────────────────────────────────────────────────
|
||||||
LAST_DNF=$(sudo -n dnf history list 2>/dev/null \
|
# v0.7+ atomic — bootc replaces dnf as the update channel. Parse
|
||||||
| awk 'NR==4 {for(i=4;i<NF;i++)printf "%s ", $i; print $NF; exit}')
|
# `bootc status --json` for the booted deployment + staged/cached image
|
||||||
[[ -n $LAST_DNF ]] && check Updates last_dnf pass "$LAST_DNF" \
|
# age. Fall back to dnf history if bootc not present (legacy v0.5.x).
|
||||||
|| check Updates last_dnf pass "(unknown — try \`sudo dnf history\`)"
|
if have bootc; then
|
||||||
|
BOOTC_JSON=$(sudo -n bootc status --json 2>/dev/null || echo "")
|
||||||
# `dnf check-update` exits 100 if updates available, 0 if not.
|
if [[ -n $BOOTC_JSON ]] && have jq; then
|
||||||
sudo -n dnf check-update -q >/dev/null 2>&1
|
BOOTED_IMG=$(jq -r '.status.booted.image.image.image // "unknown"' <<<"$BOOTC_JSON")
|
||||||
RC=$?
|
BOOTED_DIGEST=$(jq -r '.status.booted.image.imageDigest // ""' <<<"$BOOTC_JSON")
|
||||||
case $RC in
|
check Updates booted_image pass "${BOOTED_IMG}@${BOOTED_DIGEST:0:12}"
|
||||||
0) check Updates pending pass "system up-to-date" ;;
|
STAGED=$(jq -r '.status.staged.image.image.image // ""' <<<"$BOOTC_JSON")
|
||||||
100)
|
if [[ -n $STAGED ]]; then
|
||||||
AVAIL=$(sudo -n dnf check-update -q 2>/dev/null \
|
check Updates staged_image fail "staged: $STAGED — reboot to apply"
|
||||||
| awk 'NF>=3 && $1!~/^Last/ {n++} END {print n+0}')
|
else
|
||||||
check Updates pending fail "${AVAIL} update(s) available — run \`veilor-update\`"
|
check Updates staged_image pass "no staged update"
|
||||||
;;
|
fi
|
||||||
*) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
|
else
|
||||||
esac
|
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 \
|
||||||
|
| 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" \
|
||||||
|
|| check Updates last_dnf pass "(unknown — try \`sudo dnf history\`)"
|
||||||
|
sudo -n dnf check-update -q >/dev/null 2>&1
|
||||||
|
RC=$?
|
||||||
|
case $RC in
|
||||||
|
0) check Updates pending pass "system up-to-date" ;;
|
||||||
|
100)
|
||||||
|
AVAIL=$(sudo -n dnf check-update -q 2>/dev/null \
|
||||||
|
| awk 'NF>=3 && $1!~/^Last/ {n++} END {print n+0}')
|
||||||
|
check Updates pending fail "${AVAIL} update(s) available — run \`veilor-update\`"
|
||||||
|
;;
|
||||||
|
*) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
|
||||||
|
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
|
||||||
|
|
|
||||||
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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue