diff --git a/nixos/hosts/blogbox/configuration.nix b/nixos/hosts/blogbox/configuration.nix index ec91d7a..22bb128 100644 --- a/nixos/hosts/blogbox/configuration.nix +++ b/nixos/hosts/blogbox/configuration.nix @@ -37,6 +37,64 @@ let 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 = [ @@ -48,10 +106,12 @@ in 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; - useDHCP = lib.mkDefault true; firewall.allowedTCPPorts = [ 22 80 @@ -60,6 +120,21 @@ in }; 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; }; @@ -92,11 +167,19 @@ in 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 = { @@ -106,6 +189,8 @@ in 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); }; @@ -115,6 +200,11 @@ in }; }; + system.autoUpgrade = { + persistent = lib.mkForce false; + randomizedDelaySec = lib.mkForce "4h"; + }; + security = { acme = { acceptTerms = true; @@ -125,6 +215,7 @@ in services = { openssh.settings = { + KbdInteractiveAuthentication = false; PasswordAuthentication = false; PermitRootLogin = lib.mkForce "prohibit-password"; }; @@ -164,7 +255,10 @@ in Nice = 10; IOSchedulingClass = "idle"; LockPersonality = true; + MemoryHigh = "384M"; + MemoryMax = "512M"; NoNewPrivileges = true; + OOMPolicy = "stop"; PrivateDevices = true; PrivateIPC = true; ProtectClock = true; @@ -186,17 +280,106 @@ in 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 = { - OnCalendar = "5m"; + 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 = { @@ -204,13 +387,6 @@ in memoryPercent = 50; }; - swapDevices = [ - { - device = "/swapfile"; - size = 8 * 1024; - } - ]; - virtualisation = { containers.enable = lib.mkForce false; docker.enable = lib.mkForce false; diff --git a/nixos/modules/services/oci-authorized-keys.nix b/nixos/modules/services/oci-authorized-keys.nix index c6fb11d..8b9312b 100644 --- a/nixos/modules/services/oci-authorized-keys.nix +++ b/nixos/modules/services/oci-authorized-keys.nix @@ -4,25 +4,54 @@ let 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 [ -s ${lib.escapeShellArg authorizedKeysFile} ]; then - echo "OCI authorized_keys already present for ${cfg.user}" - exit 0 + 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 ${lib.escapeShellArg authorizedKeysFile} \ - ${lib.escapeShellArg cfg.metadataUrl} + --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} @@ -55,8 +84,7 @@ in config = lib.mkIf cfg.enable { systemd.services.fetch-oci-authorized-keys = { description = "Fetch OCI metadata authorized_keys for ${cfg.user}"; - wantedBy = [ "sshd.service" ]; - before = [ "sshd.service" ]; + wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; serviceConfig = { @@ -64,6 +92,7 @@ in RemainAfterExit = true; StandardError = "journal+console"; StandardOutput = "journal+console"; + TimeoutStartSec = "30s"; }; script = lib.getExe fetchOciAuthorizedKeys; };