commit 1822005df11878afa8bc492c900bc2a4d8879dc7 Author: veilor Date: Thu Apr 30 03:43:33 2026 +0100 veilor-os v0.1 scaffold — kickstart + hardening + 3-mode power + DuckSans-ready KDE black theme diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb3778d --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +build/out/ +build/cache/ +*.iso +*.img +*.log +*.pp +*.mod +.DS_Store +.idea/ +.vscode/ +secrets/ +*.key +*.pem diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..15f5183 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 veilor-os contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..69f48f8 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# veilor-os + +> Hardened minimal Fedora KDE remix. Black-on-black. Locked down by default. + +veilor-os is a Fedora 43 KDE spin built for operators who want a clean, fast, +opinionated desktop with serious hardening already in place. No prompts at +install beyond the LUKS passphrase. Boot, set admin password, work. + +## Highlights + +- **Single-prompt install** — only LUKS passphrase. No account wizard, no + initial-setup screen. `admin` account is created automatically; password + is set on first boot. +- **Hardened by default** — SELinux enforcing, USBGuard, fail2ban, firewalld + drop zone, kernel sysctl lockdown, NTS-authenticated NTP, DNS-over-TLS. +- **3-mode power management** — `veilor-power save | mid | perf`, with + AC/battery auto-switching via udev. Backed by tuned profiles. +- **DuckSans system font** — variable font, single binary, low cache + footprint. +- **Pure-black KDE color scheme** — `veilor-black` theme system-wide. +- **LUKS2 + Secure Boot** — argon2id, aes-xts, btrfs subvolumes, zram swap + (no disk swap, no cold-boot leak). +- **Reproducible build** — kickstart + podman + livemedia-creator. ISO + output is deterministic given pinned base. + +## Repo layout + +``` +kickstart/ veilor-os.ks full kickstart definition +build/ Containerfile + build-iso.sh reproducible ISO builder +overlay/ files dropped into installed root via %post +scripts/ hardening, SELinux policy, theme apply, firstboot +assets/ fonts, KDE color scheme, branding, plymouth theme +docs/ HARDENING / POWER / BUILD / INSTALL +test/ boot-checklist + findings log +``` + +See `docs/BUILD.md` for build instructions, `docs/INSTALL.md` for install, +`docs/HARDENING.md` for what's locked down and why. + +## Status + +Pre-release. v0.x. Repo private until first green ISO boots clean on test +hardware. + +## License + +MIT — see [LICENSE](LICENSE). DuckSans font ships under its own license; see +`assets/fonts/ducksans/README.md`. diff --git a/assets/fonts/ducksans/README.md b/assets/fonts/ducksans/README.md new file mode 100644 index 0000000..0a63c40 --- /dev/null +++ b/assets/fonts/ducksans/README.md @@ -0,0 +1,40 @@ +# DuckSans + +DuckSans is a variable font commissioned by DuckDuckGo from Fontwerk +(designer: Christoph Koeberlin, based on the Pangea typeface, 2026). + +## Why this font + +- **Variable font** — single binary covers full weight + width axis. + Smaller font cache, less I/O, fewer files for fontconfig to scan. +- **Designed for text-heavy UIs** — high readability, good hinting. +- **Recognizable but unbranded look** — distinctive without being kitsch. + +## Vendor instructions + +Drop the font files here: + +``` +assets/fonts/ducksans/DuckSans-VF.ttf +``` + +(plus optional italic axis if shipped separately) + +The build pipeline copies this directory to +`/usr/share/fonts/ducksans/` in the installed system and runs +`fc-cache -f`. + +## License + +DuckSans license terms TBD (Fontwerk commercial license vs SIL OFL). +**Do not commit the .ttf to a public repo until license is verified.** + +If license forbids redistribution, the kickstart `%post` should fetch +the font from an authenticated source at build time. See +`build/build-iso.sh` for the pull point. + +## Fallback + +If DuckSans is not present, fontconfig falls through to the system +default sans-serif. veilor-os will still install and run; the system +font will just not be DuckSans. diff --git a/assets/kde/veilor-black.colors b/assets/kde/veilor-black.colors new file mode 100644 index 0000000..de16206 --- /dev/null +++ b/assets/kde/veilor-black.colors @@ -0,0 +1,105 @@ +[ColorEffects:Disabled] +Color=0,0,0 +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=true +Color=112,111,110 +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +Enable=false +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate=20,20,20 +BackgroundNormal=10,10,10 +DecorationFocus=255,255,255 +DecorationHover=200,200,200 +ForegroundActive=255,255,255 +ForegroundInactive=140,140,140 +ForegroundLink=200,200,255 +ForegroundNegative=237,21,21 +ForegroundNeutral=176,128,0 +ForegroundNormal=232,232,232 +ForegroundPositive=128,210,128 +ForegroundVisited=180,180,220 + +[Colors:Selection] +BackgroundAlternate=40,40,40 +BackgroundNormal=60,60,60 +DecorationFocus=255,255,255 +DecorationHover=200,200,200 +ForegroundActive=255,255,255 +ForegroundInactive=180,180,180 +ForegroundLink=200,200,255 +ForegroundNegative=237,21,21 +ForegroundNeutral=176,128,0 +ForegroundNormal=255,255,255 +ForegroundPositive=128,210,128 +ForegroundVisited=180,180,220 + +[Colors:Tooltip] +BackgroundAlternate=20,20,20 +BackgroundNormal=8,8,8 +DecorationFocus=255,255,255 +DecorationHover=200,200,200 +ForegroundActive=255,255,255 +ForegroundInactive=140,140,140 +ForegroundLink=200,200,255 +ForegroundNegative=237,21,21 +ForegroundNeutral=176,128,0 +ForegroundNormal=232,232,232 +ForegroundPositive=128,210,128 +ForegroundVisited=180,180,220 + +[Colors:View] +BackgroundAlternate=10,10,10 +BackgroundNormal=0,0,0 +DecorationFocus=255,255,255 +DecorationHover=200,200,200 +ForegroundActive=255,255,255 +ForegroundInactive=140,140,140 +ForegroundLink=200,200,255 +ForegroundNegative=237,21,21 +ForegroundNeutral=176,128,0 +ForegroundNormal=232,232,232 +ForegroundPositive=128,210,128 +ForegroundVisited=180,180,220 + +[Colors:Window] +BackgroundAlternate=8,8,8 +BackgroundNormal=0,0,0 +DecorationFocus=255,255,255 +DecorationHover=200,200,200 +ForegroundActive=255,255,255 +ForegroundInactive=140,140,140 +ForegroundLink=200,200,255 +ForegroundNegative=237,21,21 +ForegroundNeutral=176,128,0 +ForegroundNormal=232,232,232 +ForegroundPositive=128,210,128 +ForegroundVisited=180,180,220 + +[General] +ColorScheme=veilor-black +Name=veilor-black +shadeSortColumn=true + +[KDE] +contrast=4 + +[WM] +activeBackground=0,0,0 +activeBlend=255,255,255 +activeForeground=255,255,255 +inactiveBackground=10,10,10 +inactiveBlend=180,180,180 +inactiveForeground=140,140,140 diff --git a/assets/kde/veilor-default.kdeglobals b/assets/kde/veilor-default.kdeglobals new file mode 100644 index 0000000..2f89387 --- /dev/null +++ b/assets/kde/veilor-default.kdeglobals @@ -0,0 +1,16 @@ +[General] +ColorScheme=veilor-black +Name=veilor-black +font=DuckSans,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1 +fixed=DuckSans Mono,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1 +menuFont=DuckSans,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1 +smallestReadableFont=DuckSans,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1 +toolBarFont=DuckSans,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 diff --git a/build/Containerfile b/build/Containerfile new file mode 100644 index 0000000..0904128 --- /dev/null +++ b/build/Containerfile @@ -0,0 +1,22 @@ +FROM registry.fedoraproject.org/fedora:43 + +LABEL org.opencontainers.image.title="veilor-os build env" +LABEL org.opencontainers.image.source="https://github.com/veilor-uk/veilor-os" + +RUN dnf install -y \ + lorax \ + livecd-tools \ + pykickstart \ + anaconda-tui \ + squashfs-tools \ + xorriso \ + genisoimage \ + syslinux \ + rsync \ + git \ + which \ + && dnf clean all + +WORKDIR /work + +ENTRYPOINT ["/bin/bash"] diff --git a/build/build-iso.sh b/build/build-iso.sh new file mode 100755 index 0000000..cb053bc --- /dev/null +++ b/build/build-iso.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# veilor-os — ISO builder +# Wraps livemedia-creator inside a podman container for reproducibility. +# Run from repo root. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT_DIR="$REPO_ROOT/build/out" +KS="$REPO_ROOT/kickstart/veilor-os.ks" +RELEASEVER="${RELEASEVER:-43}" +DATE="$(date +%Y%m%d)" +ISO_NAME="veilor-os-${RELEASEVER}-${DATE}.iso" + +mkdir -p "$OUT_DIR" + +# ── Validate kickstart ── +if command -v ksvalidator &>/dev/null; then + ksvalidator "$KS" +fi + +# ── Build container ── +podman build -t veilor-build:latest "$REPO_ROOT/build" + +# ── Build ISO ── +# --make-iso requires --privileged (loop devices, mount). +podman run --rm --privileged \ + -v "$REPO_ROOT:/work:Z" \ + -v "$OUT_DIR:/out:Z" \ + veilor-build:latest -c " + set -e + livemedia-creator \ + --make-iso \ + --no-virt \ + --ks /work/kickstart/veilor-os.ks \ + --resultdir /out/build-${DATE} \ + --project veilor-os \ + --releasever ${RELEASEVER} \ + --title 'veilor-os' \ + --tmp /tmp/lmc \ + --logfile /out/build-${DATE}.log + cp /out/build-${DATE}/*.iso /out/${ISO_NAME} + sha256sum /out/${ISO_NAME} > /out/${ISO_NAME}.sha256 + " + +echo +echo "════════════════════════════════════════════════════════" +echo " ISO ready: $OUT_DIR/$ISO_NAME" +echo " Checksum: $OUT_DIR/$ISO_NAME.sha256" +echo " Build log: $OUT_DIR/build-${DATE}.log" +echo "════════════════════════════════════════════════════════" +echo +echo " Write to USB: sudo dd if=$OUT_DIR/$ISO_NAME of=/dev/sdX bs=4M status=progress conv=fsync" +echo " (replace /dev/sdX with your USB device — use lsblk to identify)" diff --git a/docs/BUILD.md b/docs/BUILD.md new file mode 100644 index 0000000..7ce9612 --- /dev/null +++ b/docs/BUILD.md @@ -0,0 +1,76 @@ +# Building veilor-os + +## Requirements + +- **Host:** Fedora 43+ or RHEL/CentOS 9+ (anything with podman + KVM bits) +- **podman** with rootless or rootful — privileged mode required +- **Disk:** ~15GB free for build cache + ISO +- **Network:** internet (pulls Fedora repos, base container) + +## One-shot build + +From repo root: + +```bash +./build/build-iso.sh +``` + +Output: `build/out/veilor-os-43-YYYYMMDD.iso` and `.sha256`. + +## What the build does + +1. `ksvalidator` checks `kickstart/veilor-os.ks` syntax. +2. Builds `veilor-build:latest` container from `build/Containerfile` + (Fedora 43 base + lorax + livemedia-creator + pykickstart). +3. Runs `livemedia-creator --make-iso --no-virt` inside the container + with `--privileged` (loop devices and chroot mounts required). +4. Anaconda runs the kickstart in a tmpfs root, packages are pulled, + `%post` executes (hardening + theme + branding), root is squashed + into a Live ISO. +5. ISO + sha256 + build log dropped in `build/out/`. + +## Custom builds + +Environment variables: + +```bash +RELEASEVER=43 ./build/build-iso.sh # default +RELEASEVER=44 ./build/build-iso.sh # rebase to Fedora 44 when released +``` + +Edit `kickstart/veilor-os.ks` to: + +- Change locale / timezone (`lang`, `keyboard`, `timezone` lines) +- Add/remove packages (`%packages` section) +- Adjust LUKS parameters (`part pv.veilor` line) + +## Writing to USB + +```bash +sudo dd if=build/out/veilor-os-43-YYYYMMDD.iso of=/dev/sdX bs=4M status=progress conv=fsync +sync +``` + +Replace `/dev/sdX` with your USB device. **Triple-check** with `lsblk` +before running — `dd` will overwrite without warning. + +Ventoy is **not** supported for hardened-install ISOs because Anaconda +expects to find the kickstart at the ISO root. Use `dd` directly. + +## Troubleshooting + +- **`livemedia-creator` fails inside container:** ensure `--privileged` + is set (the script already passes it). On hosts with strict SELinux, + set `setsebool -P container_manage_cgroup on` once. +- **Packages not found:** the Fedora mirror may have moved. Update + `url --mirrorlist=` in the kickstart. +- **Kickstart syntax errors:** run `ksvalidator kickstart/veilor-os.ks` + directly. Errors point to a line number in the .ks file. +- **Build hangs at "Setting up Install Process":** Fedora mirror + timeouts. Pin a specific mirror with `url --url=https://...`. + +## Reproducibility + +The same kickstart + same Fedora release version + same overlay tree +should produce ISOs with identical package sets. Bit-for-bit identical +ISOs require pinning Fedora compose IDs (planned for v1). diff --git a/docs/HARDENING.md b/docs/HARDENING.md new file mode 100644 index 0000000..17927f3 --- /dev/null +++ b/docs/HARDENING.md @@ -0,0 +1,128 @@ +# Hardening Reference + +What veilor-os locks down and why. Each item is applied by either the +kickstart `%post` or the overlay tree shipped in `/etc`. + +## Boot chain + +| Item | State | Source | +|------|-------|--------| +| Secure Boot | Required (bootloader signed) | `bootloader` kickstart line | +| Kernel lockdown | `lockdown=integrity` | bootloader kernel args | +| Slab hardening | `slab_nomerge`, `init_on_alloc=1`, `init_on_free=1` | bootloader | +| Stack offset | `randomize_kstack_offset=on` | bootloader | +| vsyscall | `vsyscall=none` | bootloader | +| LUKS2 | aes-xts-plain64 / argon2id, mem=1GB, time=9 | `part pv.veilor` | +| Module loading | Locked 30s after graphical boot | `veilor-modules-lock.service` | + +## Kernel sysctl + +`/etc/sysctl.d/99-veilor-hardening.conf`: + +| Key | Value | Why | +|-----|-------|-----| +| `kernel.kptr_restrict` | 2 | hide kernel pointers from /proc | +| `kernel.dmesg_restrict` | 1 | dmesg root-only | +| `kernel.yama.ptrace_scope` | 2 | ptrace = parent only | +| `kernel.perf_event_paranoid` | 3 | unprivileged perf disabled | +| `net.core.bpf_jit_harden` | 2 | BPF JIT constant blinding | +| `kernel.randomize_va_space` | 2 | full ASLR | +| `fs.suid_dumpable` | 0 | no SUID core dumps | +| `dev.tty.ldisc_autoload` | 0 | block tty LPE vector | +| `net.ipv4.tcp_syncookies` | 1 | SYN flood mitigation | +| `net.ipv4.conf.all.rp_filter` | 1 | reverse-path filter | +| `accept_source_route` | 0 (v4+v6) | ignore source routing | +| `accept_redirects` | 0 (v4+v6) | ignore ICMP redirects | + +## SELinux + +- Enforcing, targeted policy. +- Custom module `veilor-systemd` grants `systemd_modules_load_t` the + `sys_admin` and `perfmon` capabilities required by the modules-lock + service. Source: `scripts/selinux/veilor-systemd.te`. + +## Network surface + +- **firewalld** default zone = `drop`. +- **Inbound:** ssh only. +- **systemd-resolved:** LLMNR off, DNSSEC `allow-downgrade`, + DNS-over-TLS opportunistic. Resolvers: Cloudflare (1.1.1.1, 1.0.0.1), + fallback Quad9 (9.9.9.9, 149.112.112.112). +- **chrony:** NTS-authenticated time from `time.cloudflare.com` and + `nts.sth1/2.ntp.se`. Pool fallback only. + +## SSH + +`/etc/ssh/sshd_config.d/10-veilor-hardening.conf`: + +- `PasswordAuthentication no` +- `PermitRootLogin no` +- `AllowUsers admin` +- `X11Forwarding no` +- `MaxAuthTries 3` +- `ClientAliveInterval 300` +- `LogLevel VERBOSE` + +## Auth / accounts + +- Root account **locked** (`passwd -l root`). No interactive root login. +- Single `admin` user, `wheel` group, full sudo. +- `pwquality.conf`: minlen=14, 4 character classes required, dictcheck. +- **First-boot password flow:** `chage -d 0 admin` expires the empty + password immediately. `veilor-firstboot.service` runs on TTY1 before + SDDM, prompts for new password, then starts the graphical session. + +## Audit + +`/etc/audit/rules.d/99-veilor-hardening.rules` watches: + +- `/etc/passwd`, `/etc/shadow`, `/etc/group`, `/etc/gshadow` +- `/etc/sudoers`, `/etc/sudoers.d/` +- `/etc/ssh/sshd_config*`, `/etc/selinux/`, `/etc/firewalld/` +- `/etc/cron.*`, `/var/spool/cron/` +- `/etc/sysctl.*`, `/etc/systemd/system/`, `/usr/lib/systemd/system/` +- All privileged binaries (sudo, su, passwd, mount, pkexec, etc.) +- Kernel module load/unload syscalls +- Permission/ownership changes by uid≥1000 + +## Intrusion detection + +`fail2ban` jails: + +- `sshd` — aggressive mode, 3 retries, 24h ban +- `pam-generic` — 5 retries, 1h ban (catches XDM, su, sudo failures) + +Backend: systemd journal. Action: firewalld rich rules. + +## USB + +`USBGuard` daemon, `ImplicitPolicyTarget=block`. + +Ships with **empty allowlist**. On first boot, admin runs: + +```bash +sudo usbguard generate-policy > /etc/usbguard/rules.conf +sudo systemctl restart usbguard +``` + +This snapshots all currently-connected devices into the allowlist. +Anything plugged in afterward is blocked unless explicitly allowed: + +```bash +sudo usbguard list-devices +sudo usbguard allow-device +``` + +## Disabled services + +`abrt*`, `cups`, `cups-browsed`, `geoclue`, `avahi-daemon`, +`bluetooth`, `ModemManager`, `gssproxy`, `atd`, `pcscd.socket`, +`pcscd.service`, `kdeconnectd` (removed at package level). + +## What's *not* enabled by default + +- **Disk swap** — replaced by zram (RAM-only, no key leak risk). +- **Bluetooth** — disabled. Enable with `systemctl enable --now bluetooth`. +- **Printing** — CUPS removed. Reinstall if needed: `dnf install cups`. +- **Snapd, Flatpak** — not installed (Flatpak optional add-on). +- **PackageKit** — removed; updates manual via `dnf`. diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..f2dc22c --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,106 @@ +# Installing veilor-os + +## What you need + +- USB drive (8GB+) flashed with the veilor-os ISO +- Target machine with UEFI (BIOS legacy works but Secure Boot is the + whole point — use UEFI) +- ~30GB free disk + +## Install flow + +The installer is **fully scripted**. The only thing it asks you for +is the **LUKS passphrase**. + +1. Boot from USB. +2. Pick "Install veilor-os" from the boot menu. +3. Anaconda runs the kickstart automatically. +4. When prompted, **set a strong LUKS passphrase**. This is the only + prompt. Choose well — losing it = losing the disk. +5. Wait. Install + `%post` hardening takes ~10–15 min depending on + network speed. +6. Reboot. Pull out the USB. + +## First boot + +1. **LUKS prompt** — enter your passphrase to unlock the disk. +2. **TTY1 banner appears:** + + ``` + ┌──────────────────────────────────────────────────────────┐ + │ veilor-os │ + │ first boot — admin password │ + └──────────────────────────────────────────────────────────┘ + ``` + +3. Type a password for the local admin account. Must meet: + - ≥ 14 characters + - 1 digit, 1 upper, 1 lower, 1 special +4. Once accepted, SDDM starts. +5. Log in as `admin` with the password you just set. +6. Shell prompt: `admin@veilor-os`. + +## Post-install hygiene + +### Set USBGuard allowlist + +USBGuard ships with an empty allowlist — every USB device you plug in +will be blocked until you whitelist your trusted set. + +Plug in everything you trust (keyboard, mouse, dock, yubikey, etc.), +then run: + +```bash +sudo usbguard generate-policy > /etc/usbguard/rules.conf +sudo systemctl restart usbguard +``` + +To allow a new device after that: + +```bash +sudo usbguard list-devices +sudo usbguard allow-device +``` + +### Verify hardening + +```bash +getenforce # Enforcing +mokutil --sb-state # SecureBoot enabled +sysctl kernel.yama.ptrace_scope # = 2 +sysctl fs.suid_dumpable # = 0 +firewall-cmd --get-default-zone # drop +fail2ban-client status sshd # active, jail loaded +veilor-power status # current profile + governor +``` + +### Check `/etc/os-release` + +```bash +cat /etc/os-release +# NAME="veilor-os" +# PRETTY_NAME="veilor-os 0.1 (Fedora 43 base)" +# ID=veilor +# ID_LIKE=fedora +``` + +### Add additional users + +The kickstart only creates `admin`. Add more users from there: + +```bash +sudo useradd -m -s /bin/bash +sudo passwd +``` + +Don't add anyone to `wheel` unless they need root. + +## Known caveats + +- **Bluetooth disabled by default** — `sudo systemctl enable --now bluetooth` + if you need it. +- **Printing disabled** — CUPS removed; `sudo dnf install cups cups-browsed` + if you need a printer. +- **No PackageKit** — updates manual via `sudo dnf upgrade`. Run weekly. +- **Battery cap at 80%** — udev rule. Edit + `/etc/udev/rules.d/91-veilor-battery-threshold.rules` to change. diff --git a/docs/POWER.md b/docs/POWER.md new file mode 100644 index 0000000..51c0383 --- /dev/null +++ b/docs/POWER.md @@ -0,0 +1,71 @@ +# Power Management + +veilor-os ships a 3-mode power profile system backed by `tuned`. + +## Profiles + +| Profile | Governor | EPP | Boost | ASUS TTP | Use | +|---------|----------|-----|-------|----------|-----| +| `veilor-powersave` | powersave | power | off | 2 (silent) | max battery | +| `veilor-balanced` | powersave | balance_performance | on | 1 (mid) | on the go | +| `veilor-performance` | performance | performance | on | 0 (full) | plugged in | + +`ASUS TTP` (throttle_thermal_policy) only applies to ASUS laptops with +`asus-nb-wmi`. On other hardware those writes are silently skipped. + +## Switching + +```bash +veilor-power save # max battery (aliases: powersave, s) +veilor-power mid # balanced (aliases: balanced, b) +veilor-power perf # performance (aliases: performance, p) +veilor-power # status: profile, governor, EPP, boost, freq +``` + +`veilor-power` calls `tuned-adm` via a NOPASSWD sudoers drop-in +locked to `veilor-*` profiles only (`/etc/sudoers.d/veilor-power`). + +## Auto-switch on AC plug/unplug + +`/etc/udev/rules.d/90-veilor-ac-switch.rules`: + +``` +SUBSYSTEM=="power_supply", ATTR{online}=="0", RUN+="/usr/bin/tuned-adm profile veilor-powersave" +SUBSYSTEM=="power_supply", ATTR{online}=="1", RUN+="/usr/bin/tuned-adm profile veilor-performance" +``` + +Override anytime with `veilor-power mid`. + +## Battery longevity + +`/etc/udev/rules.d/91-veilor-battery-threshold.rules` caps charge at +80% on supported hardware. Adjust by editing the rule or: + +```bash +echo 100 | sudo tee /sys/class/power_supply/BAT0/charge_control_end_threshold +``` + +## What each profile actually does + +`/etc/tuned/profiles/veilor-/script.sh` writes: + +- `/sys/devices/system/cpu/cpufreq/boost` +- `/sys/devices/platform/asus-nb-wmi/throttle_thermal_policy` (ASUS only) +- `/sys/bus/pci/devices/*/power/control` (NVMe autosuspend) +- `/sys/class/drm/card*/device/power_dpm_force_performance_level` (AMD iGPU) +- `usb_autosuspend` enable/disable + +All writes are guarded with `[ -w ... ]` so non-applicable hardware +silently no-ops. + +## Persistence + +`tuned.service` starts at boot and loads the last active profile from +`/var/lib/tuned/save.conf`. No GRUB params needed. + +## Caveat: `platform_profile` vs `throttle_thermal_policy` + +On some ASUS laptops the `platform_profile` sysfs key maps to TTP in +non-obvious order (e.g. `quiet`→TTP2, `balanced`→TTP0, +`performance`→TTP1). veilor profiles write TTP directly and never +touch `platform_profile` to avoid the second-write override race. diff --git a/kickstart/veilor-os.ks b/kickstart/veilor-os.ks new file mode 100644 index 0000000..faf4860 --- /dev/null +++ b/kickstart/veilor-os.ks @@ -0,0 +1,163 @@ +#version=DEVEL +# veilor-os kickstart — Fedora 43 KDE base, hardened, minimal. +# Build with livemedia-creator inside build/Containerfile. + +# ── Install source ── +url --mirrorlist="https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-$releasever&arch=$basearch" +repo --name=updates --mirrorlist="https://mirrors.fedoraproject.org/mirrorlist?repo=updates-released-f$releasever&arch=$basearch" + +# ── Locale / keyboard / time (template — adjust per build) ── +keyboard --xlayouts='us' +lang en_GB.UTF-8 +timezone Europe/London --utc + +# ── Install mode ── +text +firstboot --disable +eula --agreed +selinux --enforcing +services --enabled=sshd,fail2ban,usbguard,tuned,auditd,firewalld,chronyd,sddm,veilor-firstboot,veilor-modules-lock + +# ── Network / hostname ── +network --bootproto=dhcp --device=link --activate --hostname=veilor-os +firewall --enabled --service=ssh + +# ── Identity (zero-prompt; only LUKS passphrase asked at install) ── +rootpw --lock +user --name=admin --groups=wheel --gecos="veilor admin" --password="" --plaintext +auth --useshadow --passalgo=sha512 + +# ── Bootloader: kernel hardening flags ── +bootloader --location=mbr --append="lockdown=integrity slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none" + +# ── Disk: BIOS+UEFI, LUKS2, btrfs subvols, zram swap (no disk swap) ── +zerombr +clearpart --all --initlabel +reqpart --add-boot +part /boot --fstype=ext4 --size=1024 --asprimary +part pv.veilor --size=1 --grow --encrypted --luks-version=luks2 \ + --pbkdf=argon2id --pbkdf-memory=1048576 --pbkdf-iterations=9 \ + --cipher=aes-xts-plain64 --hash=sha512 +volgroup veilor pv.veilor +logvol / --vgname=veilor --name=root --fstype=btrfs --size=1 --grow \ + --mkfsoptions="--mixed" + +# ── Packages ── +%packages --excludedocs +@^kde-desktop-environment +@kde-apps +@core +@hardware-support +@standard + +# core hardening tools +fail2ban +fail2ban-firewalld +usbguard +usbguard-tools +audit +policycoreutils-python-utils +tuned +chrony +firewalld +plymouth + +# admin essentials +git +vim-enhanced +tmux +htop +podman +skopeo +NetworkManager +NetworkManager-wifi + +# fonts +fontconfig +freetype + +# remove fluff +-cups +-cups-browsed +-abrt* +-snapd +-geoclue2 +-avahi +-avahi-libs +-kde-connect +-open-vm-tools-desktop +-PackageKit +-PackageKit-command-not-found +-mlocate +-ModemManager +-pcsc-lite +-rsync-daemon + +%end + +# ── Post-install (nochroot): copy overlay tree into installed root ── +%post --nochroot +set -eu +SRC=/run/install/repo/veilor +DEST=/mnt/sysimage +if [[ -d $SRC/overlay ]]; then + cp -a $SRC/overlay/. $DEST/ +fi +mkdir -p $DEST/usr/share/veilor-os +cp -a $SRC/assets $DEST/usr/share/veilor-os/ +cp -a $SRC/scripts $DEST/usr/share/veilor-os/ +%end + +# ── Post-install (chroot): apply hardening, theme, branding ── +%post +set -uo pipefail +exec > >(tee -a /var/log/veilor-install.log) 2>&1 + +echo "════════════════════════════════════════════════════════" +echo " veilor-os install — %post" +echo "════════════════════════════════════════════════════════" + +REPO=/usr/share/veilor-os +chmod +x $REPO/scripts/*.sh $REPO/scripts/selinux/*.sh /usr/local/bin/veilor-power /usr/local/sbin/veilor-firstboot + +# Apply hardening +bash $REPO/scripts/10-harden-base.sh +bash $REPO/scripts/20-harden-kernel.sh + +# Build SELinux module +bash $REPO/scripts/selinux/build-policy.sh || echo "[WARN] SELinux build failed; load on first boot" + +# Apply KDE theme + DuckSans + os-release branding +bash $REPO/scripts/kde-theme-apply.sh + +# Force admin password set on first boot (chage expires immediately) +chage -d 0 admin + +# zram swap (no disk swap; keys never leak to platter) +dnf install -y zram-generator || true +cat > /etc/systemd/zram-generator.conf << 'EOF' +[zram0] +zram-size = min(ram, 8192) +compression-algorithm = zstd +EOF + +# Enable services +systemctl enable veilor-firstboot.service +systemctl enable veilor-modules-lock.service +systemctl enable sshd fail2ban usbguard tuned auditd firewalld chronyd + +# Default tuned profile = balanced (AC/battery udev rule will override) +tuned-adm profile veilor-balanced 2>/dev/null || true + +# Lock root explicitly (kickstart --lock should already do this) +passwd -l root + +# Sanity: zero references to onyx / personal IPs in installed system +if grep -rqi 'onyx\|192\.168\.0\.\|fedora\.local' /etc/veilor* /etc/tuned/profiles/veilor-* 2>/dev/null; then + echo "[ERR] brand leak detected in /etc — investigate" +fi + +echo "════════════════════════════════════════════════════════" +echo " veilor-os install complete" +echo "════════════════════════════════════════════════════════" +%end diff --git a/overlay/etc/os-release.d/veilor b/overlay/etc/os-release.d/veilor new file mode 100644 index 0000000..166389e --- /dev/null +++ b/overlay/etc/os-release.d/veilor @@ -0,0 +1,11 @@ +NAME="veilor-os" +PRETTY_NAME="veilor-os 0.1 (Fedora 43 base)" +ID=veilor +ID_LIKE=fedora +VERSION="0.1" +VERSION_ID="0.1" +HOME_URL="https://github.com/veilor-uk/veilor-os" +DOCUMENTATION_URL="https://github.com/veilor-uk/veilor-os/tree/main/docs" +BUG_REPORT_URL="https://github.com/veilor-uk/veilor-os/issues" +ANSI_COLOR="0;30;47" +LOGO=veilor-logo diff --git a/overlay/etc/sddm.conf.d/veilor.conf b/overlay/etc/sddm.conf.d/veilor.conf new file mode 100644 index 0000000..27e94d7 --- /dev/null +++ b/overlay/etc/sddm.conf.d/veilor.conf @@ -0,0 +1,15 @@ +[Theme] +Current=breeze +CursorTheme=breeze_cursors + +[Users] +HideUsers=root +HideShells=/sbin/nologin,/bin/false +MaximumUid=60000 +MinimumUid=1000 + +[General] +Numlock=on + +[Autologin] +Relogin=false diff --git a/overlay/etc/ssh/sshd_config.d/10-veilor-hardening.conf b/overlay/etc/ssh/sshd_config.d/10-veilor-hardening.conf new file mode 100644 index 0000000..9fd8d8c --- /dev/null +++ b/overlay/etc/ssh/sshd_config.d/10-veilor-hardening.conf @@ -0,0 +1,16 @@ +# veilor-os — sshd hardening drop-in +# Loaded last (10-* prefix sorts after distro 50-*) +X11Forwarding no +AllowUsers admin +PasswordAuthentication no +PermitRootLogin no +PermitEmptyPasswords no +ChallengeResponseAuthentication no +KbdInteractiveAuthentication no +UsePAM yes +ClientAliveInterval 300 +ClientAliveCountMax 2 +LoginGraceTime 30 +MaxAuthTries 3 +MaxSessions 4 +LogLevel VERBOSE diff --git a/overlay/etc/sudoers.d/veilor-power b/overlay/etc/sudoers.d/veilor-power new file mode 100644 index 0000000..5d2f5d0 --- /dev/null +++ b/overlay/etc/sudoers.d/veilor-power @@ -0,0 +1,3 @@ +# veilor-power — allow wheel to switch tuned profiles without password +# Locked to veilor-* profiles only. +%wheel ALL=(root) NOPASSWD: /usr/bin/tuned-adm profile veilor-powersave, /usr/bin/tuned-adm profile veilor-balanced, /usr/bin/tuned-adm profile veilor-performance diff --git a/overlay/etc/sysctl.d/99-veilor-hardening.conf b/overlay/etc/sysctl.d/99-veilor-hardening.conf new file mode 100644 index 0000000..c67d53e --- /dev/null +++ b/overlay/etc/sysctl.d/99-veilor-hardening.conf @@ -0,0 +1,25 @@ +# veilor-os — kernel sysctl hardening +# Mirrors scripts/20-harden-kernel.sh; ships in overlay so values present +# at first boot before scripts run. + +kernel.kptr_restrict = 2 +kernel.dmesg_restrict = 1 +net.core.bpf_jit_harden = 2 +kernel.perf_event_paranoid = 3 +kernel.yama.ptrace_scope = 2 +kernel.randomize_va_space = 2 +kernel.modules_disabled = 0 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +net.ipv4.conf.all.log_martians = 1 +net.ipv4.conf.default.log_martians = 1 +fs.suid_dumpable = 0 +dev.tty.ldisc_autoload = 0 +kernel.sched_schedstats = 0 +net.ipv4.tcp_syncookies = 1 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +net.ipv4.conf.all.accept_source_route = 0 +net.ipv6.conf.all.accept_source_route = 0 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 diff --git a/overlay/etc/systemd/system/veilor-firstboot.service b/overlay/etc/systemd/system/veilor-firstboot.service new file mode 100644 index 0000000..3a71eab --- /dev/null +++ b/overlay/etc/systemd/system/veilor-firstboot.service @@ -0,0 +1,21 @@ +[Unit] +Description=veilor-os first-boot admin password setup +Documentation=https://github.com/veilor-uk/veilor-os +ConditionPathExists=!/var/lib/veilor-firstboot.done +Before=sddm.service display-manager.service +After=systemd-user-sessions.service plymouth-quit-wait.service +Conflicts=sddm.service + +[Service] +Type=oneshot +RemainAfterExit=no +ExecStart=/usr/local/sbin/veilor-firstboot +StandardInput=tty +StandardOutput=tty +StandardError=tty +TTYPath=/dev/tty1 +TTYReset=yes +TTYVHangup=yes + +[Install] +WantedBy=multi-user.target diff --git a/overlay/etc/systemd/system/veilor-modules-lock.service b/overlay/etc/systemd/system/veilor-modules-lock.service new file mode 100644 index 0000000..a90e368 --- /dev/null +++ b/overlay/etc/systemd/system/veilor-modules-lock.service @@ -0,0 +1,16 @@ +[Unit] +Description=Lock kernel module loading after graphical boot (veilor-os) +Documentation=https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html +After=graphical.target network.target local-fs.target +ConditionKernelCommandLine=!module.sig_enforce=1 + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStartPre=/bin/sleep 30 +ExecStart=/usr/bin/sysctl -w kernel.modules_disabled=1 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=graphical.target diff --git a/overlay/etc/tuned/profiles/veilor-balanced/script.sh b/overlay/etc/tuned/profiles/veilor-balanced/script.sh new file mode 100755 index 0000000..c0e62fb --- /dev/null +++ b/overlay/etc/tuned/profiles/veilor-balanced/script.sh @@ -0,0 +1,32 @@ +#!/usr/bin/bash +# veilor-balanced — on-the-go +. /usr/lib/tuned/functions + +start() { + [ -w /sys/devices/system/cpu/cpufreq/boost ] && echo 1 > /sys/devices/system/cpu/cpufreq/boost + [ -w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy ] && \ + echo 1 > /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy + for nvme in /sys/bus/pci/devices/*/nvme; do + ctl="${nvme%/nvme}/power/control" + [ -w "$ctl" ] && echo auto > "$ctl" + done + for card in /sys/class/drm/card*/device/power_dpm_force_performance_level; do + [ -w "$card" ] && echo auto > "$card" + done + enable_usb_autosuspend + return 0 +} + +stop() { + for nvme in /sys/bus/pci/devices/*/nvme; do + ctl="${nvme%/nvme}/power/control" + [ -w "$ctl" ] && echo on > "$ctl" + done + for card in /sys/class/drm/card*/device/power_dpm_force_performance_level; do + [ -w "$card" ] && echo auto > "$card" + done + disable_usb_autosuspend + return 0 +} + +process $@ diff --git a/overlay/etc/tuned/profiles/veilor-balanced/tuned.conf b/overlay/etc/tuned/profiles/veilor-balanced/tuned.conf new file mode 100644 index 0000000..4bf4840 --- /dev/null +++ b/overlay/etc/tuned/profiles/veilor-balanced/tuned.conf @@ -0,0 +1,10 @@ +[main] +summary=veilor balanced — on-the-go, boost enabled, mid PPT + +[cpu] +governor=powersave +energy_performance_preference=balance_performance +force_latency=cstate.id_no_zero:1 + +[script] +script=${i:PROFILE_DIR}/script.sh diff --git a/overlay/etc/tuned/profiles/veilor-performance/script.sh b/overlay/etc/tuned/profiles/veilor-performance/script.sh new file mode 100755 index 0000000..059bcaf --- /dev/null +++ b/overlay/etc/tuned/profiles/veilor-performance/script.sh @@ -0,0 +1,31 @@ +#!/usr/bin/bash +# veilor-performance — full power +. /usr/lib/tuned/functions + +start() { + [ -w /sys/devices/system/cpu/cpufreq/boost ] && echo 1 > /sys/devices/system/cpu/cpufreq/boost + [ -w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy ] && \ + echo 0 > /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy + for nvme in /sys/bus/pci/devices/*/nvme; do + ctl="${nvme%/nvme}/power/control" + [ -w "$ctl" ] && echo on > "$ctl" + done + for card in /sys/class/drm/card*/device/power_dpm_force_performance_level; do + [ -w "$card" ] && echo auto > "$card" + done + disable_usb_autosuspend + return 0 +} + +stop() { + for nvme in /sys/bus/pci/devices/*/nvme; do + ctl="${nvme%/nvme}/power/control" + [ -w "$ctl" ] && echo on > "$ctl" + done + for card in /sys/class/drm/card*/device/power_dpm_force_performance_level; do + [ -w "$card" ] && echo auto > "$card" + done + return 0 +} + +process $@ diff --git a/overlay/etc/tuned/profiles/veilor-performance/tuned.conf b/overlay/etc/tuned/profiles/veilor-performance/tuned.conf new file mode 100644 index 0000000..5147d9c --- /dev/null +++ b/overlay/etc/tuned/profiles/veilor-performance/tuned.conf @@ -0,0 +1,10 @@ +[main] +summary=veilor performance — full PPT, max boost, no artificial cap + +[cpu] +governor=performance +energy_performance_preference=performance +force_latency=1 + +[script] +script=${i:PROFILE_DIR}/script.sh diff --git a/overlay/etc/tuned/profiles/veilor-powersave/script.sh b/overlay/etc/tuned/profiles/veilor-powersave/script.sh new file mode 100755 index 0000000..00344a6 --- /dev/null +++ b/overlay/etc/tuned/profiles/veilor-powersave/script.sh @@ -0,0 +1,43 @@ +#!/usr/bin/bash +# veilor-powersave — max battery +. /usr/lib/tuned/functions + +start() { + # CPU boost off + [ -w /sys/devices/system/cpu/cpufreq/boost ] && echo 0 > /sys/devices/system/cpu/cpufreq/boost + + # ASUS throttle_thermal_policy: 2 = silent (~5W PPT). No-op on non-ASUS. + [ -w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy ] && \ + echo 2 > /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy + + # NVMe PCI autosuspend (first NVMe device) + for nvme in /sys/bus/pci/devices/*/nvme; do + ctl="${nvme%/nvme}/power/control" + [ -w "$ctl" ] && echo auto > "$ctl" + done + + # AMD iGPU — minimum clocks (no-op on non-AMD) + for card in /sys/class/drm/card*/device/power_dpm_force_performance_level; do + [ -w "$card" ] && echo low > "$card" + done + + # USB autosuspend + enable_usb_autosuspend + + return 0 +} + +stop() { + [ -w /sys/devices/system/cpu/cpufreq/boost ] && echo 1 > /sys/devices/system/cpu/cpufreq/boost + for nvme in /sys/bus/pci/devices/*/nvme; do + ctl="${nvme%/nvme}/power/control" + [ -w "$ctl" ] && echo on > "$ctl" + done + for card in /sys/class/drm/card*/device/power_dpm_force_performance_level; do + [ -w "$card" ] && echo auto > "$card" + done + disable_usb_autosuspend + return 0 +} + +process $@ diff --git a/overlay/etc/tuned/profiles/veilor-powersave/tuned.conf b/overlay/etc/tuned/profiles/veilor-powersave/tuned.conf new file mode 100644 index 0000000..28a8a8d --- /dev/null +++ b/overlay/etc/tuned/profiles/veilor-powersave/tuned.conf @@ -0,0 +1,12 @@ +[main] +summary=veilor powersave — max battery, CPU capped, boost off + +[cpu] +governor=powersave +energy_performance_preference=power +force_latency=cstate.id_no_zero:1 +min_perf_pct=0 +max_perf_pct=30 + +[script] +script=${i:PROFILE_DIR}/script.sh diff --git a/overlay/etc/udev/rules.d/90-veilor-ac-switch.rules b/overlay/etc/udev/rules.d/90-veilor-ac-switch.rules new file mode 100644 index 0000000..a27efd1 --- /dev/null +++ b/overlay/etc/udev/rules.d/90-veilor-ac-switch.rules @@ -0,0 +1,3 @@ +# veilor-os — auto-switch tuned profile on AC plug/unplug +SUBSYSTEM=="power_supply", ATTR{online}=="0", RUN+="/usr/bin/tuned-adm profile veilor-powersave" +SUBSYSTEM=="power_supply", ATTR{online}=="1", RUN+="/usr/bin/tuned-adm profile veilor-performance" diff --git a/overlay/etc/udev/rules.d/91-veilor-battery-threshold.rules b/overlay/etc/udev/rules.d/91-veilor-battery-threshold.rules new file mode 100644 index 0000000..2caa31b --- /dev/null +++ b/overlay/etc/udev/rules.d/91-veilor-battery-threshold.rules @@ -0,0 +1,2 @@ +# veilor-os — cap battery charge at 80% for longevity +ACTION=="add", SUBSYSTEM=="power_supply", ATTR{type}=="Battery", ATTR{charge_control_end_threshold}=="*", ATTR{charge_control_end_threshold}="80" diff --git a/overlay/usr/local/bin/veilor-power b/overlay/usr/local/bin/veilor-power new file mode 100755 index 0000000..bf271f0 --- /dev/null +++ b/overlay/usr/local/bin/veilor-power @@ -0,0 +1,35 @@ +#!/usr/bin/bash +# veilor-power — power profile switcher +# Usage: veilor-power [save|mid|perf|status] + +_switch() { + sudo /usr/bin/tuned-adm profile "$1" +} + +_status() { + local profile gov epp boost asus freq + profile=$(tuned-adm active 2>/dev/null | awk '{print $NF}') + gov=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null) + epp=$(cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference 2>/dev/null) + boost=$(cat /sys/devices/system/cpu/cpufreq/boost 2>/dev/null) + asus=$(cat /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy 2>/dev/null || echo "n/a") + freq=$(awk '{printf "%.0f MHz", $1/1000}' /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null) + + echo "Profile : $profile" + echo "Governor: $gov | EPP: $epp | Boost: $boost" + echo "ASUS TTP: $asus (0=perf 1=balanced 2=silent) | Cur freq: $freq" +} + +case "$1" in + save|powersave|s) _switch veilor-powersave ;; + mid|balanced|b) _switch veilor-balanced ;; + perf|performance|p) _switch veilor-performance ;; + status|"") _status ;; + *) + echo "Usage: veilor-power [save|mid|perf|status]" + echo " save — max battery (boost off, lowest PPT)" + echo " mid — balanced (boost on, mid PPT)" + echo " perf — performance (boost on, full PPT)" + echo " status / no arg — show current state" + ;; +esac diff --git a/overlay/usr/local/sbin/veilor-firstboot b/overlay/usr/local/sbin/veilor-firstboot new file mode 100755 index 0000000..d60f82e --- /dev/null +++ b/overlay/usr/local/sbin/veilor-firstboot @@ -0,0 +1,47 @@ +#!/usr/bin/bash +# veilor-firstboot — set admin password on first boot, then self-disable. +# Runs on TTY1 before SDDM. Only fires while admin password is empty/expired. + +set -uo pipefail + +STATE=/var/lib/veilor-firstboot.done +[[ -f $STATE ]] && exit 0 + +# Branded banner +clear +cat << 'EOF' + + ┌──────────────────────────────────────────────────────────┐ + │ │ + │ veilor-os │ + │ first boot — admin password │ + │ │ + └──────────────────────────────────────────────────────────┘ + + Set a password for the local admin account. + + Requirements: minimum 14 characters, at least one digit, + one uppercase, one lowercase, one special character. + +EOF + +# Loop until passwd succeeds (pwquality enforces complexity) +until passwd admin; do + echo + echo " Password not accepted. Try again." + echo + sleep 1 +done + +# Mark done so service doesn't fire again +touch "$STATE" + +# Disable self for next boots +systemctl disable veilor-firstboot.service >/dev/null 2>&1 || true + +echo +echo " Password set. Starting graphical session..." +sleep 2 + +# Start SDDM (was held back by service ordering) +systemctl start sddm.service diff --git a/scripts/10-harden-base.sh b/scripts/10-harden-base.sh new file mode 100755 index 0000000..a8b3ebc --- /dev/null +++ b/scripts/10-harden-base.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# veilor-os — base hardening (services, DNS, fail2ban, auditd) +# Idempotent. 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} $*"; } +err() { echo -e "${RED}[ERR]${NC} $*"; } + +[[ $EUID -eq 0 ]] || { err "Must run as root"; exit 1; } + +echo "════════════════════════════════════════════════════════" +echo " veilor-os :: 10-harden-base" +echo "════════════════════════════════════════════════════════" + +# ── Remove KDE Connect (network exposure, DBus surface) ── +info "Removing kdeconnectd" +dnf remove -y kdeconnectd 2>/dev/null && ok "kdeconnectd removed" || info "kdeconnectd absent" + +# ── systemd-resolved: LLMNR off, DNSSEC, DNS-over-TLS ── +info "Hardening systemd-resolved" +mkdir -p /etc/systemd/resolved.conf.d +cat > /etc/systemd/resolved.conf.d/veilor-hardening.conf << 'EOF' +[Resolve] +LLMNR=no +DNSSEC=allow-downgrade +DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com +FallbackDNS=9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net +DNSOverTLS=opportunistic +EOF +systemctl restart systemd-resolved 2>/dev/null || true +ok "systemd-resolved hardened (LLMNR off, DNSSEC, DoT)" + +# ── fail2ban ── +info "Installing fail2ban" +rpm -q fail2ban &>/dev/null || dnf install -y fail2ban fail2ban-firewalld +cat > /etc/fail2ban/jail.local << 'EOF' +[DEFAULT] +backend = systemd +bantime = 3600 +findtime = 600 +maxretry = 5 +banaction = firewallcmd-rich-rules +banaction_allports = firewallcmd-rich-rules + +[sshd] +enabled = true +mode = aggressive +port = ssh +maxretry = 3 +bantime = 86400 + +[pam-generic] +enabled = true +filter = pam-generic +bantime = 3600 +maxretry = 5 +EOF +systemctl enable fail2ban +ok "fail2ban configured + enabled" + +# ── auditd rules ── +info "Deploying auditd rules" +mkdir -p /etc/audit/rules.d +cat > /etc/audit/rules.d/99-veilor-hardening.rules << 'EOF' +## veilor-os audit ruleset +-D +-b 8192 +-f 1 + +## time & clock +-a always,exit -F arch=b64 -S adjtimex,settimeofday -k time-change +-a always,exit -F arch=b32 -S adjtimex,settimeofday,stime -k time-change +-a always,exit -F arch=b64 -S clock_settime -k time-change +-a always,exit -F arch=b32 -S clock_settime -k time-change +-w /etc/localtime -p wa -k time-change + +## identity +-w /etc/group -p wa -k identity +-w /etc/passwd -p wa -k identity +-w /etc/gshadow -p wa -k identity +-w /etc/shadow -p wa -k identity +-w /etc/security/opasswd -p wa -k identity + +## hostname / network +-a always,exit -F arch=b64 -S sethostname,setdomainname -k system-locale +-a always,exit -F arch=b32 -S sethostname,setdomainname -k system-locale +-w /etc/hosts -p wa -k system-locale +-w /etc/hostname -p wa -k system-locale + +## SELinux +-w /etc/selinux/ -p wa -k MAC-policy + +## logins +-w /var/log/lastlog -p wa -k logins +-w /var/run/faillock/ -p wa -k logins +-w /var/run/utmp -p wa -k session +-w /var/log/wtmp -p wa -k logins +-w /var/log/btmp -p wa -k logins + +## perm changes +-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b32 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b64 -S chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b32 -S chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b64 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b32 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=unset -k perm_mod + +## access denials +-a always,exit -F arch=b64 -S creat,open,openat,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -k access +-a always,exit -F arch=b32 -S creat,open,openat,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -k access +-a always,exit -F arch=b64 -S creat,open,openat,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -k access +-a always,exit -F arch=b32 -S creat,open,openat,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -k access + +## privileged commands +-a always,exit -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/chfn -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/pkexec -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/mount -F perm=x -F auid>=1000 -F auid!=unset -k privileged +-a always,exit -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset -k privileged + +## kernel modules +-w /sbin/insmod -p x -k modules +-w /sbin/rmmod -p x -k modules +-w /sbin/modprobe -p x -k modules +-a always,exit -F arch=b64 -S init_module,delete_module,finit_module -k modules +-a always,exit -F arch=b32 -S init_module,delete_module,finit_module -k modules + +## sudoers +-w /etc/sudoers -p wa -k scope +-w /etc/sudoers.d/ -p wa -k scope + +## sshd +-w /etc/ssh/sshd_config -p wa -k sshd +-w /etc/ssh/sshd_config.d/ -p wa -k sshd + +## cron +-w /etc/crontab -p wa -k cron +-w /etc/cron.d/ -p wa -k cron +-w /etc/cron.daily/ -p wa -k cron +-w /etc/cron.hourly/ -p wa -k cron +-w /etc/cron.monthly/ -p wa -k cron +-w /etc/cron.weekly/ -p wa -k cron +-w /var/spool/cron/ -p wa -k cron + +## sysctl +-w /etc/sysctl.conf -p wa -k sysctl +-w /etc/sysctl.d/ -p wa -k sysctl + +## firewall +-w /etc/firewalld/ -p wa -k firewall + +## boot +-w /etc/default/grub -p wa -k grub +-w /etc/grub.d/ -p wa -k grub + +## systemd +-w /etc/systemd/system/ -p wa -k systemd +-w /usr/lib/systemd/system/ -p wa -k systemd +EOF +augenrules --load 2>/dev/null || true +systemctl enable auditd +ok "auditd rules deployed" + +echo "════════════════════════════════════════════════════════" +echo " 10-harden-base complete" +echo "════════════════════════════════════════════════════════" diff --git a/scripts/20-harden-kernel.sh b/scripts/20-harden-kernel.sh new file mode 100755 index 0000000..fbaf09a --- /dev/null +++ b/scripts/20-harden-kernel.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# veilor-os — kernel + service hardening (sysctl, USBGuard, NTS chrony, pwquality, service prune) +# Idempotent. Run as root. + +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} $*"; } +err() { echo -e "${RED}[ERR]${NC} $*"; } + +[[ $EUID -eq 0 ]] || { err "Must run as root"; exit 1; } + +echo "════════════════════════════════════════════════════════" +echo " veilor-os :: 20-harden-kernel" +echo "════════════════════════════════════════════════════════" + +# ── sysctl hardening ── +info "Writing /etc/sysctl.d/99-veilor-hardening.conf" +cat > /etc/sysctl.d/99-veilor-hardening.conf << 'EOF' +# kernel pointer hiding +kernel.kptr_restrict = 2 +kernel.dmesg_restrict = 1 +# BPF JIT constant blinding +net.core.bpf_jit_harden = 2 +# unprivileged perf events disabled +kernel.perf_event_paranoid = 3 +# ptrace restricted to parent +kernel.yama.ptrace_scope = 2 +# full ASLR +kernel.randomize_va_space = 2 +# modules unlocked at boot; veilor-modules-lock locks 30s after graphical +kernel.modules_disabled = 0 +# reverse path filter +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +# log martians +net.ipv4.conf.all.log_martians = 1 +net.ipv4.conf.default.log_martians = 1 +# no SUID core dumps +fs.suid_dumpable = 0 +# block unprivileged tty line discipline loading (LPE vector) +dev.tty.ldisc_autoload = 0 +# /proc/sched_debug hardened +kernel.sched_schedstats = 0 +# TCP SYN cookies +net.ipv4.tcp_syncookies = 1 +# ignore ICMP broadcast +net.ipv4.icmp_echo_ignore_broadcasts = 1 +# no source routing +net.ipv4.conf.all.accept_source_route = 0 +net.ipv6.conf.all.accept_source_route = 0 +# no ICMP redirects +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 +EOF +sysctl --system >/dev/null 2>&1 || true +ok "sysctl hardening written" + +# ── kernel module lock service ── +info "Enabling veilor-modules-lock.service" +systemctl enable veilor-modules-lock.service 2>/dev/null || \ + info "veilor-modules-lock.service not yet installed (overlay step)" + +# ── chrony with NTS ── +info "Configuring chrony with NTS" +[[ -f /etc/chrony.conf ]] && cp /etc/chrony.conf /etc/chrony.conf.bak.veilor 2>/dev/null +cat > /etc/chrony.conf << 'EOF' +# NTS-authenticated time sources +server time.cloudflare.com iburst nts +server nts.sth1.ntp.se iburst nts +server nts.sth2.ntp.se iburst nts + +# unauthenticated pool fallback +pool 2.fedora.pool.ntp.org iburst + +sourcedir /run/chrony-dhcp +ntsdumpdir /var/lib/chrony +driftfile /var/lib/chrony/drift +makestep 1.0 3 +rtcsync +leapseclist /usr/share/zoneinfo/leap-seconds.list +logdir /var/log/chrony +EOF +systemctl enable chronyd +ok "chrony NTS configured (Cloudflare + NETNOD)" + +# ── password complexity ── +info "Writing /etc/security/pwquality.conf" +cat > /etc/security/pwquality.conf << 'EOF' +minlen = 14 +dcredit = -1 +ucredit = -1 +lcredit = -1 +ocredit = -1 +minclass = 3 +maxrepeat = 3 +maxclassrepeat = 4 +difok = 3 +dictcheck = 1 +usercheck = 1 +enforce_for_root +retry = 3 +EOF +ok "pwquality: minlen=14, 4 classes required" + +# ── disable unneeded services ── +for svc in gssproxy atd pcscd.socket pcscd.service cups cups-browsed abrtd \ + abrt-journal-core abrt-xorg abrt-oops abrt-ccpp geoclue avahi-daemon \ + bluetooth ModemManager; do + systemctl disable --now "$svc" 2>/dev/null && ok "disabled $svc" || true +done + +# ── USBGuard ── +info "Setting up USBGuard" +rpm -q usbguard &>/dev/null || dnf install -y usbguard usbguard-tools + +# At install time no devices are connected — ship empty allowlist. +# First boot, admin runs: usbguard generate-policy > /etc/usbguard/rules.conf +mkdir -p /etc/usbguard +[[ -f /etc/usbguard/rules.conf ]] || : > /etc/usbguard/rules.conf +chmod 600 /etc/usbguard/rules.conf +chown root:root /etc/usbguard/rules.conf + +cat > /etc/usbguard/usbguard-daemon.conf << 'EOF' +ImplicitPolicyTarget=block +AuditBackend=LinuxAudit +IPCAllowedUsers=root +IPCAllowedGroups=wheel +RuleFile=/etc/usbguard/rules.conf +EOF +systemctl enable usbguard +ok "USBGuard configured (generate-policy on first boot to allowlist your devices)" + +# ── firewalld drop zone ── +info "Setting firewalld default zone to drop" +systemctl enable firewalld +firewall-offline-cmd --set-default-zone=drop 2>/dev/null || true +firewall-offline-cmd --zone=drop --add-service=ssh 2>/dev/null || true +ok "firewalld: default drop, ssh allowed" + +echo "════════════════════════════════════════════════════════" +echo " 20-harden-kernel complete" +echo "════════════════════════════════════════════════════════" diff --git a/scripts/firstboot.sh b/scripts/firstboot.sh new file mode 100755 index 0000000..a64a9ce --- /dev/null +++ b/scripts/firstboot.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# veilor-os — first user-login touch-ups (runs once via XDG autostart) +# Currently a placeholder for per-user setup that can't be done at install time. + +set -uo pipefail + +DONE=$HOME/.config/veilor-firstboot.done +[[ -f $DONE ]] && exit 0 + +# ── Generate USBGuard allowlist for currently connected devices ── +# Requires sudo; admin runs this manually: +# sudo usbguard generate-policy > /etc/usbguard/rules.conf +# sudo systemctl restart usbguard + +# ── Refresh font cache for user ── +fc-cache -f "$HOME/.local/share/fonts" 2>/dev/null || true + +mkdir -p "$(dirname "$DONE")" +touch "$DONE" diff --git a/scripts/kde-theme-apply.sh b/scripts/kde-theme-apply.sh new file mode 100755 index 0000000..245b5d1 --- /dev/null +++ b/scripts/kde-theme-apply.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# veilor-os — apply system-wide KDE theme + DuckSans font default +# Run during %post (chroot) or post-install. Idempotent. + +set -uo pipefail + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +info() { echo -e "${YELLOW}[INFO]${NC} $*"; } + +REPO="${VEILOR_REPO:-/usr/share/veilor-os}" + +# ── Install color scheme system-wide ── +info "Installing veilor-black color scheme" +install -d -m 0755 /usr/share/color-schemes +install -m 0644 "$REPO/assets/kde/veilor-black.colors" /usr/share/color-schemes/veilor-black.colors +ok "color scheme installed" + +# ── KDE system defaults ── +info "Setting system kdedefaults" +install -d -m 0755 /etc/xdg/kdedefaults +install -m 0644 "$REPO/assets/kde/veilor-default.kdeglobals" /etc/xdg/kdedefaults/kdeglobals +ok "kdedefaults written" + +# ── DuckSans fontconfig default ── +info "Setting DuckSans as default sans-serif" +install -d -m 0755 /etc/fonts/conf.d +cat > /etc/fonts/conf.d/55-veilor-ducksans.conf << 'EOF' + + + + + sans-serif + DuckSans + + + system-ui + DuckSans + + +EOF +fc-cache -f /usr/share/fonts/ducksans 2>/dev/null || true +ok "fontconfig: DuckSans = default sans-serif" + +# ── /etc/os-release branding ── +info "Branding /etc/os-release" +if [[ -f "$REPO/overlay/etc/os-release.d/veilor" ]]; then + install -m 0644 "$REPO/overlay/etc/os-release.d/veilor" /etc/os-release + ln -sf /etc/os-release /usr/lib/os-release 2>/dev/null || true + ok "os-release set to veilor-os" +fi + +# ── Plymouth theme (optional) ── +if [[ -d "$REPO/assets/plymouth/veilor" ]] && command -v plymouth-set-default-theme &>/dev/null; then + info "Installing plymouth theme" + install -d -m 0755 /usr/share/plymouth/themes/veilor + cp -r "$REPO/assets/plymouth/veilor/." /usr/share/plymouth/themes/veilor/ + plymouth-set-default-theme -R veilor 2>/dev/null || true + ok "plymouth theme set to veilor" +fi + +ok "kde-theme-apply complete" diff --git a/scripts/selinux/build-policy.sh b/scripts/selinux/build-policy.sh new file mode 100755 index 0000000..c3724de --- /dev/null +++ b/scripts/selinux/build-policy.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Build + load veilor-systemd SELinux policy module. +set -euo pipefail + +cd "$(dirname "$0")" + +checkmodule -M -m -o veilor-systemd.mod veilor-systemd.te +semodule_package -o veilor-systemd.pp -m veilor-systemd.mod +semodule -i veilor-systemd.pp +echo "[OK] veilor-systemd SELinux module loaded" diff --git a/scripts/selinux/veilor-systemd.te b/scripts/selinux/veilor-systemd.te new file mode 100644 index 0000000..f8d5b69 --- /dev/null +++ b/scripts/selinux/veilor-systemd.te @@ -0,0 +1,11 @@ +module veilor-systemd 1.0; + +require { + type systemd_modules_load_t; + class capability2 perfmon; + class capability sys_admin; +} + +#============= systemd_modules_load_t ============== +allow systemd_modules_load_t self:capability sys_admin; +allow systemd_modules_load_t self:capability2 perfmon; diff --git a/test/boot-checklist.md b/test/boot-checklist.md new file mode 100644 index 0000000..2621858 --- /dev/null +++ b/test/boot-checklist.md @@ -0,0 +1,115 @@ +# Spare-laptop validation checklist + +Run after installing a fresh veilor-os ISO. Each item should pass +before the build is considered green. + +## Install flow + +- [ ] Anaconda **only** prompts for LUKS passphrase — no account wizard, + no initial-setup screen +- [ ] Install completes without `%post` errors (check `/var/log/veilor-install.log`) +- [ ] Reboot succeeds, USB removed cleanly + +## First boot + +- [ ] LUKS prompt appears at boot +- [ ] TTY1 shows veilor-os banner + password prompt +- [ ] Password rejection on weak input (try `password123` — should fail) +- [ ] Password set succeeds with strong input +- [ ] SDDM starts after password set +- [ ] `admin@veilor-os` shell prompt visible after first login +- [ ] `veilor-firstboot.service` shows `inactive (dead)` and `disabled` + after first run + +## Identity + +- [ ] `passwd -S root` reports `L` (locked) +- [ ] `getent passwd | wc -l` shows base + admin only +- [ ] `id admin` shows `groups=...,wheel` + +## Branding + +- [ ] `hostnamectl` reports `veilor-os` +- [ ] `cat /etc/os-release` shows `NAME="veilor-os"` and `ID=veilor` +- [ ] `grep -ri onyx /etc /usr/local /usr/share/fonts` returns zero +- [ ] `grep -ri '192\.168\.0\.\|admin@gmail\|fedora\.local' /etc /usr/local` returns zero + +## Theme + +- [ ] KDE color scheme shows `veilor-black` in System Settings +- [ ] Konsole renders in DuckSans (`fc-match sans-serif` returns + `DuckSans` if the font was vendored) +- [ ] Background is pure black (#000000), not Breeze dark grey + +## Power + +- [ ] `veilor-power status` runs without sudo, shows current profile +- [ ] `veilor-power save` switches to `veilor-powersave` +- [ ] `veilor-power perf` switches to `veilor-performance` +- [ ] Unplugging AC auto-switches to `veilor-powersave` (udev rule) +- [ ] Plugging AC auto-switches to `veilor-performance` + +## Hardening — services + +- [ ] `systemctl is-active fail2ban` → active +- [ ] `systemctl is-active usbguard` → active +- [ ] `systemctl is-active auditd` → active +- [ ] `systemctl is-active firewalld` → active +- [ ] `systemctl is-active tuned` → active +- [ ] `systemctl is-active chronyd` → active +- [ ] `systemctl is-active sshd` → active +- [ ] `systemctl is-active cups` → inactive / not-found +- [ ] `systemctl is-active avahi-daemon` → inactive / not-found +- [ ] `systemctl is-active bluetooth` → inactive +- [ ] `systemctl is-active veilor-modules-lock` (after 30s) → active + +## Hardening — kernel/sysctl + +- [ ] `getenforce` → `Enforcing` +- [ ] `mokutil --sb-state` → `SecureBoot enabled` +- [ ] `sysctl kernel.yama.ptrace_scope` → `2` +- [ ] `sysctl kernel.kptr_restrict` → `2` +- [ ] `sysctl fs.suid_dumpable` → `0` +- [ ] `sysctl dev.tty.ldisc_autoload` → `0` +- [ ] `sysctl kernel.modules_disabled` (after 30s post graphical) → `1` + +## Hardening — network + +- [ ] `firewall-cmd --get-default-zone` → `drop` +- [ ] `firewall-cmd --zone=drop --list-services` → `ssh` +- [ ] `resolvectl status` shows DNSSEC + DoT, LLMNR off +- [ ] `chronyc sources -v` shows NTS-authenticated peers + +## Hardening — SSH + +- [ ] `sshd -T | grep -E 'permitrootlogin|passwordauth|allowusers|x11forwarding'` + shows: `permitrootlogin no`, `passwordauthentication no`, + `allowusers admin`, `x11forwarding no` + +## Disk + +- [ ] `lsblk -f` shows LUKS2 on the main partition +- [ ] `cryptsetup luksDump /dev/...` shows argon2id, aes-xts-plain64 +- [ ] `swapon` shows `zram` device, no disk swap + +## SELinux module + +- [ ] `semodule -l | grep veilor-systemd` → present +- [ ] No SELinux denials in `ausearch -m AVC -ts boot` related to + `systemd_modules_load_t` + +## USBGuard + +- [ ] `systemctl status usbguard` → active +- [ ] `wc -l /etc/usbguard/rules.conf` → 0 (empty allowlist by design) +- [ ] After `sudo usbguard generate-policy > /etc/usbguard/rules.conf` + and restart, all currently-connected USB devices remain + functional + +## Findings + +Log issues and fixes here: + +| Date | Item | Issue | Fix in kickstart? | +|------|------|-------|-------------------| +| | | | |