Dr460nixed NixOS ❄️
This repository contains a framework to get started with NixOS quickly, featuring opinionated and selected default settings. It also contains my personal NixOS configuration, which might serve as inspiration for other people's setups.
While starting out with NixOS, especially reading other peoples flakes proved to be very helpful, so I figured sharing my personal setup my help other people as well! Those who like the "dr460nized" look of Garuda will definitely like this one as well ☺️
So how does it work?
ISO builds are available as well as a simple installer, which employs nix flake init
to deploy a basic and generic
version of this flake's template based on user's choices.
Partitioning is achieved via disko presets, and the installer may be used on
regular NixOS live mediums as well.
Users may then continue building their own version of NixOS using code snippets of this repository and further linked
sources.
Furthermore, the flakes' outputs are automatically built and cached via garnix.
There is a documentation available about various topics, which also contains the code snippets that might provide further useful customization options.
How does it look like?
What settings does the installer provide?
Right now, it basically just bootstraps a basic desktop environment with theming, Nix, and OS-related defaults to have common things working out of the box.
{
dr460nixed = {
chromium = true;
desktops.enable = true;
example-boot.enable = true;
performance = true;
};
}
Which translates to:
- Enjoying a fully pre-configured Garuda dr460nized KDE themed setup: Dr460nixed !
- Being able to use all of Garuda Nix Subsystems modules
- The custom Linux-cachyos BORE EEVDF kernel of the Linux CachyOS developers
- Having access to additional packages not existing in Nixpkgs (yet) via Chaotic Nyx
- Using Garnix CI cache for this flakes' outputs
- Keeping all of it well organized via flake-parts
It might receive more module options in the future, though I currently believe that showing people how to achieve things in Nix might be more useful than just providing a toggle for it.
What kind of configuration is available via code snippets?
- Multiple NixOS configurations, including desktop, server, nixos-wsl , raspberry pi & live-usb
- Root-on-ZFS and secure-boot enabled systems via Lanzaboote
- Opt-in persistence through impermanence + ZFS snapshots
- Mesh networked hosts with Tailscale
- Opt-in 2FA protection of ssh and password prompts with Duo Security
- Secrets managed via nix-sops
- Always up-to-date live images automatically built via Github actions
- Custom devshells with fully declarative pre-commit-hooks
- No password prompts when having a Yubikey connected to a device
- Easily remote-controllable machines via KDEConnect and a self-hosted Rustdesk server
- Custom bleeding-edge Mesa builds
Structure
├── docker-compose
│ ├── oracle-dragon
│ └── tv-nixos
├── home-manager
├── nixos
│ ├── dragons-ryzen
│ ├── images
│ ├── modules
│ │ ├── chaotic
│ │ └── disko
│ ├── nixos-wsl
│ ├── oracle-dragon
│ ├── rpi-dragon
│ └── tv-nixos
├── overlays
└── secrets
Credits
Inspiration for parts of this configuration came from these awesome people's setups ❄️
- https://github.com/PedroHLC/system-setup
- https://github.com/Misterio77/nix-config
- https://github.com/isabelroses/dotfiles
Quick start
Using the installer
An installer has been created to ease the installation process by bootstrapping a basic flake with a personal host and usernames.
Information on how to build an ISO can be found on the here. The installer can also be used on any OS, that has nix
available. Find more information about how to proceed here.
Creating your own configuration including sops-nix
This one is the harder way and is mostly suitable for people with a basic understanding of NixOS. Several preparations have to be made to bootstrap a working installation if not using the installer:
- A working
hardware-configuration.nix
needs to be generated for the current machine to replace mine, this includes having already partitioned disks. - The hosts
*.nix
configuration should be adapted to suit the hardware's needs, eg. needed kernel modules orservices.xserver.videoDrivers
should be fitting - Since
sops-nix
is used to handle secrets, my files need to be replaced with your own ones. Usage instructions can be found here, basically one needs to create an age public key from the host's ed21559 SSH private key, which is then added to.sops.yaml
to allow the host to decrypt secrets while booting up. A fitting age key should also be generated and placed in~/.config/sops/age/keys.txt
as described in the usage instructions - this allows decrypting the secrets file to edit it. It lives insecrets/global.yaml
and contains the secrets and can be edited with sopssecrets/global.yaml
(opens a terminal text editor). - It might be easier to supply a static password in
users.nix
for bootstrapping since no login will be possible if the secrets management isn't properly set up yet. I had a few issues with this in the past while setting things up, so I felt giving this advice might help. Usernames are of course also to be changed, as well as SSH public keys.
Then, the bootstrapping process can be started. Here, nix
+ nixos-install-tools
is sufficient to set up your our configuration as follows:
export NIX_CONFIG="experimental-features = nix-command flakes" # if flakes are disabled
nixos-install --flake .#hostname
If the operation succeeds, you will be able to boot into your new installation.
How to proceed from here?
- Adapt the configurations like enabled modules and home-manager configs to your needs
- Set up CI to build your custom system configurations
- Enable secure boot via Lanzaboote
- Add your hosts to Tailscale, if you want to be using it. I can warmly recommend it for connecting with any kind of host!
- Build an ISO to play around with
nix run .#iso
- ... so much more. It never ends ❄️
Generating images
Images of this flake may easily be built by using nixos-generators.
In our case, we use it as NixOS module to be able to build the system via garuda-nix.lib.garudaSystem
.
This is important, as this function exposes all the garuda-nix-subsystem
modules for us to consume.
How to get going?
Let's build a regular dr460nixed ISO!
nix build .#nixosConfigurations.dr460nixed-desktop.config.formats.install-iso
You may also just run nix run .#iso
, which builds an image using the install-iso
format.
This is pretty much everything needed! The result will be available at ./result
, which links to the corresponding path in /nix/store
.
Likewise, all other configurations may be built by simply exchanging installer-iso
with the desired format.
Depending on what kind of image is being built it is needed to use the dr460nixed-base
system to build the image.
nix build .#nixosConfigurations.dr460nixed-base.config.formats.sdcard
For a list of other supported formats please have a look here.
Cross-compiling
It is also possible to build images for other architectures, eg. the Raspberry Pi. To allow cross-compilation, please have a look at how to set up the required options here.
TLDR: add the following to your configuration and apply it:
{
# Enable binfmt emulation of aarch64-linux.
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
}
Provided images
Prebuilt images used to be provided via GitHub actions, though lately builds are seemingly hitting the resource limits of the free GitHub actions VMs. Therefore, ISO builds are currently only provided with new releases. In order to work around the filze size limit of release attachments, ISO files need to need to be downloaded as segments. To join them to a full file, you can do the following:
cat dr460nixed-desktop.iso.part-* > dr460nixed-desktop.iso
The installer
This flake features an installer, which may be used to set up a basic dr460nixed NixOS installation. Both the dr460nixed ISO and regular NixOS live CDs are supported.
The installer uses the files of the templates
folder to run nix init -t
from and proceeds to customize the installation with users' choices.
Multiple disk formats are available via pre-configured disko configurations.
Choices during the execution of the script currently include:
- the disk to install NixOS on
- the partition layout to use during the process
- hostname
- username
The installer may only install by wiping the destination disk, custom partition layouts are currently unsupported. Both UEFI and legacy systems are supported. Nix command and flakes need to be enabled.
To begin, simply run the installer:
sudo installer # use this if booted into a dr460nixed ISO
sudo nix run github:dr460nf1r3/dr460nixed#installer # regular NixOS systems
Provide the needed input. After completion, a dr460nixed system is ready for you to use. You may customize it with configurations found in the main repository since the template has been kept as generic as possible.
Users are expected to continue building their own flake after the installation is finished. To do so, the dr460nixed repository has many exemplary configurations available. They may be inspected by browsing the "modules" section of this documentation.
Script
This is a small script which is serving as installer for this project. It basically:
- Sets up an environment to install the system from
- Asks for basic information such as the disk to be used or the username
- Sets up the partition layout via disko and mounting the partitions to
/mnt
- Provides a basic, quite generic dr460nixed template and customizes it based on previous choices
- Executes the installation
#!/usr/bin/env bash
set -eo pipefail
# Check for root rights
if [ "$EUID" -ne 0 ]; then
echo "I can only run as root!"
exit 3
fi
# Prepare our environment
prepare() {
# Clone dr460nixed repo if it is not present, otherwise use current dir
if [ ! "$(test -f flake.nix)" ]; then
test -d /tmp/dr460nixed && sudo rm -rf /tmp/dr460nixed
WORK_DIR=/tmp/dr460nixed
git clone https://github.com/dr460nf1r3/dr460nixed.git "$WORK_DIR"
cd "$WORK_DIR"
else
WORK_DIR=$(pwd)
fi
# Ensure needed "experimental" features are always enabled
export NIX_CONFIG="experimental-features = nix-command flakes"
}
# Confirmation prompt
confirm_choices() {
# Continue if the user confirms our choice
read -rp "Are you sure you want to continue? $1 [y/n] " _ANSWER
while [ -z "${KILLIT+x}" ]; do
case "${_ANSWER}" in
y | yes | Y | YES)
KILLIT=1
;;
n | no | N | NO)
exit 1
;;
*)
read -rp "Invalid input. Only yes or no is valid: " _ANSWER
;;
esac
done
}
# Create a runner for disko, directly from git
disko_runner() {
nix run github:nix-community/disko -- --mode disko "$1" --arg disks "[ \"$2\" ]"
}
# Create partitions using disko and mount them to /mnt
disko() {
echo "The following partition layouts are available:
1) BTRFS with subvolumes
2) BTRFS on LUKS with subvolumes
3) Simple EFI
4) ZFS (recommended)
5) ZFS encrypted"
read -rp "Enter the number of the partition layout you want to use: " _LAYOUT
# https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash
while [ -z "${DISKO_MODULE+x}" ]; do
case "${_LAYOUT}" in
1)
DISKO_MODULE=btrfs-subvolumes
;;
2)
DISKO_MODULE=luks-btrfs-subvolumes
;;
3)
DISKO_MODULE=simple-efi
;;
4)
DISKO_MODULE=zfs-encrypted
;;
5)
DISKO_MODULE=zfs
;;
*)
read -rp "Invalid input. Enter the number of the partition layout you want to use: " _LAYOUT
;;
esac
done
while [ -z "${_VALID_DISK+x}" ]; do
read -rp "Specify the disk you want to use, eg. \"nvme0n1\": " DISK
# Disk identifiers should at least be 3 characters long and be present in our system
# this does not prevent all possible errors (eg. nvme0 would be valid) but good enough for now
if [ ${#DISK} -gt 2 ] && lsblk | grep "$DISK"; then
_VALID_DISK=1
else
echo "The disk you entered is invalid! Try again."
fi
done
# Ask whether the hard drive should really be wiped
echo "The disk you chose to format is $DISK."
confirm_choices "This will start the wiping process!"
# Create partitions and set up /mnt
disko_runner ./nixos/modules/disko/"$DISKO_MODULE".nix /dev/"$DISK"
}
# Create initial configuration
create_config() {
NIX_ROOT=/mnt/etc/nixos
read -rp "Enter the hostname you want to use: " HOSTNAME
read -rp "Enter your desired username: " USER
[ -d /sys/firmware/efi ] && SYSTEM_TYPE=systemd-boot || SYSTEM_TYPE=grub
# Create config without filesystems as disko provides those already
# also apply our dr460nixed template
nixos-generate-config --no-filesystems --root /mnt
pushd "$NIX_ROOT" || exit 2
nix flake init --template "$WORK_DIR"#dr460nixed
mv ./hardware-configuration.nix ./nixos/example-host
rm ./configuration.nix # we are using flakes, no need for that anymore
mv ./nixos/example-host ./nixos/"$HOSTNAME"
mv ./nixos/"$HOSTNAME"/example-host.nix ./nixos/"$HOSTNAME"/"$HOSTNAME".nix
sed -i s/example-boot/"$SYSTEM_TYPE"/g ./nixos/"$HOSTNAME"/"$HOSTNAME".nix
sed -i s/example-disk/"$DISK"/g .{/nixos/flake-module.nix,nixos/"$HOSTNAME"/"$HOSTNAME".nix}
sed -i s/example-hostname/"$HOSTNAME"/g {./nixos/flake-module.nix,nixos/"$HOSTNAME"/"$HOSTNAME".nix}
sed -i s/example-layout/"$DISKO_MODULE"/g ./nixos/flake-module.nix
sed -i s/example-user/"$USER"/g ./nixos/modules/users.nix
echo "Configuration successfully created.
You made the following choices:
hostname: $HOSTNAME
user: $USER"
confirm_choices "This starts the installation process."
popd || exit 2
}
# Install basic dr460nixed system
install_system() {
nixos-install --flake "$NIX_ROOT#$HOSTNAME" --verbose
}
# Notices
finish() {
echo "The installation finished successfully. You may now reboot into your new system."
confirm_choices "This will remove the temporary directory an reboot the system."
umount -Rf /mnt
rm -rf "$WORK_DIR"
}
# Actually execute our functions
prepare
disko
create_config
install_system
finish
Apps
{
config,
inputs,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed;
# godot = pkgs.godot_4-mono;
# extra-path = with pkgs; [
# dotnetCorePackages.sdk_8_0_3xx
# dotnetPackages.Nuget
# godot
# mono
# msbuild
# ];
# extra-lib = [];
# rider = pkgs.jetbrains.rider.overrideAttrs (attrs: {
# postInstall =
# ''
# # Wrap rider with extra tools and libraries
# mv $out/bin/rider $out/bin/.rider-toolless
# makeWrapper $out/bin/.rider-toolless $out/bin/rider \
# --argv0 rider \
# --prefix PATH : "${lib.makeBinPath extra-path}" \
# --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath extra-lib}"
# ''
# + attrs.postInstall or "";
# });
in {
# These are the packages I always need
environment.systemPackages = let
required-packages = with pkgs; [
bind
btop
cached-nix-shell
duf
eza
jq
killall
micro
nettools
python3
tldr
tmux
traceroute
tmuxPlugins.catppuccin
ugrep
wget
];
in
required-packages
++ lib.optionals cfg.desktops.enable (with pkgs; [
appimage-run
aspell
aspellDicts.de
aspellDicts.en
boxbuddy
brave
catppuccinifier-cli
chromium-flagged
firedragon
gimp
hunspell
hunspellDicts.de_DE
hunspellDicts.en_US
inputs.ayugram-desktop.packages.${pkgs.system}.ayugram-desktop
inputs.kwin-effects-forceblur.packages.${pkgs.system}.default
libreoffice-qt6-fresh
kde-rounded-corners
kdePackages.kdenlive
kdePackages.kleopatra
kdePackages.krdc
kdePackages.krfb
libsecret
libva-utils
lm_sensors
movit
obs-studio-wrapped
obsidian
okular
plasmusic-toolbar
rustdesk-flutter
signal-desktop
syncthingtray
trayscale
usbutils
vesktop
vorta
vulkan-tools
xdg-utils
yt-dlp_git
])
++ lib.optionals cfg.development.enable (with pkgs; [
ansible
beekeeper-studio
cloudflared
cmake
deno
distrobox_git
docker-compose
fishPlugins.wakatime-fish
gh
# godot
gnumake
heroku
hoppscotch
hugo
jetbrains.gateway
jetbrains.idea-ultimate
jetbrains.webstorm
mdbook
mdbook-admonish
mdbook-emojicodes
mdbook-linkcheck
nil
nix-prefetch-git
nixd
nixos-generators
nixpkgs-lint
nixpkgs-review
nodePackages_latest.bash-language-server
nodePackages_latest.pnpm
nodejs_latest
# rider
shellcheck
shfmt
sops
speedcrunch
termius
ventoy-full
(vscode-with-extensions.override {
vscodeExtensions =
[
vscode-extensions.bbenoist.nix
vscode-extensions.catppuccin.catppuccin-vsc
vscode-extensions.catppuccin.catppuccin-vsc-icons
vscode-extensions.charliermarsh.ruff
vscode-extensions.davidanson.vscode-markdownlint
vscode-extensions.eamodio.gitlens
vscode-extensions.esbenp.prettier-vscode
vscode-extensions.foxundermoon.shell-format
vscode-extensions.github.codespaces
vscode-extensions.github.copilot
vscode-extensions.github.vscode-github-actions
vscode-extensions.github.vscode-pull-request-github
vscode-extensions.jnoortheen.nix-ide
vscode-extensions.kamadorueda.alejandra
vscode-extensions.mads-hartmann.bash-ide-vscode
vscode-extensions.ms-azuretools.vscode-docker
vscode-extensions.ms-vscode.makefile-tools
vscode-extensions.ms-python.python
vscode-extensions.ms-python.vscode-pylance
vscode-extensions.ms-vscode-remote.remote-ssh
vscode-extensions.ms-vsliveshare.vsliveshare
vscode-extensions.redhat.vscode-xml
vscode-extensions.redhat.vscode-yaml
vscode-extensions.timonwong.shellcheck
vscode-extensions.tyriar.sort-lines
vscode-extensions.wakatime.vscode-wakatime
]
++ vscode-utils.extensionsFromVscodeMarketplace [
{
# Available in nixpkgs, but outdated (0.4.0) at the time of adding
name = "vscode-tailscale";
publisher = "tailscale";
sha256 = "sha256-MKiCZ4Vu+0HS2Kl5+60cWnOtb3udyEriwc+qb/7qgUg=";
version = "1.0.0";
}
];
})
(yarn-berry.override {
nodejs = pkgs.nodejs_latest;
})
zed-editor_git
])
++ lib.optionals cfg.yubikey (with pkgs; [
yubikey-manager-qt
yubioath-flutter
])
++ lib.optionals cfg.school (with pkgs; [
speedcrunch
teams-for-linux
])
++ lib.optionals cfg.gaming.enable (with pkgs; [
lutris
prismlauncher
])
++ lib.optionals cfg.live-cd (with pkgs; [
btrfs-progs
chntpw
cryptsetup
dosfstools
e2fsprogs
efibootmgr
flashrom
gnutar
gparted
hexedit
home-manager
hwinfo
inxi
memtest86-efi
ntfs3g
nvme-cli
p7zip
pciutils
perl
qemu-utils
rsync
tcpdump
testdisk
util-linux
wipe
xfsprogs
]);
xdg.mime.defaultApplications = let
# Take from the respective mimetype files
images = [
"image/bmp"
"image/gif"
"image/jpeg"
"image/jpg"
"image/pjpeg"
"image/png"
"image/svg+xml"
"image/svg+xml-compressed"
"image/tiff"
"image/vnd.wap.wbmp;image/x-icns"
"image/x-bmp"
"image/x-gray"
"image/x-icb"
"image/x-ico"
"image/x-pcx"
"image/x-png"
"image/x-portable-anymap"
"image/x-portable-bitmap"
"image/x-portable-graymap"
"image/x-portable-pixmap"
"image/x-xbitmap"
"image/x-xpixmap"
];
urls = [
"text/html"
"x-scheme-handler/about"
"x-scheme-handler/http"
"x-scheme-handler/https"
"x-scheme-handler/unknown"
];
documents = [
"application/illustrator"
"application/oxps"
"application/pdf"
"application/postscript"
"application/vnd.comicbook+zip"
"application/vnd.comicbook-rar"
"application/vnd.ms-xpsdocument"
"application/x-bzdvi"
"application/x-bzpdf"
"application/x-bzpostscript"
"application/x-cb7"
"application/x-cbr"
"application/x-cbt"
"application/x-cbz"
"application/x-dvi"
"application/x-ext-cb7"
"application/x-ext-cbr"
"application/x-ext-cbt"
"application/x-ext-cbz"
"application/x-ext-djv"
"application/x-ext-djvu"
"application/x-ext-dvi"
"application/x-ext-eps"
"application/x-ext-pdf"
"application/x-ext-ps"
"application/x-gzdvi"
"application/x-gzpdf"
"application/x-gzpostscript"
"application/x-xzpdf"
"image/tiff"
"image/vnd.djvu+multipage"
"image/x-bzeps"
"image/x-eps"
"image/x-gzeps"
];
audioVideo = [
"application/mxf"
"application/ogg"
"application/sdp"
"application/smil"
"application/streamingmedia"
"application/vnd.apple.mpegurl"
"application/vnd.ms-asf"
"application/vnd.rn-realmedia"
"application/vnd.rn-realmedia-vbr"
"application/x-cue"
"application/x-extension-m4a"
"application/x-extension-mp4"
"application/x-matroska"
"application/x-mpegurl"
"application/x-ogg"
"application/x-ogm"
"application/x-ogm-audio"
"application/x-ogm-video"
"application/x-shorten"
"application/x-smil"
"application/x-streamingmedia"
"audio/3gpp"
"audio/3gpp2"
"audio/AMR"
"audio/aac"
"audio/ac3"
"audio/aiff"
"audio/amr-wb"
"audio/dv"
"audio/eac3"
"audio/flac"
"audio/m3u"
"audio/m4a"
"audio/mp1"
"audio/mp2"
"audio/mp3"
"audio/mp4"
"audio/mpeg"
"audio/mpeg2"
"audio/mpeg3"
"audio/mpegurl"
"audio/mpg"
"audio/musepack"
"audio/ogg"
"audio/opus"
"audio/rn-mpeg"
"audio/scpls"
"audio/vnd.dolby.heaac.1"
"audio/vnd.dolby.heaac.2"
"audio/vnd.dts"
"audio/vnd.dts.hd"
"audio/vnd.rn-realaudio"
"audio/vorbis"
"audio/wav"
"audio/webm"
"audio/x-aac"
"audio/x-adpcm"
"audio/x-aiff"
"audio/x-ape"
"audio/x-m4a"
"audio/x-matroska"
"audio/x-mp1"
"audio/x-mp2"
"audio/x-mp3"
"audio/x-mpegurl"
"audio/x-mpg"
"audio/x-ms-asf"
"audio/x-ms-wma"
"audio/x-musepack"
"audio/x-pls"
"audio/x-pn-au"
"audio/x-pn-realaudio"
"audio/x-pn-wav"
"audio/x-pn-windows-pcm"
"audio/x-realaudio"
"audio/x-scpls"
"audio/x-shorten"
"audio/x-tta"
"audio/x-vorbis"
"audio/x-vorbis+ogg"
"audio/x-wav"
"audio/x-wavpack"
"video/3gp"
"video/3gpp"
"video/3gpp2"
"video/avi"
"video/divx"
"video/dv"
"video/fli"
"video/flv"
"video/mkv"
"video/mp2t"
"video/mp4"
"video/mp4v-es"
"video/mpeg"
"video/msvideo"
"video/ogg"
"video/quicktime"
"video/vnd.divx"
"video/vnd.mpegurl"
"video/vnd.rn-realvideo"
"video/webm"
"video/x-avi"
"video/x-flc"
"video/x-flic"
"video/x-flv"
"video/x-m4v"
"video/x-matroska"
"video/x-mpeg2"
"video/x-mpeg3"
"video/x-ms-afs"
"video/x-ms-asf"
"video/x-ms-wmv"
"video/x-ms-wmx"
"video/x-ms-wvxvideo"
"video/x-msvideo"
"video/x-ogm"
"video/x-ogm+ogg"
"video/x-theora"
"video/x-theora+ogg"
];
archives = [
"application/bzip2"
"application/gzip"
"application/vnd.android.package-archive"
"application/vnd.debian.binary-package"
"application/vnd.ms-cab-compressed"
"application/x-7z-compressed"
"application/x-7z-compressed-tar"
"application/x-ace"
"application/x-alz"
"application/x-ar"
"application/x-archive"
"application/x-arj"
"application/x-brotli"
"application/x-bzip"
"application/x-bzip-brotli-tar"
"application/x-bzip-compressed-tar"
"application/x-bzip1"
"application/x-bzip1-compressed-tar"
"application/x-cabinet"
"application/x-cd-image"
"application/x-chrome-extension"
"application/x-compress"
"application/x-compressed-tar"
"application/x-cpio"
"application/x-deb"
"application/x-ear"
"application/x-gtar"
"application/x-gzip"
"application/x-gzpostscript"
"application/x-java-archive"
"application/x-lha"
"application/x-lhz"
"application/x-lrzip"
"application/x-lrzip-compressed-tar"
"application/x-lz4"
"application/x-lz4-compressed-tar"
"application/x-lzip"
"application/x-lzip-compressed-tar"
"application/x-lzma"
"application/x-lzma-compressed-tar"
"application/x-lzop"
"application/x-ms-dos-executable"
"application/x-ms-wim"
"application/x-rar"
"application/x-rar-compressed"
"application/x-rpm"
"application/x-rzip"
"application/x-rzip-compressed-tar"
"application/x-source-rpm"
"application/x-stuffit"
"application/x-tar"
"application/x-tarz"
"application/x-tzo"
"application/x-war"
"application/x-xar"
"application/x-xz"
"application/x-xz-compressed-tar"
"application/x-zip"
"application/x-zip-compressed"
"application/x-zoo"
"application/x-zstd-compressed-tar"
"application/zip"
"application/zstd"
];
code = [
"application/x-shellscript"
"text/english"
"text/plain"
"text/x-c"
"text/x-c++"
"text/x-c++hdr"
"text/x-c++src"
"text/x-chdr"
"text/x-csrc"
"text/x-java"
"text/x-makefile"
"text/x-moc"
"text/x-pascal"
"text/x-tcl"
"text/x-tex"
];
in
lib.mkForce (
# Overriding garuda-nix flakes defaults here
(lib.genAttrs code (_: ["code.desktop"]))
// (lib.genAttrs archives (_: ["ark.desktop"]))
// (lib.genAttrs audioVideo (_: ["vlc.desktop"]))
// (lib.genAttrs documents (_: ["okular.desktop"]))
// (lib.genAttrs images (_: ["okular.desktop"]))
// (lib.genAttrs urls (_: ["chromium.desktop"]))
);
}
Boot
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.systemd-boot;
cfgGrub = config.dr460nixed.grub;
cfgLanza = config.dr460nixed.lanzaboote;
inherit (pkgs) plymouth;
in {
options.dr460nixed.grub = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Configures the system to install GRUB to a particular device, which enables booting
on non-UEFI systems.
'';
};
enableCryptodisk =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Whether to enable GRUB cryptodisk support.
'';
};
device =
mkOption
{
default = null;
type = types.str;
description = mdDoc ''
Defines which device to install GRUB to.
'';
};
};
options.dr460nixed.systemd-boot = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Configures common options for a quiet systemd-boot.
'';
};
};
options.dr460nixed.lanzaboote = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Configures common options using Lanzaboote as secure boot manager.
'';
};
};
config = {
boot = {
kernelParams = [
"acpi_backlight=native"
"iommu=full"
"page_alloc.shuffle=1"
"processor.max_cstate=5"
"quiet"
"rd.systemd.show_status=auto"
"rd.udev.log_level=3"
"rootflags=noatime"
"usbcore.autosuspend=-1"
"vt.global_cursor_default=0"
];
lanzaboote = lib.mkIf cfgLanza.enable {
enable = true;
pkiBundle = "/etc/secureboot";
};
loader = {
grub = {
device = lib.mkIf cfgGrub.enable cfgGrub.device;
enable =
if cfgGrub.enable
then true
else false;
enableCryptodisk = true;
useOSProber = false;
};
generationsDir.copyKernels = lib.mkIf cfg.enable true;
timeout = 1;
systemd-boot = lib.mkIf cfg.enable {
consoleMode = "max";
editor = false;
enable = true;
};
};
};
# Make plymouth work with sleep
powerManagement = lib.mkIf config.boot.plymouth.enable {
powerDownCommands = ''
${plymouth} --show-splash
'';
resumeCommands = ''
${plymouth} --quit
'';
};
environment.systemPackages = lib.mkIf cfgLanza.enable [pkgs.sbctl];
};
}
COmmon
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed;
in {
options.dr460nixed = with lib; {
common = {
enable =
mkOption
{
default = true;
type = types.bool;
description = mdDoc ''
Whether to enable common system configurations.
'';
};
};
rpi =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Whether this is a Raspberry Pi.
'';
};
nodocs =
mkOption
{
default = true;
type = types.bool;
description = mdDoc ''
Whether to disable the documentation.
'';
};
};
config = lib.mkIf cfg.common.enable {
# A few kernel tweaks
boot.kernelParams = ["noresume"];
# Disable unprivileged user namespaces, unless containers are enabled
security = {
# User namespaces are required for sandboxing
allowUserNamespaces = true;
# This is only required for containers
unprivilegedUsernsClone = config.virtualisation.containers.enable;
# Force-enable the Page Table Isolation (PTI) Linux kernel feature
forcePageTableIsolation = true;
};
# Allow wheel group users to use sudo
security.sudo.execWheelOnly = true;
# This is the default sops file that will be used for all secrets
sops = {
age.sshKeyPaths = ["/etc/ssh/ssh_host_ed25519_key"];
defaultSopsFile = ../../secrets/global.yaml;
};
# Increase open file limit for sudoers
security.pam.loginLimits = [
{
domain = "@wheel";
item = "nofile";
type = "soft";
value = "524288";
}
{
domain = "@wheel";
item = "nofile";
type = "hard";
value = "1048576";
}
];
# Always needed applications
programs = {
git = {
enable = true;
lfs.enable = true;
};
# The GnuPG agent
gnupg.agent = {
enable = true;
pinentryPackage = lib.mkForce pkgs.pinentry-curses;
};
};
# https://gitlab.com/ananicy-cpp/ananicy-cpp/-/issues/40#note_1986279383
systemd.services.ananicy-cpp = {
serviceConfig = {
Delegate-cpu = "cpuset io memory pids";
ExecStartPre = "${pkgs.coreutils}/bin/sleep 30";
};
};
# Who needs documentation when there is the internet? #bl04t3d
documentation = lib.mkIf cfg.nodocs {
dev.enable = false;
doc.enable = false;
enable = true;
info.enable = false;
man.enable = false;
nixos.enable = true;
};
# Enable all hardware drivers
hardware.enableRedistributableFirmware = true;
# No need for that in real NixOS systems
garuda.garuda-nix-manager.enable = false;
# Custom label for boot menu entries (otherwise set to "garuda-nix-subsystem")
system.nixos.label = lib.mkForce (builtins.concatStringsSep "-" ["dr460nixed-"] + config.system.nixos.version);
};
}
Desktops
{
config,
inputs,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.desktops;
spicePkgs = inputs.spicetify-nix.legacyPackages.${pkgs.system};
in {
options.dr460nixed.desktops = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Whether to enable basic dr460nized desktop theming.
'';
};
};
config = lib.mkIf cfg.enable {
# Enable the Catppuccinified desktops settings
garuda.catppuccin.enable = true;
environment = {
variables = {
VISUAL = lib.mkForce "vscode";
};
};
# Allow better Syncthing speeds
services.syncthing.openDefaultPorts = true;
# Fancy themed, enhanced Spotify
programs.spicetify = {
colorScheme = "catppuccin-mocha";
enable = true;
enabledCustomApps = with spicePkgs.apps; [
lyricsPlus
newReleases
];
enabledExtensions = with spicePkgs.extensions; [
autoSkipVideo
beautifulLyrics
betterGenres
bookmark
fullAlbumDate
fullAppDisplayMod
groupSession
hidePodcasts
history
playlistIcons
seekSong
songStats
];
theme = spicePkgs.themes.comfy;
};
# Disable QML cache for better performance / less issues
environment.variables = {
QML_DISABLE_DISK_CACHE = "1";
};
};
}
Deterministic ids
# https://github.com/oddlama/nix-config/blob/main/modules/system/deteministic-ids.nix
{
lib,
config,
...
}: let
inherit
(lib)
concatLists
flip
mapAttrsToList
mdDoc
mkDefault
mkIf
mkOption
types
;
cfg = config.users.deterministicIds;
in {
options = {
users.deterministicIds = mkOption {
default = {};
description = mdDoc ''
Maps a user or group name to its expected uid/gid values. If a user/group is
used on the system without specifying a uid/gid, this module will assign the
corresponding ids defined here, or show an error if the definition is missing.
'';
type = types.attrsOf (types.submodule {
options = {
uid = mkOption {
type = types.nullOr types.int;
default = null;
description = mdDoc "The uid to assign if it is missing in `users.users.<name>`.";
};
gid = mkOption {
type = types.nullOr types.int;
default = null;
description = mdDoc "The gid to assign if it is missing in `users.groups.<name>`.";
};
};
});
};
users.users = mkOption {
type = types.attrsOf (types.submodule ({name, ...}: {
config.uid = let
deterministicUid = cfg.${name}.uid or null;
in
mkIf (deterministicUid != null) (mkDefault deterministicUid);
}));
};
users.groups = mkOption {
type = types.attrsOf (types.submodule ({name, ...}: {
config.gid = let
deterministicGid = cfg.${name}.gid or null;
in
mkIf (deterministicGid != null) (mkDefault deterministicGid);
}));
};
};
config = {
assertions =
concatLists
(flip mapAttrsToList config.users.users (name: user: [
{
assertion = user.uid != null;
message = "non-deterministic uid detected for '${name}', please assign one via `users.deterministicIds`";
}
{
assertion = !user.autoSubUidGidRange;
message = "non-deterministic subUids/subGids detected for: ${name}";
}
]))
++ flip mapAttrsToList config.users.groups (name: group: {
assertion = group.gid != null;
message = "non-deterministic gid detected for '${name}', please assign one via `users.deterministicIds`";
});
};
}
Development
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.development;
# Distrobox setup scripts
additionalPackages = ''
--additional-packages "git tmux micro fish fastfetch wlroots"
'';
distrobox-setup = pkgs.writeScriptBin "distrobox-setup" ''
distrobox create --name arch \
--init --image quay.io/toolbx/arch-toolbox:latest \
--additional-packages "git tmux micro fish base-devel pacman-contrib fastfetch"
distrobox generate-entry arch
distrobox create --name kali \
--init --image docker.io/kalilinux/kali-rolling:latest \
${additionalPackages}
distrobox generate-entry kali
'';
in {
options.dr460nixed.development = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Enables commonly used development tools.
'';
};
};
config = lib.mkIf cfg.enable {
# Import secrets needed for development
sops.secrets."api_keys/sops" = {
mode = "0600";
owner = config.users.users.nico.name;
path = "/home/nico/.config/sops/age/keys.txt";
};
sops.secrets."api_keys/heroku" = {
mode = "0600";
owner = config.users.users.nico.name;
path = "/home/nico/.netrc";
};
sops.secrets."api_keys/cloudflared" = {
mode = "0600";
owner = config.users.users.nico.name;
path = "/home/nico/.cloudflared/cert.pem";
};
# Conflicts with virtualisation.containers if enabled
boot.enableContainers = false;
# Allow building sdcard images for Raspi
nixpkgs.config.allowUnsupportedSystem = true;
# Wireshark
programs.wireshark.enable = true;
# Virtualbox KVM & Podman with docker alias
virtualisation = {
containers.enable = true;
lxd.enable = false;
podman = {
autoPrune.enable = true;
defaultNetwork.settings.dns_enabled = true;
# dockerCompat = true;
# dockerSocket.enable = true;
enable = true;
};
docker = {
enable = true;
autoPrune.enable = true;
};
virtualbox.host = {
addNetworkInterface = false;
enable = true;
enableExtensionPack = true;
enableHardening = true;
# enableKvm = true;
};
};
# For Redis
boot.kernel.sysctl = {"vm.overcommit_memory" = "1";};
# Archlinux development
environment.systemPackages = [
distrobox-setup
];
# Local instances
networking.hosts = {
"127.0.0.1" = ["metrics.chaotic.local" "backend.chaotic.local"];
};
# Allow cross-compiling to aarch64
boot.binfmt.emulatedSystems = ["aarch64-linux"];
# In case I need to fix my phone
programs.adb.enable = true;
};
}
Compose runner
{
lib,
pkgs,
config,
...
}: let
cfg = config.dr460nixed.compose-runner;
in {
options.dr460nixed.compose-runner = lib.mkOption (with lib; {
type = types.attrsOf (types.submodule {
options = {
source = mkOption {
default = null;
description = "Folder containing a compose file.";
type = types.path;
};
envfile = mkOption {
default = null;
description = "Direct path to a valid .env file";
type = types.nullOr types.path;
};
};
});
default = {};
});
config = {
systemd.services =
lib.mapAttrs'
(name: value:
lib.nameValuePair ("compose-runner-" + name) (
let
output = derivation {
builder = pkgs.writeShellScript "build" ''
PATH="${pkgs.rsync}/bin:${pkgs.coreutils}/bin:${pkgs.gnused}/bin"
set -e
mkdir "$out"
sed -r 's/(^\s+restart:\s*)(unless-stopped|always)(\s*($|#))/\1on-failure\3/g' "$src/compose.yml" > "$out/compose.yml"
rsync -a "$src/" "$out"
'';
name = "compose-runner-" + name;
src = value.source;
inherit (pkgs.hostPlatform) system;
};
statepath = "/var/compose-runner/${name}";
in {
description = "Compose runner for ${name}";
path = with pkgs; [rsync docker-compose podman bash];
serviceConfig = {
ExecStart = pkgs.writeShellScript ("execstart-compose-runner-" + name) ''
set -e
mkdir -p "${statepath}"
rsync -a --no-owner --size-only "${output}/" "${statepath}"
${lib.optionalString (value.envfile != null) ''
cp "${value.envfile}" "${statepath}/.env"
chmod 600 "${statepath}/.env"
''}
cd "${statepath}"
docker-compose up
'';
ExecStopPost = pkgs.writeShellScript ("execstop-compose-runner-" + name) ''
set -e
cd "${statepath}"
docker-compose down
'';
};
unitConfig = {
After = "podman.socket";
Requisite = "podman.socket";
StopPropagatedFrom = "podman.socket";
};
wantedBy = ["multi-user.target"];
}
))
cfg;
environment.systemPackages = lib.mkIf (cfg != {}) [pkgs.docker-compose];
virtualisation.podman.enable = lib.mkIf (cfg != {}) true;
networking.firewall.interfaces."podman1".allowedUDPPorts = lib.mkIf (cfg != {}) [53];
virtualisation.podman.defaultNetwork.settings = {dns_enabled = true;};
};
}
Gaming
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.gaming;
in {
options.dr460nixed.gaming = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Whether this device is used for gaming.
'';
};
};
config = lib.mkIf cfg.enable {
# Gamemode
programs.gamemode.enable = true;
# Enable Steam
programs.steam = {
enable = true;
extraCompatPackages = with pkgs; [proton-ge-custom];
};
};
}
Hardening
{
config,
# inputs,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.hardening;
cfgServers = config.dr460nixed.servers.enable;
in {
options.dr460nixed.hardening = with lib; {
enable =
mkOption
{
default = true;
example = false;
type = types.bool;
description = mdDoc ''
Whether the operating system should be hardened.
'';
};
duosec =
mkOption
{
default = false;
example = true;
type = types.bool;
description = mdDoc ''
Whether logins should be protected by Duo Security.
'';
};
};
config = lib.mkIf cfg.enable {
# Disable some of it
# nm-overrides = {
# compatibility = {
# binfmt-misc.enable = true;
# ip-forward.enable = true;
# };
# desktop = {
# allow-multilib.enable = true;
# allow-unprivileged-userns.enable = true;
# home-exec.enable = true;
# tmp-exec.enable = true;
# usbguard-allow-at-boot.enable = true;
# };
# performance = {
# allow-smt.enable = true;
# };
# security = {
# tcp-timestamp-disable.enable = true;
# disable-intelme-kmodules.enable = true;
# };
# };
boot.blacklistedKernelModules = [
# Obscure network protocols
"ax25"
"netrom"
"rose"
# Old or rare or insufficiently audited filesystems
"adfs"
"affs"
"befs"
"bfs"
"btusb"
"cifs"
"cramfs"
"cramfs"
"efs"
"erofs"
"exofs"
"f2fs"
"freevxfs"
"freevxfs"
"gfs2"
"hfs"
"hfsplus"
"hpfs"
"jffs2"
"jfs"
"ksmbd"
"minix"
"nfs"
"nfsv3"
"nfsv4"
"nilfs2"
"omfs"
"qnx4"
"qnx6"
"sysv"
"udf"
"vivid"
];
# Protect logins and sudo on servers via DUO
# leaving EnvFactor enabled for other apps
security.duosec = lib.mkIf cfg.duosec {
acceptEnvFactor = true;
autopush = true;
failmode = "safe";
host = "api-a7b9f5f3.duosecurity.com";
integrationKey = "DID3CH2NCQ2H24L1GUUN";
pam.enable = true;
prompts = 1;
pushinfo = true;
secretKeyFile = config.sops.secrets."api_keys/duo".path;
ssh.enable = true;
};
sops.secrets."api_keys/duo" = lib.mkIf cfg.duosec {
mode = "0600";
path = "/run/secrets/api_keys/duo";
};
security.pam.services = lib.mkIf cfg.duosec {
"login".duoSecurity.enable = true;
"sddm".duoSecurity.enable = lib.mkIf config.dr460nixed.desktops.enable true;
"sudo".duoSecurity.enable = lib.mkIf config.dr460nixed.servers.enable true;
};
# Disable root login & password authentication on sshd
# also, apply recommendations of ssh-audit.com and enable Duo 2FA
# (for whatever reason the default config did not work a at all?
# maybe related to https://github.com/NixOS/nixpkgs/issues/115044)
services.openssh = {
extraConfig =
if cfg.duosec
then ''
AllowTcpForwarding no
ForceCommand /usr/bin/env login_duo
HostKeyAlgorithms ssh-ed25519,[email protected],[email protected],[email protected],rsa-sha2-256,rsa-sha2-512,[email protected],[email protected]
PermitTunnel no
''
else ''
AllowTcpForwarding no
HostKeyAlgorithms ssh-ed25519,[email protected],[email protected],[email protected],rsa-sha2-256,rsa-sha2-512,[email protected],[email protected]
PermitTunnel no
'';
settings = {
Ciphers = [
"[email protected]"
"aes256-ctr,aes192-ctr"
"aes128-ctr"
"[email protected]"
];
KbdInteractiveAuthentication = false;
KexAlgorithms = [
"curve25519-sha256"
"[email protected]"
"diffie-hellman-group16-sha512"
"diffie-hellman-group18-sha512"
"[email protected]"
];
Macs = [
"[email protected]"
"[email protected]"
"[email protected]"
];
PasswordAuthentication = false;
PermitRootLogin = "no";
X11Forwarding = false;
};
};
# Client side SSH configuration
programs.ssh = {
ciphers = [
"[email protected]"
"aes256-ctr,aes192-ctr"
"aes128-ctr"
"[email protected]"
"[email protected]"
];
hostKeyAlgorithms = [
"ssh-ed25519"
"[email protected]"
"[email protected]"
"[email protected]"
"rsa-sha2-512"
"[email protected]"
"rsa-sha2-256"
"[email protected]"
];
kexAlgorithms = [
"curve25519-sha256"
"[email protected]"
"diffie-hellman-group16-sha512"
"diffie-hellman-group18-sha512"
"[email protected]"
];
knownHosts = {
aur-rsa = {
hostNames = ["aur.archlinux.org"];
publicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDKF9vAFWdgm9Bi8uc+tYRBmXASBb5cB5iZsB7LOWWFeBrLp3r14w0/9S2vozjgqY5sJLDPONWoTTaVTbhe3vwO8CBKZTEt1AcWxuXNlRnk9FliR1/eNB9uz/7y1R0+c1Md+P98AJJSJWKN12nqIDIhjl2S1vOUvm7FNY43fU2knIhEbHybhwWeg+0wxpKwcAd/JeL5i92Uv03MYftOToUijd1pqyVFdJvQFhqD4v3M157jxS5FTOBrccAEjT+zYmFyD8WvKUa9vUclRddNllmBJdy4NyLB8SvVZULUPrP3QOlmzemeKracTlVOUG1wsDbxknF1BwSCU7CmU6UFP90kpWIyz66bP0bl67QAvlIc52Yix7pKJPbw85+zykvnfl2mdROsaT8p8R9nwCdFsBc9IiD0NhPEHcyHRwB8fokXTajk2QnGhL+zP5KnkmXnyQYOCUYo3EKMXIlVOVbPDgRYYT/XqvBuzq5S9rrU70KoI/S5lDnFfx/+lPLdtcnnEPk=";
};
aur-ed25519 = {
hostNames = ["aur.archlinux.org"];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN";
};
github-rsa = {
hostNames = ["github.com"];
publicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=";
};
github-ed25519 = {
hostNames = ["github.com"];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl";
};
gitlab-rsa = {
hostNames = ["gitlab.com"];
publicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9";
};
gitlab-ed25519 = {
hostNames = ["gitlab.com"];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf";
};
};
macs = [
"[email protected]"
"[email protected]"
"[email protected]"
];
};
# Timeout TTY after 1 hour
programs.bash.interactiveShellInit = "if [[ $(tty) =~ /dev\\/tty[1-6] ]]; then TMOUT=3600; fi";
# Don't lock kernel modules, this is also enabled by the hardening profile by default
security.lockKernelModules = false;
# Run security analysis
environment.systemPackages = with pkgs; [lynis];
# Technically we don't need this as we use pubkey authentication
services.fail2ban = lib.mkIf cfgServers {
enable = true;
ignoreIP = [
"100.0.0.0/8"
"127.0.0.1/8"
];
};
};
}
Impermanence
{
lib,
config,
...
}: {
# Rollback function for BTRFS rootfs
# This assumes we have a LUKS volume named "crypted"
# https://mt-caret.github.io/blog/posts/2020-06-29-optin-state.html
boot.initrd = {
enable = true;
supportedFilesystems = ["btrfs"];
systemd.services.restore-root = {
description = "Rollback BTRFS rootfs";
wantedBy = ["initrd.target"];
after = ["[email protected]"];
before = ["sysroot.mount"];
unitConfig.DefaultDependencies = "no";
serviceConfig.Type = "oneshot";
script = ''
mkdir -p /mnt
# We first mount the btrfs root to /mnt
# so we can manipulate btrfs subvolumes.
mount -o subvol=/ /dev/mapper/crypted /mnt
# While we're tempted to just delete /root and create
# a new snapshot from /root-blank, /root is already
# populated at this point with a number of subvolumes,
# which makes `btrfs subvolume delete` fail.
# So, we remove them first.
btrfs subvolume list -o /mnt/root |
cut -f9 -d' ' |
while read subvolume; do
echo "deleting /$subvolume subvolume..."
btrfs subvolume delete "/mnt/$subvolume"
done &&
echo "deleting /root subvolume..." &&
btrfs subvolume delete /mnt/root
echo "restoring blank /root subvolume..."
btrfs subvolume snapshot /mnt/root-blank /mnt/root
# Once we're done rolling back to a blank snapshot,
# we can unmount /mnt and continue on the boot process.
umount /mnt
'';
};
};
# Rollback results in sudo lectures after each reboot
security.sudo.extraConfig = ''
Defaults lecture = never
'';
# Persistent files
environment.persistence."/persist" = {
hideMounts = true;
directories =
[
"/etc/NetworkManager/system-connections"
"/etc/nixos"
"/etc/pacman.d/gnupg"
"/etc/secureboot"
"/var/cache/tailscale"
"/var/lib/AccountsService/icons"
"/var/lib/bluetooth"
"/var/lib/containers"
"/var/lib/flatpak"
"/var/lib/libvirt"
"/var/lib/machines"
"/var/lib/sddm"
"/var/lib/systemd"
"/var/lib/tailscale"
"/var/lib/nixos"
"/var/lib/upower"
"/var/lib/vnstat"
"/var/cache"
{
directory = "/var/lib/iwd";
mode = "u=rwx,g=,o=";
}
]
++ lib.optionals config.security.acme.acceptTerms [
{
directory = "/var/lib/acme";
user = "acme";
group = "acme";
mode = "0755";
}
]
++ lib.optionals config.services.printing.enable [
{
directory = "/var/lib/cups";
user = "root";
group = "root";
mode = "0700";
}
]
++ lib.optionals config.services.fail2ban.enable [
{
directory = "/var/lib/fail2ban";
user = "fail2ban";
group = "fail2ban";
mode = "0750";
}
]
++ lib.optionals config.services.postgresql.enable [
{
directory = "/var/lib/postgresql";
user = "postgres";
group = "postgres";
mode = "0700";
}
]
++ lib.optionals config.services.loki.enable [
{
directory = "/var/lib/loki";
user = "loki";
group = "loki";
mode = "0700";
}
]
++ lib.optionals config.services.grafana.enable [
{
directory = config.services.grafana.dataDir;
user = "grafana";
group = "grafana";
mode = "0700";
}
]
++ lib.optionals config.services.vaultwarden.enable [
{
directory = "/var/lib/vaultwarden";
user = "vaultwarden";
group = "vaultwarden";
mode = "0700";
}
]
++ lib.optionals config.services.influxdb2.enable [
{
directory = "/var/lib/influxdb2";
user = "influxdb2";
group = "influxdb2";
mode = "0700";
}
]
++ lib.optionals config.services.telegraf.enable [
{
directory = "/var/lib/telegraf";
user = "telegraf";
group = "telegraf";
mode = "0700";
}
]
++ lib.optionals config.services.adguardhome.enable [
{
directory = "/var/lib/private/AdGuardHome";
user = "root";
group = "root";
mode = "0700";
}
];
users."root" = {
home = "/root";
directories = [
{
directory = ".gnupg";
mode = "0700";
}
{
directory = ".ssh";
mode = "0700";
}
];
};
users."nico" = {
directories = [
# Important user data
".android"
".ansible"
".config"
".firedragon"
".gitkraken"
".java"
".local/share/AyuGramDesktop"
".local/share/JetBrains"
".local/share/Nextcloud"
".local/share/PrismLauncher"
".local/share/SpeedCrunch"
".local/share/Steam"
".local/share/TelegramDesktop"
".local/share/Vorta"
".local/share/baloo"
".local/share/containers"
".local/share/direnv"
".local/share/dolphin"
".local/share/fish"
".local/share/godot"
".local/share/heroku"
".local/share/jupyter"
".local/share/kactivitymanagerd"
".local/share/klipper"
".local/share/knewstuff3"
".local/share/konsole"
".local/share/kpeoplevcard"
".local/share/krita"
".local/share/kscreen"
".local/share/kwalletd"
".local/share/lutris"
".local/share/plasma"
".local/share/plasma-systemmonitor"
".local/share/zed"
".mozilla/native-messaging-hosts"
".pki"
".steam"
".thunderbird/default"
".tldrc"
".wakatime"
".yubico"
"Documents"
"Downloads"
"Games"
"Music"
"Nextcloud"
"Pictures"
"School"
"Sync"
"Videos"
"VirtualBox VMs"
# Cache stuff, not actual user data
".cache/JetBrains"
".cache/bookmarksrunner"
".cache/chromium"
".cache/containers"
".cache/firedragon"
".cache/konsole"
".cache/lutris"
".cache/mesa_shader_cache"
".cache/mozilla"
".cache/spotify"
".cache/systemsettings"
".cache/thunderbird"
".local/share/Trash"
".local/state/syncthing"
".local/state/wireplumber"
# Special permissions needed for those
{
directory = ".gnupg";
mode = "0700";
}
{
directory = ".local/share/keybase";
mode = "0700";
}
{
directory = ".local/share/keyrings";
mode = "0700";
}
{
directory = ".ssh";
mode = "0700";
}
];
files = [
".bash_history"
".wakatime.bdb"
".wakatime.cfg"
];
};
};
}
Locales
{
config,
lib,
...
}: let
cfg = config.dr460nixed.locales;
de = "de_DE.UTF-8";
defaultLocale = "en_GB.UTF-8";
in {
options.dr460nixed.locales = with lib; {
enable =
mkOption
{
default = true;
type = types.bool;
description = mdDoc ''
Whether the operating system be having a default set of locales set.
'';
};
};
config = lib.mkIf cfg.enable {
# Timezone
time = {
hardwareClockInLocalTime = true;
timeZone = "Europe/Berlin";
};
# Common locale settings
i18n = {
inherit defaultLocale;
extraLocaleSettings = {
LANG = defaultLocale;
LC_COLLATE = defaultLocale;
LC_CTYPE = defaultLocale;
LC_MESSAGES = defaultLocale;
LC_ADDRESS = de;
LC_IDENTIFICATION = de;
LC_MEASUREMENT = de;
LC_MONETARY = de;
LC_NAME = de;
LC_NUMERIC = de;
LC_PAPER = de;
LC_TELEPHONE = de;
LC_TIME = de;
};
supportedLocales = [
"C.UTF-8/UTF-8"
"de_DE.UTF-8/UTF-8"
"en_GB.UTF-8/UTF-8"
"en_US.UTF-8/UTF-8"
];
};
# Console font
console.keyMap = "de";
};
}
MSMTP
{
config,
lib,
...
}: let
cfg = config.dr460nixed.smtp;
in {
options.dr460nixed.smtp = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Enable sending mails via CMD using msmtp.
'';
};
};
config = lib.mkIf cfg.enable {
programs.msmtp = {
enable = true;
setSendmail = true;
defaults = {
aliases = "/etc/aliases";
auth = "login";
port = 465;
tls = "on";
tls_starttls = "off";
tls_trust_file = "/etc/ssl/certs/ca-certificates.crt";
};
accounts = {
default = {
from = "[email protected]";
host = "mail.garudalinux.net";
passwordeval = "cat /run/secrets/passwords/[email protected]";
user = "[email protected]";
};
};
};
environment.etc = {
"aliases".text = ''
root: [email protected]
'';
};
};
}
Networking
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed;
in {
# We want to use NetworkManager on desktops
networking = {
# Pointing to NextDNS via Tailscale
# if not, Cloudflare would still be my choice
nameservers = [
"1.1.1.1"
"2606:4700:4700::1111"
"1.0.0.1"
"2606:4700:4700::1001"
];
networkmanager = lib.mkIf cfg.desktops.enable {
# This is required to workaround Tailscale not recovering from net change
# https://github.com/tailscale/tailscale/issues/8223
dispatcherScripts = [
{
source = pkgs.writeScript "restartTailscaled" ''
#!/usr/bin/env ${pkgs.bash}/bin/bash
if [[ "$1" != "wlan0" ]]; then
exit 0
fi
if [[ "$2" == "up" ]]; then
if [[ $(${pkgs.iputils}/bin/ping -W 1 -c 1 garudalinux.org) != 0 ]]; then
logger "Wlan0 up, restarting tailscaled"
${pkgs.systemd}/bin/systemctl restart tailscaled
fi
fi
'';
type = "basic";
}
];
dns = "none";
enable = true;
};
# Enable nftables instead of iptables
nftables.enable = true;
};
# Enable SSHD & bandwidth usage tracking
services = {
openssh.enable = true;
vnstat.enable = true;
};
# Enable Mosh, a replacement for OpenSSH
programs.mosh.enable = true;
}
Nix
{
config,
inputs,
lib,
pkgs,
...
}: let
cfgRemote = config.dr460nixed.remote-build;
in {
options.dr460nixed = with lib; {
remote-build = {
enable = mkOption {
default = false;
example = true;
type = types.bool;
description = mdDoc ''
Enable the capability of building via nix on a remote machine when specified via command line flag.
'';
};
enableGlobally = mkOption {
default = false;
example = true;
type = types.bool;
description = mdDoc ''
Enables remote builds via enableDistributedBuild rather than making it opt-in via command line.
'';
};
host = mkOption {
default = "";
type = types.str;
example = "dragons-ryzen";
description = mdDoc ''
Specifies the target host for remote builds.
'';
};
port = mkOption {
default = 22;
type = types.int;
example = 1022;
description = mdDoc ''
Specifies the target port for remote builds.
'';
};
trustedPublicKey = mkOption {
default = null;
type = types.str;
example = "remote-build:8vrLBvFoMiKVKRYD//30bhUBTEEiuupfdQzl2UoMms4=";
description = mdDoc ''
Specifies the substitutors cache signing key for remote builds.
'';
};
user = mkOption {
default = null;
type = types.str;
example = "build";
description = mdDoc ''
Specifies the target user for remote builds.
'';
};
};
};
config = {
# General nix settings
nix = {
# The remote builder to use for distributed builds
buildMachines = lib.mkIf cfgRemote.enable [
{
hostName = cfgRemote.host;
maxJobs = 16;
protocol = "ssh-ng";
supportedFeatures = ["nixos-test" "benchmark" "big-parallel" "kvm"];
systems = ["x86_64-linux" "aarch64-linux"];
}
];
# Allow distributed builds
distributedBuilds = lib.mkIf cfgRemote.enableGlobally true;
# Dont warn about dirty flakes and accept flake configs by default
extraOptions = ''
http-connections = 0
warn-dirty = false
'';
# Set the nix path, needed e.g. for Nixd
nixPath = ["nixpkgs=${inputs.nixpkgs}"];
# Nix.conf settings
settings = {
# Accept flake configs by default
accept-flake-config = true;
# Test out ca-derivations (https://nixos.wiki/wiki/Ca-derivations)
experimental-features = ["ca-derivations"];
# Lix cache
extra-substituters = ["https://cache.lix.systems"];
# For direnv GC roots
keep-derivations = true;
keep-outputs = true;
# Continue building derivations if one fails
keep-going = true;
# Show more log lines for failed builds
log-lines = 20;
# Max number of parallel jobs
max-jobs = "auto";
# Enable certain system features
system-features = ["big-parallel" "kvm"];
# Build inside sandboxed environments
sandbox = pkgs.stdenv.isLinux;
# Trust the remote machines cache signatures
trusted-substituters = lib.mkIf cfgRemote.enable ["ssh-ng://${cfgRemote.host}"];
# Specify the path to the nix registry
flake-registry = "/etc/nix/registry.json";
substituters = [
"https://cache.garnix.io" # extra things here and there
# "https://cache.saumon.network/proxmox-nixos" # proxmox on NixOS - SSL failure as of 240817
"https://catppuccin.cachix.org" # a cache for Catppuccin-nix
"https://nix-community.cachix.org" # nix-community cache
"https://nix-gaming.cachix.org" # nix-gaming
"https://nixpkgs-unfree.cachix.org" # unfree-package cache
"https://numtide.cachix.org" # another unfree package cache
"https://pre-commit-hooks.cachix.org" # pre-commit hooks
];
trusted-public-keys = [
"cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="
"catppuccin.cachix.org-1:noG/4HkbhJb+lUAdKrph6LaozJvAeEEZj4N732IysmU="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
"nix-gaming.cachix.org-1:nbjlureqMbRAxR1gJ/f3hxemL9svXaZF/Ees8vCUUs4="
"nixpkgs-unfree.cachix.org-1:hqvoInulhbV4nJ9yJOEr+4wxhDV4xq2d1DK7S6Nj6rs="
"numtide.cachix.org-1:2ps1kLBUWjxIneOy1Ik6cQjb41X0iXVXeHigGmycPPE="
"pre-commit-hooks.cachix.org-1:Pkk3Panw5AW24TOv6kz3PvLhlH8puAsJTBbOPmBo7Rc="
# "proxmox-nixos:nveXDuVVhFDRFx8Dn19f1WDEaNRJjPrF2CPD2D+m1ys="
];
};
};
environment = {
etc = with inputs; {
# set channels (backwards compatibility)
"nix/flake-channels/home-manager".source = home-manager;
"nix/flake-channels/nixpkgs".source = nixpkgs;
"nix/flake-channels/system".source = self;
# preserve current flake in /etc
"nixos/flake".source = self;
};
# Git is required for flakes, and cachix for binary substituters
systemPackages = with pkgs; [git cachix];
};
# Let root ssh into the remote builder seamlessly
home-manager.users."root" = lib.mkIf cfgRemote.enable {
home.stateVersion = "24.05"; # Specify this since its otherwise unset
programs.ssh.extraConfig = ''
Host ${cfgRemote.host}
HostName ${cfgRemote.host}
Port ${toString cfgRemote.port}
User ${cfgRemote.user}
'';
};
# Supply a shortcut for the remote builder
programs = {
bash.shellAliases = {
"rem" = "sudo nix build -v --builders ssh://${cfgRemote.host}";
"remb" = "sudo nixos-rebuild switch -v --builders ssh://${cfgRemote.host} --flake";
};
fish = {
shellAbbrs = {
"rem" = "sudo nix build -v --builders ssh://${cfgRemote.host}";
"remb" = "sudo nixos-rebuild switch -v --builders ssh://${cfgRemote.host} --flake";
};
};
};
};
}
OCI
{
config,
lib,
...
}: let
cfg = config.dr460nixed.oci;
in {
options.dr460nixed.oci = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Enable common options for Oracle cloud instances.
'';
};
};
config = lib.mkIf cfg.enable {
# Taken from /proc/cmdline of Ubuntu 20.04.2 LTS on OCI
boot = {
kernelParams = [
"nvme.shutdown_timeout=10"
"nvme_core.shutdown_timeout=10"
"libiscsi.debug_libiscsi_eh=1"
"crash_kexec_post_notifiers"
"console=tty1"
"console=ttyS0"
"console=ttyAMA0,115200"
];
loader.grub = {
device = "nodev";
efiInstallAsRemovable = true;
efiSupport = true;
enable = lib.mkForce true; # overrides our boot module
extraConfig = ''
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
terminal_input --append serial
terminal_output --append serial
'';
splashImage = null;
};
};
# https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/configuringntpservice.htm#Configuring_the_Oracle_Cloud_Infrastructure_NTP_Service_for_an_Instance
networking.timeServers = ["169.254.169.254"];
# Slows down write operations considerably
nix.settings.auto-optimise-store = lib.mkForce false;
# This is needed as the packages are marked unsupported
hardware.cpu = {
amd.updateMicrocode = lib.mkForce false;
intel.updateMicrocode = lib.mkForce false;
};
};
}
Servers
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.servers;
in {
options.dr460nixed.servers = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Whether this device is a server.
'';
};
monitoring =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Whether to enable monitoring via Netdata.
'';
};
};
config = lib.mkIf cfg.enable {
# The common used config is not available
programs.fish.shellInit = lib.mkForce ''
set fish_greeting
fastfetch -l nixos
'';
# The excellent CachyOS kernel
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_cachyos-server;
# Automatic server upgrades
dr460nixed.auto-upgrade = lib.mkDefault true;
# No custom aliases
dr460nixed.shells.enable = lib.mkDefault false;
# These aren't needed on servers, but default on GNS
garuda = {
audio.pipewire.enable = false;
hardware.enable = false;
networking.enable = false;
};
boot.plymouth.enable = false;
# Enable the Netdata daemon
services.netdata.enable = lib.mkIf cfg.monitoring true;
services.netdata.config = {
global = {
"dbengine disk space" = "512";
"memory mode" = "dbengine";
"update every" = "2";
};
ml = {"enabled" = "yes";};
};
services.netdata.configDir = {
"go.d.conf" = pkgs.writeText "go.d.conf" ''
enabled: yes
modules:
nginx: yes
web_log: yes
'';
"python.d.conf" = pkgs.writeText "python.d.conf" ''
postgres: no
web_log: no
'';
"go.d/nginx.conf" =
lib.mkIf config.services.nginx.enable
(pkgs.writeText "nginx.conf" ''
jobs:
- name: local
url: http://127.0.0.1/nginx_status
'');
};
# Extra Python & system packages required for Netdata to function
services.netdata.package = pkgs.netdata.override {withCloud = true;};
services.netdata.python.extraPackages = ps: [ps.psycopg2];
systemd.services.netdata = {path = with pkgs; [jq];};
# Connect to Netdata Cloud easily
services.netdata.claimTokenFile = config.sops.secrets."api_keys/netdata".path;
sops.secrets."api_keys/netdata" = {
mode = "0600";
owner = "netdata";
path = "/run/secrets/api_keys/netdata";
};
# The Nginx QUIC package with Brotli modules
services.nginx.package = pkgs.nginxQuic;
services.nginx.additionalModules = with pkgs; [nginxModules.brotli];
# Recommended settings replacing custom configuration
services.nginx = {
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedTlsSettings = true;
};
# Statuspage for Netdata to consume
services.nginx.statusPage = true;
# Upstream resolvers
services.nginx.resolver = {
addresses = ["100.100.100.100"];
valid = "60s";
};
# Global Nginx configuration
services.nginx.appendConfig = ''
worker_processes auto;
'';
# Logformat to use for Netdata & extra config that doesn't exist as separate key in NixOS
services.nginx.commonHttpConfig = ''
# Custom log format for Netdata to analyze
log_format custom '"$http_referer" "$http_user_agent" '
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent';
# Brotli compression
brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_types application/atom+xml application/javascript application/json application/rss+xml
application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype
application/x-font-ttf application/x-javascript application/xhtml+xml application/xml
font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon
image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml;
# Misc
aio threads;
'';
# Diffie-Hellman parameter for DHE ciphersuites
security.dhparams = lib.mkIf config.services.nginx.enable {
defaultBitSize = 3072;
enable = true;
params.nginx = {};
};
services.nginx.sslDhparam = config.security.dhparams.params.nginx.path;
# Default catch-all for unknown domains
services.nginx.virtualHosts."_" = lib.mkIf config.services.nginx.enable {
addSSL = true;
extraConfig = ''
log_not_found off;
return 404;
'';
http3 = true;
quic = true;
useACMEHost = "dr460nf1r3.org";
};
# Need to explicitly open our web server ports
networking.firewall = lib.mkIf config.services.nginx.enable {
allowedTCPPorts = [80 443];
allowedUDPPorts = [443];
};
# Make cloudflared happy (https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size)
boot.kernel.sysctl = lib.mkIf config.services.cloudflared.enable {
"net.core.rmem_max" = 7500000;
"net.core.wmem_max" = 7500000;
};
# SSL certs for the server
security.acme = {
acceptTerms = true;
defaults = {
group = "nginx";
email = "[email protected]";
};
certs."dr460nf1r3.org" = {
extraDomainNames = ["*.dr460nf1r3.org"];
dnsProvider = "cloudflare";
dnsPropagationCheck = true;
credentialsFile = config.sops.secrets."api_keys/cloudflare".path;
};
};
sops.secrets."api_keys/cloudflare" = {
mode = "0400";
owner = "acme";
path = "/run/secrets/api_keys/cloudflare";
};
};
}
Shells
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.shells;
in {
options.dr460nixed.shells.enable = with lib;
mkOption
{
default = true;
type = types.bool;
description = mdDoc ''
Whether the shell should receive our aliases and themes.
'';
};
config = lib.mkIf cfg.enable {
# Programs & global config
programs = {
bash.shellAliases = {
"bootsda" = ''
sudo qemu-kvm \
-m 4G \
-drive file=/dev/sda,format=raw,index=0,media=disk \
-net user,hostfwd=tcp:127.0.0.1:2222-:22 \
-net nic
'';
"bootsdb" = ''
sudo qemu-kvm
-m 4G \
-drive file=/dev/sdb,format=raw,index=0,media=disk \
-net user,hostfwd=tcp:127.0.0.1:2222-:22 \
-net nic
'';
"gpl" = "${pkgs.curl}/bin/curl https://www.gnu.org/licenses/gpl-3.0.txt -o LICENSE";
"grep" = "${pkgs.ugrep}/bin/ugrep";
};
fish = {
shellAbbrs = {
"bootusb" = ''
sudo qemu-kvm \
-m 4G \
-drive file=/dev/sda,format=raw,index=0,media=disk \
-net user,hostfwd=tcp:127.0.0.1:2222-:22 \
-net nic
'';
"gpl" = "${pkgs.curl}/bin/curl https://www.gnu.org/licenses/gpl-3.0.txt -o LICENSE";
};
shellAliases = {
"grep" = "${pkgs.ugrep}/bin/ugrep";
};
};
};
};
}
Syncthing
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.syncthing;
settingsFormat = pkgs.formats.json {};
in {
options.dr460nixed.syncthing = {
enable =
lib.mkOption
{
default = false;
type = lib.types.bool;
description = lib.mdDoc ''
Enable common file synchronisation between devices.
'';
};
key =
lib.mkOption
{
default = "";
type = lib.types.str;
description = lib.mdDoc ''
The key to use for Syncthing.
'';
};
cert =
lib.mkOption
{
default = "";
type = lib.types.str;
description = lib.mdDoc ''
The cert to use for Syncthing.
'';
};
devices =
lib.mkOption
{
default = [];
type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
freeformType = settingsFormat.type;
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
description = lib.mdDoc ''
The name of the device.
'';
};
id = lib.mkOption {
type = lib.types.str;
description = lib.mdDoc ''
The device ID. See <https://docs.syncthing.net/dev/device-ids.html>.
'';
};
autoAcceptFolders = lib.mkOption {
type = lib.types.bool;
default = false;
description = lib.mdDoc ''
Automatically create or share folders that this device advertises at the default path.
See <https://docs.syncthing.net/users/config.html?highlight=autoaccept#config-file-format>.
'';
};
};
}));
description = lib.mdDoc ''
The devices to sync with.
'';
};
devicesNames =
lib.mkOption
{
default = [];
type = lib.types.listOf lib.types.str;
description = lib.mdDoc ''
The names of the devices to sync with.
'';
};
user =
lib.mkOption
{
default = "";
type = lib.types.str;
description = lib.mdDoc ''
The user to run syncthing as.
'';
};
};
config = lib.mkIf cfg.enable {
services.syncthing = {
inherit (cfg) cert;
dataDir = "/home/${cfg.user}";
enable = true;
inherit (cfg) key;
settings = {
inherit (cfg) devices;
folders = {
"/home/nico/Music" = {
id = "ybqqh-as53c";
devices = cfg.devicesNames;
};
"/home/nico/Pictures" = {
id = "9gj2u-j3m9s";
devices = cfg.devicesNames;
};
"/home/nico/School" = {
id = "g5jha-cnrr4";
devices = cfg.devicesNames;
};
"/home/nico/Sync" = {
id = "u62ge-wzsau";
devices = cfg.devicesNames;
};
"/home/nico/Videos" = {
id = "nxhpo-c2j5b";
devices = cfg.devicesNames;
};
};
options = {
localAnnounceEnabled = true;
urAccepted = -1;
};
};
inherit (cfg) user;
};
};
}
Tailscale TLS
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.tailscale-tls;
domainExpression =
if cfg.domain-override != null
then cfg.domain-override
else "$(${pkgs.tailscale}/bin/tailscale cert 2>&1 | grep use | cut -d '\"' -f2)";
in {
options.dr460nixed.tailscale-tls = with lib; {
enable = mkEnableOption "Automatic Tailscale certificates renewal";
target = mkOption {
type = types.str;
description = "Where to put certificates";
default = "/var/lib/tailscale-tls";
};
mode = mkOption {
type = types.str;
description = "File mode for certificates";
default = "0640";
};
domain-override = mkOption {
type = types.nullOr types.str;
description = "Override domain. Defaults to suggested one by tailscale";
default = null;
};
};
config = lib.mkIf cfg.enable {
users.users.tailscale-tls = {
group = "tailscale-tls";
home = "/var/lib/tailscale-tls";
isSystemUser = true;
};
users.groups.tailscale-tls = {};
systemd.services.tailscale-tls = {
description = "Automatic Tailscale certificates";
after = ["network-pre.target" "tailscale.service"];
wants = ["network-pre.target" "tailscale.service"];
wantedBy = ["multi-user.target"];
serviceConfig.Type = "oneshot";
script = ''
status="Starting"
until [ $status = "Running" ]; do
sleep 2
status=$(${pkgs.tailscale}/bin/tailscale status -json | ${pkgs.jq}/bin/jq -r .BackendState)
done
mkdir -p "${cfg.target}"
DOMAIN=${domainExpression}
${pkgs.tailscale}/bin/tailscale cert \
--cert-file "${cfg.target}/cert.crt" \
--key-file "${cfg.target}/key.key" \
"$DOMAIN"
chown -R tailscale-tls:tailscale-tls "${cfg.target}"
chmod ${cfg.mode} "${cfg.target}/cert.crt" "${cfg.target}/key.key"
'';
};
systemd.timers.tailscale-tls = {
description = "Automatic Tailscale certificates renewal";
after = ["network-pre.target" "tailscale.service"];
wants = ["network-pre.target" "tailscale.service"];
wantedBy = ["multi-user.target"];
timerConfig = {
OnCalendar = "weekly";
Persistent = "true";
Unit = "schedule-test.service";
};
};
};
}
Tailscale
{
config,
lib,
pkgs,
...
}: let
cfg = config.dr460nixed.tailscale;
tailscaleJoinArgsList =
[
"-authkey"
"$(cat ${cfg.authFile})"
]
++ cfg.extraUpArgs;
tailscaleJoinArgsString = builtins.concatStringsSep " " tailscaleJoinArgsList;
tailscaleUpScript = ''
sleep 2
status="$(${pkgs.tailscale}/bin/tailscale status -json | ${pkgs.jq}/bin/jq -r .BackendState)"
if [ $status = "Running" ]; then # if so, then do nothing
exit 0
fi
${pkgs.tailscale}/bin/tailscale up ${tailscaleJoinArgsString}
'';
in {
options.dr460nixed.tailscale = with lib; {
enable = mkEnableOption "Tailscale client daemon";
autoConnect = mkOption {
type = types.bool;
default = false;
description = "Whether to automatically connect to Tailscale using an auth key";
};
authFile = mkOption {
type = types.path;
example = "/run/secrets/tailscale-key";
description = "File location storing tailscale auth-key";
};
extraUpArgs = mkOption {
type = with types; listOf str;
default = [];
description = "Extra args for tailscale up";
};
};
config = lib.mkIf cfg.enable {
# Enable Tailscale service
services.tailscale.enable = true;
# Install Tailscale systray
environment.systemPackages = lib.mkIf config.dr460nixed.desktops.enable [pkgs.tailscale-systray];
# Allow Tailscale devices to connect
networking.firewall.trustedInterfaces = ["tailscale0"];
# Connect to Tailnet automatically
systemd.services.tailscale-autoconnect = lib.mkIf cfg.autoConnect {
description = "Automatic connection to Tailscale";
# Make sure tailscale is running before trying to connect to tailscale
after = ["network-pre.target" "tailscale.service"];
wants = ["network-pre.target" "tailscale.service"];
wantedBy = ["multi-user.target"];
serviceConfig.Type = "oneshot";
script = tailscaleUpScript;
};
};
}
Users
{
config,
keys,
lib,
...
}: let
# Use fixed UIDs/GIDs
deterministicIds = let
uidGid = id: {
gid = id;
uid = id;
};
in {
acme = uidGid 999;
adbusers = uidGid 998;
adguard = uidGid 977;
avahi = uidGid 997;
chaotic_op = uidGid 996;
cloudflared = uidGid 972;
code-server = uidGid 967;
dhcpcd = uidGid 976;
fwupd-refresh = uidGid 975;
flatpak = uidGid 974;
forgejo = uidGid 963;
gamemode = uidGid 969;
grafana = uidGid 995;
incus-admin = uidGid 665;
influxdb2 = uidGid 994;
jellyfin = uidGid 970;
loki = uidGid 993;
minecraft = uidGid 973;
netdata = uidGid 979;
nico = uidGid 1000;
nm-iodine = uidGid 992;
node-exporter = uidGid 991;
nscd = uidGid 990;
plocate = uidGid 989;
polkituser = uidGid 988;
paperless = uidGid 966;
podman = uidGid 968;
proc = uidGid 971;
promtail = uidGid 987;
redis-paperless = uidGid 965;
resolvconf = uidGid 964;
rtkit = uidGid 986;
sshd = uidGid 985;
systemd-coredump = uidGid 984;
systemd-oom = uidGid 983;
tailscale-tls = uidGid 978;
telegraf = uidGid 982;
vnstatd = uidGid 981;
wakapi = uidGid 962;
wireshark = uidGid 980;
};
# Add groups to user only if they exist
ifTheyExist = groups: builtins.filter (group: builtins.hasAttr group config.users.groups) groups;
in {
# This is needed for early set up of user accounts
sops.secrets = {
"passwords/nico".neededForUsers = true;
"passwords/root".neededForUsers = true;
};
users = {
inherit deterministicIds;
# All users are immuntable; if a password is required it needs to be set via passwordFile
mutableUsers = false;
users = {
nico = {
autoSubUidGidRange = false; # Use fixed UIDs/GIDs
extraGroups =
[
"audio"
"video"
"wheel"
]
++ ifTheyExist [
"adbusers"
"chaotic_op"
"deluge"
"disk"
"docker"
"flatpak"
"git"
"kvm"
"libvirtd"
"minecraft"
"mysql"
"network"
"networkmanager"
"podman"
"systemd-journal"
"wireshark"
];
hashedPasswordFile = config.sops.secrets."passwords/nico".path;
home = "/home/nico";
isNormalUser = true;
openssh.authorizedKeys.keyFiles = [keys.nico];
subGidRanges = lib.mkIf config.virtualisation.podman.enable [
{
count = 65536;
startGid = 615536;
}
];
subUidRanges = [
{
count = 65536;
startUid = 615536;
}
];
};
# Lock root password
root.hashedPasswordFile = config.sops.secrets."passwords/root".path;
};
};
security.sudo.extraRules = [
{
# allow wheel group to run nixos-rebuild without password
groups = ["wheel"];
commands = let
currentSystem = "/run/current-system/";
storePath = "/nix/store/";
in [
{
command = "${storePath}/*/bin/switch-to-configuration";
options = ["SETENV" "NOPASSWD"];
}
{
command = "${currentSystem}/sw/bin/nix-store";
options = ["SETENV" "NOPASSWD"];
}
{
command = "${currentSystem}/sw/bin/nixos-rebuild";
options = ["NOPASSWD"];
}
{
# let wheel group collect garbage without password
command = "${currentSystem}/sw/bin/nix-collect-garbage";
options = ["SETENV" "NOPASSWD"];
}
{
# let wheel group interact with systemd without password
command = "${currentSystem}/sw/bin/systemctl";
options = ["NOPASSWD"];
}
];
}
];
# Allow pushing to Cachix
# sops.secrets."api_keys/cachix" = {
# mode = "0600";
# owner = config.users.users.nico.name;
# path = "/home/nico/.config/cachix/cachix.dhall";
# };
}
ZFS
{
config,
lib,
...
}: let
cfg = config.dr460nixed.zfs;
in {
options.dr460nixed.zfs = with lib; {
enable =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Configures common options for using ZFS on NixOS.
'';
};
sendMails =
mkOption
{
default = false;
type = types.bool;
description = mdDoc ''
Enables sending status reports about ZFS maintenance via email.
'';
};
};
config = lib.mkIf cfg.enable {
# Support booting off ZFS
boot.supportedFilesystems = ["zfs"];
# Always request encryption credentials to open rootfs
boot.zfs.requestEncryptionCredentials = true;
# Useful ZFS maintenance
services.zfs = {
autoScrub = {
enable = true;
interval = "weekly";
};
trim = {
enable = true;
interval = "weekly";
};
};
# Enable configuration of msmtp
dr460nixed.smtp.enable = lib.mkIf cfg.sendMails true;
# Configure ZFS Event Daemon to use msmtp
# commented until python2.7-oildev is fixed
# services.zfs.zed.settings = mkIf cfg.sendMails {
# ZED_DEBUG_LOG = "/tmp/zed.debug.log";
# ZED_EMAIL_ADDR = ["root"];
# ZED_EMAIL_OPTS = "@ADDRESS@";
# ZED_EMAIL_PROG = "${pkgs.msmtp}/bin/msmtp";
# ZED_NOTIFY_INTERVAL_SECS = 3600;
# ZED_NOTIFY_VERBOSE = true;
# ZED_SCRUB_AFTER_RESILVER = true;
# ZED_USE_ENCLOSURE_LEDS = true;
# };
# This option does not work; will return error
services.zfs.zed.enableMail = lib.mkIf cfg.sendMails false;
# Metrics
services.telegraf.extraConfig.inputs = lib.mkIf config.services.telegraf.enable {
zfs.poolMetrics = true;
};
};
}
Credits
Main sources
A special thanks to PedroHLC, who always gives great advice and who is also the reason I'm using NixOS today. Also, I studied Mysterio77's and NotAShelf's Nix configurations while building this one.