blumeops/nixos/ringtail/configuration.nix
Erich Blume dd1cf4f198 Configure Librewolf to delegate claude-cli:// URIs to xdg-open
The xdg desktop entry and mimeapps were already registered but
Librewolf doesn't delegate unknown URI schemes to the system
handler by default. This adds user.js prefs to complete the chain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:37:16 -07:00

583 lines
17 KiB
Nix

{ config, pkgs, lib, ... }:
let
# Libraries needed by mise-compiled runtimes (python-build, etc.)
buildDeps = with pkgs; [ zlib readline bzip2 xz libffi ncurses sqlite openssl ];
in
{
# Allow unfree packages (NVIDIA drivers, Steam)
nixpkgs.config.allowUnfree = true;
# Bootloader
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# No TPM module on this board
systemd.tpm2.enable = false;
# Networking
networking.hostName = "ringtail";
networking.networkmanager.enable = true;
# Time zone
time.timeZone = "America/Los_Angeles";
# Locale
i18n.defaultLocale = "en_US.UTF-8";
# NVIDIA proprietary drivers
hardware.graphics.enable = true;
services.xserver.videoDrivers = [ "nvidia" ];
hardware.nvidia = {
modesetting.enable = true;
open = false; # Use proprietary driver for RTX 4080
nvidiaSettings = true;
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
# NVIDIA container toolkit (CDI specs + runtime for containerd/k3s GPU pods)
hardware.nvidia-container-toolkit.enable = true;
# Stable path to NVIDIA driver libraries for k3s device plugin pod mounts.
# Avoids mounting all of /nix/store — only the driver derivation is needed.
environment.etc."nvidia-driver/lib".source = "${config.hardware.nvidia.package}/lib";
# Stable-path wrapper for nvidia-container-runtime.cdi (the CDI-based OCI
# runtime that injects GPU devices/libs from NixOS-generated CDI specs).
# The wrapper adds runc to PATH since k3s doesn't ship a standalone runc binary.
environment.etc."nvidia-container-runtime/nvidia-runtime-cdi-wrapper" = {
mode = "0755";
text = ''
#!/bin/sh
export PATH="${pkgs.runc}/bin:$PATH"
exec ${pkgs.nvidia-container-toolkit.tools}/bin/nvidia-container-runtime.cdi "$@"
'';
};
# NFS client support (required for k3s to mount NFS PersistentVolumes)
boot.supportedFilesystems = [ "nfs" ];
# Wayland / Sway
programs.sway = {
enable = true;
wrapperFeatures.gtk = true;
extraSessionCommands = ''
export WLR_NO_HARDWARE_CURSORS=1
'';
extraPackages = with pkgs; [
swaylock
swayidle
wezterm # terminal
wmenu # app launcher
mako # notifications
grim # screenshots
slurp # region selection
];
};
security.polkit.enable = true;
security.pam.services.swaylock = {}; # Allow swaylock to authenticate
security.sudo.wheelNeedsPassword = false;
# Enable greetd as display manager for sway
services.greetd = {
enable = true;
settings = {
default_session = {
command = "${pkgs.tuigreet}/bin/tuigreet --time --cmd 'sway --unsupported-gpu'";
user = "greeter";
};
};
};
# PipeWire for audio
services.pipewire = {
enable = true;
alsa.enable = true;
pulse.enable = true;
};
# Bluetooth
hardware.bluetooth = {
enable = true;
powerOnBoot = true;
};
services.blueman.enable = true;
# Fish shell
programs.fish.enable = true;
# 1Password (modules handle CLI group/setgid and polkit for GUI integration)
programs._1password.enable = true;
programs._1password-gui = {
enable = true;
polkitPolicyOwners = [ "eblume" ];
};
# K3s single-node cluster
services.k3s = {
enable = true;
role = "server";
tokenFile = "/etc/k3s/token";
extraFlags = toString [
"--disable=traefik"
"--disable=servicelb"
"--disable=metrics-server"
"--write-kubeconfig-mode=644"
"--tls-san=ringtail.tail8d86e.ts.net"
];
containerdConfigTemplate = ''
{{ template "base" . }}
[plugins.'io.containerd.cri.v1.runtime']
enable_cdi = true
cdi_spec_dirs = ["/var/run/cdi", "/etc/cdi"]
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.nvidia]
privileged_without_host_devices = false
runtime_type = "io.containerd.runc.v2"
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.nvidia.options]
BinaryName = "/etc/nvidia-container-runtime/nvidia-runtime-cdi-wrapper"
'';
};
# K3s containerd registry mirrors (pull through Zot on indri)
environment.etc."rancher/k3s/registries.yaml".source = ./k3s-registries.yaml;
# Tailscale
services.tailscale = {
enable = true;
extraUpFlags = [ "--accept-routes" "--ssh" ];
};
# Trust Tailscale and k3s CNI interfaces
# - tailscale0: ArgoCD on indri connects via tailnet
# - cni0/flannel.1: k3s pod overlay network (pods must reach host API server)
networking.firewall.trustedInterfaces = [ "tailscale0" "cni0" "flannel.1" ];
# SSH
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
# User account
users.users.eblume = {
isNormalUser = true;
shell = pkgs.fish;
extraGroups = [ "wheel" "networkmanager" "video" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILmh1SSCdDAyu3vkSQH7kAXEPDi8APyjo9JXDTjtha2j"
];
};
# System packages
environment.systemPackages = with pkgs; [
git
kubectl
python3 # required for Ansible
vim
htop
curl
wget
chezmoi
neovim
eza
fd
fzf
zoxide
starship
atuin
bat
ripgrep
mise
gcc
gnumake
pkg-config
openssl
gnupg
unzip
fuzzel
pulseaudio
librewolf
];
# Allow running dynamically linked binaries (mise-installed runtimes, etc.)
programs.nix-ld.enable = true;
programs.nix-ld.libraries = buildDeps ++ [ pkgs.icu ];
# Compile-time flags for mise python-build and similar source builds
environment.sessionVariables = {
PKG_CONFIG_PATH = lib.makeSearchPath "lib/pkgconfig" (map lib.getDev buildDeps);
CFLAGS = lib.concatMapStringsSep " " (p: "-I${lib.getDev p}/include") buildDeps;
LDFLAGS = lib.concatMapStringsSep " " (p: "-L${lib.getLib p}/lib") buildDeps;
};
# Fonts
fonts.packages = with pkgs; [
nerd-fonts.victor-mono
];
# Home Manager (minimal — chezmoi owns dotfiles, this is ringtail-specific)
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.eblume = {
home.stateVersion = "25.11";
# Librewolf: delegate claude-cli:// URIs to system handler (xdg-open)
home.file.".config/librewolf/librewolf/backlhkh.default/user.js".text = ''
user_pref("network.protocol-handler.expose.claude-cli", false);
user_pref("network.protocol-handler.external.claude-cli", true);
user_pref("network.protocol-handler.warn-external.claude-cli", false);
'';
wayland.windowManager.sway = {
enable = true;
checkConfig = false;
config = {
terminal = "wezterm";
modifier = "Mod4";
fonts = {
names = [ "VictorMono Nerd Font" ];
size = 10.0;
};
bars = [{ command = "waybar"; }];
gaps = {
inner = 8;
outer = 4;
};
window = {
border = 2;
titlebar = false;
commands = [
{ command = "inhibit_idle fullscreen"; criteria = { class = ".*"; }; }
{ command = "inhibit_idle fullscreen"; criteria = { app_id = ".*"; }; }
{ command = "fullscreen enable"; criteria = { class = "steam_app_1174180"; }; }
];
};
colors = {
focused = {
border = "#8aadf4";
background = "#24273a";
text = "#cad3f5";
indicator = "#c6a0f6";
childBorder = "#8aadf4";
};
focusedInactive = {
border = "#494d64";
background = "#1e2030";
text = "#a5adcb";
indicator = "#494d64";
childBorder = "#494d64";
};
unfocused = {
border = "#363a4f";
background = "#1e2030";
text = "#6e738d";
indicator = "#363a4f";
childBorder = "#363a4f";
};
urgent = {
border = "#ed8796";
background = "#24273a";
text = "#cad3f5";
indicator = "#ed8796";
childBorder = "#ed8796";
};
};
input = {
"*" = {
xkb_options = "ctrl:nocaps";
};
};
output = {
"DP-1" = {
mode = "2560x1440@165Hz";
adaptive_sync = "on";
bg = "~/.config/sway/wallpaper.jpg fill";
};
};
keybindings = let mod = "Mod4"; in {
"${mod}+Return" = "exec wezterm";
"${mod}+Shift+q" = "kill";
"${mod}+d" = "exec wmenu-run";
"${mod}+space" = "exec fuzzel";
"${mod}+Shift+c" = "reload";
"${mod}+l" = "exec swaylock -f";
"--locked XF86AudioMute" = "exec pactl set-sink-mute @DEFAULT_SINK@ toggle";
"--locked XF86AudioLowerVolume" = "exec pactl set-sink-volume @DEFAULT_SINK@ -5%";
"--locked XF86AudioRaiseVolume" = "exec pactl set-sink-volume @DEFAULT_SINK@ +5%";
"--locked XF86AudioMicMute" = "exec pactl set-source-mute @DEFAULT_SOURCE@ toggle";
};
startup = [
{ command = "1password"; }
{ command = "steam"; }
];
};
};
programs.swaylock = {
enable = true;
settings = {
color = "24273a";
font = "VictorMono Nerd Font";
font-size = 24;
indicator-radius = 100;
indicator-thickness = 7;
inside-color = "24273a";
inside-clear-color = "24273a";
inside-ver-color = "24273a";
inside-wrong-color = "24273a";
key-hl-color = "8aadf4";
bs-hl-color = "ed8796";
ring-color = "363a4f";
ring-clear-color = "f5a97f";
ring-ver-color = "8aadf4";
ring-wrong-color = "ed8796";
line-color = "00000000";
line-clear-color = "00000000";
line-ver-color = "00000000";
line-wrong-color = "00000000";
separator-color = "00000000";
text-color = "cad3f5";
text-clear-color = "cad3f5";
text-ver-color = "cad3f5";
text-wrong-color = "ed8796";
show-failed-attempts = true;
};
};
services.swayidle = {
enable = true;
events = [
{ event = "before-sleep"; command = "${pkgs.swaylock}/bin/swaylock -f"; }
{ event = "lock"; command = "${pkgs.swaylock}/bin/swaylock -f"; }
];
timeouts = [
{
timeout = 900; # 15 minutes — lock screen
command = "${pkgs.swaylock}/bin/swaylock -f";
}
{
timeout = 3600; # 60 minutes — turn off display
command = "${pkgs.sway}/bin/swaymsg 'output * dpms off'";
resumeCommand = "${pkgs.sway}/bin/swaymsg 'output * dpms on'";
}
];
};
# Claude Code OAuth callback handler (claude-cli:// URI scheme)
xdg.desktopEntries.claude-code-url-handler = {
name = "Claude Code URL Handler";
exec = "/run/current-system/sw/bin/mise exec -- claude --handle-uri %u";
type = "Application";
noDisplay = true;
mimeType = [ "x-scheme-handler/claude-cli" ];
};
xdg.mimeApps = {
enable = true;
defaultApplications = {
"x-scheme-handler/claude-cli" = [ "claude-code-url-handler.desktop" ];
};
};
programs.fuzzel = {
enable = true;
settings = {
main = {
font = "VictorMono Nerd Font:size=14";
terminal = "wezterm";
width = 40;
horizontal-pad = 16;
vertical-pad = 8;
border-radius = 8;
border-width = 2;
};
colors = {
background = "24273add";
text = "cad3f5ff";
match = "8aadf4ff";
selection = "363a4fff";
selection-text = "cad3f5ff";
selection-match = "8aadf4ff";
border = "8aadf4ff";
};
};
};
programs.waybar = {
enable = true;
settings = [{
layer = "top";
position = "top";
height = 30;
modules-left = [ "sway/workspaces" "sway/mode" ];
modules-center = [ "sway/window" ];
modules-right = [ "pulseaudio" "bluetooth" "network" "clock" "tray" ];
tray = { spacing = 8; };
clock = { format = "{:%a %b %d %H:%M}"; };
network = {
interval = 2;
format-ethernet = "{bandwidthDownBits} down {bandwidthUpBits} up";
format-wifi = "{essid} {bandwidthDownBits} down {bandwidthUpBits} up";
format-disconnected = "disconnected";
};
pulseaudio = {
format = "{icon} {volume}%";
format-muted = " muted";
format-icons = {
headphone = "";
default = [ "" "" "" ];
};
};
}];
style = ''
* {
font-family: "VictorMono Nerd Font";
font-size: 13px;
border: none;
border-radius: 0;
min-height: 0;
}
window#waybar {
background-color: rgba(30, 32, 48, 0.9);
color: #cad3f5;
margin: 4px 4px 0 4px;
}
#workspaces button {
padding: 0 8px;
margin: 0 2px;
color: #6e738d;
background: transparent;
border-radius: 4px;
}
#workspaces button.focused {
color: #8aadf4;
background: #363a4f;
border-bottom: 2px solid #8aadf4;
}
#workspaces button.urgent {
color: #ed8796;
}
#window {
color: #a5adcb;
}
#bluetooth {
color: #8aadf4;
}
#bluetooth.off, #bluetooth.disabled {
color: #6e738d;
}
#clock, #network, #pulseaudio, #bluetooth, #tray {
padding: 0 12px;
margin: 4px 2px;
color: #cad3f5;
background: #363a4f;
border-radius: 4px;
}
#clock {
color: #8aadf4;
}
#pulseaudio {
color: #f5a97f;
}
#network {
color: #a6da95;
}
#network.disconnected {
color: #ed8796;
}
'';
};
};
# Ensure mounted drives are owned by eblume
systemd.tmpfiles.rules = [
"d /mnt/games 0755 eblume users -"
"d /mnt/storage1 0755 eblume users -"
"d /mnt/storage2 0755 eblume users -"
];
# Container config for skopeo (used by the forgejo runner to push images)
# and for unqualified image pulls via Zot pull-through cache
environment.etc."containers/policy.json".text = builtins.toJSON {
default = [{ type = "insecureAcceptAnything"; }];
};
environment.etc."containers/registries.conf".text = ''
unqualified-search-registries = ["registry.ops.eblu.me", "docker.io", "ghcr.io", "quay.io"]
'';
# Tor Snowflake proxy (anti-censorship bridge, not an exit node)
systemd.services.snowflake-proxy = {
description = "Tor Snowflake Proxy";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = toString [
"${pkgs.snowflake}/bin/proxy"
"-metrics" "-metrics-address" "0.0.0.0"
"-geoipdb" "${pkgs.tor.geoip}/share/tor/geoip"
"-geoip6db" "${pkgs.tor.geoip}/share/tor/geoip6"
];
DynamicUser = true;
Restart = "always";
RestartSec = 10;
# Hardening
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
RestrictRealtime = true;
MemoryDenyWriteExecute = true;
MemoryMax = "512M";
};
};
# Forgejo Actions runner (nix container builder)
services.gitea-actions-runner = {
package = pkgs.forgejo-runner;
instances.nix_container_builder = {
enable = true;
name = "ringtail-nix-builder";
url = "https://forge.ops.eblu.me";
tokenFile = "/etc/forgejo-runner/token.env";
labels = [ "nix-container-builder:host" ];
hostPackages = with pkgs; [
bash coreutils curl gawk gitMinimal gnused jq nodejs wget
nix skopeo
];
settings = {
log.level = "info";
runner = {
capacity = 1;
timeout = "3h";
};
};
};
};
# Enable nix flakes
nix.settings.experimental-features = [ "nix-command" "flakes" ];
# Allow the runner's dynamic user to access the nix daemon
nix.settings.trusted-users = [ "gitea-runner" ];
# Prevent machine from sleeping (workstation should stay on)
systemd.sleep.extraConfig = ''
AllowSuspend=no
AllowHibernation=no
AllowHybridSleep=no
AllowSuspendThenHibernate=no
'';
# NixOS release
system.stateVersion = "25.11";
}