diff --git a/.sops.yaml b/.sops.yaml index d93b121..7f329f8 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -16,7 +16,6 @@ keys: - &gpd-win4 age1ceg548u5ma6rgu3xgvd254y5xefqrdqfqhcjsjp3255q976fgd2qaua53d - &nb age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz - &amzebs-01 age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w - - &nas age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek creation_rules: - path_regex: ^[^/]+\.yaml$ @@ -81,14 +80,6 @@ creation_rules: - *dominik2 - *nb - *amzebs-01 - - path_regex: hosts/nas/[^/]+\.yaml$ - key_groups: - - age: - - *bitwarden - - *dominik - - *dominik2 - - *nb - - *nas - path_regex: hosts/mail/[^/]+\.yaml$ key_groups: - age: diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d745136..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,92 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Repository Overview - -This is a NixOS infrastructure repository managing multiple hosts (servers and personal machines) using a modular Nix configuration approach with SOPS for secrets management and Bento for deployment. - -## Build and Test Commands - -```bash -# Enter development shell (sets up MCP configs) -nix-shell - -# Test configuration before deployment (required before PRs) -./scripts/test-configuration -./scripts/test-configuration -v # with --show-trace - -# Edit encrypted secrets -nix-shell -p sops --run 'sops hosts//secrets.yaml' - -# Update secrets keys after adding new age keys -./scripts/update-secrets-keys - -# Format Nix files -nix run nixpkgs#nixpkgs-fmt . - -# Compute hash for new packages -nix hash to-sri --type sha256 $(nix-prefetch-url https://example.com/file.tar.gz) -``` - -## Architecture - -### Host Structure -Each host in `hosts//` contains: -- `configuration.nix` - Main entry point importing modules -- `hardware-configuration.nix` - Machine-specific hardware config -- `secrets.yaml` - SOPS-encrypted secrets -- `modules/` - Host-specific service configurations -- `fleet.nix` → symlink to root `fleet.nix` (SFTP user provisioning) -- `utils/` → symlink to root `utils/` (shared modules) - -Current hosts: `fw` (firewall/router), `nb` (notebook), `web-arm`, `mail`, `amzebs-01` - -### Shared Components (`utils/`) -- `modules/` - Reusable NixOS modules (nginx, sops, borgbackup, lego, promtail, etc.) -- `overlays/` - Nixpkgs overlays -- `pkgs/` - Custom package derivations -- `bento.nix` - Deployment helper module - -### Secrets Management -- SOPS with age encryption; keys defined in `.sops.yaml` -- Each host has its own age key derived from SSH host key -- Host secrets in `hosts//secrets.yaml` -- Shared module secrets in `utils/modules//secrets.yaml` - -**IMPORTANT: Never modify secrets files directly.** Instead, tell the user which secrets need to be added and where, so they can edit the encrypted files themselves using: -```bash -nix-shell -p sops --run 'sops hosts//secrets.yaml' -``` - -### Deployment -The Git runner handles deployment automatically when changes merge to main. A successful `./scripts/test-configuration ` dry-build is the gate before pushing. - -## Custom Packages - -When creating a new package in `utils/pkgs/`, always include an `update.sh` script to automate version updates. See `utils/pkgs/claude-code/update.sh` for the pattern: - -1. Fetch latest version from upstream (npm, GitHub, etc.) -2. Update version string in `default.nix` -3. Update source hash using `nix-prefetch-url` -4. Update dependency hashes (e.g., `npmDepsHash`) by triggering a build with a fake hash -5. Verify the final build succeeds - -Example structure: -``` -utils/pkgs// -├── default.nix -├── update.sh # Always include this -└── (other files like patches, lock files) -``` - -## Workflow - -**IMPORTANT: Always run `./scripts/test-configuration ` after making any changes** to verify the NixOS configuration builds successfully. This is required before committing. - -## Conventions - -- Nix files: two-space indentation, lower kebab-case naming -- Commits: Conventional Commits format (`fix:`, `feat:`, `chore:`), scope by host when relevant (`fix(mail):`) -- Modules import via explicit paths, not wildcards -- Comments explain non-obvious decisions (open ports, unusual service options) diff --git a/README.md b/README.md index 288cbe2..f5be6f7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ cat ~/.ssh/id_rsa.pub | ssh -p23 u149513-subx@u149513-subx.your-backup.de instal # 4. Add new Host ```console -sftp host@git.cloonar.com:/config/bootstrap.sh ./ +sftp host.cloonar.com@git.cloonar.com:/config/bootstrap.sh ./ ``` # 5. Yubikey diff --git a/fleet.nix b/fleet.nix index e8a8ab5..bc49e64 100644 --- a/fleet.nix +++ b/fleet.nix @@ -47,11 +47,6 @@ username = "gpd-win4"; key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjfS2DtS8PQgkf86dU+EVu5t+r/QlCWmY7+RPYprQrO"; } - { - username = "nas"; - key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICS6b97LPUpr7/kWvOcI40s5e+gfbfz0I2/hAPL6zTmU"; - } - { username = "amzebs-01"; key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMkFZ60SPl8pzEtGrFq1+n6ZkDuNe3xJaccJMjr3y/q"; diff --git a/hosts/fw/configuration.nix b/hosts/fw/configuration.nix index 543f492..be2f706 100644 --- a/hosts/fw/configuration.nix +++ b/hosts/fw/configuration.nix @@ -49,7 +49,7 @@ ./modules/firefox-sync.nix ./modules/fivefilters.nix - # ./modules/pyload + ./modules/pyload # home assistant ./modules/home-assistant diff --git a/hosts/fw/modules/dnsmasq.nix b/hosts/fw/modules/dnsmasq.nix index d2884e3..c323f0b 100644 --- a/hosts/fw/modules/dnsmasq.nix +++ b/hosts/fw/modules/dnsmasq.nix @@ -73,7 +73,6 @@ "02:00:00:00:00:04,${config.networkPrefix}.97.6,matrix" "ea:db:d4:c1:18:ba,${config.networkPrefix}.97.50,git" "c2:4f:64:dd:13:0c,${config.networkPrefix}.97.20,home-assistant" - "6c:1f:f7:8e:a9:86,${config.networkPrefix}.97.11,nas" "1a:c4:04:6e:29:02,${config.networkPrefix}.101.25,deconz" "c4:a7:2b:c7:ea:30,${config.networkPrefix}.99.10,metz" diff --git a/hosts/fw/modules/web/proxies.nix b/hosts/fw/modules/web/proxies.nix index 5e62a11..5991ea5 100644 --- a/hosts/fw/modules/web/proxies.nix +++ b/hosts/fw/modules/web/proxies.nix @@ -41,7 +41,6 @@ # Restrict to internal LAN only extraConfig = '' allow ${config.networkPrefix}.96.0/24; - allow ${config.networkPrefix}.97.0/24; allow ${config.networkPrefix}.98.0/24; deny all; ''; @@ -60,7 +59,6 @@ # Restrict to internal LAN only extraConfig = '' allow ${config.networkPrefix}.96.0/24; - allow ${config.networkPrefix}.97.0/24; allow ${config.networkPrefix}.98.0/24; allow ${config.networkPrefix}.99.0/24; deny all; diff --git a/hosts/nas/configuration.nix b/hosts/nas/configuration.nix deleted file mode 100644 index d07c2e5..0000000 --- a/hosts/nas/configuration.nix +++ /dev/null @@ -1,87 +0,0 @@ -# NAS host configuration -{ config, lib, pkgs, ... }: -let - impermanence = builtins.fetchTarball "https://github.com/nix-community/impermanence/archive/master.tar.gz"; -in { - nixpkgs.config.allowUnfree = true; - - imports = [ - "${impermanence}/nixos.nix" - ./utils/bento.nix - ./utils/modules/sops.nix - - ./modules/pyload.nix - ./modules/jellyfin.nix - - ./hardware-configuration.nix - ]; - - nixpkgs.overlays = [ - (import ./utils/overlays/packages.nix) - ]; - - # Hostname - networking.hostName = "nas"; - - # Timezone - time.timeZone = "Europe/Vienna"; - console.keyMap = "de"; - - # SSH server - services.openssh.enable = true; - users.users.root.openssh.authorizedKeys.keys = [ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7" - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRQuPqH5fdX3KEw7DXzWEdO3AlUn1oSmtJtHB71ICoH Generated By Termius" - ]; - - # Firewall - networking.firewall.enable = true; - networking.firewall.allowedTCPPorts = [ 22 ]; - - # SOPS configuration - sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; - sops.defaultSopsFile = ./secrets.yaml; - - # Btrfs maintenance - services.btrfs.autoScrub = { - enable = true; - interval = "monthly"; - fileSystems = [ "/nix" ]; - }; - - # Impermanence - persist important directories - # Note: /var/lib/downloads and /var/lib/multimedia are mounted from LVM on RAID - environment.persistence."/nix/persist/system" = { - hideMounts = true; - directories = [ - "/var/lib/pyload" - "/var/lib/jellyfin" - "/var/log" - "/var/lib/nixos" - "/var/bento" - "/root/.ssh" - ]; - files = [ - "/etc/machine-id" - { file = "/etc/ssh/ssh_host_ed25519_key"; parentDirectory = { mode = "u=rwx,g=,o="; }; } - { file = "/etc/ssh/ssh_host_ed25519_key.pub"; parentDirectory = { mode = "u=rwx,g=,o="; }; } - { file = "/etc/ssh/ssh_host_rsa_key"; parentDirectory = { mode = "u=rwx,g=,o="; }; } - { file = "/etc/ssh/ssh_host_rsa_key.pub"; parentDirectory = { mode = "u=rwx,g=,o="; }; } - ]; - }; - - # Nix settings - nix = { - settings = { - auto-optimise-store = true; - experimental-features = [ "nix-command" "flakes" ]; - }; - gc = { - automatic = true; - dates = "weekly"; - options = "--delete-older-than 14d"; - }; - }; - - system.stateVersion = "24.05"; -} diff --git a/hosts/nas/fleet.nix b/hosts/nas/fleet.nix deleted file mode 120000 index 5b16de1..0000000 --- a/hosts/nas/fleet.nix +++ /dev/null @@ -1 +0,0 @@ -../../fleet.nix \ No newline at end of file diff --git a/hosts/nas/hardware-configuration.nix b/hosts/nas/hardware-configuration.nix deleted file mode 100644 index a3c3edd..0000000 --- a/hosts/nas/hardware-configuration.nix +++ /dev/null @@ -1,92 +0,0 @@ -# Hardware configuration for NAS -# TODO: Update disk labels/UUIDs after installation -{ config, lib, pkgs, modulesPath, ... }: -{ - imports = [ - (modulesPath + "/installer/scan/not-detected.nix") - ]; - - boot.loader.systemd-boot = { - enable = true; - configurationLimit = 5; - }; - boot.loader.efi.canTouchEfiVariables = true; - boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usb_storage" "sd_mod" ]; - boot.initrd.kernelModules = [ "dm-snapshot" ]; - boot.kernelModules = [ "kvm-intel" ]; - boot.extraModulePackages = [ ]; - - # RAID 1 array for data storage - boot.swraid = { - enable = true; - mdadmConf = '' - DEVICE /dev/disk/by-id/ata-ST18000NM000J-2TV103_ZR52TBSB-part1 - DEVICE /dev/disk/by-id/ata-ST18000NM000J-2TV103_ZR52V9QX-part1 - ''; - }; - - # Tmpfs root filesystem (ephemeral - resets on reboot) - fileSystems."/" = { - device = "none"; - fsType = "tmpfs"; - options = [ "size=8G" "mode=755" ]; - }; - - # Boot partition - EFI - fileSystems."/boot" = { - device = "/dev/disk/by-partlabel/BOOT"; - fsType = "vfat"; - }; - - fileSystems."/nix" = { - device = "/dev/disk/by-partlabel/NIXOS"; - fsType = "btrfs"; - neededForBoot = true; - options = [ - "subvol=@" - "compress=zstd:1" - "noatime" - "commit=120" - ]; - }; - - fileSystems."/nix/store" = { - device = "/dev/disk/by-partlabel/NIXOS"; - fsType = "btrfs"; - neededForBoot = true; - options = [ - "subvol=@nix-store" - "compress=zstd:1" - "noatime" - "commit=120" - ]; - }; - - fileSystems."/nix/persist" = { - device = "/dev/disk/by-partlabel/NIXOS"; - fsType = "btrfs"; - neededForBoot = true; - options = [ - "subvol=@nix-persist" - "compress=zstd:1" - "noatime" - "commit=120" - ]; - }; - - # LVM volumes on RAID array - fileSystems."/var/lib/downloads" = { - device = "/dev/vg-data/lv-downloads"; - fsType = "xfs"; - }; - - fileSystems."/var/lib/multimedia" = { - device = "/dev/vg-data/lv-multimedia"; - fsType = "xfs"; - }; - - # DHCP networking - networking.useDHCP = lib.mkDefault true; - - hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; -} diff --git a/hosts/nas/modules/filebot-process.nix b/hosts/nas/modules/filebot-process.nix deleted file mode 100644 index dd818e9..0000000 --- a/hosts/nas/modules/filebot-process.nix +++ /dev/null @@ -1,89 +0,0 @@ -{ pkgs }: - -pkgs.writeShellScriptBin "filebot-process" '' - #!/usr/bin/env bash - set -euo pipefail - - # FileBot AMC script for automated media organization - # Called by PyLoad's package_extracted hook with parameters: - # $1 = package_id - # $2 = package_name - # $3 = download_folder (actual path to extracted files) - # $4 = password (optional) - - PACKAGE_ID="''${1:-}" - PACKAGE_NAME="''${2:-unknown}" - DOWNLOAD_DIR="''${3:-/var/lib/downloads}" - PASSWORD="''${4:-}" - - OUTPUT_DIR="/var/lib/multimedia" - LOG_FILE="/var/lib/pyload/filebot-amc.log" - EXCLUDE_LIST="/var/lib/pyload/filebot-exclude-list.txt" - - # Ensure FileBot data directory exists - mkdir -p /var/lib/pyload/.local/share/filebot/data - mkdir -p "$(dirname "$LOG_FILE")" - touch "$EXCLUDE_LIST" - - # Install FileBot license if not already installed - if [ ! -f /var/lib/pyload/.local/share/filebot/data/.license ]; then - echo "$(date): Installing FileBot license..." >> "$LOG_FILE" - ${pkgs.filebot}/bin/filebot --license /var/lib/pyload/filebot-license.psm || true - fi - - echo "===========================================" >> "$LOG_FILE" - echo "$(date): PyLoad package extracted hook triggered" >> "$LOG_FILE" - echo "Package ID: $PACKAGE_ID" >> "$LOG_FILE" - echo "Package Name: $PACKAGE_NAME" >> "$LOG_FILE" - echo "Download Directory: $DOWNLOAD_DIR" >> "$LOG_FILE" - echo "===========================================" >> "$LOG_FILE" - - # Check if download directory exists and has media files - if [ ! -d "$DOWNLOAD_DIR" ]; then - echo "$(date): Download directory does not exist: $DOWNLOAD_DIR" >> "$LOG_FILE" - exit 0 - fi - - # Check if there are any video/media files to process - if ! find "$DOWNLOAD_DIR" -type f \( -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.m4v" -o -iname "*.mov" \) -print -quit | grep -q .; then - echo "$(date): No media files found in: $DOWNLOAD_DIR" >> "$LOG_FILE" - echo "$(date): Skipping FileBot processing" >> "$LOG_FILE" - exit 0 - fi - - echo "$(date): Starting FileBot processing" >> "$LOG_FILE" - - # Run FileBot AMC script - set +e # Temporarily disable exit on error to capture exit code - ${pkgs.filebot}/bin/filebot \ - -script fn:amc \ - --output "$OUTPUT_DIR" \ - --action move \ - --conflict auto \ - -non-strict \ - --log-file "$LOG_FILE" \ - --def \ - excludeList="$EXCLUDE_LIST" \ - movieFormat="$OUTPUT_DIR/movies/{n} ({y})/{n} ({y}) - {vf}" \ - seriesFormat="$OUTPUT_DIR/tv-shows/{n}/Season {s.pad(2)}/{n} - {s00e00} - {t}" \ - ut_dir="$DOWNLOAD_DIR" \ - ut_kind=multi \ - clean=y \ - skipExtract=y - - FILEBOT_EXIT_CODE=$? - set -e # Re-enable exit on error - - if [ $FILEBOT_EXIT_CODE -ne 0 ]; then - echo "$(date): FileBot processing failed with exit code $FILEBOT_EXIT_CODE" >> "$LOG_FILE" - exit 0 # Don't fail the hook even if FileBot fails - fi - - echo "$(date): FileBot processing completed successfully" >> "$LOG_FILE" - - # Clean up any remaining empty directories - find "$DOWNLOAD_DIR" -type d -empty -delete 2>/dev/null || true - - echo "$(date): All processing completed" >> "$LOG_FILE" - exit 0 -'' diff --git a/hosts/nas/modules/jellyfin.nix b/hosts/nas/modules/jellyfin.nix deleted file mode 100644 index 5243800..0000000 --- a/hosts/nas/modules/jellyfin.nix +++ /dev/null @@ -1,51 +0,0 @@ -{ lib, pkgs, ... }: { - # Intel graphics support for hardware transcoding - hardware.graphics = { - enable = true; - extraPackages = with pkgs; [ - intel-media-driver - vpl-gpu-rt - intel-compute-runtime - ]; - }; - - # Set VA-API driver to iHD (modern Intel driver) - environment.sessionVariables = { - LIBVA_DRIVER_NAME = "iHD"; - }; - - # Jellyfin user with render/video groups for GPU access - users.users.jellyfin = { - isSystemUser = true; - group = "jellyfin"; - home = "/var/lib/jellyfin"; - createHome = true; - extraGroups = [ "render" "video" ]; - }; - users.groups.jellyfin = {}; - - # Create jellyfin directory - systemd.tmpfiles.rules = [ - "d /var/lib/jellyfin 0755 jellyfin jellyfin - -" - ]; - - services.jellyfin = { - enable = true; - openFirewall = true; - }; - - # Override systemd hardening for GPU access - systemd.services.jellyfin = { - serviceConfig = { - PrivateUsers = lib.mkForce false; # Disable user namespacing - breaks GPU device access - DeviceAllow = [ - "/dev/dri/card0 rw" - "/dev/dri/renderD128 rw" - ]; - SupplementaryGroups = [ "render" "video" ]; # Critical: Explicit group membership for GPU access - }; - environment = { - LIBVA_DRIVER_NAME = "iHD"; # Ensure service sees this variable - }; - }; -} diff --git a/hosts/nas/modules/pyload.nix b/hosts/nas/modules/pyload.nix deleted file mode 100644 index ecb44b0..0000000 --- a/hosts/nas/modules/pyload.nix +++ /dev/null @@ -1,104 +0,0 @@ -{ config, pkgs, lib, ... }: -let - filebotScript = pkgs.callPackage ./filebot-process.nix {}; -in -{ - nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ - "unrar" - "filebot" - ]; - - environment.systemPackages = with pkgs; [ - unrar # Required for RAR archive extraction - p7zip # Required for 7z and other archive formats - ]; - - # Create directory structure - systemd.tmpfiles.rules = [ - "d /var/lib/downloads 0755 pyload pyload - -" - "d /var/lib/multimedia 0775 root jellyfin - -" - "d /var/lib/multimedia/movies 0775 jellyfin jellyfin - -" - "d /var/lib/multimedia/tv-shows 0775 jellyfin jellyfin - -" - "d /var/lib/multimedia/music 0755 jellyfin jellyfin - -" - - # PyLoad hook scripts directory - "d /var/lib/pyload/config 0755 pyload pyload - -" - "d /var/lib/pyload/config/scripts 0755 pyload pyload - -" - "d /var/lib/pyload/config/scripts/package_extracted 0755 pyload pyload - -" - "L+ /var/lib/pyload/config/scripts/package_extracted/filebot-process.sh - - - - ${filebotScript}/bin/filebot-process" - ]; - - # FileBot license secret (only if secrets.yaml exists) - sops.secrets.filebot-license = { - mode = "0440"; - owner = "pyload"; - group = "pyload"; - path = "/var/lib/pyload/filebot-license.psm"; - }; - - # PyLoad user with jellyfin group membership for multimedia access - users.users.pyload = { - isSystemUser = true; - group = "pyload"; - home = "/var/lib/pyload"; - createHome = true; - extraGroups = [ "jellyfin" ]; - }; - users.groups.pyload = {}; - - services.pyload = { - enable = true; - downloadDirectory = "/var/lib/downloads"; - listenAddress = "0.0.0.0"; - port = 8000; - }; - - # Configure pyload service - systemd.services.pyload = { - # Add extraction tools to service PATH - path = with pkgs; [ - unrar # For RAR extraction - p7zip # For 7z extraction - ]; - - environment = { - # Disable SSL certificate verification - PYLOAD__GENERAL__SSL_VERIFY = "0"; - - # Download speed limiting (150 Mbit/s = 19200 KiB/s) - PYLOAD__DOWNLOAD__LIMIT_SPEED = "1"; - PYLOAD__DOWNLOAD__MAX_SPEED = "19200"; - - # Enable ExtractArchive plugin - PYLOAD__EXTRACTARCHIVE__ENABLED = "1"; - PYLOAD__EXTRACTARCHIVE__DELETE = "1"; - PYLOAD__EXTRACTARCHIVE__DELTOTRASH = "0"; - PYLOAD__EXTRACTARCHIVE__REPAIR = "1"; - PYLOAD__EXTRACTARCHIVE__RECURSIVE = "1"; - PYLOAD__EXTRACTARCHIVE__FULLPATH = "1"; - - # Enable ExternalScripts plugin for hooks - PYLOAD__EXTERNALSCRIPTS__ENABLED = "1"; - PYLOAD__EXTERNALSCRIPTS__UNLOCK = "1"; # Run hooks asynchronously - }; - - serviceConfig = { - # Bind mount multimedia directory as writable for FileBot hook scripts - BindPaths = [ "/var/lib/multimedia" ]; - - # Override SystemCallFilter to allow @resources syscalls - # FileBot (Java) needs resource management syscalls like setpriority - # during cleanup operations. Still block privileged syscalls for security. - SystemCallFilter = lib.mkForce [ - "@system-service" - "@resources" # Explicitly allow resource management syscalls - "~@privileged" # Still block privileged operations - "fchown" # Re-allow fchown for FileBot file operations - "fchown32" # 32-bit compatibility - ]; - }; - }; - - # Open firewall for PyLoad web interface - networking.firewall.allowedTCPPorts = [ 8000 ]; -} diff --git a/hosts/nas/secrets.yaml b/hosts/nas/secrets.yaml deleted file mode 100644 index 3d36fe8..0000000 --- a/hosts/nas/secrets.yaml +++ /dev/null @@ -1,43 +0,0 @@ -filebot-license: ENC[AES256_GCM,data:7fmczgcLOp/tkdoMVGfwESam9NwhZrNJXNcnebMKfilEKc81V4fdzS3/hxlueun2At6047UAdDi+6jAO3FvTmzmpmOFnrQxhojUKX2kj9SMSuDlskqxNt04O8wpGy69T8uOeV3ZUcRASzmfzQmuVU+zPKoIV2nq34mhXeIl17V22ZvoTDtFLs2w3IzxtKMZqLtDH4GZ4dkRcQBFcGY96T9R4CLcBqyZeBYb15ZaVLN9dHpQfExtpsBMVB3NikdWgUK9IAbjff1Xtkn+RT9gpQggCyQUl6UbtGqienbCq8ATORe1f89s+q6x/KneC5bdVZKQ4IqZrzDcuQ5aEJUEslc3aVITI3THEBWLOhYANy/4mMyw7R2KRIiHLrXAiiuxi3JumBvev27S6ilKdNo2By8OsbAB/Oj2YvDc5EBTePLCU0OloHIMSxWv5VkLQ2rM94bYtqXN269Lv6w4zmpOF0QblrvtoXnhhf4BQyf9owRDuNMrbp3Iu9Xnj87GoQVnulpwsMSifFxIibd94sRxS/N/5JTTih3Twj/Y8ZpmvoR8t9vCJ14vwBbMXheBpIYzxTk1phZKK1jLfi49DpWudop6iKNOCZdMeXxVRyAfTpOZSGqmdmsqOEDZESCnOAcNYlTxXmLmTdeqJfvTzsnWUi6kbbMYzaSaUlG7UB+RYnt97mcT5NtmbwCRDuFEOdqKGiR/vi7natH8VXe6nlY7xZxei0kkKAhIwyXV+mj/xurbQP3SuyYZriO2luCfuazxmVR0FpBqv+UZ6LwldmQ5e+O7hveYd+o1IjqjT67pVgLyFo1XY95Phq4/vFNvcZKhP7dqKlddkO5RCqjxHCmLowPSUfO2G4k7NV9/UioudATZnWGBpMfSp0pSuD9GXrPg=,iv:5BP77BRudjLiIKI5973BWbQlftupAdfd/aqFeN7DYLM=,tag:q2CAzGZ8lXPS41Uf2NjJTg==,type:str] -sops: - age: - - recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQWUI4YUsvODJtdTN4NDNn - QkFDVmFSV2Q1Q04rcWtiazZEMzFCYzRJMEJrCjM5RE41TjE0eURrNi9iQnBTR1Fy - SENYYmloSjI1c25pck5CSTJZTDhCeTQKLS0tIEs0SnFSNUdsdzZWS0loTEdBN1RD - ZnhBREtlR3o4VTVMZ1RtY1lVbG40YkUK2isPCoJSTQ6CUbHftSDoUZC8MMTqr512 - lCoeGQqnArTO8CWDJxIxRczooTo4mW7vDqD7idWdPgOdWZI8hWPE5Q== - -----END AGE ENCRYPTED FILE----- - - recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwdnJIeWh1RkhvLzF2QUlm - ZmNHaFpzL0M3eUdCdk1GL1g2MTdtdTdjVmdjCmVRTWJsajhKT0E5STN2SEUzWHFa - ak1NelloQnNiY3FaUm9oVGg5eit2eTAKLS0tIEoxcURjUkJsRENtblZpKy9QT0gx - ME5kM1EwYUFNMVFkT3VWZmpGSzRoWFUKzGNK5FzRWiY+E1Je6l0veoN5Z3K2TFMY - pm9+FGuYs+wxSrhLwajITj+NuH0+zK81mrYsugH+6OTNb7cDbLgh/g== - -----END AGE ENCRYPTED FILE----- - - recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlWCs0a3EvZC9sWmI5S2tU - aFpRMVlrZG1zL1ZJSklJMkNQY1RLMW53YUhZCjNpQ1pHUE0yVUxleVovcVRLMFNh - amVFTnJteW8xRjlFY29HWkJrcVJiQzgKLS0tIFJrWWloc2ZWdGdPNlNQM2szTkZI - Zk42dFgrcUJOa3UwSDB3MnpMcVRLdmsKOKbF18HnowVhiEHO2B+BZqpM8Oc8vbDh - hczIpcezwMvv96L2/seX86Hv5mEAQvwN2CVA+sknnDL1XNA/2Ng9cw== - -----END AGE ENCRYPTED FILE----- - - recipient: age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6L1pVc2lRYVVnMU1uR1hn - aldPODkvTVFzNDRuWWFTTU5jU0dyOHFMNkZBCnQ0ZGxUcGR5d0FqK2pOenJzSEN4 - ak12VXhQSnZlbSs1V3BxZnBIQ0xKV1EKLS0tIEkrc00wTzJzVjVDd0o4WHNQVDV6 - WGlpR1kvdXFnMkxOQVVuL3pIckdLRGcK+xoZE63l+9mlR5ufN9kEtgKEHdIUcGbI - CpNhd8RE23tPKaVa0XbQA3bMqc1J9jST3vSWWewexwdLvfjrooSFZw== - -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-11-28T18:05:54Z" - mac: ENC[AES256_GCM,data:rmGDt0ZvZ8S//X1sqzkM9GdsoLBTB9dUprWdVN5M9F4/Zpq6Mpyk04VdGxYz421Gi+AsvhAkBaKi+XJjiEjRf9dYON/N18bWeRe3mJMLVOOoxGz+PQOeAuCyphZEKsCkae79WtbRZqONkU+kSqT5ED6iLjhOpLn1h6Cuw4wV1Xc=,iv:XWjRyxlGP4a14eUaJvZpizy2UiCSIi/PIUyaZg6GCJY=,tag:ZNp/U1O3wkcb8o5s1USrsw==,type:str] - unencrypted_suffix: _unencrypted - version: 3.11.0 diff --git a/hosts/nas/utils b/hosts/nas/utils deleted file mode 120000 index 6b18391..0000000 --- a/hosts/nas/utils +++ /dev/null @@ -1 +0,0 @@ -../../utils \ No newline at end of file diff --git a/hosts/nb/modules/development/nvim/config/sops.lua b/hosts/nb/modules/development/nvim/config/sops.lua index d11a388..e35d153 100644 --- a/hosts/nb/modules/development/nvim/config/sops.lua +++ b/hosts/nb/modules/development/nvim/config/sops.lua @@ -44,7 +44,7 @@ vim.api.nvim_create_autocmd("BufReadPre", { pattern = secrets_patterns, callback = function(args) -- Set filetype to yaml before the file is read so syntax highlighting works - vim.bo[args.buf].filetype = "yaml" + vim.bo.filetype = "yaml" end, }) @@ -53,7 +53,7 @@ vim.api.nvim_create_autocmd("BufReadPost", { group = sops_group, pattern = secrets_patterns, callback = function(args) - local filepath = vim.api.nvim_buf_get_name(args.buf) + local filepath = vim.fn.expand("%:p") -- Only decrypt if file exists and has content if vim.fn.filereadable(filepath) == 1 and vim.fn.getfsize(filepath) > 0 then @@ -98,27 +98,26 @@ vim.api.nvim_create_autocmd("BufReadPost", { return end - -- Detach LSP clients BEFORE replacing buffer to prevent sync errors - detach_lsp_clients(args.buf) - -- Replace buffer content with decrypted content - -- Strip trailing newline to avoid adding extra empty line - vim.api.nvim_buf_set_lines(args.buf, 0, -1, false, vim.split(result:gsub("\n$", ""), "\n")) + vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(result, "\n")) -- Mark buffer as not modified (since we just loaded it) - vim.bo[args.buf].modified = false + vim.bo.modified = false -- Restore cursor position pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos) -- Disable swap, backup, and undo files for security - vim.bo[args.buf].swapfile = false - vim.bo[args.buf].backup = false - vim.bo[args.buf].writebackup = false - vim.bo[args.buf].undofile = false + vim.bo.swapfile = false + vim.bo.backup = false + vim.bo.writebackup = false + vim.bo.undofile = false -- Ensure filetype is set to yaml for syntax highlighting - vim.bo[args.buf].filetype = "yaml" + vim.bo.filetype = "yaml" + + -- Detach LSP clients to prevent sync errors when buffer content is replaced + detach_lsp_clients(0) vim.notify("SOPS: File decrypted successfully", vim.log.levels.INFO) else @@ -133,22 +132,17 @@ vim.api.nvim_create_autocmd("BufWriteCmd", { group = sops_group, pattern = secrets_patterns, callback = function(args) - local filepath = vim.api.nvim_buf_get_name(args.buf) + local filepath = vim.fn.expand("%:p") - if not is_secrets_file(filepath) then - return - end + if is_secrets_file(filepath) then + -- Guard against double-execution + if currently_saving[filepath] then + return + end + currently_saving[filepath] = true - -- Guard against double-execution - if currently_saving[filepath] then - return - end - currently_saving[filepath] = true - - -- Use pcall to ensure guard is always cleared, even on unexpected errors - local ok, err = pcall(function() -- Get current buffer content - local lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, false) + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local content = table.concat(lines, "\n") -- Check buffer content size before encrypting @@ -159,6 +153,8 @@ vim.api.nvim_create_autocmd("BufWriteCmd", { string.format("SOPS: Buffer content too large (%sMB > %sMB limit). Cannot encrypt.", size_mb, limit_mb), vim.log.levels.ERROR ) + -- Don't write anything, leave buffer marked as modified + currently_saving[filepath] = nil return end @@ -166,21 +162,22 @@ vim.api.nvim_create_autocmd("BufWriteCmd", { -- This avoids /dev/stdin issues while keeping secrets secure (not in /tmp) local dir = vim.fn.fnamemodify(filepath, ":h") local filename = vim.fn.fnamemodify(filepath, ":t") - local temp_file = string.format("%s/.%s.sops_tmp_%d_%d", dir, filename, os.time(), vim.loop.hrtime() % 1000000) + local temp_file = string.format("%s/.%s.sops_tmp_%d", dir, filename, os.time()) -- Write plaintext content to temp file local temp_f, temp_err = io.open(temp_file, "w") if not temp_f then vim.notify("SOPS: Failed to create temp file: " .. (temp_err or "unknown error"), vim.log.levels.ERROR) + -- Don't write anything, leave buffer marked as modified + currently_saving[filepath] = nil return end - temp_f:write(content .. "\n") + temp_f:write(content) temp_f:close() -- Encrypt temp file with filename override so SOPS matches .sops.yaml rules -- Uses real filepath for rule matching, temp file for content - local cmd = string.format("timeout %d sops --encrypt --filename-override %s %s", - SOPS_TIMEOUT, + local cmd = string.format("sops --encrypt --filename-override %s %s", vim.fn.shellescape(filepath), vim.fn.shellescape(temp_file)) local encrypted = vim.fn.system(cmd) @@ -189,25 +186,16 @@ vim.api.nvim_create_autocmd("BufWriteCmd", { -- Always clean up temp file, even on error os.remove(temp_file) - -- Check for timeout (exit code 124 from timeout command) - if sops_exit_code == 124 then - vim.notify( - string.format("SOPS: Encryption timed out after %d seconds.", SOPS_TIMEOUT), - vim.log.levels.ERROR - ) - return - end - if sops_exit_code == 0 then -- Write encrypted content directly to file - local file, file_err = io.open(filepath, "w") + local file, err = io.open(filepath, "w") if file then local success, write_err = file:write(encrypted) file:close() if success then -- Mark buffer as saved - vim.bo[args.buf].modified = false + vim.bo.modified = false vim.notify("SOPS: File encrypted and saved successfully", vim.log.levels.INFO) -- Re-decrypt to show plaintext in buffer @@ -219,38 +207,37 @@ vim.api.nvim_create_autocmd("BufWriteCmd", { -- Save cursor position local cursor_pos = vim.api.nvim_win_get_cursor(0) - -- Detach LSP clients BEFORE replacing buffer to prevent sync errors - detach_lsp_clients(args.buf) - -- Replace buffer with decrypted content - -- Strip trailing newline to avoid adding extra empty line - vim.api.nvim_buf_set_lines(args.buf, 0, -1, false, vim.split(decrypted:gsub("\n$", ""), "\n")) + vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(decrypted, "\n")) -- Mark as not modified since we just saved - vim.bo[args.buf].modified = false + vim.bo.modified = false -- Restore cursor position pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos) + + -- Detach LSP clients to prevent sync errors when buffer content is replaced + detach_lsp_clients(0) else vim.notify("SOPS: Could not re-decrypt after save. Buffer may show encrypted content.", vim.log.levels.WARN) end + -- Clear guard after successful save + currently_saving[filepath] = nil else vim.notify("SOPS: Failed to write encrypted content: " .. (write_err or "unknown error"), vim.log.levels.ERROR) + -- Don't mark as saved, keep buffer marked as modified + currently_saving[filepath] = nil end else - vim.notify("SOPS: Failed to open file for writing: " .. (file_err or "unknown error"), vim.log.levels.ERROR) + vim.notify("SOPS: Failed to open file for writing: " .. (err or "unknown error"), vim.log.levels.ERROR) + -- Don't mark as saved, keep buffer marked as modified + currently_saving[filepath] = nil end else vim.notify("SOPS: Failed to encrypt file - NOT SAVED! Error: " .. encrypted, vim.log.levels.ERROR) + -- Don't write anything, leave buffer marked as modified + currently_saving[filepath] = nil end - end) - - -- Always clear guard, even if pcall caught an error - currently_saving[filepath] = nil - - -- Re-throw unexpected errors so they're visible - if not ok then - vim.notify("SOPS: Unexpected error during save: " .. tostring(err), vim.log.levels.ERROR) end end, }) @@ -260,7 +247,7 @@ vim.api.nvim_create_autocmd("BufLeave", { group = sops_group, pattern = secrets_patterns, callback = function(args) - if vim.bo[args.buf].modified then + if vim.bo.modified then vim.notify("Warning: Unsaved changes in secrets file!", vim.log.levels.WARN) end end,