diff --git a/flake.nix b/flake.nix index 5d1fcde..f3074c1 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,7 @@ mkSharedModules = pkgs: pkgs-unstable: [ ./nixos/modules/base.nix + ./nixos/modules/services/wireguard-peer.nix inputs.home-manager.nixosModules.home-manager ({ ... }: { home-manager.useGlobalPkgs = true; @@ -75,6 +76,7 @@ nixosModules ? [ ], hmModules ? [ ], extraModules ? [ ], + homeManagerUsers ? true, }: let pkgs = mkPkgs system; @@ -91,7 +93,7 @@ ++ [ (./nixos/hosts + "/${hostName}/configuration.nix") ] ++ nixosModules ++ extraModules - ++ [ + ++ nixpkgs.lib.optionals homeManagerUsers [ { home-manager.users.alisceon.imports = [ @@ -167,6 +169,14 @@ nixosModules = serverModules; hmModules = serverHomeModules; }; + + blogbox = mkHost { + hostName = "blogbox"; + system = "x86_64-linux"; + nixosModules = serverModules; + hmModules = serverHomeModules; + homeManagerUsers = false; + }; }; }; } diff --git a/home/modules/programs/waybar/default.nix b/home/modules/programs/waybar/default.nix index 5889e60..e2def8f 100644 --- a/home/modules/programs/waybar/default.nix +++ b/home/modules/programs/waybar/default.nix @@ -1,8 +1,9 @@ -{ pkgs, ... }: +{ pkgs, repoRoot, ... }: let commands = import ../../../../lib/commands.nix { inherit pkgs; }; inherit (commands) uwsm term; height = 20; + wireguardToggle = "${pkgs.xonsh}/bin/xonsh ${repoRoot}/util/toggle_wg.xsh"; in { imports = [ @@ -35,11 +36,12 @@ in }; "modules" = [ "clock" + "idle_inhibitor" "sway/language" "network" + "custom/wireguard" "bluetooth" "pulseaudio" - "idle_inhibitor" ]; }; "sway/workspaces" = { @@ -53,7 +55,7 @@ in spacing = 8; }; idle_inhibitor = { - format = "| {icon}"; + format = "| {icon} "; start-activated = true; format-icons = { activated = "🫨"; @@ -61,7 +63,8 @@ in }; }; "sway/language" = { - format = "| {short}"; + # this one gobbles all trailing ascii whitespace for some reason. use this unicode instead + format = "| {short} "; tooltip-format = "{long}"; }; clock = { @@ -72,29 +75,36 @@ in }; battery = { interval = 60; - format = "| {capacity}%"; - format-charging = "| ch:{capacity}%"; + format = "| {capacity}% "; + format-charging = "| ch:{capacity}% "; }; network = { tooltip-format = "{ifname} = {ipaddr}/{cidr}"; - format-wifi = "| w:{ipaddr}"; - format-ethernet = "| e:{ipaddr}"; - format-linked = "| l:{ipaddr}"; - format-disconnected = "| d"; + format-wifi = "| w:{essid} "; + format-ethernet = "| e:{ipaddr} "; + format-linked = "| l:{ipaddr} "; + format-disconnected = "| w:d "; interval = 15; on-click = "${uwsm} ${term} -e nmtui"; }; + "custom/wireguard" = { + exec = "${wireguardToggle} status"; + on-click = "${wireguardToggle} toggle"; + format = "| wg:{text} "; + interval = 15; + tooltip = false; + }; bluetooth = { - format = "| bt:{num_connections}"; + format = "| bt:{num_connections} "; format-disabled = ""; format-no-controller = ""; interval = 15; on-click = "${uwsm} ${term} -e bluetui"; }; pulseaudio = { - format = "| snd{volume}%"; - format-muted = "| snd:-"; - format-bluetooth = "| snd(bt):{volume}%"; + format = "| snd{volume}% "; + format-muted = "| snd:- "; + format-bluetooth = "| snd(bt):{volume}% "; on-click = "${uwsm} pavucontrol"; }; }; diff --git a/nixos/hosts/alisceon-core/configuration.nix b/nixos/hosts/alisceon-core/configuration.nix index 8468905..222cfa6 100644 --- a/nixos/hosts/alisceon-core/configuration.nix +++ b/nixos/hosts/alisceon-core/configuration.nix @@ -1,63 +1,12 @@ { lib, pkgs, modulesPath, ... }: -let - forgejoDomain = "git.alisceon.com"; - forgejoRunnerTokenFile = "/var/lib/forgejo/runner_token"; - - fetchOciAuthorizedKeys = pkgs.writeShellApplication { - name = "fetch-oci-authorized-keys"; - runtimeInputs = [ - pkgs.coreutils - pkgs.curl - ]; - text = '' - install -d -m 0700 -o alisceon -g users /home/alisceon/.ssh - - if [ -s /home/alisceon/.ssh/authorized_keys ]; then - echo "OCI authorized_keys already present for alisceon" - exit 0 - fi - - curl --fail --silent --show-error --location \ - --header "Authorization: Bearer Oracle" \ - --output /home/alisceon/.ssh/authorized_keys \ - http://169.254.169.254/opc/v2/instance/metadata/ssh_authorized_keys - - chown alisceon:users /home/alisceon/.ssh/authorized_keys - chmod 0600 /home/alisceon/.ssh/authorized_keys - ''; - }; - - generateForgejoRunnerToken = pkgs.writeShellApplication { - name = "generate-forgejo-runner-token"; - runtimeInputs = [ - pkgs.coreutils - pkgs.util-linux - ]; - text = '' - token_file=${lib.escapeShellArg forgejoRunnerTokenFile} - - if [ -s "$token_file" ]; then - chmod 0600 "$token_file" - chown root:root "$token_file" - exit 0 - fi - - install -d -m 0750 -o forgejo -g forgejo /var/lib/forgejo - token="$(runuser -u forgejo -- env \ - FORGEJO_WORK_DIR=/var/lib/forgejo \ - FORGEJO_CUSTOM=/var/lib/forgejo/custom \ - ${lib.getExe pkgs.forgejo-lts} actions generate-runner-token)" - - umask 0077 - printf 'TOKEN=%s\n' "$token" > "$token_file" - chown root:root "$token_file" - chmod 0600 "$token_file" - ''; - }; -in { imports = [ "${modulesPath}/virtualisation/oci-image.nix" + ../../modules/services/cloud-init.nix + ../../modules/services/forgejo.nix + ../../modules/services/nginx.nix + ../../modules/services/oci-authorized-keys.nix + ../../modules/services/tor.nix ]; nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; @@ -69,8 +18,12 @@ in 22 80 443 + 22000 24601 ]; + firewall.allowedUDPPorts = [ + 22000 + ]; }; boot = { @@ -110,6 +63,14 @@ in users.users.alisceon.extraGroups = [ "systemd-journal" ]; + alisceon = { + cloud-init = { + enable = true; + defaultShell = "/run/current-system/sw/bin/xonsh"; + }; + ociAuthorizedKeys.enable = true; + }; + security = { acme = { acceptTerms = true; @@ -123,169 +84,94 @@ in PermitRootLogin = lib.mkForce "prohibit-password"; }; - services.forgejo = { + services.syncthing = { enable = true; - package = pkgs.forgejo-lts; - database.type = "sqlite3"; - lfs.enable = true; + dataDir = "/var/lib/syncthing"; + guiAddress = "127.0.0.1:8384"; + openDefaultPorts = false; + overrideDevices = false; + overrideFolders = false; settings = { - server = { - DOMAIN = forgejoDomain; - ROOT_URL = "https://${forgejoDomain}/"; - HTTP_ADDR = "127.0.0.1"; - HTTP_PORT = 3000; - SSH_DOMAIN = forgejoDomain; - SSH_PORT = 22; - DISABLE_SSH = false; + gui = { + insecureAdminAccess = false; + insecureSkipHostcheck = false; }; - session.COOKIE_SECURE = true; - service = { - DISABLE_REGISTRATION = true; - REQUIRE_SIGNIN_VIEW = false; - }; - actions.ENABLED = true; - repository = { - DEFAULT_PRIVATE = "private"; - DISABLE_HTTP_GIT = false; - }; - "cron.archive_cleanup" = { - ENABLED = true; - RUN_AT_START = true; - SCHEDULE = "@every 24h"; - OLDER_THAN = "72h"; - }; - log.LEVEL = "Warn"; - }; - }; - - services.gitea-actions-runner = { - package = pkgs.forgejo-runner; - instances.alisceon-core-podman = { - enable = true; - name = "alisceon-core-podman"; - url = "https://${forgejoDomain}"; - tokenFile = forgejoRunnerTokenFile; - labels = [ - "ubuntu-latest:docker://node:22-bookworm" - "debian-latest:docker://node:22-bookworm" - ]; - settings = { - container = { - network = "host"; - privileged = false; - valid_volumes = [ ]; - }; - cache.enabled = false; + options = { + globalAnnounceEnabled = false; + localAnnounceEnabled = false; + listenAddresses = [ + "tcp://0.0.0.0:22000" + "quic://0.0.0.0:22000" + ]; + natEnabled = false; + relaysEnabled = false; + urAccepted = -1; }; }; }; - services.nginx = { - enable = true; - recommendedGzipSettings = true; - recommendedOptimisation = true; - recommendedProxySettings = true; - recommendedTlsSettings = true; - virtualHosts = { - "_" = { - default = true; - rejectSSL = true; + alisceon.forgejo.domain = "forgejo.alisceon.com"; + + services.gitea-actions-runner.instances.alisceon-core-podman.labels = [ + "podman" + "aarch64" + "arm64" + ]; + + services.nginx.virtualHosts = { + ${"forgejo.alisceon.com"} = { + serverName = "forgejo.alisceon.com"; + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:3000"; + recommendedProxySettings = true; + }; + }; + ${"syncthing.alisceon.com"} = { + serverName = "syncthing.alisceon.com"; + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8384"; + recommendedProxySettings = false; extraConfig = '' - return 421; + proxy_set_header Host $proxy_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 600s; + proxy_send_timeout 600s; ''; }; - ${forgejoDomain} = { - serverName = forgejoDomain; - forceSSL = true; - enableACME = true; - locations."/" = { - proxyPass = "http://127.0.0.1:3000"; - recommendedProxySettings = true; - }; - }; }; }; - services.tor = { - enable = true; - client.enable = false; - relay = { - enable = true; - role = "relay"; - }; - settings = { - Nickname = "alisceondotcom"; - ORPort = 24601; - DataDirectory = "/var/lib/tor"; - ExitRelay = false; - ExitPolicy = [ "reject *:*" ]; - RelayBandwidthRate = "25 MBytes"; - RelayBandwidthBurst = "25 MBytes"; - BandwidthRate = "25 MBytes"; - BandwidthBurst = "25 MBytes"; - AccountingStart = "month 1 00:00"; - AccountingMax = "8500 GBytes"; - DirCache = true; - AvoidDiskWrites = 1; - Sandbox = false; - }; - }; - - services.cloud-init = { - enable = true; - network.enable = true; - settings = { - datasource_list = [ "Oracle" "ConfigDrive" "NoCloud" ]; - users = [ "default" ]; - system_info.default_user = { - name = "alisceon"; - gecos = "Alisceon"; - groups = [ "wheel" "systemd-journal" ]; - shell = "/run/current-system/sw/bin/xonsh"; - lock_passwd = true; - }; - }; - }; - - systemd.services.fetch-oci-authorized-keys = { - description = "Fetch OCI metadata authorized_keys for alisceon"; - wantedBy = [ "sshd.service" ]; - before = [ "sshd.service" ]; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; + systemd.services.syncthing = { serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - StandardError = "journal+console"; - StandardOutput = "journal+console"; + LockPersonality = true; + PrivateIPC = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectHome = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + ReadWritePaths = [ "/var/lib/syncthing" ]; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + "AF_UNIX" + ]; + SystemCallArchitectures = "native"; + UMask = "0077"; }; - script = lib.getExe fetchOciAuthorizedKeys; - }; - - systemd.services.fetch-ssh-keys.enable = false; - - systemd.services.forgejo-runner-token = { - description = "Generate Forgejo runner registration token"; - wantedBy = [ "multi-user.target" ]; - after = [ "forgejo.service" ]; - requires = [ "forgejo.service" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - StandardError = "journal+console"; - StandardOutput = "journal+console"; - }; - script = lib.getExe generateForgejoRunnerToken; - }; - - systemd.services."gitea-runner-alisceon\\x2dcore\\x2dpodman" = { - after = [ "forgejo-runner-token.service" ]; - requires = [ "forgejo-runner-token.service" ]; }; environment.systemPackages = with pkgs; [ curl - forgejo-lts git htop jq diff --git a/nixos/hosts/blogbox/configuration.nix b/nixos/hosts/blogbox/configuration.nix new file mode 100644 index 0000000..22bb128 --- /dev/null +++ b/nixos/hosts/blogbox/configuration.nix @@ -0,0 +1,401 @@ +{ lib, pkgs, modulesPath, ... }: +let + siteDomain = "blogbox.alisceon.com"; + repoDir = "/home/alisceon/blogbox-site"; + stateDir = "/var/lib/blogbox"; + publicDir = "${stateDir}/www"; + + updateBlogboxSite = pkgs.writeShellApplication { + name = "update-blogbox-site"; + runtimeInputs = [ + pkgs.coreutils + pkgs.git + pkgs.hugo + pkgs.rsync + ]; + text = '' + set -euo pipefail + + if [ ! -d ${lib.escapeShellArg repoDir}/.git ]; then + echo "${repoDir} is not a git checkout yet; skipping Hugo publish" + exit 0 + fi + + install -d -m 0755 ${lib.escapeShellArg stateDir} ${lib.escapeShellArg publicDir} + + git -C ${lib.escapeShellArg repoDir} pull --ff-only + git -C ${lib.escapeShellArg repoDir} submodule sync --recursive + git -C ${lib.escapeShellArg repoDir} submodule update --init --recursive + + rm -rf ${lib.escapeShellArg stateDir}/hugo-public + hugo \ + --source ${lib.escapeShellArg repoDir} \ + --destination ${lib.escapeShellArg stateDir}/hugo-public \ + --minify \ + --cleanDestinationDir + + rsync -a --delete ${lib.escapeShellArg stateDir}/hugo-public/ ${lib.escapeShellArg publicDir}/ + ''; + }; + + updateNamecheapDyndns = pkgs.writeShellApplication { + name = "update-namecheap-dyndns"; + runtimeInputs = [ + pkgs.coreutils + pkgs.ddclient + ]; + text = '' + set -euo pipefail + + : "''${NAMECHEAP_DOMAIN:?Set NAMECHEAP_DOMAIN in /etc/blogbox-namecheap-ddns.env}" + : "''${NAMECHEAP_PASSWORD:?Set NAMECHEAP_PASSWORD in /etc/blogbox-namecheap-ddns.env}" + : "''${NAMECHEAP_HOSTS:?Set NAMECHEAP_HOSTS in /etc/blogbox-namecheap-ddns.env}" + + config_file="''${RUNTIME_DIRECTORY}/ddclient.conf" + install -m 0600 /dev/null "$config_file" + + { + printf 'daemon=0\n' + printf 'cache=/var/cache/blogbox-dyndns/ddclient.cache\n' + printf 'ssl=yes\n' + printf 'protocol=namecheap\n' + printf 'usev4=webv4, webv4=dynamicdns.park-your-domain.com/getip\n' + printf 'server=dynamicdns.park-your-domain.com\n' + printf 'login=%s\n' "$NAMECHEAP_DOMAIN" + printf 'password=%s\n' "$NAMECHEAP_PASSWORD" + printf '%s\n' "$NAMECHEAP_HOSTS" + } > "$config_file" + + ddclient -file "$config_file" + ''; + }; + + ensureBlogboxSwapfile = pkgs.writeShellApplication { + name = "ensure-blogbox-swapfile"; + runtimeInputs = [ + pkgs.coreutils + pkgs.gnugrep + pkgs.util-linux + ]; + text = '' + set -euo pipefail + + if swapon --show=NAME --noheadings | grep -Fxq /swapfile; then + exit 0 + fi + + if [ ! -f /swapfile ] || [ "$(stat -c %s /swapfile)" -ne 8589934592 ]; then + rm -f /swapfile + install -m 0600 /dev/null /swapfile + fallocate -l 8G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=8192 status=none + chmod 0600 /swapfile + fi + + mkswap -f /swapfile + swapon /swapfile + ''; + }; +in +{ + imports = [ + "${modulesPath}/virtualisation/oci-image.nix" + ../../modules/services/cloud-init.nix + ../../modules/services/nginx.nix + ../../modules/services/oci-authorized-keys.nix + ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + + oci.efi = lib.mkForce false; + virtualisation.diskSize = lib.mkForce (16 * 1024); + + networking = { + hostName = "blogbox"; + networkmanager.enable = lib.mkForce false; + firewall.allowedTCPPorts = [ + 22 + 80 + 443 + ]; + }; + + boot = { + initrd.availableKernelModules = [ + "virtio_pci" + "virtio_blk" + "virtio_scsi" + "virtio_net" + "xhci_pci" + ]; + kernelParams = lib.mkForce [ + "nvme.shutdown_timeout=10" + "nvme_core.shutdown_timeout=10" + "libiscsi.debug_libiscsi_eh=1" + "crash_kexec_post_notifiers" + "console=tty1" + "console=ttyS0,115200n8" + ]; + kernelPackages = lib.mkForce pkgs.linuxPackages; + loader.systemd-boot.configurationLimit = lib.mkForce 3; + }; + + documentation = { + enable = lib.mkForce false; + man.enable = lib.mkForce false; + doc.enable = lib.mkForce false; + info.enable = lib.mkForce false; + nixos.enable = lib.mkForce false; + }; + + environment = { + defaultPackages = lib.mkForce [ ]; + shells = lib.mkForce [ pkgs.bash ]; + systemPackages = lib.mkForce (with pkgs; [ + curl + git + hugo + vim + ]); + }; + + programs = { + command-not-found.enable = lib.mkForce false; + fish.enable = lib.mkForce false; + fzf.fuzzyCompletion = lib.mkForce false; + xonsh.enable = lib.mkForce false; + }; + + users = { + defaultUserShell = lib.mkForce pkgs.bash; + groups.blogbox-dyndns = { }; + users.alisceon = { + createHome = true; + extraGroups = lib.mkForce [ "wheel" "systemd-journal" ]; + openssh.authorizedKeys.keys = [ + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPN1Cd2UlHo03Jqgi5Yb4io/3gh/X4wCb8LcmKlpAovQa271CKDBtYOUKn+Fts03g6dBMfaWMty6VGPMGDMONmc= alisceon@electra" + ]; + shell = lib.mkForce pkgs.bash; + }; + users.blogbox-dyndns = { + group = "blogbox-dyndns"; + isSystemUser = true; + }; + }; + + alisceon = { + cloud-init.enable = true; + ociAuthorizedKeys.enable = true; + }; + + nix = { + settings = { + cores = lib.mkForce 1; + max-jobs = lib.mkForce 1; + min-free = lib.mkForce (256 * 1024 * 1024); + max-free = lib.mkForce (1024 * 1024 * 1024); + }; + gc = { + dates = lib.mkForce "daily"; + options = lib.mkForce "--delete-older-than 3d"; + }; + }; + + system.autoUpgrade = { + persistent = lib.mkForce false; + randomizedDelaySec = lib.mkForce "4h"; + }; + + security = { + acme = { + acceptTerms = true; + defaults.email = "acme@alisceon.com"; + }; + sudo-rs.wheelNeedsPassword = false; + }; + + services = { + openssh.settings = { + KbdInteractiveAuthentication = false; + PasswordAuthentication = false; + PermitRootLogin = lib.mkForce "prohibit-password"; + }; + + journald.extraConfig = '' + SystemMaxUse=64M + RuntimeMaxUse=32M + ''; + + nginx.virtualHosts.${siteDomain} = { + serverName = siteDomain; + forceSSL = true; + enableACME = true; + root = publicDir; + locations."/".extraConfig = '' + try_files $uri $uri/ =404; + ''; + }; + }; + + systemd = { + tmpfiles.rules = [ + "d ${repoDir} 0755 alisceon users - -" + "d ${stateDir} 0755 alisceon users - -" + "d ${publicDir} 0755 alisceon users - -" + ]; + + services.update-blogbox-site = { + description = "Pull and publish the Blogbox Hugo site"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + User = "alisceon"; + Group = "users"; + ExecStart = lib.getExe updateBlogboxSite; + Nice = 10; + IOSchedulingClass = "idle"; + LockPersonality = true; + MemoryHigh = "384M"; + MemoryMax = "512M"; + NoNewPrivileges = true; + OOMPolicy = "stop"; + PrivateDevices = true; + PrivateIPC = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + ReadWritePaths = [ + repoDir + stateDir + ]; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + TimeoutStartSec = "15min"; + UMask = "0022"; + }; + }; + + services.blogbox-dyndns = { + description = "Update Namecheap dynamic DNS records for Blogbox"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + unitConfig.ConditionPathExists = "/etc/blogbox-namecheap-ddns.env"; + serviceConfig = { + Type = "oneshot"; + User = "blogbox-dyndns"; + Group = "blogbox-dyndns"; + ExecStart = lib.getExe updateNamecheapDyndns; + CacheDirectory = "blogbox-dyndns"; + EnvironmentFile = "/etc/blogbox-namecheap-ddns.env"; + LockPersonality = true; + MemoryMax = "128M"; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + OOMPolicy = "stop"; + PrivateDevices = true; + PrivateIPC = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RuntimeDirectory = "blogbox-dyndns"; + RuntimeDirectoryMode = "0700"; + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = "@system-service"; + TimeoutStartSec = "2min"; + UMask = "0177"; + }; + }; + + timers.update-blogbox-site = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "10min"; + OnUnitInactiveSec = "5min"; + Persistent = true; + }; + }; + + timers.blogbox-dyndns = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5min"; + OnUnitInactiveSec = "10min"; + Persistent = true; + }; + }; + }; + + systemd.services.nixos-upgrade.serviceConfig = { + IOSchedulingClass = "idle"; + MemoryHigh = "512M"; + MemoryMax = "768M"; + Nice = 15; + OOMPolicy = "stop"; + }; + + systemd.services.growpart.serviceConfig = { + IOSchedulingClass = "idle"; + Nice = 15; + TimeoutStartSec = "2min"; + }; + + systemd.services.blogbox-swapfile = { + description = "Create and enable Blogbox swapfile"; + after = [ + "sshd.service" + "systemd-growfs-root.service" + ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = lib.getExe ensureBlogboxSwapfile; + IOSchedulingClass = "idle"; + Nice = 19; + RemainAfterExit = true; + TimeoutStartSec = "20min"; + }; + }; + + zramSwap = { + enable = true; + memoryPercent = 50; + }; + + virtualisation = { + containers.enable = lib.mkForce false; + docker.enable = lib.mkForce false; + libvirtd = { + enable = lib.mkForce false; + qemu.swtpm.enable = lib.mkForce false; + }; + podman.enable = lib.mkForce false; + }; + + system.stateVersion = lib.mkForce "25.11"; +} diff --git a/nixos/modules/profiles/workstation.nix b/nixos/modules/profiles/workstation.nix index d39d714..2fadb28 100644 --- a/nixos/modules/profiles/workstation.nix +++ b/nixos/modules/profiles/workstation.nix @@ -24,6 +24,8 @@ in security.sudo.wheelNeedsPassword = false; + alisceon.wireguardPeer.enable = true; + services = { printing.enable = true; pulseaudio.enable = false; diff --git a/nixos/modules/services/cloud-init.nix b/nixos/modules/services/cloud-init.nix new file mode 100644 index 0000000..b5dd3ec --- /dev/null +++ b/nixos/modules/services/cloud-init.nix @@ -0,0 +1,71 @@ +{ config, lib, ... }: +let + cfg = config.alisceon.cloud-init; + defaultShell = + if cfg.defaultShell != null then + cfg.defaultShell + else + lib.getExe config.users.users.${cfg.user}.shell; +in +{ + options.alisceon.cloud-init = { + enable = lib.mkEnableOption "shared cloud-init defaults"; + + user = lib.mkOption { + type = lib.types.str; + default = "alisceon"; + description = "Default cloud-init user to configure."; + }; + + gecos = lib.mkOption { + type = lib.types.str; + default = "Alisceon"; + description = "GECOS field for the default cloud-init user."; + }; + + groups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "wheel" + "systemd-journal" + ]; + description = "Groups assigned to the default cloud-init user."; + }; + + defaultShell = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Shell path for the default cloud-init user."; + }; + + datasourceList = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "Oracle" + "ConfigDrive" + "NoCloud" + ]; + description = "cloud-init datasources to allow."; + }; + }; + + config = lib.mkIf cfg.enable { + networking.useNetworkd = lib.mkDefault true; + + services.cloud-init = { + enable = true; + network.enable = true; + settings = { + datasource_list = cfg.datasourceList; + users = [ "default" ]; + system_info.default_user = { + name = cfg.user; + gecos = cfg.gecos; + groups = cfg.groups; + shell = defaultShell; + lock_passwd = true; + }; + }; + }; + }; +} diff --git a/nixos/modules/services/forgejo.nix b/nixos/modules/services/forgejo.nix new file mode 100644 index 0000000..5b5b878 --- /dev/null +++ b/nixos/modules/services/forgejo.nix @@ -0,0 +1,118 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.alisceon.forgejo; + forgejoDomain = cfg.domain; + + generateForgejoRunnerToken = pkgs.writeShellApplication { + name = "generate-forgejo-runner-token"; + runtimeInputs = [ + pkgs.coreutils + pkgs.util-linux + ]; + text = '' + token_file=${lib.escapeShellArg "/var/lib/forgejo/runner_token"} + + if [ -s "$token_file" ]; then + chmod 0600 "$token_file" + chown root:root "$token_file" + exit 0 + fi + + install -d -m 0750 -o forgejo -g forgejo /var/lib/forgejo + token="$(runuser -u forgejo -- env \ + FORGEJO_WORK_DIR=/var/lib/forgejo \ + FORGEJO_CUSTOM=/var/lib/forgejo/custom \ + ${lib.getExe pkgs.forgejo-lts} actions generate-runner-token)" + + umask 0077 + printf 'TOKEN=%s\n' "$token" > "$token_file" + chown root:root "$token_file" + chmod 0600 "$token_file" + ''; + }; +in +{ + options.alisceon.forgejo.domain = lib.mkOption { + type = lib.types.str; + description = "Public domain name for Forgejo."; + }; + + config = { + services.forgejo = { + enable = true; + package = pkgs.forgejo-lts; + database.type = "sqlite3"; + lfs.enable = true; + settings = { + server = { + DOMAIN = forgejoDomain; + ROOT_URL = "https://${forgejoDomain}/"; + HTTP_ADDR = "127.0.0.1"; + HTTP_PORT = 3000; + SSH_DOMAIN = forgejoDomain; + SSH_PORT = 22; + DISABLE_SSH = false; + }; + session.COOKIE_SECURE = true; + service = { + DISABLE_REGISTRATION = true; + REQUIRE_SIGNIN_VIEW = false; + }; + actions.ENABLED = true; + repository = { + DEFAULT_PRIVATE = "private"; + DISABLE_HTTP_GIT = false; + }; + "cron.archive_cleanup" = { + ENABLED = true; + RUN_AT_START = true; + SCHEDULE = "@every 24h"; + OLDER_THAN = "72h"; + }; + log.LEVEL = "Warn"; + }; + }; + + services.gitea-actions-runner = { + package = pkgs.forgejo-runner; + instances.alisceon-core-podman = { + enable = true; + name = "alisceon-core-podman"; + url = "https://${forgejoDomain}"; + tokenFile = "/var/lib/forgejo/runner_token"; + labels = lib.mkDefault [ + "podman" + ]; + settings = { + container = { + network = "host"; + privileged = false; + valid_volumes = [ ]; + }; + cache.enabled = false; + }; + }; + }; + + systemd.services.forgejo-runner-token = { + description = "Generate Forgejo runner registration token"; + wantedBy = [ "multi-user.target" ]; + after = [ "forgejo.service" ]; + requires = [ "forgejo.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + StandardError = "journal+console"; + StandardOutput = "journal+console"; + }; + script = lib.getExe generateForgejoRunnerToken; + }; + + systemd.services."gitea-runner-alisceon\\x2dcore\\x2dpodman" = { + after = [ "forgejo-runner-token.service" ]; + requires = [ "forgejo-runner-token.service" ]; + }; + + environment.systemPackages = [ pkgs.forgejo-lts ]; + }; +} diff --git a/nixos/modules/services/nginx.nix b/nixos/modules/services/nginx.nix new file mode 100644 index 0000000..4846f56 --- /dev/null +++ b/nixos/modules/services/nginx.nix @@ -0,0 +1,17 @@ +{ ... }: +{ + services.nginx = { + enable = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + virtualHosts."_" = { + default = true; + rejectSSL = true; + extraConfig = '' + return 421; + ''; + }; + }; +} diff --git a/nixos/modules/services/oci-authorized-keys.nix b/nixos/modules/services/oci-authorized-keys.nix new file mode 100644 index 0000000..8b9312b --- /dev/null +++ b/nixos/modules/services/oci-authorized-keys.nix @@ -0,0 +1,102 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.alisceon.ociAuthorizedKeys; + home = config.users.users.${cfg.user}.home; + sshDir = "${home}/.ssh"; + authorizedKeysFile = "${sshDir}/authorized_keys"; + staticAuthorizedKeys = config.users.users.${cfg.user}.openssh.authorizedKeys.keys or [ ]; + staticAuthorizedKeysText = + (lib.concatStringsSep "\n" staticAuthorizedKeys) + + lib.optionalString (staticAuthorizedKeys != [ ]) "\n"; + staticAuthorizedKeysFile = pkgs.writeText "static-authorized-keys-${cfg.user}" staticAuthorizedKeysText; + + fetchOciAuthorizedKeys = pkgs.writeShellApplication { + name = "fetch-oci-authorized-keys"; + runtimeInputs = [ + pkgs.coreutils + pkgs.curl + pkgs.gnugrep + ]; + text = '' + install -d -m 0700 -o ${lib.escapeShellArg cfg.user} -g ${lib.escapeShellArg cfg.group} ${lib.escapeShellArg sshDir} + + if [ ! -e ${lib.escapeShellArg authorizedKeysFile} ]; then + install -m 0600 -o ${lib.escapeShellArg cfg.user} -g ${lib.escapeShellArg cfg.group} /dev/null ${lib.escapeShellArg authorizedKeysFile} + fi + + chown ${lib.escapeShellArg "${cfg.user}:${cfg.group}"} ${lib.escapeShellArg authorizedKeysFile} + chmod 0600 ${lib.escapeShellArg authorizedKeysFile} + + append_keys() { + while IFS= read -r key; do + [ -n "$key" ] || continue + grep -qxF -- "$key" ${lib.escapeShellArg authorizedKeysFile} || printf '%s\n' "$key" >> ${lib.escapeShellArg authorizedKeysFile} + done < "$1" + } + + append_keys ${lib.escapeShellArg staticAuthorizedKeysFile} + + metadata_keys="$(mktemp)" + trap 'rm -f "$metadata_keys"' EXIT + + curl --fail --silent --show-error --location \ + --connect-timeout 3 \ + --max-time 10 \ + --retry 3 \ + --retry-delay 2 \ + --header ${lib.escapeShellArg "Authorization: Bearer Oracle"} \ + --output "$metadata_keys" \ + ${lib.escapeShellArg cfg.metadataUrl} || { + echo "Unable to fetch OCI authorized_keys for ${cfg.user}; leaving existing keys unchanged" + exit 0 + } + + append_keys "$metadata_keys" + + chown ${lib.escapeShellArg "${cfg.user}:${cfg.group}"} ${lib.escapeShellArg authorizedKeysFile} + chmod 0600 ${lib.escapeShellArg authorizedKeysFile} + ''; + }; +in +{ + options.alisceon.ociAuthorizedKeys = { + enable = lib.mkEnableOption "fetching SSH authorized_keys from OCI metadata"; + + user = lib.mkOption { + type = lib.types.str; + default = "alisceon"; + description = "User whose authorized_keys file should be populated."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "users"; + description = "Group owner for the user's SSH files."; + }; + + metadataUrl = lib.mkOption { + type = lib.types.str; + default = "http://169.254.169.254/opc/v2/instance/metadata/ssh_authorized_keys"; + description = "OCI metadata endpoint containing SSH authorized keys."; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.fetch-oci-authorized-keys = { + description = "Fetch OCI metadata authorized_keys for ${cfg.user}"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + StandardError = "journal+console"; + StandardOutput = "journal+console"; + TimeoutStartSec = "30s"; + }; + script = lib.getExe fetchOciAuthorizedKeys; + }; + + systemd.services.fetch-ssh-keys.enable = false; + }; +} diff --git a/nixos/modules/services/tor.nix b/nixos/modules/services/tor.nix new file mode 100644 index 0000000..29ee2ff --- /dev/null +++ b/nixos/modules/services/tor.nix @@ -0,0 +1,27 @@ +{ ... }: +{ + services.tor = { + enable = true; + client.enable = false; + relay = { + enable = true; + role = "relay"; + }; + settings = { + Nickname = "alisceondotcom"; + ORPort = 24601; + DataDirectory = "/var/lib/tor"; + ExitRelay = false; + ExitPolicy = [ "reject *:*" ]; + RelayBandwidthRate = "25 MBytes"; + RelayBandwidthBurst = "25 MBytes"; + BandwidthRate = "25 MBytes"; + BandwidthBurst = "25 MBytes"; + AccountingStart = "month 1 00:00"; + AccountingMax = "8500 GBytes"; + DirCache = true; + AvoidDiskWrites = 1; + Sandbox = false; + }; + }; +} diff --git a/nixos/modules/services/wireguard-peer.nix b/nixos/modules/services/wireguard-peer.nix new file mode 100644 index 0000000..65d07a1 --- /dev/null +++ b/nixos/modules/services/wireguard-peer.nix @@ -0,0 +1,57 @@ +{ config, lib, pkgs, repoLocalPath, ... }: + +let + cfg = config.alisceon.wireguardPeer; +in +{ + options.alisceon.wireguardPeer = { + enable = lib.mkEnableOption "a single WireGuard peer managed by wg-quick"; + + interface = lib.mkOption { + type = lib.types.str; + default = "wg0"; + description = "WireGuard interface name."; + }; + + configFile = lib.mkOption { + type = lib.types.str; + default = "/etc/wireguard/${cfg.interface}.conf"; + defaultText = "/etc/wireguard/.conf"; + description = '' + Path to an external wg-quick config file. Keep it root-owned and mode + 0600 so private keys and peer material stay outside Git and the Nix store. + ''; + }; + + autostart = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to bring the WireGuard interface up at boot."; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = lib.hasPrefix "/" cfg.configFile; + message = "alisceon.wireguardPeer.configFile must be an absolute path outside the repo."; + } + { + assertion = !(lib.hasPrefix repoLocalPath cfg.configFile); + message = "alisceon.wireguardPeer.configFile must be outside ${repoLocalPath}."; + } + ]; + + networking.wg-quick.interfaces.${cfg.interface} = { + inherit (cfg) autostart configFile; + }; + + systemd.services."wg-quick-${cfg.interface}".unitConfig.ConditionPathExists = cfg.configFile; + + environment.systemPackages = [ pkgs.wireguard-tools ]; + + systemd.tmpfiles.rules = [ + "d /etc/wireguard 0700 root root -" + ]; + }; +} diff --git a/util/toggle_wg.xsh b/util/toggle_wg.xsh new file mode 100755 index 0000000..bc88bc4 --- /dev/null +++ b/util/toggle_wg.xsh @@ -0,0 +1,25 @@ +import sys + +isup = "does not exist." not in $(ip link show dev wg0 2>&1) + +try: + match sys.argv[1]: + case "toggle": + if isup: + footclient wg-quick down wg0 + else: + footclient wg-quick up wg0 + case "status": + if isup: + print("u") + else: + print("d") + case _: + raise RuntimeError + +except (RuntimeError, IndexError): + print('"toggle" or "status" must be provided') + exit(1) + +exit(0) +