veilor-modules-lock sets kernel.modules_disabled=1 about 30s after graphical.target. Without uas already loaded, hot-plugged USB-SATA bridges (ASMedia / JMicron / Realtek) that advertise both BBB and UAS alt-settings fail to bind — the kernel prefers uas, usb-storage stands down, and modprobe uas is denied by the lock. Add /etc/modules-load.d/veilor-storage.conf via the overlay so systemd-modules-load.service preloads uas + usb-storage at boot, before the lock engages. Document the rationale and a runtime quirks workaround in docs/HARDENING.md. Incident: 2026-05-13, onyx, SK Hynix SC311 in ASMT105x (174c:55aa).
8.6 KiB
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-systemdgrantssystemd_modules_load_tthesys_adminandperfmoncapabilities required by the modules-lock service. Source:scripts/selinux/veilor-systemd.te.
veilor-firstboot SELinux confinement
The first-boot password service is privileged (it has to write
/etc/shadow) but small. Module veilor-firstboot carves a tight domain:
- Allowed: read
/etc/passwd, execpasswd(1), write/var/lib/veilor-firstboot.done, write/etc/sddm.conf.d/, startsddm.service. neverallowrules block: network sockets (no phone-home),home_root_t/user_home_taccess,sys_module,sys_ptrace,sys_rawio.
Source: scripts/selinux/veilor-firstboot.te. Build & load with
scripts/selinux/build-policy.sh (loads all modules in one pass).
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.comandnts.sth1/2.ntp.se. Pool fallback only.
SSH
/etc/ssh/sshd_config.d/10-veilor-hardening.conf:
PasswordAuthentication noPermitRootLogin noAllowUsers adminX11Forwarding noMaxAuthTries 3ClientAliveInterval 300LogLevel VERBOSE
Auth / accounts
- Root account locked (
passwd -l root). No interactive root login. - Single
adminuser,wheelgroup, full sudo. pwquality.conf: minlen=14, 4 character classes required, dictcheck.- First-boot password flow:
chage -d 0 adminexpires the empty password immediately.veilor-firstboot.serviceruns 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 banpam-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:
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:
sudo usbguard list-devices
sudo usbguard allow-device <id>
Removable storage (UAS preload)
veilor-modules-lock.service flips kernel.modules_disabled=1 30s after
graphical boot. Any kernel module not loaded by then is permanently blocked
for the session. usb-storage ships built into the kernel and auto-loads,
but uas (USB Attached SCSI) is a separate module that the kernel only
loads on demand when a UAS-capable USB-SATA bridge appears at boot. If no
such device is present at boot, uas never loads, the lock engages, and
hot-plugging an ASMedia / JMicron / Realtek UAS enclosure later fails — the
bridge's interface descriptor advertises both BBB and UAS alt-settings,
the kernel prefers UAS, and usb-storage stands down expecting uas to
claim. Result: device authorizes in USBGuard but no sd* node appears.
Fix shipped in overlay: /etc/modules-load.d/veilor-storage.conf lists
uas + usb-storage for systemd-modules-load.service to preload at
boot, before the modules lock engages.
To verify after install:
lsmod | grep -E '^uas|^usb_storage' # both should show
cat /proc/sys/kernel/modules_disabled # 1 after 30s post-graphical
If a future enclosure still fails to bind, the runtime workaround (no
reboot) is to force usb-storage to claim by quirking UAS off for that
vendor:product:
echo "<vid>:<pid>:u" | sudo tee /sys/module/usb_storage/parameters/quirks
sudo bash -c 'echo 0 > /sys/bus/usb/devices/<dev>/authorized; sleep 2; echo 1 > /sys/bus/usb/devices/<dev>/authorized'
Persistent quirk for known-bad enclosures: add
usb-storage.quirks=<vid>:<pid>:u to the kernel cmdline.
Disabled services
abrt*, cups, cups-browsed, geoclue, avahi-daemon,
bluetooth, ModemManager, gssproxy, atd, pcscd.socket,
pcscd.service, kdeconnectd (removed at package level).
AppArmor (v0.5)
Fedora 43 ships AppArmor alongside SELinux. veilor-os keeps SELinux as the primary MAC layer (enforcing, targeted) but ships AppArmor profile skeletons for high-risk userland binaries that benefit from a second, binary-scoped policy on top of SELinux's role-based one.
Profiles live in scripts/apparmor/:
| Profile | Target | Default mode |
|---|---|---|
usr.bin.thorium |
Thorium browser | complain |
usr.local.bin.lm-studio |
LM Studio LLM runner | complain |
usr.bin.veilor-power |
Power profile switcher | enforce |
Profiles are not loaded automatically — they are opt-in until v0.5. Enable a profile post-install with:
sudo dnf install apparmor-utils apparmor-parser
sudo install -m 0644 scripts/apparmor/usr.bin.thorium /etc/apparmor.d/
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.thorium
sudo aa-complain /etc/apparmor.d/usr.bin.thorium # log only
sudo aa-enforce /etc/apparmor.d/usr.bin.thorium # block
Refine complain-mode profiles with aa-logprof after exercising the
app through normal use; it converts logged denials into rule additions
interactively.
Audit log shipping (optional)
Local journald is the default audit sink. For off-device shipping to a trusted log collector (Loki / Wazuh / Splunk), veilor-os ships a disabled-by-default plugin template:
/etc/audit/plugins.d/veilor-remote.conf— auditd plugin shim (setactive = yesto enable)./etc/audisp/audisp-remote.conf.disabled— audisp-remote target config template (rename toaudisp-remote.confand editremote_serverto enable).
Warning: enabling remote audit shipping leaks every privileged syscall, file-watch hit, and auth event off-device. Treat the collector as a host with the same trust level as root. Only enable if the collector itself is hardened and the transport is TLS or kerberized.
Reference integration paths in the template: Loki via promtail/vector syslog source, Wazuh via local wazuh-agent (no network shipping needed), Splunk via HEC bridge.
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.