From 3850948f71fe6bfb9fae7c7320939f8b567dfcfd Mon Sep 17 00:00:00 2001 From: Jet Date: Sat, 21 Mar 2026 16:05:47 -0700 Subject: [PATCH 1/5] 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 From f9afc7285fe37133bae57de30063585a5a04e6ff Mon Sep 17 00:00:00 2001 From: Jet Date: Wed, 25 Mar 2026 19:26:45 -0700 Subject: [PATCH 2/5] feat: update readme to be about prototype --- README.md | 57 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f4c284f..1274e67 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,37 @@ -# Noisebridge Wiki Infra +# Noisebridge Wiki *2.0 Prototype* -This repo manages the Noisebridge wiki hosts on NixOS. +This repo manages the Noisebridge wiki. It is currently for the Noisebridge Wiki 2.0 Prototype that is planned to eventually replace the current Noisebridge wiki infrastructure. + +## Development Hosts + +- primary wiki: `main-wiki.extremist.software` +- read-only replica: `replica-wiki.extremist.software` +- deployment/admin SSH user: `jet` *this is hoped to expand soon!* + +A note here, once this project is underway, CI/CD should only allow changes to come through reviewed PRs into the main branch. These changes would then be built and deployed from an automated Github action (could be forgejo actions in the future) + +The current repo is the deployment foundation for a two-machine MediaWiki stack: + +- primary host: MediaWiki, MariaDB primary, Caddy, agenix-managed secrets +- replica host: MediaWiki, MariaDB read-only replica, Caddy, agenix-managed secrets + +We haven't fully implemented all the features, but ones that are needed before we do the big swap are: + ## Commands Bootstrap a brand new Ubuntu 22.04 DigitalOcean VPS into NixOS: ```sh -nix run .#bootstrap-host -- [ssh-identity-file] -nix run .#bootstrap-host -- [ssh-identity-file] +nix run .#bootstrap-host -- [--admin ] [ssh-identity-file] +nix run .#bootstrap-host -- [--admin ] [ssh-identity-file] ``` Example: ```sh nix run .#bootstrap-host -- main-wiki root@203.0.113.10 ~/.ssh/do-bootstrap +nix run .#bootstrap-host -- --admin jet 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 ``` @@ -25,6 +42,15 @@ What bootstrap does: - converts the machine to NixOS with the `jet` admin user - disables direct root SSH - fixes the known bad IPv6 routes generated by `nixos-infect` +- verifies that `jet` login and `sudo` work and that the host reaches `running` + +What bootstrap is not: + +- it is not the normal long-term deploy path +- it is not the full application rollout +- it is only the one-off Ubuntu-to-NixOS installer step + +> This is made to only be run once and to potentially prop up new servers if needed Deploy all already-bootstrapped hosts: @@ -57,24 +83,5 @@ 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 and comes up over hardened public SSH as `jet`. -4. Future changes use `nix run .#deploy`. - -## GitHub Settings - -To require pull requests and auto-deploy only from `main`, set branch protection or a ruleset on `main` with: - -- require a pull request before merging -- do not allow direct pushes to `main` -- require status checks to pass before merging -- select the CI check job from this repo -- optionally require approvals before merging - -This repo already deploys on pushes to `main` in `.github/workflows/ci.yml`. - -That means the intended flow is: - -1. open a PR -2. CI passes -3. merge into `main` -4. GitHub Actions runs `nix run .#deploy` +3. The machine installs NixOS and comes up over public SSH. +4. Future configuration changes would be made through CI/CD. From 98d51970565fca5d2818390ff321038c7ebecd7d Mon Sep 17 00:00:00 2001 From: Jet Date: Wed, 25 Mar 2026 19:26:45 -0700 Subject: [PATCH 3/5] fix: use individual admins --- .gitignore | 1 + flake.nix | 12 +++--- scripts/bootstrap-host.sh | 90 ++++++++++++++++++++++++++++++--------- 3 files changed, 77 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 85bc657..41d2d77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .bootstrap/ .direnv/ +dump/ diff --git a/flake.nix b/flake.nix index 1f1d05f..d3dc134 100644 --- a/flake.nix +++ b/flake.nix @@ -153,13 +153,13 @@ type = "app"; 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 - ) - )) + "@ADMIN_USERS_JSON@" + "@JQ@" + ] + [ + (builtins.toJSON siteConfig.adminUsers) + "${pkgs.jq}/bin/jq" ] (builtins.readFile ./scripts/bootstrap-host.sh) )}"; diff --git a/scripts/bootstrap-host.sh b/scripts/bootstrap-host.sh index 5795eb4..be9f27b 100644 --- a/scripts/bootstrap-host.sh +++ b/scripts/bootstrap-host.sh @@ -3,29 +3,39 @@ set -euo pipefail usage() { cat <<'USAGE' Usage: - nix run .#bootstrap-host -- [ssh-identity-file] - nix run .#bootstrap-host -- [ssh-identity-file] + nix run .#bootstrap-host -- [--admin ] [ssh-identity-file] + nix run .#bootstrap-host -- [--admin ] [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 +admin_users_json='@ADMIN_USERS_JSON@' + +bootstrap_admin="jet" ssh_identity_file="" main_target="" replica_target="" +failures=() + +if [ "${1:-}" = "--admin" ]; then + if [ "$#" -lt 4 ]; then + usage + exit 1 + fi + bootstrap_admin="$2" + shift 2 +fi + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + usage + exit 1 +fi case "$1" in main-wiki|replica-wiki) @@ -43,8 +53,22 @@ case "$1" in ;; esac +admin_keys() { + printf '%s' "$admin_users_json" | @JQ@ -r --arg user "$1" '.[$user].openssh.authorizedKeys.keys[]? | " \"" + . + "\""' +} + +admin_exists() { + printf '%s' "$admin_users_json" | @JQ@ -e --arg user "$1" 'has($user)' >/dev/null +} + +if ! admin_exists "$bootstrap_admin"; then + printf 'Unknown admin user for bootstrap: %s\n' "$bootstrap_admin" >&2 + exit 1 +fi + make_host_module() { local module_file="$1" + local admin_name="$2" cat > "$module_file" <<'MODULE' { ... }: @@ -73,7 +97,7 @@ make_host_module() { }; }; - users.users.jet = { + users.users.BOOTSTRAP_ADMIN = { isNormalUser = true; extraGroups = [ "wheel" ]; openssh.authorizedKeys.keys = [ @@ -86,6 +110,9 @@ make_host_module() { services.do-agent.enable = false; } MODULE + + sed -i "s/BOOTSTRAP_ADMIN/$admin_name/" "$module_file" + perl -0pi -e 's/\n@ADMIN_KEYS@/\n'"$(admin_keys "$admin_name" | sed 's/[\/&]/\\&/g')"'/g' "$module_file" } run_bootstrap() { @@ -97,12 +124,14 @@ run_bootstrap() { local try local ssh_cmd=(ssh -o StrictHostKeyChecking=accept-new) local scp_cmd=(scp -o StrictHostKeyChecking=accept-new) + local admin_target work_dir="$(mktemp -d)" module_file="$work_dir/host-bootstrap.nix" remote_target="$target_host:/etc/nixos/host-bootstrap.nix" + admin_target="${bootstrap_admin}@${target_host#*@}" - make_host_module "$module_file" + make_host_module "$module_file" "$bootstrap_admin" if [ -n "$ssh_identity_file" ]; then ssh_cmd+=( -i "$ssh_identity_file" ) @@ -118,27 +147,48 @@ run_bootstrap() { 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 + if "${ssh_cmd[@]}" -o ConnectTimeout=5 "$admin_target" 'grep -q "^ID=nixos" /etc/os-release' 2>/dev/null; then break fi sleep 5 done + if ! "${ssh_cmd[@]}" -o ConnectTimeout=5 "$admin_target" 'grep -q "^ID=nixos" /etc/os-release' 2>/dev/null; then + printf 'Bootstrap failed for %s: host did not come back as NixOS with %s access\n' "$host_name" "$bootstrap_admin" >&2 + failures+=( "$host_name" ) + rm -rf "$work_dir" + return 1 + fi + 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 + "${ssh_cmd[@]}" "$admin_target" ' + sudo sed -i "/defaultGateway6 = {/,/};/d" /etc/nixos/networking.nix 2>/dev/null || true + sudo sed -i "/ipv6.routes = \[ { address = \"\"; prefixLength = 128; } \];/d" /etc/nixos/networking.nix 2>/dev/null || true + sudo nixos-rebuild switch ' + if ! "${ssh_cmd[@]}" "$admin_target" 'sudo -n true >/dev/null && test "$(systemctl is-system-running || true)" = running' 2>/dev/null; then + printf 'Bootstrap verification failed for %s: host is not healthy after first switch\n' "$host_name" >&2 + failures+=( "$host_name" ) + rm -rf "$work_dir" + return 1 + fi + + printf 'Bootstrap verified for %s\n' "$host_name" + rm -rf "$work_dir" } if [ -n "${main_target:-}" ]; then - run_bootstrap main-wiki "$main_target" + run_bootstrap main-wiki "$main_target" || true fi if [ -n "${replica_target:-}" ]; then - run_bootstrap replica-wiki "$replica_target" + run_bootstrap replica-wiki "$replica_target" || true fi -printf '\nBootstrap complete. The hosts should now be reachable as minimal NixOS systems over public SSH.\n' +if [ "${#failures[@]}" -ne 0 ]; then + printf '\nBootstrap failed for: %s\n' "${failures[*]}" >&2 + exit 1 +fi + +printf '\nBootstrap complete. The hosts should now be reachable as NixOS systems over public SSH as %s.\n' "$bootstrap_admin" From b00bd87046240e08a4884b3fe35e6d7f9813d9fc Mon Sep 17 00:00:00 2001 From: Jet Date: Wed, 25 Mar 2026 22:19:04 -0700 Subject: [PATCH 4/5] feat: list features that we need to implement --- README.md | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1274e67..361b53a 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,135 @@ The current repo is the deployment foundation for a two-machine MediaWiki stack: - primary host: MediaWiki, MariaDB primary, Caddy, agenix-managed secrets - replica host: MediaWiki, MariaDB read-only replica, Caddy, agenix-managed secrets -We haven't fully implemented all the features, but ones that are needed before we do the big swap are: - +We haven't fully implemented all the features, but the remaining work is tracked here so this README can act as the main working checklist. + +## Implementation Checklist + +### Core rollout + +- [ ] Finish `hosts/main-wiki.nix` with MediaWiki, MariaDB primary, Caddy, and agenix-managed secrets +- [ ] Finish `hosts/replica-wiki.nix` with MediaWiki, MariaDB replica, Caddy, and agenix-managed secrets +- [ ] Make the flake configuration fully express hostnames, domains, shared secrets, and host-only secrets +- [ ] Wire all required agenix secrets into services on both hosts +- [ ] Keep both machine closures building cleanly from the flake +- [ ] Keep `deploy-rs` as the standard deployment path for both machines + +### Database and replication + +- [ ] Create the MediaWiki MariaDB database and application user on the primary +- [ ] Configure MariaDB replication user and secure replication credentials +- [ ] Configure the replica host as a real read-only MariaDB replica +- [ ] Verify replication from primary to replica under normal wiki writes +- [ ] Document promotion and rebuild expectations for the replica + +### MediaWiki application + +- [ ] Choose and pin the exact MediaWiki version for the new stack +- [ ] Recreate the current wiki configuration from `LocalSettings.php` in a maintainable Nix-managed form +- [ ] Configure uploads, logo, favicon, and local asset paths +- [ ] Reinstall and validate required extensions +- [ ] Reinstall and validate any non-default skins +- [ ] Recreate job queue or maintenance task execution needed by the wiki +- [ ] Confirm the primary wiki is writable +- [ ] Confirm the replica wiki is publicly readable and actually read-only + +### Web serving and TLS + +- [ ] Configure Caddy virtual hosts for `main-wiki.extremist.software` +- [ ] Configure Caddy virtual hosts for `replica-wiki.extremist.software` +- [ ] Configure TLS and renewal behavior for both public hosts +- [ ] Recreate any needed redirects, asset routes, and static file handling +- [ ] Verify PHP-FPM and web serving behavior under the chosen runtime + +### Migration and cutover + +- [ ] Dump the current MediaWiki database +- [ ] Verify the actual table prefix used in production +- [ ] Copy uploaded files from `/srv/mediawiki/noisebridge.net/images/` +- [ ] Copy local static assets from `/srv/mediawiki/noisebridge.net/img/` +- [ ] Copy and review the current `LocalSettings.php` +- [ ] Inventory current secrets including DB credentials, MediaWiki secret keys, upgrade key, and ReCaptcha keys +- [ ] Inventory the exact live MediaWiki version, extensions, skins, and Composer-managed dependencies +- [ ] Inventory current Caddy and PHP-FPM runtime configuration +- [ ] Inventory cron or systemd jobs related to MediaWiki maintenance, backups, or queues +- [ ] Measure current database and upload sizes for migration planning +- [ ] Produce rollback notes for the final cutover +- [ ] Import production data into the new primary host +- [ ] Verify the replica catches up from the imported primary state +- [ ] Smoke test reading, editing, login, uploads, search, history, and diff behavior before cutover + +### CI/CD and repository workflow + +- [ ] Require reviewed PRs before merge to `main` +- [ ] Block direct pushes to `main` +- [ ] Keep `nix flake check` required in CI +- [ ] Keep both host builds required in CI +- [ ] Keep automatic deploys on pushes to `main` +- [ ] Add post-deploy smoke checks if needed +- [ ] Optionally move from GitHub Actions to Forgejo Actions later + +### Security and access policy + +- [ ] Explicitly define anonymous user permissions in MediaWiki +- [ ] Keep account creation invite-only +- [ ] Allow read, search, history, and diff access where desired +- [ ] Restrict broader special-page use for anonymous traffic +- [ ] Review SSH/admin access model beyond the initial `jet` user + +## Future Features Checklist + +### Edge and public access + +- [ ] Use the final apex domain for the primary wiki +- [ ] Serve the final replica from `replica.` +- [ ] Support a direct-to-origin non-Cloudflare deployment mode +- [ ] Add a separate Cloudflare-proxied deployment mode later + +### Performance and abuse controls + +- [ ] Add aggressive anonymous rate limiting in Caddy +- [ ] Add cache policy that favors anonymous page views +- [ ] Reduce or bypass caching for logged-in and dynamic traffic +- [ ] Preserve good behavior for logged-in editors while limiting abuse +- [ ] Add stronger service and access logging for tuning + +### Database evolution + +- [ ] Revisit the long-term database backend after the baseline is stable +- [ ] Evaluate migration away from MariaDB if a better fit emerges + +### Observability + +- [ ] Add public read-only Grafana +- [ ] Add a public status page +- [ ] Add email alerts +- [ ] Add Discord webhook alerts +- [ ] Add more detailed dashboards + +### Backups and exports + +- [ ] Add encrypted client-side backups to Backblaze B2 +- [ ] Define a retention policy +- [ ] Write a restore runbook +- [ ] Support volunteer-hosted backup targets later +- [ ] Provide a scraper-friendly export API instead of forcing heavy live-site scraping +- [ ] Publish a sanitized public SQL subset rather than a raw production dump +- [ ] Generate daily export snapshots +- [ ] Host downloads away from the primary, ideally from the replica side +- [ ] Publish stable JSON metadata for latest export, export history, checksums, and download URLs +- [ ] Add additional public export formats beyond SQL subset dumps + +### Tor and alternative access + +- [ ] Add a stable onion service for the primary host +- [ ] Add a stable onion service for the replica host +- [ ] Manage onion private keys with agenix so addresses survive rebuilds + +### Longer-term operations + +- [ ] Write a formal failover and promotion runbook +- [ ] Add stronger deployment protections +- [ ] Add a scheduled flake lock update workflow or an admin-run update script with PR review before merge ## Commands From 01c0fa76cb3472f4c9e6619a3dfc136ed7956306 Mon Sep 17 00:00:00 2001 From: Jet Date: Wed, 25 Mar 2026 23:04:45 -0700 Subject: [PATCH 5/5] feat: remove default admin --- README.md | 13 +++++++------ scripts/bootstrap-host.sh | 33 ++++++++++++++++----------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 361b53a..bd38a7a 100644 --- a/README.md +++ b/README.md @@ -150,26 +150,27 @@ We haven't fully implemented all the features, but the remaining work is tracked Bootstrap a brand new Ubuntu 22.04 DigitalOcean VPS into NixOS: ```sh -nix run .#bootstrap-host -- [--admin ] [ssh-identity-file] -nix run .#bootstrap-host -- [--admin ] [ssh-identity-file] +nix run .#bootstrap-host -- --admin [ssh-identity-file] +nix run .#bootstrap-host -- --admin [ssh-identity-file] ``` Example: ```sh -nix run .#bootstrap-host -- main-wiki root@203.0.113.10 ~/.ssh/do-bootstrap nix run .#bootstrap-host -- --admin jet 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 +nix run .#bootstrap-host -- --admin jet root@203.0.113.10 root@203.0.113.11 ~/.ssh/do-bootstrap ``` +`--admin ` is required. The admin must exist in `siteConfig.adminUsers` in `flake.nix`. + What bootstrap does: - 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 +- converts the machine to NixOS with the requested admin user - disables direct root SSH - fixes the known bad IPv6 routes generated by `nixos-infect` -- verifies that `jet` login and `sudo` work and that the host reaches `running` +- verifies that the requested admin login and `sudo` work and that the host reaches `running` What bootstrap is not: diff --git a/scripts/bootstrap-host.sh b/scripts/bootstrap-host.sh index be9f27b..78fd40e 100644 --- a/scripts/bootstrap-host.sh +++ b/scripts/bootstrap-host.sh @@ -3,8 +3,8 @@ set -euo pipefail usage() { cat <<'USAGE' Usage: - nix run .#bootstrap-host -- [--admin ] [ssh-identity-file] - nix run .#bootstrap-host -- [--admin ] [ssh-identity-file] + nix run .#bootstrap-host -- --admin [ssh-identity-file] + nix run .#bootstrap-host -- --admin [ssh-identity-file] USAGE } @@ -17,21 +17,23 @@ fi admin_users_json='@ADMIN_USERS_JSON@' -bootstrap_admin="jet" +pinned_nix_install_url='https://releases.nixos.org/nix/nix-2.24.14/install' + +bootstrap_admin="" ssh_identity_file="" main_target="" replica_target="" failures=() -if [ "${1:-}" = "--admin" ]; then - if [ "$#" -lt 4 ]; then - usage - exit 1 - fi - bootstrap_admin="$2" - shift 2 +if [ "${1:-}" != "--admin" ] || [ "$#" -lt 4 ]; then + printf 'Bootstrap requires --admin \n' >&2 + usage + exit 1 fi +bootstrap_admin="$2" +shift 2 + if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then usage exit 1 @@ -70,7 +72,7 @@ make_host_module() { local module_file="$1" local admin_name="$2" - cat > "$module_file" <<'MODULE' + cat > "$module_file" </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 + "umount /boot/efi 2>/dev/null || true; curl -fsSL https://raw.githubusercontent.com/elitak/nixos-infect/36f48d8feb89ca508261d7390355144fc0048932/nixos-infect | env NIX_INSTALL_URL='$pinned_nix_install_url' 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