From 2ac05607a252a6da9bcf8aeeaae584d447de879a Mon Sep 17 00:00:00 2001 From: alisceon Date: Fri, 29 May 2026 19:30:07 +0200 Subject: [PATCH] add blogbox target --- flake.nix | 11 +- nixos/hosts/alisceon-core/configuration.nix | 82 ++----- nixos/hosts/blogbox/configuration.nix | 225 ++++++++++++++++++ nixos/modules/services/cloud-init.nix | 71 ++++++ .../modules/services/oci-authorized-keys.nix | 73 ++++++ 5 files changed, 394 insertions(+), 68 deletions(-) create mode 100644 nixos/hosts/blogbox/configuration.nix create mode 100644 nixos/modules/services/cloud-init.nix create mode 100644 nixos/modules/services/oci-authorized-keys.nix diff --git a/flake.nix b/flake.nix index 77d2c4f..f3074c1 100644 --- a/flake.nix +++ b/flake.nix @@ -76,6 +76,7 @@ nixosModules ? [ ], hmModules ? [ ], extraModules ? [ ], + homeManagerUsers ? true, }: let pkgs = mkPkgs system; @@ -92,7 +93,7 @@ ++ [ (./nixos/hosts + "/${hostName}/configuration.nix") ] ++ nixosModules ++ extraModules - ++ [ + ++ nixpkgs.lib.optionals homeManagerUsers [ { home-manager.users.alisceon.imports = [ @@ -168,6 +169,14 @@ nixosModules = serverModules; hmModules = serverHomeModules; }; + + blogbox = mkHost { + hostName = "blogbox"; + system = "x86_64-linux"; + nixosModules = serverModules; + hmModules = serverHomeModules; + homeManagerUsers = false; + }; }; }; } diff --git a/nixos/hosts/alisceon-core/configuration.nix b/nixos/hosts/alisceon-core/configuration.nix index d51f60f..222cfa6 100644 --- a/nixos/hosts/alisceon-core/configuration.nix +++ b/nixos/hosts/alisceon-core/configuration.nix @@ -1,38 +1,11 @@ { lib, pkgs, modulesPath, ... }: -let - forgejoDomain = "git.alisceon.com"; - syncthingDomain = "syncthing.alisceon.com"; - - 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 - ''; - }; - -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 ]; @@ -90,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; @@ -129,7 +110,7 @@ in }; }; - alisceon.forgejo.domain = forgejoDomain; + alisceon.forgejo.domain = "forgejo.alisceon.com"; services.gitea-actions-runner.instances.alisceon-core-podman.labels = [ "podman" @@ -138,8 +119,8 @@ in ]; services.nginx.virtualHosts = { - ${forgejoDomain} = { - serverName = forgejoDomain; + ${"forgejo.alisceon.com"} = { + serverName = "forgejo.alisceon.com"; forceSSL = true; enableACME = true; locations."/" = { @@ -147,8 +128,8 @@ in recommendedProxySettings = true; }; }; - ${syncthingDomain} = { - serverName = syncthingDomain; + ${"syncthing.alisceon.com"} = { + serverName = "syncthing.alisceon.com"; forceSSL = true; enableACME = true; locations."/" = { @@ -167,39 +148,6 @@ in }; }; - 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" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - StandardError = "journal+console"; - StandardOutput = "journal+console"; - }; - script = lib.getExe fetchOciAuthorizedKeys; - }; - - systemd.services.fetch-ssh-keys.enable = false; - systemd.services.syncthing = { serviceConfig = { LockPersonality = true; diff --git a/nixos/hosts/blogbox/configuration.nix b/nixos/hosts/blogbox/configuration.nix new file mode 100644 index 0000000..ec91d7a --- /dev/null +++ b/nixos/hosts/blogbox/configuration.nix @@ -0,0 +1,225 @@ +{ 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}/ + ''; + }; +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"; + + networking = { + hostName = "blogbox"; + networkmanager.enable = lib.mkForce false; + useDHCP = lib.mkDefault true; + firewall.allowedTCPPorts = [ + 22 + 80 + 443 + ]; + }; + + boot = { + 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; + users.alisceon = { + createHome = true; + extraGroups = lib.mkForce [ "wheel" "systemd-journal" ]; + shell = lib.mkForce pkgs.bash; + }; + }; + + alisceon = { + cloud-init.enable = true; + ociAuthorizedKeys.enable = true; + }; + + nix = { + settings = { + 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"; + }; + }; + + security = { + acme = { + acceptTerms = true; + defaults.email = "acme@alisceon.com"; + }; + sudo-rs.wheelNeedsPassword = false; + }; + + services = { + openssh.settings = { + 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; + NoNewPrivileges = true; + 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"; + UMask = "0022"; + }; + }; + + timers.update-blogbox-site = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "5m"; + Persistent = true; + }; + }; + }; + + zramSwap = { + enable = true; + memoryPercent = 50; + }; + + swapDevices = [ + { + device = "/swapfile"; + size = 8 * 1024; + } + ]; + + 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/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/oci-authorized-keys.nix b/nixos/modules/services/oci-authorized-keys.nix new file mode 100644 index 0000000..c6fb11d --- /dev/null +++ b/nixos/modules/services/oci-authorized-keys.nix @@ -0,0 +1,73 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.alisceon.ociAuthorizedKeys; + home = config.users.users.${cfg.user}.home; + sshDir = "${home}/.ssh"; + authorizedKeysFile = "${sshDir}/authorized_keys"; + + fetchOciAuthorizedKeys = pkgs.writeShellApplication { + name = "fetch-oci-authorized-keys"; + runtimeInputs = [ + pkgs.coreutils + pkgs.curl + ]; + 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 + fi + + curl --fail --silent --show-error --location \ + --header ${lib.escapeShellArg "Authorization: Bearer Oracle"} \ + --output ${lib.escapeShellArg authorizedKeysFile} \ + ${lib.escapeShellArg cfg.metadataUrl} + + 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 = [ "sshd.service" ]; + before = [ "sshd.service" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + StandardError = "journal+console"; + StandardOutput = "journal+console"; + }; + script = lib.getExe fetchOciAuthorizedKeys; + }; + + systemd.services.fetch-ssh-keys.enable = false; + }; +}