From 3850948f71fe6bfb9fae7c7320939f8b567dfcfd Mon Sep 17 00:00:00 2001 From: Jet Date: Sat, 21 Mar 2026 16:05:47 -0700 Subject: [PATCH] feat: get to a solid bootstrap on public ssh --- .github/workflows/ci.yml | 7 - README.md | 17 +- disk-config.nix | 34 --- flake.lock | 175 --------------- flake.nix | 211 ++++-------------- hosts/{main-wiki/default.nix => common.nix} | 2 +- hosts/{shared => }/hardware-configuration.nix | 7 +- hosts/main-wiki.nix | 6 + hosts/replica-wiki.nix | 6 + hosts/replica-wiki/default.nix | 11 - modules/admin-users.nix | 13 ++ modules/caddy.nix | 31 --- modules/common.nix | 2 + modules/deploy-ssh.nix | 29 +++ modules/mediawiki-base.nix | 109 --------- modules/security.nix | 18 -- modules/tailscale.nix | 32 --- modules/wiki-primary/mediawiki.nix | 29 --- modules/wiki-primary/mysql.nix | 123 ---------- modules/wiki-replica/mediawiki.nix | 9 - modules/wiki-replica/mysql.nix | 92 -------- scripts/bootstrap-host.sh | 144 ++++++++++++ secrets/mediawiki-admin-password.age | Bin 257 -> 453 bytes secrets/mysql-mediawiki.age | 14 +- secrets/mysql-replication.age | Bin 257 -> 453 bytes secrets/secrets.nix | 1 - secrets/tailscale-auth.age | 5 - 27 files changed, 262 insertions(+), 865 deletions(-) delete mode 100644 disk-config.nix rename hosts/{main-wiki/default.nix => common.nix} (81%) rename hosts/{shared => }/hardware-configuration.nix (71%) create mode 100644 hosts/main-wiki.nix create mode 100644 hosts/replica-wiki.nix delete mode 100644 hosts/replica-wiki/default.nix create mode 100644 modules/admin-users.nix delete mode 100644 modules/caddy.nix create mode 100644 modules/deploy-ssh.nix delete mode 100644 modules/mediawiki-base.nix delete mode 100644 modules/security.nix delete mode 100644 modules/tailscale.nix delete mode 100644 modules/wiki-primary/mediawiki.nix delete mode 100644 modules/wiki-primary/mysql.nix delete mode 100644 modules/wiki-replica/mediawiki.nix delete mode 100644 modules/wiki-replica/mysql.nix create mode 100644 scripts/bootstrap-host.sh delete mode 100644 secrets/tailscale-auth.age diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e207d2f..a040053 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,13 +38,6 @@ jobs: - uses: DeterminateSystems/nix-installer-action@main - - name: Connect to Tailscale - uses: tailscale/github-action@v2 - with: - oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} - oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} - tags: tag:ci - - name: Configure SSH run: | mkdir -p ~/.ssh diff --git a/README.md b/README.md index 56039ce..f4c284f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Noisebridge Wiki Infra -This repo manages the Noisebridge MediaWiki primary and replica on NixOS. +This repo manages the Noisebridge wiki hosts on NixOS. ## Commands -Bootstrap a brand new VPS into NixOS and seed its stable agenix host key: +Bootstrap a brand new Ubuntu 22.04 DigitalOcean VPS into NixOS: ```sh nix run .#bootstrap-host -- [ssh-identity-file] @@ -20,12 +20,11 @@ nix run .#bootstrap-host -- root@203.0.113.10 root@203.0.113.11 ~/.ssh/do-bootst What bootstrap does: -- generates or reuses `.bootstrap//host.age` -- writes the matching public recipient to `secrets/hosts/.age.pub` -- rekeys the agenix secrets with `agenix -r` -- runs `nixos-anywhere` against one or both raw VPS targets -- installs `/var/lib/agenix/host.age` onto the new machine -- lets the machine decrypt its Tailscale auth secret and come up on Tailscale with its configured hostname +- copies a first-boot module to the host +- runs `nixos-infect` on the Ubuntu VPS +- converts the machine to NixOS with the `jet` admin user +- disables direct root SSH +- fixes the known bad IPv6 routes generated by `nixos-infect` Deploy all already-bootstrapped hosts: @@ -58,7 +57,7 @@ nix flake check 'path:.' --accept-flake-config 1. Create a raw VPS. 2. Run `nix run .#bootstrap-host -- ...` from the repo root on an admin laptop. -3. The machine installs NixOS, gets its host agenix key, and joins Tailscale. +3. The machine installs NixOS and comes up over hardened public SSH as `jet`. 4. Future changes use `nix run .#deploy`. ## GitHub Settings diff --git a/disk-config.nix b/disk-config.nix deleted file mode 100644 index c6db013..0000000 --- a/disk-config.nix +++ /dev/null @@ -1,34 +0,0 @@ -{ - disko.devices = { - disk.main = { - type = "disk"; - device = "/dev/vda"; - content = { - type = "gpt"; - partitions = { - bios = { - size = "1M"; - type = "EF02"; - }; - esp = { - size = "512M"; - type = "EF00"; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; - }; - }; - root = { - size = "100%"; - content = { - type = "filesystem"; - format = "ext4"; - mountpoint = "/"; - }; - }; - }; - }; - }; - }; -} diff --git a/flake.lock b/flake.lock index 0c708a0..b1cb602 100644 --- a/flake.lock +++ b/flake.lock @@ -67,48 +67,6 @@ "type": "github" } }, - "disko": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1773889306, - "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", - "owner": "nix-community", - "repo": "disko", - "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "disko", - "type": "github" - } - }, - "disko_2": { - "inputs": { - "nixpkgs": [ - "nixos-anywhere", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1769524058, - "narHash": "sha256-zygdD6X1PcVNR2PsyK4ptzrVEiAdbMqLos7utrMDEWE=", - "owner": "nix-community", - "repo": "disko", - "rev": "71a3fc97d80881e91710fe721f1158d3b96ae14d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "ref": "master", - "repo": "disko", - "type": "github" - } - }, "flake-compat": { "flake": false, "locked": { @@ -125,27 +83,6 @@ "type": "github" } }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "nixos-anywhere", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768135262, - "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, "home-manager": { "inputs": { "nixpkgs": [ @@ -167,95 +104,6 @@ "type": "github" } }, - "nix-vm-test": { - "inputs": { - "nixpkgs": [ - "nixos-anywhere", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1769079217, - "narHash": "sha256-R6qzhu+YJolxE2vUsPQWWwUKMbAG5nXX3pBtg8BNX38=", - "owner": "Enzime", - "repo": "nix-vm-test", - "rev": "58c15f78947b431d6c206e0966500c7e9139bd2f", - "type": "github" - }, - "original": { - "owner": "Enzime", - "ref": "pr-105-latest", - "repo": "nix-vm-test", - "type": "github" - } - }, - "nixos-anywhere": { - "inputs": { - "disko": "disko_2", - "flake-parts": "flake-parts", - "nix-vm-test": "nix-vm-test", - "nixos-images": "nixos-images", - "nixos-stable": "nixos-stable", - "nixpkgs": [ - "nixpkgs" - ], - "treefmt-nix": "treefmt-nix" - }, - "locked": { - "lastModified": 1769956140, - "narHash": "sha256-D+RQ+DaIC/GVwv5lUs7e8jSmh8aPc77Kg/gRjaS25Zk=", - "owner": "nix-community", - "repo": "nixos-anywhere", - "rev": "92f82c5196a5f8588be4967e535c4cfd35e85902", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixos-anywhere", - "type": "github" - } - }, - "nixos-images": { - "inputs": { - "nixos-stable": [ - "nixos-anywhere", - "nixos-stable" - ], - "nixos-unstable": [ - "nixos-anywhere", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1766770015, - "narHash": "sha256-kUmVBU+uBUPl/v3biPiWrk680b8N9rRMhtY97wsxiJc=", - "owner": "nix-community", - "repo": "nixos-images", - "rev": "e4dba54ddb6b2ad9c6550e5baaed2fa27938a5d2", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixos-images", - "type": "github" - } - }, - "nixos-stable": { - "locked": { - "lastModified": 1769598131, - "narHash": "sha256-e7VO/kGLgRMbWtpBqdWl0uFg8Y2XWFMdz0uUJvlML8o=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "fa83fd837f3098e3e678e6cf017b2b36102c7211", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1773821835, @@ -276,8 +124,6 @@ "inputs": { "agenix": "agenix", "deploy-rs": "deploy-rs", - "disko": "disko", - "nixos-anywhere": "nixos-anywhere", "nixpkgs": "nixpkgs" } }, @@ -311,27 +157,6 @@ "type": "github" } }, - "treefmt-nix": { - "inputs": { - "nixpkgs": [ - "nixos-anywhere", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1769691507, - "narHash": "sha256-8aAYwyVzSSwIhP2glDhw/G0i5+wOrren3v6WmxkVonM=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "28b19c5844cc6e2257801d43f2772a4b4c050a1b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" - } - }, "utils": { "inputs": { "systems": "systems_2" diff --git a/flake.nix b/flake.nix index 72f0e57..1f1d05f 100644 --- a/flake.nix +++ b/flake.nix @@ -16,14 +16,6 @@ url = "github:serokell/deploy-rs"; inputs.nixpkgs.follows = "nixpkgs"; }; - disko = { - url = "github:nix-community/disko"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - nixos-anywhere = { - url = "github:nix-community/nixos-anywhere"; - inputs.nixpkgs.follows = "nixpkgs"; - }; }; outputs = @@ -32,8 +24,6 @@ nixpkgs, agenix, deploy-rs, - disko, - nixos-anywhere, ... }: let @@ -45,7 +35,14 @@ wikiName = "Noisebridge"; baseDomain = "noisebridge.net"; replicaSubdomain = "replica"; - sshUser = "root"; + sshUser = "jet"; + adminUsers = { + jet = { + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu jetthomaspham@gmail.com" + ]; + }; + }; primaryHostName = "main-wiki"; replicaHostName = "replica-wiki"; @@ -58,11 +55,11 @@ hosts = { primary = { nixosName = primaryHostName; - tailscaleName = primaryHostName; + publicIpv4 = "134.199.221.52"; }; replica = { nixosName = replicaHostName; - tailscaleName = replicaHostName; + publicIpv4 = "167.99.174.109"; }; }; }; @@ -82,12 +79,8 @@ publicDomain = mkPublicDomain role; }; - mkHost = - { - hostMeta, - hostModule, - roleModules, - }: + mkDeployHost = + hostModule: hostMeta: lib.nixosSystem { inherit system; specialArgs = { @@ -95,16 +88,11 @@ }; modules = [ agenix.nixosModules.default - disko.nixosModules.disko hostModule - ./disk-config.nix ./modules/common.nix - ./modules/security.nix - ./modules/tailscale.nix - ./modules/caddy.nix - ./modules/mediawiki-base.nix - ] - ++ roleModules; + ./modules/admin-users.nix + ./modules/deploy-ssh.nix + ]; }; primaryMeta = mkHostMeta "primary"; @@ -112,40 +100,28 @@ in { nixosConfigurations = { - main-wiki = mkHost { - hostMeta = primaryMeta; - hostModule = ./hosts/main-wiki; - roleModules = [ - ./modules/wiki-primary/mysql.nix - ./modules/wiki-primary/mediawiki.nix - ]; - }; + main-wiki = mkDeployHost ./hosts/main-wiki.nix primaryMeta; - replica-wiki = mkHost { - hostMeta = replicaMeta; - hostModule = ./hosts/replica-wiki; - roleModules = [ - ./modules/wiki-replica/mysql.nix - ./modules/wiki-replica/mediawiki.nix - ]; - }; + replica-wiki = mkDeployHost ./hosts/replica-wiki.nix replicaMeta; }; deploy.nodes = { main-wiki = { - hostname = primaryMeta.tailscaleName; + hostname = primaryMeta.publicIpv4; + remoteBuild = true; + sshUser = siteConfig.sshUser; profiles.system = { - user = siteConfig.sshUser; - sshUser = siteConfig.sshUser; + user = "root"; path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.main-wiki; }; }; replica-wiki = { - hostname = replicaMeta.tailscaleName; + hostname = replicaMeta.publicIpv4; + remoteBuild = true; + sshUser = siteConfig.sshUser; profiles.system = { - user = siteConfig.sshUser; - sshUser = siteConfig.sshUser; + user = "root"; path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.replica-wiki; }; }; @@ -175,129 +151,19 @@ bootstrap-host = { type = "app"; - program = "${pkgs.writeShellScript "bootstrap-host" '' - set -euo pipefail - - usage() { - cat <<'EOF' - Usage: - nix run .#bootstrap-host -- [ssh-identity-file] - nix run .#bootstrap-host -- [ssh-identity-file] - - Examples: - nix run .#bootstrap-host -- main-wiki root@203.0.113.10 ~/.ssh/do-bootstrap - nix run .#bootstrap-host -- root@203.0.113.10 root@203.0.113.11 ~/.ssh/do-bootstrap - EOF - } - - if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then - usage - exit 1 - fi - - repo_root="$(pwd)" - if [ ! -f "$repo_root/flake.nix" ]; then - printf 'Run bootstrap-host from the repo root\n' >&2 - exit 1 - fi - - ssh_identity_file="" - main_target="" - replica_target="" - selected_host="" - - case "$1" in - main-wiki|replica-wiki) - selected_host="$1" - if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then - usage - exit 1 - fi - if [ "$selected_host" = "main-wiki" ]; then - main_target="$2" - else - replica_target="$2" - fi - ssh_identity_file="''${3:-}" - ;; - *) - main_target="$1" - replica_target="$2" - ssh_identity_file="''${3:-}" - ;; - esac - - install_root="$(mktemp -d)" - cleanup() { - rm -rf "$install_root" - } - trap cleanup EXIT - - ensure_host_key() { - local host_name="$1" - local private_key_file="$repo_root/.bootstrap/$host_name/host.age" - local public_key_file="$repo_root/secrets/hosts/$host_name.age.pub" - local public_key - - mkdir -p "$(dirname "$private_key_file")" "$(dirname "$public_key_file")" - - if [ ! -s "$private_key_file" ]; then - public_key="$(${pkgs.age}/bin/age-keygen -o "$private_key_file" | sed 's/^Public key: //')" - chmod 600 "$private_key_file" - printf 'Generated host age key for %s\n' "$host_name" - else - public_key="$(${pkgs.age}/bin/age-keygen -y "$private_key_file")" - printf 'Reused existing host age key for %s\n' "$host_name" - fi - - printf '%s\n' "$public_key" > "$public_key_file" - } - - run_bootstrap() { - local host_name="$1" - local target_host="$2" - local private_key_file="$repo_root/.bootstrap/$host_name/host.age" - local -a nixos_anywhere_args - - mkdir -p "$install_root/var/lib/agenix" - ${pkgs.coreutils}/bin/install -m 0400 "$private_key_file" "$install_root/var/lib/agenix/host.age" - - nixos_anywhere_args=( - --extra-files "$install_root" - --flake "path:$repo_root#$host_name" - ) - - if [ -n "$ssh_identity_file" ]; then - nixos_anywhere_args+=( -i "$ssh_identity_file" ) - fi - - printf 'Bootstrapping %s onto %s\n' "$host_name" "$target_host" - ${ - nixos-anywhere.packages.${system}.default - }/bin/nixos-anywhere "''${nixos_anywhere_args[@]}" "$target_host" - rm -f "$install_root/var/lib/agenix/host.age" - } - - if [ -n "$main_target" ]; then - ensure_host_key main-wiki - fi - if [ -n "$replica_target" ]; then - ensure_host_key replica-wiki - fi - - printf 'Rekeying agenix secrets\n' - ${agenix.packages.${system}.default}/bin/agenix -r - - if [ -n "$main_target" ]; then - run_bootstrap main-wiki "$main_target" - fi - if [ -n "$replica_target" ]; then - run_bootstrap replica-wiki "$replica_target" - fi - - printf '\nBootstrap complete. The hosts should now join Tailscale using their configured hostnames.\n' - ''}"; - meta.description = "Install NixOS on one or both raw hosts and seed agenix identities"; + program = "${pkgs.writeShellScript "bootstrap-host" ( + builtins.replaceStrings + [ "@ADMIN_KEYS@" ] + [ + (lib.concatMapStringsSep "\n" (key: " \"${key}\"") ( + lib.flatten ( + lib.mapAttrsToList (_: userCfg: userCfg.openssh.authorizedKeys.keys or [ ]) siteConfig.adminUsers + ) + )) + ] + (builtins.readFile ./scripts/bootstrap-host.sh) + )}"; + meta.description = "Convert one or both Ubuntu DigitalOcean hosts into a minimal NixOS bootstrap config with nixos-infect"; }; }; @@ -305,7 +171,6 @@ packages = with pkgs; [ agenix.packages.${system}.default deploy-rs.packages.${system}.default - nixos-anywhere.packages.${system}.default mariadb.client rsync curl diff --git a/hosts/main-wiki/default.nix b/hosts/common.nix similarity index 81% rename from hosts/main-wiki/default.nix rename to hosts/common.nix index 0780e08..5c28328 100644 --- a/hosts/main-wiki/default.nix +++ b/hosts/common.nix @@ -1,7 +1,7 @@ { hostMeta, siteConfig, ... }: { imports = [ - ../shared/hardware-configuration.nix + ./hardware-configuration.nix ]; networking.hostName = hostMeta.nixosName; diff --git a/hosts/shared/hardware-configuration.nix b/hosts/hardware-configuration.nix similarity index 71% rename from hosts/shared/hardware-configuration.nix rename to hosts/hardware-configuration.nix index e3a8322..f4afa72 100644 --- a/hosts/shared/hardware-configuration.nix +++ b/hosts/hardware-configuration.nix @@ -4,8 +4,11 @@ (modulesPath + "/virtualisation/digital-ocean-config.nix") ]; - boot.loader.systemd-boot.enable = true; - boot.loader.efi.canTouchEfiVariables = true; + boot.loader.grub = { + enable = true; + efiSupport = false; + devices = lib.mkForce [ "/dev/vda" ]; + }; fileSystems."/" = { device = lib.mkDefault "/dev/disk/by-label/nixos"; diff --git a/hosts/main-wiki.nix b/hosts/main-wiki.nix new file mode 100644 index 0000000..6da8357 --- /dev/null +++ b/hosts/main-wiki.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./common.nix + ]; +} diff --git a/hosts/replica-wiki.nix b/hosts/replica-wiki.nix new file mode 100644 index 0000000..6da8357 --- /dev/null +++ b/hosts/replica-wiki.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./common.nix + ]; +} diff --git a/hosts/replica-wiki/default.nix b/hosts/replica-wiki/default.nix deleted file mode 100644 index 0780e08..0000000 --- a/hosts/replica-wiki/default.nix +++ /dev/null @@ -1,11 +0,0 @@ -{ hostMeta, siteConfig, ... }: -{ - imports = [ - ../shared/hardware-configuration.nix - ]; - - networking.hostName = hostMeta.nixosName; - networking.domain = siteConfig.baseDomain; - - system.stateVersion = "24.11"; -} diff --git a/modules/admin-users.nix b/modules/admin-users.nix new file mode 100644 index 0000000..c294b86 --- /dev/null +++ b/modules/admin-users.nix @@ -0,0 +1,13 @@ +{ lib, siteConfig, ... }: +{ + users.users = lib.mapAttrs ( + _: userCfg: + { + isNormalUser = true; + extraGroups = [ "wheel" ]; + } + // userCfg + ) siteConfig.adminUsers; + + security.sudo.wheelNeedsPassword = false; +} diff --git a/modules/caddy.nix b/modules/caddy.nix deleted file mode 100644 index 92e3343..0000000 --- a/modules/caddy.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ config, hostMeta, ... }: -{ - services.caddy = { - enable = true; - virtualHosts.${hostMeta.publicDomain}.extraConfig = '' - encode zstd gzip - - handle /health { - respond "ok" 200 - } - - ${ - if hostMeta.role == "replica" then - '' - header { - X-Wiki-Mode "read-only" - } - - '' - else - "" - }php_fastcgi unix//run/phpfpm/mediawiki.sock { - root ${config.services.mediawiki.finalPackage}/share/mediawiki - } - - file_server { - root ${config.services.mediawiki.finalPackage}/share/mediawiki - } - ''; - }; -} diff --git a/modules/common.nix b/modules/common.nix index e28e43c..da88f0d 100644 --- a/modules/common.nix +++ b/modules/common.nix @@ -25,6 +25,8 @@ time.timeZone = "America/Los_Angeles"; i18n.defaultLocale = "en_US.UTF-8"; + services.journald.storage = "persistent"; + services.timesyncd.enable = true; systemd.tmpfiles.rules = [ diff --git a/modules/deploy-ssh.nix b/modules/deploy-ssh.nix new file mode 100644 index 0000000..59dd56b --- /dev/null +++ b/modules/deploy-ssh.nix @@ -0,0 +1,29 @@ +{ ... }: +{ + networking.firewall = { + enable = true; + allowedTCPPorts = [ 22 ]; + }; + + services.openssh = { + enable = true; + openFirewall = true; + settings = { + AllowAgentForwarding = false; + AllowGroups = [ "wheel" ]; + AllowTcpForwarding = false; + ClientAliveCountMax = 2; + ClientAliveInterval = 300; + KbdInteractiveAuthentication = false; + LoginGraceTime = 20; + MaxAuthTries = 3; + MaxSessions = 4; + PasswordAuthentication = false; + PermitRootLogin = "no"; + PermitTunnel = false; + PermitUserEnvironment = false; + StreamLocalBindUnlink = false; + X11Forwarding = false; + }; + }; +} diff --git a/modules/mediawiki-base.nix b/modules/mediawiki-base.nix deleted file mode 100644 index 272ca2d..0000000 --- a/modules/mediawiki-base.nix +++ /dev/null @@ -1,109 +0,0 @@ -{ - config, - hostMeta, - siteConfig, - ... -}: -{ - services.mediawiki = { - enable = true; - name = siteConfig.wikiName; - url = "https://${hostMeta.publicDomain}"; - webserver = "none"; - passwordFile = config.age.secrets.mediawiki-admin-password.path; - - database = { - type = "mysql"; - name = siteConfig.database.name; - user = siteConfig.database.mediawikiUser; - passwordFile = config.age.secrets.mysql-mediawiki.path; - socket = "/run/mysqld/mysqld.sock"; - createLocally = false; - }; - - extensions = { - ParserFunctions = null; - WikiEditor = null; - }; - - extraConfig = '' - $wgServer = "https://${hostMeta.publicDomain}"; - $wgSitename = "${siteConfig.wikiName}"; - $wgMetaNamespace = "${siteConfig.wikiName}"; - $wgScriptPath = ""; - $wgArticlePath = "/wiki/$1"; - $wgUsePathInfo = true; - wfLoadSkin('Vector'); - - $wgEnableUploads = true; - $wgEnableEmail = false; - $wgShowIPinHeader = false; - - $wgGroupPermissions['*']['edit'] = false; - $wgGroupPermissions['*']['createpage'] = false; - $wgGroupPermissions['*']['createtalk'] = false; - $wgGroupPermissions['*']['writeapi'] = false; - $wgGroupPermissions['*']['createaccount'] = false; - - $wgGroupPermissions['user']['edit'] = true; - $wgGroupPermissions['user']['createpage'] = true; - $wgGroupPermissions['user']['createtalk'] = true; - $wgGroupPermissions['user']['upload'] = true; - $wgGroupPermissions['user']['writeapi'] = true; - ''; - }; - - services.mediawiki.poolConfig = - if hostMeta.role == "primary" then - { - "pm" = "dynamic"; - "pm.max_children" = 12; - "pm.start_servers" = 3; - "pm.min_spare_servers" = 2; - "pm.max_spare_servers" = 4; - "pm.max_requests" = 500; - } - else - { - "pm" = "dynamic"; - "pm.max_children" = 6; - "pm.start_servers" = 2; - "pm.min_spare_servers" = 1; - "pm.max_spare_servers" = 3; - "pm.max_requests" = 500; - }; - - services.phpfpm.pools.mediawiki.phpOptions = - if hostMeta.role == "primary" then - '' - opcache.enable=1 - opcache.memory_consumption=192 - opcache.interned_strings_buffer=16 - opcache.max_accelerated_files=10000 - realpath_cache_size=4096K - realpath_cache_ttl=600 - '' - else - '' - opcache.enable=1 - opcache.memory_consumption=128 - opcache.interned_strings_buffer=16 - opcache.max_accelerated_files=10000 - realpath_cache_size=2048K - realpath_cache_ttl=600 - ''; - - age.secrets.mediawiki-admin-password = { - file = ../secrets/mediawiki-admin-password.age; - owner = "mediawiki"; - group = "mediawiki"; - mode = "0400"; - }; - - age.secrets.mysql-mediawiki = { - file = ../secrets/mysql-mediawiki.age; - owner = "mediawiki"; - group = "mediawiki"; - mode = "0400"; - }; -} diff --git a/modules/security.nix b/modules/security.nix deleted file mode 100644 index 7aae683..0000000 --- a/modules/security.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ ... }: -{ - networking.firewall = { - enable = true; - allowedTCPPorts = [ 80 443 ]; - }; - - services.openssh = { - enable = true; - openFirewall = false; - settings = { - PasswordAuthentication = false; - KbdInteractiveAuthentication = false; - PermitRootLogin = "prohibit-password"; - X11Forwarding = false; - }; - }; -} diff --git a/modules/tailscale.nix b/modules/tailscale.nix deleted file mode 100644 index 4056016..0000000 --- a/modules/tailscale.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ - config, - hostMeta, - ... -}: -{ - age.secrets.tailscale-auth = { - file = ../secrets/tailscale-auth.age; - owner = "root"; - mode = "0400"; - }; - - services.tailscale = { - enable = true; - authKeyFile = config.age.secrets.tailscale-auth.path; - extraUpFlags = [ "--hostname=${hostMeta.tailscaleName}" ]; - }; - - networking.firewall.interfaces.tailscale0.allowedTCPPorts = - if hostMeta.role == "primary" then - [ - 22 - 3306 - ] - else - [ - 22 - 873 - ]; - networking.firewall.allowedUDPPorts = [ config.services.tailscale.port ]; - networking.firewall.checkReversePath = "loose"; -} diff --git a/modules/wiki-primary/mediawiki.nix b/modules/wiki-primary/mediawiki.nix deleted file mode 100644 index eaa2d77..0000000 --- a/modules/wiki-primary/mediawiki.nix +++ /dev/null @@ -1,29 +0,0 @@ -{ pkgs, siteConfig, ... }: -{ - systemd.services.mediawiki-image-sync = { - description = "Sync MediaWiki uploads to the replica"; - after = [ "tailscale-autoconnect.service" ]; - wants = [ "tailscale-autoconnect.service" ]; - path = [ pkgs.rsync ]; - serviceConfig.Type = "oneshot"; - script = '' - set -euo pipefail - - ${pkgs.rsync}/bin/rsync \ - -az \ - --delete \ - /var/lib/mediawiki/images/ \ - "rsync://${siteConfig.hosts.replica.tailscaleName}/mediawiki-images/" - ''; - }; - - systemd.timers.mediawiki-image-sync = { - description = "Periodic upload sync from primary to replica"; - wantedBy = [ "timers.target" ]; - timerConfig = { - OnCalendar = "hourly"; - Persistent = true; - RandomizedDelaySec = "10m"; - }; - }; -} diff --git a/modules/wiki-primary/mysql.nix b/modules/wiki-primary/mysql.nix deleted file mode 100644 index cf4699d..0000000 --- a/modules/wiki-primary/mysql.nix +++ /dev/null @@ -1,123 +0,0 @@ -{ - config, - pkgs, - siteConfig, - ... -}: -let - dbName = siteConfig.database.name; - wikiUser = siteConfig.database.mediawikiUser; - replicationUser = siteConfig.database.replicationUser; - mysqlCli = "${pkgs.mariadb}/bin/mysql -u root"; - bootstrapSql = pkgs.writeText "mysql-bootstrap.sql" '' - SET @wiki_pass = FROM_BASE64('$wiki_pass_b64'); - SET @repl_pass = FROM_BASE64('$repl_pass_b64'); - - CREATE DATABASE IF NOT EXISTS \`${dbName}\`; - - SET @create_wiki_user = CONCAT( - "CREATE USER IF NOT EXISTS '${wikiUser}'@'localhost' IDENTIFIED BY ", - QUOTE(@wiki_pass) - ); - PREPARE stmt FROM @create_wiki_user; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - SET @alter_wiki_user = CONCAT( - "ALTER USER '${wikiUser}'@'localhost' IDENTIFIED BY ", - QUOTE(@wiki_pass) - ); - PREPARE stmt FROM @alter_wiki_user; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - GRANT ALL PRIVILEGES ON \`${dbName}\`.* TO '${wikiUser}'@'localhost'; - - SET @create_repl_user = CONCAT( - "CREATE USER IF NOT EXISTS '${replicationUser}'@'%' IDENTIFIED BY ", - QUOTE(@repl_pass) - ); - PREPARE stmt FROM @create_repl_user; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - SET @alter_repl_user = CONCAT( - "ALTER USER '${replicationUser}'@'%' IDENTIFIED BY ", - QUOTE(@repl_pass) - ); - PREPARE stmt FROM @alter_repl_user; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO '${replicationUser}'@'%'; - FLUSH PRIVILEGES; - ''; -in -{ - services.mysql = { - enable = true; - package = pkgs.mariadb; - - settings.mysqld = { - bind-address = "0.0.0.0"; - server-id = 1; - log_bin = "mysql-bin"; - log_slave_updates = 1; - binlog_format = "ROW"; - gtid_strict_mode = 1; - innodb_file_per_table = 1; - innodb_buffer_pool_size = "4G"; - innodb_log_file_size = "512M"; - innodb_flush_method = "O_DIRECT"; - innodb_flush_neighbors = 0; - innodb_io_capacity = 1000; - innodb_io_capacity_max = 2000; - max_connections = 80; - thread_cache_size = 100; - table_open_cache = 4000; - tmp_table_size = "64M"; - max_heap_table_size = "64M"; - performance_schema = true; - slow_query_log = 1; - long_query_time = 1; - skip_name_resolve = 1; - }; - }; - - age.secrets.mysql-replication = { - file = ../../secrets/mysql-replication.age; - owner = "mysql"; - group = "mysql"; - mode = "0400"; - }; - - systemd.services.mysql-bootstrap = { - description = "Create MediaWiki database and users"; - after = [ "mysql.service" ]; - requires = [ "mysql.service" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - set -euo pipefail - - read_secret_b64() { - tr -d '\n' < "$1" | base64 -w0 - } - - wiki_pass_b64="$(read_secret_b64 ${config.age.secrets.mysql-mediawiki.path})" - repl_pass_b64="$(read_secret_b64 ${config.age.secrets.mysql-replication.path})" - - ${mysqlCli} < [ssh-identity-file] + nix run .#bootstrap-host -- [ssh-identity-file] + +Examples: + nix run .#bootstrap-host -- main-wiki root@203.0.113.10 ~/.ssh/do-bootstrap + nix run .#bootstrap-host -- root@203.0.113.10 root@203.0.113.11 ~/.ssh/do-bootstrap +USAGE +} + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + usage + exit 1 +fi + +repo_root="$(pwd)" +if [ ! -f "$repo_root/flake.nix" ]; then + printf 'Run bootstrap-host from the repo root\n' >&2 + exit 1 +fi + +ssh_identity_file="" +main_target="" +replica_target="" + +case "$1" in + main-wiki|replica-wiki) + if [ "$1" = "main-wiki" ]; then + main_target="$2" + else + replica_target="$2" + fi + ssh_identity_file="${3:-}" + ;; + *) + main_target="$1" + replica_target="$2" + ssh_identity_file="${3:-}" + ;; +esac + +make_host_module() { + local module_file="$1" + + cat > "$module_file" <<'MODULE' +{ ... }: +{ + services.journald.storage = "persistent"; + + services.openssh = { + enable = true; + openFirewall = true; + settings = { + AllowAgentForwarding = false; + AllowGroups = [ "wheel" ]; + AllowTcpForwarding = false; + ClientAliveCountMax = 2; + ClientAliveInterval = 300; + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + LoginGraceTime = 20; + MaxAuthTries = 3; + MaxSessions = 4; + PermitRootLogin = "no"; + PermitTunnel = false; + PermitUserEnvironment = false; + StreamLocalBindUnlink = false; + X11Forwarding = false; + }; + }; + + users.users.jet = { + isNormalUser = true; + extraGroups = [ "wheel" ]; + openssh.authorizedKeys.keys = [ +@ADMIN_KEYS@ + ]; + }; + + security.sudo.wheelNeedsPassword = false; + + services.do-agent.enable = false; +} +MODULE +} + +run_bootstrap() { + local host_name="$1" + local target_host="$2" + local work_dir + local module_file + local remote_target + local try + local ssh_cmd=(ssh -o StrictHostKeyChecking=accept-new) + local scp_cmd=(scp -o StrictHostKeyChecking=accept-new) + + work_dir="$(mktemp -d)" + module_file="$work_dir/host-bootstrap.nix" + remote_target="$target_host:/etc/nixos/host-bootstrap.nix" + + make_host_module "$module_file" + + if [ -n "$ssh_identity_file" ]; then + ssh_cmd+=( -i "$ssh_identity_file" ) + scp_cmd+=( -i "$ssh_identity_file" ) + fi + + "${ssh_cmd[@]}" "$target_host" 'mkdir -p /etc/nixos' + "${scp_cmd[@]}" "$module_file" "$remote_target" + + printf 'Infecting %s onto %s\n' "$host_name" "$target_host" + "${ssh_cmd[@]}" "$target_host" \ + 'umount /boot/efi 2>/dev/null || true; curl -fsSL https://raw.githubusercontent.com/elitak/nixos-infect/36f48d8feb89ca508261d7390355144fc0048932/nixos-infect | env PROVIDER=digitalocean doNetConf=y NIX_CHANNEL=nixos-24.05 NIXOS_IMPORT=./host-bootstrap.nix bash -x' || true + + printf 'Waiting for %s to reboot into NixOS\n' "$host_name" + for try in $(seq 1 60); do + if "${ssh_cmd[@]}" -o ConnectTimeout=5 "$target_host" 'grep -q "^ID=nixos" /etc/os-release'; then + break + fi + sleep 5 + done + + printf 'Finalizing network config on %s\n' "$host_name" + "${ssh_cmd[@]}" "$target_host" ' + sed -i "/defaultGateway6 = {/,/};/d" /etc/nixos/networking.nix 2>/dev/null || true + sed -i "/ipv6.routes = \[ { address = \"\"; prefixLength = 128; } \];/d" /etc/nixos/networking.nix 2>/dev/null || true + nixos-rebuild switch + ' + + rm -rf "$work_dir" +} + +if [ -n "${main_target:-}" ]; then + run_bootstrap main-wiki "$main_target" +fi +if [ -n "${replica_target:-}" ]; then + run_bootstrap replica-wiki "$replica_target" +fi + +printf '\nBootstrap complete. The hosts should now be reachable as minimal NixOS systems over public SSH.\n' diff --git a/secrets/mediawiki-admin-password.age b/secrets/mediawiki-admin-password.age index 686f5b89a493079824e823acd82551b29758b6c3..2dffda85811acd3c2813f5490f39a0e1a319cd32 100644 GIT binary patch delta 419 zcmZoGriKxuqwbL$;8yh*x#chB{0#^*vBH!q|DL)$%_1B&oUP;vpiSB z0{=?0zyOn+9CtU*kkqiK5a)F5;L?(?;B3=U50i`#SFTVGzw~4y7heO9ya=uU0nqipNLd#kF>%R?=&DyGx)!7eA%_5`}c#z602UlIFRc6C_~NC z)6BGby2aOJ&-srq7`EM%m^h0+dkUkBUwZrD%K}Ex!5amNTHPFuy)%7Wo)>WA*gj*^ FtpHKYk?H^d delta 221 zcmX@g+{iRRr{174#3(s2Al=NwwbC^x-!R$D(>>Qe-y)|ftja&tI59t{GONnmD=#C^ zk;^a1JJiv`JXgQO+0n$y*weY(->JgVKhP^Iv?$pkC%e$2%G0zgywEK#nM+q!S0Od6 z$e_f?zbYr$C)+2$J=xscEz~PGBh|H{I42}D$I;E-KP=qeFv%;U!i4L@9yf(ZPJ7#l zpSK(3XH}Wit(%m-zT&U%lQ6#}>!$WCzj$BjyQW_1%PnuqPpw(I^7GY&t_POoe4fPp Z_@Vpq&5A+#%QpAK?fR1z9dqN67XZWZSzrJF diff --git a/secrets/mysql-mediawiki.age b/secrets/mysql-mediawiki.age index 4a2945c..ed725f2 100644 --- a/secrets/mysql-mediawiki.age +++ b/secrets/mysql-mediawiki.age @@ -1,5 +1,11 @@ age-encryption.org/v1 --> ssh-ed25519 Ziw7aw dohDDxsPMp1TRbvNh2qAPUFmW4cuMLnjislpRleHEHI -9MEq670uN4CzO8U/2HZAXr8MpI/vte/5pC2yQPKWemg ---- 2npxqFr1cEdGtDhZlb4zVpy04F4Xsfb2NAu3eTDUTYg -عLT Lt2l!q>0"H5^輗 ^j5QRw^?MVoniĵ,dDS-q \ No newline at end of file +-> ssh-ed25519 Ziw7aw 5sI+R2UT4nP6T6WTm1EJ4elU8KXBlI7jzgTxVOXi/Sg +pQcdU7F0xoTiZQMyA+8L8bAUgg+7ftk1cQPCP5UOWvc +-> X25519 PRy6mfo+WzqtakJny8K9A0M8ilnEmkpREpa0wuMGNVI +Szmhsy/l6Qs5pX6TwcjPg3gxgQtNT1y0KTXEMPG/wZg +-> X25519 ja3QNUKZfQIkTCt8O41f0+3oh1dXk/v86ZKHUcvvKxI +6Qb1KMzfbyZMT2OJsckGRiWrcxS3xgCL7jXz0QWucZw +--- 7d03+3jqRYD4cII07DMedPieU+qIyl+b3KL+xcEUwro +휫5Dh?G^|ƤJd`g4U ' :t + LNIIfc՛3rF R9 +vD67 \ No newline at end of file diff --git a/secrets/mysql-replication.age b/secrets/mysql-replication.age index f465ff57b7ddbc5eed30cba3ebb5926ecd184863..2d677eb2bd96bf811413da0f117d0132a50d6898 100644 GIT binary patch delta 419 zcmZoRz#l^qCEjPn7H_BMQz{JTtDLbts&ppSjte_;#C#~4eD73`f-K)65 zmCHZ4C@?iUxWF?_yELq*svs!P)4)YL-L)vdJGU^f%E%}&*CV9ZB}?1OlS|i5A;QSi z)X-8Pwb;iv$TiO~J-IM4v)nzi!a3h3rN|)ABgHA-sJ_gyGFZPj**P^QIXldktJE~Y zDb3q7rNlMI!naI2#mF@*)!envAUr5PvZA~w**!Nu*R8@c$0fNK$%-tqlJF3B(;UyT zj7TF>^N0+`41MQv_iS&|3bR08?}&(Wx5z?s5BKn3SFTFWs(|#+06%T#+{i4?dUwZA z%MuqiBmV+ZZPyBe6f*bO%#=V6 z)8rtFL~Xys00Tp#+_W60RF6Vm*K)4jmMtQ`({FYb?u@qMGWgOEF@M2^`|Bdl@$EWq zD=(Amo7%qZ`290g`D;XHiv3A`e{qrwciL)i_4_x2t?SM$77N%h$?@R@{geCwFRdTs GS^@y{xRiVV delta 221 zcmX@g+{iRRr`|H9C_>-ZSv$weJ2WEH*~B?1)IG=7Cn+O2Ju_23Jj6ZBD=N_-%%{r8 zg3GZgG0noDs35|)*xj_$w4@*`vMkcm#n05EAh|Td-87}l%fvG$E2KClnM+q!SHalV z&7#ybGt#2Suc#z9J;$di$TZB$#JQ}@+$ks|)!ezTDlE(?rO?8>G@t8cH*;1=zs;WJ zFQ3-viNCh+W1Rc4c-J+Ct8Diki@(Wn6iSpVi=3l#)7n$N>W|ftr=ruoAN2aGZFuQn YzHNay|BYugM!b2qE7j)M|7=qN00NCv5&!@I diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 7d3acc3..bd2e9b2 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -14,7 +14,6 @@ let ]; in { - "tailscale-auth.age".publicKeys = adminKeys ++ allHosts; "mysql-mediawiki.age".publicKeys = adminKeys ++ allHosts; "mysql-replication.age".publicKeys = adminKeys ++ allHosts; "mediawiki-admin-password.age".publicKeys = adminKeys ++ allHosts; diff --git a/secrets/tailscale-auth.age b/secrets/tailscale-auth.age deleted file mode 100644 index e1db629..0000000 --- a/secrets/tailscale-auth.age +++ /dev/null @@ -1,5 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 Ziw7aw Fa0iIVw0anW/5eEZeeHjPQub7dSfowddlBHGnv/zZ2E -cXCR7bmRgwlrRGvJxAXmeceG8IZeEikBYpcK23WPn/c ---- tE9ZGzyHyQpZoBGr8m3YySYeSbpNlISsxKG7fIgqdwg -?\\tr-D{)>Jr XzZ]Ee1Wc[QƳ2ŏcfaf95 \ No newline at end of file