Handbook
Overview
krypt/sh is a source-based Linux distribution built on a pure LLVM/musl
toolchain. The entire system is compiled with clang/clang++
against musl libc — there is no GCC and no glibc anywhere in the base system.
Requirements
Architecture: x86_64 only.
Firmware: UEFI only — legacy BIOS is not supported.
RAM: 16 GB recommended for building packages from source.
Disk: 20 GB minimum for a base system. More for a full ports tree with sources.
Toolchain
The core toolchain consists of LLVM (clang, lld, llvm-tools),
musl (libc), compiler-rt (builtins and sanitizers),
libc++ (C++ standard library), and libunwind
(stack unwinding). GCC compatibility shims are provided where needed —
libgcc_s.so symlinks to libunwind and libstdc++.so
symlinks to libc++ — so software expecting GNU toolchain components works
without modification.
LLVM GPU targets
The llvm-toolchain port is built with
AMDGPU as the default GPU backend target. This is
required by Mesa for AMD Radeon shader compilation (radeonsi, radv)
and by the LLVM-based software rasterizer (llvmpipe). If your
system uses different hardware, you can change the targets and
rebuild:
# cd /ports/core/llvm-toolchain
# vi MAKEPKG
# Find the -DLLVM_TARGETS_TO_BUILD line and adjust:
# AMDGPU — AMD Radeon (required for radeonsi/radv)
# NVPTX — NVIDIA (compute/CUDA)
# X86 — always required
# Rebuild and upgrade
# mkpkg -F
# pkg.add -u llvm-toolchain#*.pkg.tar.gz
# Then rebuild Mesa to pick up the new backend
# cd /usr/ports/xorg/mesa
# mkpkg -F
# pkg.add -u mesa#*.pkg.tar.gz
AMDGPU
entirely to reduce the LLVM build time and binary size.
Installation model
krypt/sh uses a hybrid installation model. During initial
installation, pre-built binary packages are fetched over the network and
installed with pkg.add. Once the system is running, you
transition to source-based updates and customization through the
ports system — syncing port trees, building packages with
mkpkg, and managing dependencies with tux.
Getting help
Join #krypt/sh on Libera.Chat for support, questions, and discussion.
Filesystem Layout
krypt/sh uses a flat library layout — there is no
lib32, lib64, or multilib. Libraries live in
/lib and /usr/lib, period. This reflects the
single-arch, single-libc nature of the system.
The filesystem splits into two install prefixes. The ports tree mirrors this split directly:
The rule is simple: packages in /ports/core install into the
root prefix (/bin, /sbin, /lib).
Packages in /usr/ports/* install into the /usr
prefix (/usr/bin, /usr/lib). This is set by
--prefix=/ vs --prefix=/usr in each port's
build function.
The setup-filesystem script creates this entire skeleton,
including /var/lib/pkg/db (the empty package database),
default /etc/passwd, /etc/group,
/etc/profile, and other base configuration files.
It must be run before any packages are installed.
XDG runtime directories
krypt/sh sets up XDG base directories through two scripts rather than relying on elogind, dumb_runtime_dir, or session managers. This means Wayland compositors work out of the box with no extra session infrastructure.
/etc/profile.d/xdg.sh exports the environment variables
on login:
export XDG_DATA_HOME="$HOME/.local/share"
export XDG_CONFIG_HOME="$HOME/.config"
export XDG_CACHE_HOME="$HOME/.cache"
export XDG_RUNTIME_DIR=/run/user/$(id -u)
/etc/boot.d/xdg-runtime creates the runtime directories
at boot for each user:
# Create XDG_RUNTIME_DIR for root and regular users
for user in root $(awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd); do
uid=$(id -u "$user")
dir=/run/user/$uid
mkdir -p "$dir"
chmod 0700 "$dir"
chown "$user":"$user" "$dir"
done
No PAM, no elogind
krypt/sh does not use Linux-PAM or
elogind. Authentication is handled by traditional
shadow utilities. Session setup — XDG runtime directories, seat
management via seatd — is done with minimal,
purpose-built scripts that interface directly with the system
rather than through abstraction layers.
Installation
krypt/sh ships as a UEFI-bootable disk image
(disk.img). Write it to a USB drive and boot it — you get a
live shell with networking, package tools, and everything needed to install
the system. Type setup-help at any time for a quick reference.
/etc/fstab is correct.
Write the image
Download from krypt.sh/download. For help, join #krypt/sh on Libera.Chat.
$ sha256sum disk.img
73457cf67bac1b7012eb0f2d67170e4157b7a6d7e17169cdbbb1c984003c4dba disk.img
# write to USB
$ dd if=disk.img of=/dev/sdX bs=4M status=progress
Boot the USB drive from your UEFI firmware. You will land at a root shell.
Network
# WiFi (built-in Ralink/Realtek)
# wpa_passphrase "SSID" "password" > /tmp/wpa.conf
# wpa_supplicant -B -i wlan0 -c /tmp/wpa.conf
# dhcpcd wlan0
Partition and mount
# Create a GPT label, an EFI system partition, and a root partition
# mkfs.fat -F32 /dev/sdX1
# mkfs.ext4 /dev/sdX2
# mount /dev/sdX2 /mnt
# mkdir -p /mnt/boot/efi
# mount /dev/sdX1 /mnt/boot/efi
Install packages
# setup-filesystem /mnt
# Install core system + build toolchain
# install --target /mnt core devel
The install script fetches pre-built binary packages from
rsync://krypt.sh/bulk and installs them with
pkg.add.
core — the base system (~57 packages):
devel — LLVM toolchain and build tools:
Generate fstab and chroot
# cat /mnt/etc/fstab
# enter-chroot /mnt
Configure the system
# echo myhostname > /etc/hostname
# ln -sf /usr/share/zoneinfo/Region/City /etc/localtime
Boot with EFISTUB
krypt/sh boots the kernel directly via UEFI — no bootloader required. Copy the kernel to the EFI system partition and create a boot entry:
# efibootmgr -c -d /dev/sdX -p 1 \
-L 'krypt/sh X.Y.Z' \
-l '\vmlinuz-X.Y.Z.efi' \
-u 'root=/dev/sdXn rw quiet net.ifnames=0'
/etc/fstab is correct, grub-mkconfig or
os-prober from the host system can detect and boot it.
Package Management
krypt/sh has three layers of package management: low-level tools
(pkg.add, pkg.del, pkg.info) that
operate on binary archives and the package database, the build tool
(mkpkg) that compiles ports into packages, and the
high-level manager (tux) that resolves
dependencies and orchestrates everything.
Binary package tools
| Command | Description |
|---|---|
| pkg.add pkg.tar.gz | Install a binary package archive |
| pkg.add -u pkg.tar.gz | Upgrade an installed package |
| pkg.del name | Remove an installed package |
| pkg.info -i | List all installed packages |
| pkg.info -l name | List files owned by a package |
| pkg.info -o pattern | Find which package owns a file |
Configuration files under /etc are preserved on upgrade by
default. See pkg.add.conf(5) for details.
The tux package manager
| Command | Description |
|---|---|
| tux install pkg | Install a package and all dependencies |
| tux update pkg | Update a package to the ports tree version |
| tux remove pkg | Remove an installed package |
| tux sysup | Update all outdated packages |
| tux diff | Show version differences (installed vs ports) |
| tux depends pkg | Show the dependency tree |
| tux rdepends pkg | Show reverse dependencies |
| tux autoremove | Remove orphaned dependency packages |
| tux lock pkg | Prevent a package from being updated |
tux searches port directories in the order defined in
/etc/tux.conf. When multiple ports provide the same
package name, the last match wins. See tux(8)
and tux.conf(5).
Typical workflow
# ports -u
# See what's outdated
# tux diff
# Update everything
# tux sysup
# Install something new
# tux install firefox
# Clean up
# tux autoremove
Reverse dependency checking (revdep)
When a shared library is updated, binaries linked against the old
version will break — the dynamic linker cannot find the previous
.so name. The revdep tool scans every
installed binary and reports packages with broken library links.
Here is a real-world example: upgrading libvpx and
nettle bumps their soname versions. Any package linked
against the old libraries will fail to start:
XPCOMGlueLoad error for file /usr/lib/firefox/libxul.so:
Error loading shared library libvpx.so.11: No such file or directory
Couldn't load XPCOM.
Instead of guessing which packages are affected, run
revdep to find them all:
# pkg.add -u libvpx#1.15.1-1.pkg.tar.gz
# pkg.add -u nettle#3.10.1-1.pkg.tar.gz
# 2. Find every package with broken links
# revdep
firefox
thunderbird
ffmpeg
mpv
rdfind
# 3. Force-rebuild and upgrade each one
# cd /usr/ports/opt/firefox && mkpkg -F && pkg.add -u firefox#*.pkg.tar.gz
# cd /usr/ports/opt/thunderbird && mkpkg -F && pkg.add -u thunderbird#*.pkg.tar.gz
# cd /usr/ports/opt/ffmpeg && mkpkg -F && pkg.add -u ffmpeg#*.pkg.tar.gz
# cd /usr/ports/opt/mpv && mkpkg -F && pkg.add -u mpv#*.pkg.tar.gz
# cd /usr/ports/opt/rdfind && mkpkg -F && pkg.add -u rdfind#*.pkg.tar.gz
# 4. Verify — should produce no output
# revdep
Note the use of mkpkg -F (force) rather than
mkpkg -u — the package version has not changed, only
its library dependencies have. Without -F, mkpkg would
see the same version string and skip the build.
/lib. A single library bump can silently break
dozens of packages — revdep catches them all in one pass.
Building Ports
Every package in krypt/sh is defined by a MAKEPKG file — a
shell-like recipe that describes how to fetch, build, and package software.
See MAKEPKG(5) for the full specification.
Syncing ports
# ports -u
# Sync a specific collection
# ports -u core
# List all available ports
# ports -l
Port collections are configured via /etc/ports/*.rsync files
and synced from rsync.krypt.sh. See
ports(8).
Building a package
# cd /usr/ports/opt/nftables
# Install build dependencies
# tux prepare
# Download and build
# mkpkg -d
# Build and install directly
# mkpkg -d -i
# Build and upgrade an existing package
# mkpkg -d -u
MAKEPKG anatomy
# *** krypt/sh ***
# description: Shows the full path of shell commands
# url: https://www.gnu.org/software/which/
name=which
version=2.21
release=1
depends=()
source=(https://ftp.gnu.org/gnu/$name/$name-$version.tar.gz)
sha256sums=(abc123...)
build() {
cd $name-$version
./configure --prefix=/
make
make DESTDIR=$PKG install
}
Key arrays: depends=() for runtime dependencies,
makedeps=() for build-time dependencies,
groups=() for declarative user/group creation,
services=() for runit service enablement, and
permissions=() for file ownership. Note:
krypt/sh uses makedeps=(), not makedepends=().
Build configuration
Compiler flags and build settings live in /etc/mkpkg.conf.
Only the shell(), build(), and
post_build() hooks source this file — extract()
and patch() do not need build flags so they skip it.
See mkpkg.conf(5).
Services & Init
krypt/sh uses runit as its init system with a vanilla
setup. The supervised service directory is /service —
not /var/service like some other distributions.
Service definitions live in /etc/sv/ and are activated
by symlinking into /service/.
That said, krypt/sh is not bound to runit. The ports tree ships it as the default, but you are free to build and use the init system of your choice — the system has no hard dependency on any specific init.
Managing services
# ln -s /etc/sv/dhcpcd /service/
# Check status
# sv status dhcpcd
# Stop / start / restart
# sv stop dhcpcd
# sv start dhcpcd
# sv restart dhcpcd
# Disable a service
# rm /service/dhcpcd
# List enabled services
# ls /service/
# List available services
# ls /etc/sv/
Service file structure
Each service directory under /etc/sv/ contains a
run script and optionally a log/run
for logging via svlogd. Packages that ship runit
services declare them in the services=() array of
their MAKEPKG file, and pkg.add creates the
/service symlink automatically.
Example: simple daemon
A typical service run script for a daemon like dhcpcd:
exec 2>&1
exec dhcpcd -B -M
The corresponding log script sends output to svlogd:
mkdir -p /var/log/dhcpcd
exec svlogd -tt /var/log/dhcpcd
Example: nftables (one-shot)
Services that load configuration and exit (like a firewall) use a
finish script and sv once:
exec 2>&1
nft -f /etc/nftables.conf
exec pause
Example: seatd with cleanup
Services that create runtime files can use a finish script for cleanup:
exec 2>&1
exec seatd -g seat
rm -f /run/seatd.sock
run, finish, and
log/run scripts must be executable (chmod 755).
Daemons should run in the foreground — runit supervises the process directly.
Use exec so the daemon replaces the shell process.
Kernel
The kernel is installed as part of the linux group during
installation. The kernel image lands at /boot/vmlinuz-X.Y.Z
and modules go into /lib/modules/X.Y.Z.
Updating the kernel
After building or installing a new kernel version, copy it to the EFI system partition and create a new boot entry:
# efibootmgr -c -d /dev/sdX -p 1 \
-L 'krypt/sh X.Y.Z' \
-l '\vmlinuz-X.Y.Z.efi' \
-u 'root=/dev/sdXn rw quiet net.ifnames=0'
# Optionally remove old entries
# efibootmgr
# efibootmgr -b XXXX -B
Since krypt/sh uses EFISTUB directly, each kernel version gets its own EFI boot entry. You can keep multiple versions and select between them from the UEFI firmware boot menu — no bootloader configuration needed.
Building a custom kernel
krypt/sh ships a default kernel via the linux port, but
building a custom kernel is straightforward. Download the sources,
copy the running config as a starting point, and build with LLVM:
# tar xf linux-X.Y.Z.tar.xz
# cd linux-X.Y.Z
# Start from the running config
# zcat /proc/config.gz > .config
# make LLVM=1 olddefconfig
# Customize
# make LLVM=1 menuconfig
# Build
# make LLVM=1 -j$(nproc)
# make LLVM=1 modules_install
# cp arch/x86/boot/bzImage /boot/vmlinuz-X.Y.Z
clang, lld, and LLVM tools for the entire
build. This is the only supported method on krypt/sh since GCC is
not available.
Loading modules
krypt/sh boots without an initramfs — there is no dracut,
mkinitramfs, or initial ramdisk of any kind. The kernel
mounts the root filesystem directly. This means any kernel modules
your hardware needs at boot must either be compiled into the kernel
(=y) or loaded early from
/etc/rc.modules.
The /etc/rc.modules script is executed at boot and is
the place to modprobe drivers that are built as
modules (=m) rather than built-in. A typical setup:
modprobe iwlmvm
modprobe iwlwifi
modprobe amdgpu
chmod +x /etc/rc.modules.
If you add new hardware or enable new module-only drivers, add the
corresponding modprobe line here.
Since there is no initramfs, drivers that are critical for reaching
the root filesystem — your storage controller, filesystem driver,
and root disk encryption if used — must be built
into the kernel with =y, not as modules. If they are
only modules, the kernel cannot mount root and will panic. A safe
rule of thumb:
| Driver type | Recommendation |
|---|---|
| NVMe / AHCI / SCSI | Built-in (=y) — needed to mount root |
| ext4 / XFS / btrfs | Built-in (=y) — your root filesystem |
| dm-crypt / LUKS | Built-in (=y) — if using encryption |
| GPU (amdgpu, i915) | Module (=m) — loaded via rc.modules |
| WiFi (iwlwifi, ath) | Module (=m) — loaded via rc.modules |
| Bluetooth | Module (=m) — loaded via rc.modules |
| Sound (snd-hda) | Module (=m) — loaded via rc.modules |
| USB HID / input | Built-in (=y) — keyboard/mouse at boot |
To check what modules are currently loaded on a running system, use
lsmod. To see which module handles a specific device,
use lspci -k or lsusb -t.
EFISTUB configuration
krypt/sh boots the kernel directly from UEFI firmware without a bootloader. The following options must be enabled for EFISTUB to work:
CONFIG_EFI=y
CONFIG_EFI_STUB=y
CONFIG_EFI_MIXED=y
# EFI filesystem access
CONFIG_EFIVAR_FS=y
CONFIG_EFI_VARS=y
# EFI framebuffer (console before GPU driver loads)
CONFIG_FB_EFI=y
CONFIG_FRAMEBUFFER_CONSOLE=y
# GPT partition table support
CONFIG_EFI_PARTITION=y
Without CONFIG_EFI_STUB=y, the kernel image cannot be
loaded directly by the UEFI firmware and you would need a bootloader
like GRUB or systemd-boot. The EFI_MIXED option allows
a 64-bit kernel to boot on 32-bit EFI firmware, which some older
machines have.
Security & sandboxing
These options enable the kernel features that sandboxing tools, browsers (Firefox, Chromium), and container runtimes depend on. They are strongly recommended for any system running untrusted code:
CONFIG_SECCOMP=y
CONFIG_SECCOMP_FILTER=y
# User namespaces — unprivileged sandboxing (Flatpak, Chromium)
CONFIG_USER_NS=y
# PID, network, mount namespaces — process isolation
CONFIG_PID_NS=y
CONFIG_NET_NS=y
CONFIG_UTS_NS=y
CONFIG_IPC_NS=y
# Landlock — unprivileged filesystem sandboxing (kernel 5.13+)
CONFIG_SECURITY_LANDLOCK=y
# Yama — ptrace scope restrictions
CONFIG_SECURITY_YAMA=y
# Kernel hardening
CONFIG_RANDOMIZE_BASE=y
CONFIG_RANDOMIZE_MEMORY=y
CONFIG_STACKPROTECTOR=y
CONFIG_STACKPROTECTOR_STRONG=y
CONFIG_HARDENED_USERCOPY=y
CONFIG_FORTIFY_SOURCE=y
CONFIG_SECCOMP_FILTER=y is missing. This is the single
most important security option on a desktop system.
Cgroups & containers
If you plan to run containers, lightweight VMs, or resource-limited services, enable cgroups v2 and the relevant controllers:
CONFIG_CGROUPS=y
CONFIG_CGROUP_V2=y
# Resource controllers
CONFIG_MEMCG=y
CONFIG_CGROUP_PIDS=y
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_BPF=y
# Overlay filesystem (container image layers)
CONFIG_OVERLAY_FS=y
# Virtual ethernet pairs (container networking)
CONFIG_VETH=y
CONFIG_BRIDGE=y
CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y
To boot with cgroups v2 as default, add
cgroup_no_v1=all systemd.unified_cgroup_hierarchy=1
to your kernel command line in the efibootmgr -u
parameters. Since krypt/sh uses runit (not systemd), only the
cgroup_no_v1=all part is strictly needed.
Recommended options
Additional options that are good to have on a general-purpose krypt/sh system:
CONFIG_IKCONFIG=y
CONFIG_IKCONFIG_PROC=y
# tmpfs — used for /tmp, /run, build directories
CONFIG_TMPFS=y
CONFIG_TMPFS_POSIX_ACL=y
CONFIG_TMPFS_XATTR=y
# Devtmpfs — automatic /dev population
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y
# inotify — file watchers (build tools, editors, browsers)
CONFIG_INOTIFY_USER=y
# Multiqueue block IO — better SSD performance
CONFIG_BLK_MQ_VIRTIO=y
CONFIG_BLK_DEV_NVME=y
# Kernel module compression (saves /lib/modules space)
CONFIG_MODULE_COMPRESS_ZSTD=y
# Magic SysRq — emergency debugging keys
CONFIG_MAGIC_SYSRQ=y
make LLVM=1 olddefconfig to resolve any dependency
issues — some options automatically enable prerequisites.
Use scripts/diffconfig to see changes between two
.config files.
Networking
krypt/sh uses simple, traditional networking tools — no NetworkManager,
no systemd-networkd. Network interfaces are configured through
dhcpcd for DHCP, manual ip commands or
boot scripts for static addressing, and wpa_supplicant
for WiFi. All network services run under runit supervision.
Static IP
For a static IP configuration, create a boot script that brings up
the interface. This runs at boot via the /etc/boot.d/
mechanism:
ip addr add 192.168.1.100/24 dev eth0
ip link set eth0 up
ip route add default via 192.168.1.1
Make it executable with chmod +x /etc/boot.d/network.
Note that krypt/sh uses net.ifnames=0 on the kernel
command line by default, so interfaces use classic names like
eth0 and wlan0 rather than predictable
names like enp3s0.
DHCP
dhcpcd handles DHCP for both wired and wireless
interfaces. Enable it as a runit service:
# ln -s /etc/sv/dhcpcd /service/
# Check it's running
# sv status dhcpcd
# Or run manually for a specific interface
# dhcpcd eth0
By default, dhcpcd will configure all available
interfaces. To restrict it to a specific interface, edit
/etc/dhcpcd.conf and add
allowinterfaces eth0.
WiFi (wpa_supplicant)
WiFi is handled by wpa_supplicant. For a persistent
setup that survives reboots, create a configuration file and a
runit service.
First, generate the configuration:
# wpa_passphrase "MyNetwork" "MyPassword" > /etc/wpa_supplicant.conf
# Verify it looks right
# cat /etc/wpa_supplicant.conf
Then create a runit service to start it at boot:
exec 2>&1
exec wpa_supplicant -i wlan0 -c /etc/wpa_supplicant.conf
# ln -s /etc/sv/wpa_supplicant /service/
With both wpa_supplicant and dhcpcd
running as services, WiFi will connect and obtain an address
automatically at boot. For multiple networks, add additional
network={} blocks to
/etc/wpa_supplicant.conf —
wpa_supplicant will choose the best available one.
/lib/firmware. The linux-firmware
package provides these. Intel chips (iwlwifi) also need the driver
loaded via /etc/rc.modules — see the
Loading modules section.
DNS resolution
DNS is configured in /etc/resolv.conf. When using
dhcpcd, this file is updated automatically. For
static setups, set it manually:
nameserver 9.9.9.9
If dhcpcd overwrites your manual settings, you can
prevent this by adding nohook resolv.conf to
/etc/dhcpcd.conf, or simply make the file immutable
with chattr +i /etc/resolv.conf.
Firewall (nftables)
krypt/sh uses nftables for firewalling — not
iptables. A sensible default configuration that blocks incoming
connections while allowing outbound traffic:
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow established/related connections
ct state established,related accept
# Allow loopback
iif lo accept
# Allow ICMP (ping)
meta l4proto icmp accept
# Allow SSH
tcp dport 22 accept
# Drop everything else
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
The nftables service loads this configuration at boot as a one-shot service (see Services & Init):
# ln -s /etc/sv/nftables /service/
# Verify rules are loaded
# nft list ruleset
# Reload after editing nftables.conf
# nft -f /etc/nftables.conf
tcp dport { 80, 443 } accept to the input chain.
For more complex setups, nftables supports sets, maps, rate
limiting, and logging — see man nft.
Users & Permissions
krypt/sh does not use Linux-PAM. User management is handled by
traditional shadow utilities — useradd,
usermod, userdel, passwd,
and groupadd. Authentication goes through
/etc/shadow directly.
Adding users
Create a regular user with a home directory and login shell:
# useradd -m -s /bin/bash username
# Set password
# passwd username
# Add to supplementary groups
# usermod -aG wheel,audio,video,seat username
# Verify
# id username
The -m flag creates /home/username.
The XDG runtime directory at /run/user/UID is
created at boot by the /etc/boot.d/xdg-runtime
script for any user with a UID ≥ 1000 (see
Filesystem Layout).
Groups & privileges
Group membership controls hardware and service access. The important groups on an krypt/sh system:
| Group | Purpose |
|---|---|
| wheel | Can use su / doas for root access |
| audio | Direct access to ALSA sound devices |
| video | Direct access to GPU and framebuffer devices |
| seat | Access to seatd for Wayland compositors |
| input | Access to /dev/input devices |
| kvm | Access to /dev/kvm for virtualization |
| disk | Raw disk device access (use with caution) |
Packages that need system groups declare them in the
groups=() array of their MAKEPKG file.
pkg.add creates the groups automatically during
installation.
Privilege escalation
Without PAM, sudo does not work out of the box.
krypt/sh provides two alternatives:
su — the traditional tool. Members of the
wheel group can switch to root:
$ su -
# Run a single command as root
$ su -c 'tux sysup'
doas — a minimal sudo replacement from OpenBSD. Install it from the ports tree and create a configuration:
# Allow wheel group members to run commands as root
# echo 'permit :wheel' > /etc/doas.conf
# Optionally persist authentication for 5 minutes
# echo 'permit persist :wheel' > /etc/doas.conf
# Set restrictive permissions
# chmod 600 /etc/doas.conf
sudo
package has a hard dependency on PAM for session and
authentication handling. Since krypt/sh does not ship PAM,
doas is the recommended alternative — it works
directly with /etc/shadow and has a much smaller
attack surface.
Login shell & environment
The default login shell is set per-user in /etc/passwd
and can be changed with usermod -s /bin/bash username.
Environment setup happens through /etc/profile and
drop-in scripts in /etc/profile.d/.
krypt/sh ships two profile scripts by default:
/etc/profile.d/xdg.sh (XDG base directories) and
/etc/profile.d/path.sh (PATH additions). You can
add your own by dropping executable .sh files in
/etc/profile.d/ — they are sourced in alphabetical
order on login.
Graphics & Wayland
krypt/sh is a pure Wayland system — X11 is not
part of the core. The graphics stack consists of kernel DRM
drivers, Mesa for userspace OpenGL/Vulkan, seatd
for seat management, and a Wayland compositor of your choice.
The graphics stack
The layers involved in getting pixels on screen:
| Layer | Component |
|---|---|
| Kernel | DRM driver (amdgpu, i915, nouveau) |
| Userspace driver | Mesa (radeonsi, iris, nouveau, llvmpipe) |
| Seat management | seatd — grants GPU/input access |
| Wayland protocol | wayland, wayland-protocols |
| Compositor | labwc, sway, or other wlroots-based |
| Toolkit | GTK4, Qt6 (with Wayland backend) |
There is no display manager — you launch the compositor directly from a TTY login. The compositor handles window management, input, and output all in one process.
Seat management (seatd)
seatd is a minimal seat management daemon that
replaces elogind and systemd-logind.
It grants unprivileged users access to GPU, input, and DRM
devices through the seat group.
# usermod -aG seat username
# Enable the seatd service
# ln -s /etc/sv/seatd /service/
# Verify it's running
# sv status seatd
The seatd runit service runs with
-g seat, restricting access to members of the
seat group. The socket at
/run/seatd.sock is cleaned up by the service's
finish script.
Failed to connect to seat or
libseat: Could not connect, check that seatd is
active and your user is in the seat group.
GPU drivers
GPU drivers are kernel modules loaded via
/etc/rc.modules (see
Loading modules). Mesa provides
the userspace OpenGL and Vulkan implementations:
| GPU | Kernel module | Mesa driver |
|---|---|---|
| AMD Radeon (GCN+) | amdgpu | radeonsi (GL), radv (Vulkan) |
| Intel (Gen 8+) | i915 | iris (GL), anv (Vulkan) |
| Intel (Arc) | xe / i915 | iris (GL), anv (Vulkan) |
| NVIDIA (open) | nouveau | nouveau (GL, limited) |
| Software | — | llvmpipe (CPU fallback) |
Add the appropriate modprobe line to
/etc/rc.modules. AMD and Intel GPUs work well with
the open-source stack. For NVIDIA, the proprietary driver is not
available on musl — nouveau provides basic support but lacks
reclocking on most cards.
/sys/bus/pci/devices/.../power/control.
Compositors (labwc / sway)
krypt/sh ships labwc and sway in the ports tree — both are wlroots-based Wayland compositors. Launch them directly from a TTY after logging in:
$ labwc
# Option B: sway (tiling compositor, i3-like)
$ sway
labwc is a stacking window manager similar to
Openbox. Configuration lives in
~/.config/labwc/ with files for
rc.xml (keybinds, window rules),
menu.xml (right-click menu), and
autostart (programs to launch). It is a good choice
if you prefer a traditional floating window experience.
sway is an i3-compatible tiling compositor.
Configuration lives in ~/.config/sway/config. If
you are familiar with i3, the transition is seamless — most
configuration directives work identically.
Both compositors need the XDG_RUNTIME_DIR
environment variable set and seatd running. If
you followed the installation guide, both are already in place.
~/.profile or ~/.bash_profile:
[ "$(tty)" = "/dev/tty1" ] && exec labwc
Troubleshooting
Common graphics issues and how to diagnose them:
| Symptom | Cause / fix |
|---|---|
| Permission denied on /dev/dri | User not in video and seat groups |
| Failed to connect to seat | seatd not running — check sv status seatd |
| No XDG_RUNTIME_DIR | Missing /etc/profile.d/xdg.sh or /etc/boot.d/xdg-runtime |
| Black screen on launch | GPU module not loaded — check /etc/rc.modules |
| Software rendering only | Mesa not finding GPU — run LIBGL_DEBUG=verbose glxinfo |
| Compositor crashes on exit | Normal if seatd.sock isn't cleaned — the finish script handles it |
For debugging, run the compositor with verbose output:
WLR_RENDERER=vulkan sway or
WAYLAND_DEBUG=1 labwc. Check
dmesg for DRM/GPU errors and
lsmod to confirm the right driver is loaded.
Encryption (LUKS2)
krypt/sh uses LUKS2 via cryptsetup for
full disk encryption. The stack is: cryptsetup →
device-mapper → argon2 (key derivation).
No initramfs — the kernel must have dm-crypt built in.
Setting up LUKS2
# cryptsetup luksFormat --type luks2 --pbkdf argon2id /dev/nvme0n1p7
# Open the container
# cryptsetup open /dev/nvme0n1p7 cryptroot
# Format the plaintext device
# mkfs.ext4 /dev/mapper/cryptroot
# Mount and install as usual
# mount /dev/mapper/cryptroot /mnt
Opening at boot
Without an initramfs, LUKS volumes must be opened by a boot script before the root filesystem is mounted — or, for non-root volumes, by a runit service early in the boot sequence.
cryptsetup open /dev/nvme0n1p7 whonix-gw
cryptsetup open /dev/nvme0n1p8 whonix-ws
cryptsetup open /dev/nvme0n1p9 fedora
CONFIG_DM_CRYPT=y built in — not as a module.
If dm-crypt is a module (=m), the kernel cannot open
the LUKS container before mounting root and will panic.
The example above prompts for a passphrase for each volume
separately. For systems with many encrypted partitions there is a
cleaner pattern: dedicate one small partition as a
keys partition. Open it with a single passphrase,
mount it, then open every other volume using a keyfile read from
inside it. Once all volumes are open, unmount and close the keys
partition. By the time runit stage 2 starts, the keys partition is
gone from the device mapper — no keyfiles are reachable from a
running shell, even as root. You type one password at boot and all
volumes open automatically from that point. The implementation
belongs in /etc/boot.d/ as a stage 1 script alongside
an /etc/crypttab that maps each volume name to its
device and keyfile path. Adapt the logic to your own partition
layout — do not copy it verbatim from any example.
Kernel requirements
CONFIG_BLK_DEV_DM=y
# dm-crypt — LUKS encryption
CONFIG_DM_CRYPT=y
# Crypto algorithms
CONFIG_CRYPTO_AES=y
CONFIG_CRYPTO_XTS=y
CONFIG_CRYPTO_SHA256=y
CONFIG_CRYPTO_SHA512=y
CONFIG_CRYPTO_ARGON2=y
QEMU
krypt/sh runs QEMU fully rootless. Networking is provided by
passt — a userspace network stack that requires no
privileges and no tun/tap devices. Display is via
-display gtk using the Wayland backend.
Installation
Your user needs to be in the kvm group for hardware
acceleration: usermod -aG kvm username.
Running a VM
$ passt --socket /tmp/passt-kali.sock &
# Launch VM
$ qemu-system-x86_64 \
-enable-kvm \
-m 2048 \
-cpu host -smp 4 \
-drive file=kali.qcow2,format=qcow2,if=virtio \
-netdev stream,id=net0,server=off,addr.type=unix,addr.path=/tmp/passt-kali.sock \
-device virtio-net-pci,netdev=net0 \
-device virtio-tablet-pci \
-device virtio-vga,xres=1920,yres=1080 \
-display gtk &
Rootless networking (passt)
passt provides NAT networking to QEMU VMs without root, tun/tap, or kernel modules. For an internal network between two VMs (such as Whonix Gateway and Workstation), use a multicast socket:
-netdev socket,id=internal,mcast=230.0.0.1:1234,localaddr=127.0.0.1 \
-device virtio-net-pci,netdev=internal \
# Workstation — same mcast address
-netdev socket,id=internal,mcast=230.0.0.1:1234,localaddr=127.0.0.1 \
-device virtio-net-pci,netdev=internal \
Clipboard sharing
Clipboard sharing between the krypt/sh host and QEMU guests uses
the qemu-vdagent chardev — the agent protocol without
the SPICE display stack. Build QEMU with
--enable-gtk-clipboard and add to the launch command:
-device virtio-serial,packed=on,ioeventfd=on \
-device virtserialport,name=com.redhat.spice.0,chardev=vdagent0 \
-chardev qemu-vdagent,id=vdagent0,name=vdagent,clipboard=on,mouse=off \
# Inside the guest — install and start spice-vdagent
$ systemctl enable --now spice-vdagentd
Podman
Podman runs containers rootlessly on krypt/sh. The runtime stack is podman → crun (OCI runtime) → aardvark-dns + netavark (networking) → catatonit (init). No Docker daemon. No root required.
Installation
The kernel must have user namespaces, cgroups v2, and overlay filesystem enabled — see the Security & sandboxing and Cgroups sections.
Rootless containers
Configure subuid and subgid ranges for your user — required for rootless operation:
# echo "username:100000:65536" >> /etc/subuid
# echo "username:100000:65536" >> /etc/subgid
# Initialize the podman user configuration
$ podman system migrate
Running containers
$ podman run --rm -it alpine sh
# Run detached with a name
$ podman run -d --name myapp -p 8080:80 nginx
# List running containers
$ podman ps
# Stop and remove
$ podman stop myapp && podman rm myapp
Xen
krypt/sh supports Xen as a paravirtualised hypervisor for hard VM isolation — each domU runs in its own hardware-enforced trust boundary. The boot path is limine → multiboot2 → xen.gz + vmlinuz. Direct xen.efi EFI boot is not supported on all firmware — limine is the correct and tested bootloader.
dom0 kernel
The Linux kernel running as dom0 requires Xen-specific options.
Build the kernel with make LLVM=1 and enable:
CONFIG_XEN_DOM0=y
CONFIG_XEN_PRIVILEGED_GUEST=y
CONFIG_XEN_BALLOON=y
CONFIG_XEN_SCRUB_PAGES_DEFAULT=y
CONFIG_XEN_DEV_EVTCHN=y
CONFIG_XEN_BACKEND=y
CONFIG_XEN_NETDEV_BACKEND=m
CONFIG_XEN_BLKDEV_BACKEND=m
CONFIG_XEN_PCIDEV_BACKEND=m
CONFIG_XEN_GNTDEV=y
CONFIG_XEN_GRANT_DEV_ALLOC=y
CONFIG_SWIOTLB_XEN=y
CONFIG_XEN_FBDEV_FRONTEND=y
CONFIG_XEN_SAVE_RESTORE=y
CONFIG_XEN_ACPI_PROCESSOR=y
Booting with limine
Install limine as BOOTX64.EFI and configure it to load Xen via
multiboot2. Place xen.gz and the dom0 kernel on the
ESP:
default_entry: 1
/Xen dom0 krypt/sh
protocol: multiboot2
path: boot():/xen.gz
cmdline: dom0_mem=6144M,max:6144M dom0_max_vcpus=4 iommu=no noreboot
module_path: boot():/vmlinuz-6.12.77
module_cmdline: root=/dev/nvme0n1p6 rw quiet
/boot/xen.gz to /boot/efi/xen.gz
after every Xen package update. limine reads from the ESP only.
Xen runit services
Three runit services are required for Xen toolstack operation.
All services guard against non-dom0 boots — they exit cleanly if
/proc/xen/capabilities does not contain
control_d. Do not symlink them into
/service/ until after a confirmed first Xen boot.
| Service | Purpose |
|---|---|
| xenstored | XenStore daemon — required first, all other tools depend on it |
| xencommons | Xencommons setup — hotplug and backend services |
| xenconsoled | Console multiplexer — access domU consoles via xl console |
# ln -s /etc/sv/xencommons /service/
# ln -s /etc/sv/xenconsoled /service/
Creating domUs
domUs are created with xl. A basic PVH domU
configuration using a LUKS-encrypted partition:
type = "pvh"
memory = 4096
vcpus = 4
disk = [ "/dev/mapper/fedora,raw,xvda,rw" ]
vif = [ "bridge=xenbr0" ]
bootloader = "pygrub"
# cryptsetup open /dev/nvme0n1p9 fedora
# Create and start the domU
# xl create fedora.cfg
# List running domains
# xl list
# Connect to console
# xl console fedora
# Shutdown
# xl shutdown fedora