veilor-os v0.1 scaffold — kickstart + hardening + 3-mode power + DuckSans-ready KDE black theme

This commit is contained in:
veilor 2026-04-30 03:43:33 +01:00
commit 1822005df1
37 changed files with 1733 additions and 0 deletions

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
build/out/
build/cache/
*.iso
*.img
*.log
*.pp
*.mod
.DS_Store
.idea/
.vscode/
secrets/
*.key
*.pem

21
LICENSE Normal file
View file

@ -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.

49
README.md Normal file
View file

@ -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`.

View file

@ -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.

View file

@ -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

View file

@ -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

22
build/Containerfile Normal file
View file

@ -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"]

54
build/build-iso.sh Executable file
View file

@ -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)"

76
docs/BUILD.md Normal file
View file

@ -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).

128
docs/HARDENING.md Normal file
View file

@ -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 <id>
```
## 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`.

106
docs/INSTALL.md Normal file
View file

@ -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 ~1015 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 <id>
```
### 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 <name>
sudo passwd <name>
```
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.

71
docs/POWER.md Normal file
View file

@ -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-<profile>/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.

163
kickstart/veilor-os.ks Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 $@

View file

@ -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

View file

@ -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 $@

View file

@ -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

View file

@ -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 $@

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

175
scripts/10-harden-base.sh Executable file
View file

@ -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 "════════════════════════════════════════════════════════"

145
scripts/20-harden-kernel.sh Executable file
View file

@ -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 "════════════════════════════════════════════════════════"

19
scripts/firstboot.sh Executable file
View file

@ -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"

62
scripts/kde-theme-apply.sh Executable file
View file

@ -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'
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>sans-serif</family>
<prefer><family>DuckSans</family></prefer>
</alias>
<alias>
<family>system-ui</family>
<prefer><family>DuckSans</family></prefer>
</alias>
</fontconfig>
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"

10
scripts/selinux/build-policy.sh Executable file
View file

@ -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"

View file

@ -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;

115
test/boot-checklist.md Normal file
View file

@ -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? |
|------|------|-------|-------------------|
| | | | |