blumeops/nixos/ringtail/configuration.nix
Erich Blume c5d82b0942 Trust k3s CNI interfaces in ringtail NixOS firewall
The NixOS firewall was blocking pod-to-host TCP traffic because only
tailscale0 was trusted. Pods could ping the host but not reach the
API server (port 6443), breaking Tailscale Ingress TLS cert refresh
and all ringtail services (authentik, frigate, ntfy, ollama).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 11:15:02 -08:00

534 lines
15 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" ];
};
# Steam
programs.steam = {
enable = true;
dedicatedServer.openFirewall = true;
};
# 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";
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 = ".*"; }; }
];
};
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'";
}
];
};
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"]
'';
# 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";
}