Security
Overview
UraniumOS 0.1.1 is meant for one user, or a few users, on an x86-64 machine with UEFI. This page describes the settings that the system ships with.
Defaults
- The system does not accept connections from the network to a service that you did not intend to expose. The input policy is drop, and a port is open only if a file in
/etc/nftables.dor the ssh rule opens it. The services that are linked into/etc/servicein the stage areautoframe,crond,dhcp,getty-tty1,getty-ttyS0,ntpdandtrial;sshdis not among them. See blastdoor. - An unprivileged local program may not read kernel addresses or the kernel log, attach to the memory of other processes, use
perfagainst the kernel, or load BPF programs. See the sysctl table below. - A program that you run from an unknown source, and that you want to keep away from your files, may be run with hazmat, which gives it an empty home and a read-only system. With
-nit also has no network. - The exploitation of memory-corruption bugs in the kernel is made more difficult: the layout is randomized, kernel code is write-protected, allocations are zeroed, and the heap and the stack are hardened. These measures raise the cost of an exploit, and they do not remove the bug. See kernel.
- A bad update or a bad change to the system can be undone. A frame is taken before every package operation, and every update runs on trial and is reverted if geiger reports a failure. See scram.
- A tampered download of the release files is detected, provided that you verify the signature against a key that you obtained by another route. See Signatures.
Notes
| Unprivileged user namespaces are on | Bubblewrap creates the sandboxes of hazmat and silo without a setuid helper, so the kernel has CONFIG_USER_NS=y, and no sysctl turns it off. Any local user can therefore create namespaces, and can reach kernel code, for example nftables, that is otherwise limited to root. See kernel. |
| The sandboxes share the kernel and the user id | A kernel bug crosses the sandbox. Inside it, the process has your numeric user and group id (root when it was started by root), and not a separate one. Neither tool passes --new-session, and the kernel has LEGACY_TIOCSTI=y with dev.tty.legacy_tiocsti unset in /etc/sysctl.conf, so a sandboxed process can push input into the terminal from which it was started. |
| An empty home covers files only | The sandbox hides files. It does not limit the network (which is shared unless -n is given), the CPU, the memory, the processes that it can spawn, or the host store: /nix is mounted read-only in full, and every store path is readable. |
| The one setuid program is doas | It is a copy at /rod/wrappers/doas with mode 4755, which is made when the stage is built, because files in the store cannot be setuid. The tools and the installer do not refresh the copy. doas is built without PAM and reads /etc/shadow itself. |
| ssh allows passwords | PasswordAuthentication yes is set in /etc/ssh/sshd_config. The firewall limits new ssh connections to ten a minute for all sources together, which slows down guessing and allows one source to keep others out. |
| Frames are on the same disk | They are not a backup. See Frames. |
| Nix on a non-NixOS layout | The layout is not FHS. /lib links into the profile, and a prebuilt foreign binary fails unless its libraries are installed. See packages not in rod. |
| No build sandbox | /etc/nix/nix.conf sets sandbox = false and an empty build-users-group. |
Home and /tmp allow execution | noexec is set on /boot and /dev/shm only. A downloaded program may therefore run from the home directory or from /tmp. |
Where each setting lives
The files below are installed from stage/skel/etc in the source tree into /etc of the system. The kernel settings come from recipes and are read from /rod/profile/boot/config on a running system.
/rod/recipes/linux-uranium.config | Kernel options, merged over the default x86-64 configuration. The result is /rod/profile/boot/config. See kernel. |
/etc/cmdline | Kernel command line additions. Read by scram boot. |
/etc/sysctl.conf | Runtime kernel settings, applied by /etc/rc with sysctl -q -p. |
/etc/nftables.conf, /etc/nftables.d/*.nft | Firewall. Loaded by /etc/rc. |
/etc/doas.conf, /rod/wrappers/doas | Privilege escalation. The recipe recipes/doas-uranium.nix builds doas with withPAM = false; withTimestamp = true. |
/etc/ssh/sshd_config | The ssh server. See ssh. |
/etc/fstab, /etc/rc | Mount options. The installer writes /etc/fstab, and /etc/rc mounts /tmp and /dev/shm. |
/etc/nix/nix.conf | Build and cache settings. |
/etc/scram.d/00-uranium.conf | Paths that frames leave out. |
/etc/signify/uranium-0.1.pub | The public release key. |
Accounts and doas
The file /etc/doas.conf contains one line:
permit persist :wheel
The recipe compiles in timestamp support (withTimestamp = true). setup puts the first user in wheel.
$ doas rod +i git
The root account has a password only if the installer or setup --root-password-stdin set one. setup stores passwords with chpasswd in /etc/shadow, which has mode 600. On the console, login is busybox login. Over ssh, PermitRootLogin no.
Network
The firewall rules are listed rule by rule in blastdoor. Input and forward drop by default. Loopback, replies, all ICMP and ICMPv6 and DHCP replies are accepted, new ssh connections are accepted at ten a minute, and the output chain is open. The ruleset is loaded at boot with errors discarded; geiger warns when no policy is loaded.
The ssh server is off after a fresh install of the minimal profile. setup --ssh and the server profile enable it. The settings in /etc/ssh/sshd_config:
Port 22
HostKey /etc/ssh/ssh_host_ed25519_key
PermitRootLogin no
PasswordAuthentication yes
KbdInteractiveAuthentication no
UsePAM no
PrintMotd no
AllowTcpForwarding yes
X11Forwarding no
Only an Ed25519 host key is used. The run script creates it on first start. TCP forwarding is allowed. The server separates privileges with the user sshd (uid 74, home /var/empty, shell /rod/profile/bin/false).
Kernel
Options are described one by one in kernel. The command line is in /etc/cmdline:
slab_nomerge vsyscall=none debugfs=off randomize_kstack_offset=on
CONFIG_MODULES is off, so no module can be loaded. CONFIG_USER_NS is on. CONFIG_BPF_SYSCALL is on, BPF_UNPRIV_DEFAULT_OFF is on and BPF_JIT is not set. KEXEC, HIBERNATION and PROC_KCORE are off. Yama and Landlock are built, and SELinux is compiled in but does not start. The kernel lockdown module is not built.
Runtime settings
/etc/sysctl.conf is applied by /etc/rc at every boot. Output and errors of the command are discarded, so a failing key is not reported.
kernel.kptr_restrict = 2- Kernel pointers in
/procand in the log print as zeros for every user. kernel.dmesg_restrict = 1- Only root reads the kernel log. The kernel is also built with
SECURITY_DMESG_RESTRICT. kernel.perf_event_paranoid = 3- The value 3 is defined by patches that mainline Linux does not have. In the mainline kernel of UraniumOS a value above 2 acts as 2: users without privileges cannot profile the kernel, but they can still use
perfon their own processes in user space. kernel.yama.ptrace_scope = 2- Only a process with
CAP_SYS_PTRACEcan use ptrace.gdbandstraceneeddoas. kernel.unprivileged_bpf_disabled = 1- Unprivileged
bpf()is off until the next reboot. The setting cannot be changed back at run time. kernel.randomize_va_space = 2- Stack, heap and mappings of programs are randomized. This is the kernel default.
kernel.sysrq = 0- The magic SysRq key is off.
kernel.core_pattern = |/bin/false,fs.suid_dumpable = 0- Core dumps are piped to
/bin/falseand discarded. Setuid programs do not dump. For debugging, setkernel.core_pattern = corewithdoas sysctl -w. fs.protected_symlinks = 1,fs.protected_hardlinks = 1,fs.protected_fifos = 2,fs.protected_regular = 2- In sticky world-writable directories such as
/tmp, a program does not follow a symlink, or open a fifo or a regular file, that another user created. net.ipv4.tcp_syncookies = 1,net.ipv4.tcp_rfc1337 = 1- SYN cookies under SYN flood, and protection of connections in TIME-WAIT from forged resets.
net.ipv4.conf.all.rp_filter = 1,net.ipv4.conf.default.rp_filter = 1- Strict reverse-path filtering: packets whose source address would not be routed back through the interface they came in on are dropped.
net.ipv4.conf.{all,default}.accept_redirects = 0,net.ipv6.conf.{all,default}.accept_redirects = 0- ICMP redirects are not accepted.
net.ipv4.conf.all.send_redirects = 0- The machine does not send redirects.
net.ipv4.conf.all.accept_source_route = 0,net.ipv6.conf.all.accept_source_route = 0- Source-routed packets are refused.
net.ipv4.conf.all.log_martians = 1- Packets with impossible source addresses are logged to the kernel log.
net.ipv4.icmp_echo_ignore_broadcasts = 1- Echo requests sent to a broadcast address are ignored.
For the redirect and source-route keys the kernel combines the all value with the value of each interface, so an interface that was configured seperately can behave differently.
Mounts
/ | btrfs subvolume @, noatime. Setuid and device files work on it, so doas lives here. |
/home | btrfs subvolume @home, noatime,nodev,nosuid. |
/boot | vfat EFI System Partition, umask=0077,nodev,nosuid,noexec. Only root can read it. |
/tmp | tmpfs, nosuid,nodev,mode=1777, set in /etc/rc. |
/dev/shm | tmpfs, nosuid,nodev,noexec,mode=1777, set in /etc/rc. |
A setuid file or a device node in a home directory or in /tmp has no effect. /var/tmp is on the root subvolume with mode 1777 and has none of these options.
Sandboxes and containers
Both tools replace themselves with bubblewrap. The comments in the code blocks are not part of the call.
hazmat
nix shell -f /rod/nixpkgs PACKAGE... -c
bwrap --unshare-all [--share-net] --die-with-parent
--ro-bind /nix /nix --ro-bind /rod/profile /rod/profile
--ro-bind /bin /bin --ro-bind /usr /usr
--ro-bind /lib /lib --ro-bind /lib64 /lib64
--ro-bind /etc/resolv.conf /etc/resolv.conf --ro-bind /etc/ssl /etc/ssl
--proc /proc --dev /dev --tmpfs /tmp --tmpfs /home
[--ro-bind P P | --bind P P]... # from -r and -w
--setenv HOME /tmp --chdir /tmp
sh -c COMMAND
hazmat -s (services)
bwrap --unshare-all [--share-net] --die-with-parent
--proc /proc --dev /dev --tmpfs /tmp --chdir /
[--ro-bind P P | --bind P P]... # from the sandbox file
--ro-bind X X # for each of /nix /rod/profile /bin /usr /lib /lib64 /etc/ssl /etc/resolv.conf that exists
COMMAND...
silo
bwrap --unshare-all [--share-net] --die-with-parent --hostname NAME
--ro-bind /nix /nix --ro-bind PROFILE /rod/profile
--dir /usr --symlink rod/profile/bin /bin --symlink ../rod/profile/bin /usr/bin
--symlink rod/profile/bin /sbin --symlink rod/profile/lib /lib
--bind C/etc /etc
--ro-bind /etc/resolv.conf /etc/resolv.conf --ro-bind /etc/ssl /etc/ssl
--bind C/home /root
--proc /proc --dev /dev --tmpfs /tmp
--setenv HOME /root --setenv PATH /rod/profile/bin --setenv PS1 PROMPT
--chdir /root
/rod/profile/bin/sh -c COMMAND
--unshare-all in bubblewrap means new user, IPC, PID, UTS and cgroup namespaces and a network namespace, plus the mount namespace that bubblewrap always creates. The user namespace is created with --unshare-user-try. --share-net keeps the network of the host and is passed unless -n is given. The tools give the process these things:
- Isolated: the files of the host that are not bound (home directories,
/etcof the host except two entries,/var,/boot), the process list, the hostname (for silo), SysV IPC, the cgroup view, and with-nthe network. - Shared: the kernel, the numeric user id, the whole store at
/nix(read-only), the resolver configuration and the certificates, the network without-n, and the terminal. - Not applied: seccomp filters, capability limits beyond the user namespace, resource limits,
--new-session.
The unprivileged user namespace is why CONFIG_USER_NS stays on. With user.max_user_namespaces = 0 these two tools cannot run. Details of each tool are in hazmat and silo.
Packages and updates
Packages come from nixpkgs through nix-env profiles on a layout that is not NixOS, managed by rod. The system uses the binary cache https://cache.nixos.org (substituters in /etc/nix/nix.conf). The file sets no trusted-public-keys, so nix uses it's built-in default, which is the key of that cache, and checks signatures of what it downloads. A package that the cache does not have is built on the machine. The file also contains:
build-users-group =
sandbox = false
experimental-features = nix-command
A local build therefore runs as the calling user with access to the file system and the network. The index is pinned to one nixpkgs revision and moves only when you run rod +s or rod +u. A security fix reaches the machine when the pin moves and you update. geiger warns when the index is more than 60 days old. rod +h shows what an update would change and rod +u builds it in a copy of the system. The pin and the file from rod +o give the same package set again. They do not give identical binaries, since a build may differ between machines.
Frames
scram takes a frame before every package operation, every update and every flash, and the autoframe service takes one every hour. A frame is a read-only btrfs snapshot of the root subvolume @ and of @home. The root holds /etc, /nix, /rod and /var, so the frame holds the package store and the profiles, the firewall, the users and the containers.
A frame leaves out:
- The EFI System Partition.
scram bootkeeps the kernel and initramfs of each frame there, at most three besides the current one, and rewriteslimine.conf. Nothing else on it is versioned. - The paths in
/etc/scram.d/00-uranium.conf:/rod/nixpkgs(the index, so going back leaves the index as it is),/var/cacheand/var/tmp, and the directories you exclude in/etc/scram,/etc/scram.d/or~/.config/scram. Each is its own subvolume. - Other disks, the partition table, the UEFI variables and the LUKS header.
A flash replaces the live subvolumes with the frame. It does not undo what happened outside them: data on excluded paths, other machines, secrets that were already copied away. It also returns home directories to the time of the frame unless --root is given. Frames contain files deleted later, so a secret removed from the live system stays in older frames until they are pruned. Frames are read-only to your user, root can delete them, and they share the disk with the system: a failed disk takes them with it. For copies on another machine see backups.
An update that is committed boots on trial. The service trial runs scram verify, which waits five minutes, then runs geiger up to three times, and reboots to revert the update if all runs fail. The scram manual page says that a system that cannot boot at all reverts at the second power cycle.
Signatures
stage/release.sh writes the file SHA256 with sha256sum for the ISO, the stage tarball and the tools tarball, and signs that file with signify into SHA256.sig. The public key is uranium-0.1.pub. It is on the download page, in the live system and in an installed system as /etc/signify/uranium-0.1.pub. To check the list and then the files:
$ signify -V -p uranium-0.1.pub -m SHA256 -x SHA256.sig
$ sha256sum -c --ignore-missing SHA256
See download.