feat: get to a solid bootstrap on public ssh
This commit is contained in:
parent
642869ce9b
commit
3850948f71
27 changed files with 262 additions and 865 deletions
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
|
|
@ -38,13 +38,6 @@ jobs:
|
||||||
|
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- 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
|
- name: Configure SSH
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
|
|
|
||||||
17
README.md
17
README.md
|
|
@ -1,10 +1,10 @@
|
||||||
# Noisebridge Wiki Infra
|
# Noisebridge Wiki Infra
|
||||||
|
|
||||||
This repo manages the Noisebridge MediaWiki primary and replica on NixOS.
|
This repo manages the Noisebridge wiki hosts on NixOS.
|
||||||
|
|
||||||
## Commands
|
## 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
|
```sh
|
||||||
nix run .#bootstrap-host -- <main-wiki|replica-wiki> <target-host> [ssh-identity-file]
|
nix run .#bootstrap-host -- <main-wiki|replica-wiki> <target-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:
|
What bootstrap does:
|
||||||
|
|
||||||
- generates or reuses `.bootstrap/<host>/host.age`
|
- copies a first-boot module to the host
|
||||||
- writes the matching public recipient to `secrets/hosts/<host>.age.pub`
|
- runs `nixos-infect` on the Ubuntu VPS
|
||||||
- rekeys the agenix secrets with `agenix -r`
|
- converts the machine to NixOS with the `jet` admin user
|
||||||
- runs `nixos-anywhere` against one or both raw VPS targets
|
- disables direct root SSH
|
||||||
- installs `/var/lib/agenix/host.age` onto the new machine
|
- fixes the known bad IPv6 routes generated by `nixos-infect`
|
||||||
- lets the machine decrypt its Tailscale auth secret and come up on Tailscale with its configured hostname
|
|
||||||
|
|
||||||
Deploy all already-bootstrapped hosts:
|
Deploy all already-bootstrapped hosts:
|
||||||
|
|
||||||
|
|
@ -58,7 +57,7 @@ nix flake check 'path:.' --accept-flake-config
|
||||||
|
|
||||||
1. Create a raw VPS.
|
1. Create a raw VPS.
|
||||||
2. Run `nix run .#bootstrap-host -- ...` from the repo root on an admin laptop.
|
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`.
|
4. Future changes use `nix run .#deploy`.
|
||||||
|
|
||||||
## GitHub Settings
|
## GitHub Settings
|
||||||
|
|
|
||||||
|
|
@ -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 = "/";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
175
flake.lock
generated
175
flake.lock
generated
|
|
@ -67,48 +67,6 @@
|
||||||
"type": "github"
|
"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-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
|
|
@ -125,27 +83,6 @@
|
||||||
"type": "github"
|
"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": {
|
"home-manager": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
|
|
@ -167,95 +104,6 @@
|
||||||
"type": "github"
|
"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": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1773821835,
|
"lastModified": 1773821835,
|
||||||
|
|
@ -276,8 +124,6 @@
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"agenix": "agenix",
|
"agenix": "agenix",
|
||||||
"deploy-rs": "deploy-rs",
|
"deploy-rs": "deploy-rs",
|
||||||
"disko": "disko",
|
|
||||||
"nixos-anywhere": "nixos-anywhere",
|
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -311,27 +157,6 @@
|
||||||
"type": "github"
|
"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": {
|
"utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": "systems_2"
|
"systems": "systems_2"
|
||||||
|
|
|
||||||
211
flake.nix
211
flake.nix
|
|
@ -16,14 +16,6 @@
|
||||||
url = "github:serokell/deploy-rs";
|
url = "github:serokell/deploy-rs";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
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 =
|
outputs =
|
||||||
|
|
@ -32,8 +24,6 @@
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
agenix,
|
agenix,
|
||||||
deploy-rs,
|
deploy-rs,
|
||||||
disko,
|
|
||||||
nixos-anywhere,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
|
@ -45,7 +35,14 @@
|
||||||
wikiName = "Noisebridge";
|
wikiName = "Noisebridge";
|
||||||
baseDomain = "noisebridge.net";
|
baseDomain = "noisebridge.net";
|
||||||
replicaSubdomain = "replica";
|
replicaSubdomain = "replica";
|
||||||
sshUser = "root";
|
sshUser = "jet";
|
||||||
|
adminUsers = {
|
||||||
|
jet = {
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu jetthomaspham@gmail.com"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
primaryHostName = "main-wiki";
|
primaryHostName = "main-wiki";
|
||||||
replicaHostName = "replica-wiki";
|
replicaHostName = "replica-wiki";
|
||||||
|
|
||||||
|
|
@ -58,11 +55,11 @@
|
||||||
hosts = {
|
hosts = {
|
||||||
primary = {
|
primary = {
|
||||||
nixosName = primaryHostName;
|
nixosName = primaryHostName;
|
||||||
tailscaleName = primaryHostName;
|
publicIpv4 = "134.199.221.52";
|
||||||
};
|
};
|
||||||
replica = {
|
replica = {
|
||||||
nixosName = replicaHostName;
|
nixosName = replicaHostName;
|
||||||
tailscaleName = replicaHostName;
|
publicIpv4 = "167.99.174.109";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -82,12 +79,8 @@
|
||||||
publicDomain = mkPublicDomain role;
|
publicDomain = mkPublicDomain role;
|
||||||
};
|
};
|
||||||
|
|
||||||
mkHost =
|
mkDeployHost =
|
||||||
{
|
hostModule: hostMeta:
|
||||||
hostMeta,
|
|
||||||
hostModule,
|
|
||||||
roleModules,
|
|
||||||
}:
|
|
||||||
lib.nixosSystem {
|
lib.nixosSystem {
|
||||||
inherit system;
|
inherit system;
|
||||||
specialArgs = {
|
specialArgs = {
|
||||||
|
|
@ -95,16 +88,11 @@
|
||||||
};
|
};
|
||||||
modules = [
|
modules = [
|
||||||
agenix.nixosModules.default
|
agenix.nixosModules.default
|
||||||
disko.nixosModules.disko
|
|
||||||
hostModule
|
hostModule
|
||||||
./disk-config.nix
|
|
||||||
./modules/common.nix
|
./modules/common.nix
|
||||||
./modules/security.nix
|
./modules/admin-users.nix
|
||||||
./modules/tailscale.nix
|
./modules/deploy-ssh.nix
|
||||||
./modules/caddy.nix
|
];
|
||||||
./modules/mediawiki-base.nix
|
|
||||||
]
|
|
||||||
++ roleModules;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
primaryMeta = mkHostMeta "primary";
|
primaryMeta = mkHostMeta "primary";
|
||||||
|
|
@ -112,40 +100,28 @@
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
nixosConfigurations = {
|
nixosConfigurations = {
|
||||||
main-wiki = mkHost {
|
main-wiki = mkDeployHost ./hosts/main-wiki.nix primaryMeta;
|
||||||
hostMeta = primaryMeta;
|
|
||||||
hostModule = ./hosts/main-wiki;
|
|
||||||
roleModules = [
|
|
||||||
./modules/wiki-primary/mysql.nix
|
|
||||||
./modules/wiki-primary/mediawiki.nix
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
replica-wiki = mkHost {
|
replica-wiki = mkDeployHost ./hosts/replica-wiki.nix replicaMeta;
|
||||||
hostMeta = replicaMeta;
|
|
||||||
hostModule = ./hosts/replica-wiki;
|
|
||||||
roleModules = [
|
|
||||||
./modules/wiki-replica/mysql.nix
|
|
||||||
./modules/wiki-replica/mediawiki.nix
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
deploy.nodes = {
|
deploy.nodes = {
|
||||||
main-wiki = {
|
main-wiki = {
|
||||||
hostname = primaryMeta.tailscaleName;
|
hostname = primaryMeta.publicIpv4;
|
||||||
|
remoteBuild = true;
|
||||||
|
sshUser = siteConfig.sshUser;
|
||||||
profiles.system = {
|
profiles.system = {
|
||||||
user = siteConfig.sshUser;
|
user = "root";
|
||||||
sshUser = siteConfig.sshUser;
|
|
||||||
path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.main-wiki;
|
path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.main-wiki;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
replica-wiki = {
|
replica-wiki = {
|
||||||
hostname = replicaMeta.tailscaleName;
|
hostname = replicaMeta.publicIpv4;
|
||||||
|
remoteBuild = true;
|
||||||
|
sshUser = siteConfig.sshUser;
|
||||||
profiles.system = {
|
profiles.system = {
|
||||||
user = siteConfig.sshUser;
|
user = "root";
|
||||||
sshUser = siteConfig.sshUser;
|
|
||||||
path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.replica-wiki;
|
path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.replica-wiki;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -175,129 +151,19 @@
|
||||||
|
|
||||||
bootstrap-host = {
|
bootstrap-host = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${pkgs.writeShellScript "bootstrap-host" ''
|
program = "${pkgs.writeShellScript "bootstrap-host" (
|
||||||
set -euo pipefail
|
builtins.replaceStrings
|
||||||
|
[ "@ADMIN_KEYS@" ]
|
||||||
usage() {
|
[
|
||||||
cat <<'EOF'
|
(lib.concatMapStringsSep "\n" (key: " \"${key}\"") (
|
||||||
Usage:
|
lib.flatten (
|
||||||
nix run .#bootstrap-host -- <main-wiki|replica-wiki> <target-host> [ssh-identity-file]
|
lib.mapAttrsToList (_: userCfg: userCfg.openssh.authorizedKeys.keys or [ ]) siteConfig.adminUsers
|
||||||
nix run .#bootstrap-host -- <main-target-host> <replica-target-host> [ssh-identity-file]
|
)
|
||||||
|
))
|
||||||
Examples:
|
]
|
||||||
nix run .#bootstrap-host -- main-wiki root@203.0.113.10 ~/.ssh/do-bootstrap
|
(builtins.readFile ./scripts/bootstrap-host.sh)
|
||||||
nix run .#bootstrap-host -- root@203.0.113.10 root@203.0.113.11 ~/.ssh/do-bootstrap
|
)}";
|
||||||
EOF
|
meta.description = "Convert one or both Ubuntu DigitalOcean hosts into a minimal NixOS bootstrap config with nixos-infect";
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -305,7 +171,6 @@
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
agenix.packages.${system}.default
|
agenix.packages.${system}.default
|
||||||
deploy-rs.packages.${system}.default
|
deploy-rs.packages.${system}.default
|
||||||
nixos-anywhere.packages.${system}.default
|
|
||||||
mariadb.client
|
mariadb.client
|
||||||
rsync
|
rsync
|
||||||
curl
|
curl
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{ hostMeta, siteConfig, ... }:
|
{ hostMeta, siteConfig, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../shared/hardware-configuration.nix
|
./hardware-configuration.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
networking.hostName = hostMeta.nixosName;
|
networking.hostName = hostMeta.nixosName;
|
||||||
|
|
@ -4,8 +4,11 @@
|
||||||
(modulesPath + "/virtualisation/digital-ocean-config.nix")
|
(modulesPath + "/virtualisation/digital-ocean-config.nix")
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.loader.systemd-boot.enable = true;
|
boot.loader.grub = {
|
||||||
boot.loader.efi.canTouchEfiVariables = true;
|
enable = true;
|
||||||
|
efiSupport = false;
|
||||||
|
devices = lib.mkForce [ "/dev/vda" ];
|
||||||
|
};
|
||||||
|
|
||||||
fileSystems."/" = {
|
fileSystems."/" = {
|
||||||
device = lib.mkDefault "/dev/disk/by-label/nixos";
|
device = lib.mkDefault "/dev/disk/by-label/nixos";
|
||||||
6
hosts/main-wiki.nix
Normal file
6
hosts/main-wiki.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./common.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
6
hosts/replica-wiki.nix
Normal file
6
hosts/replica-wiki.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./common.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{ hostMeta, siteConfig, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../shared/hardware-configuration.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
networking.hostName = hostMeta.nixosName;
|
|
||||||
networking.domain = siteConfig.baseDomain;
|
|
||||||
|
|
||||||
system.stateVersion = "24.11";
|
|
||||||
}
|
|
||||||
13
modules/admin-users.nix
Normal file
13
modules/admin-users.nix
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{ lib, siteConfig, ... }:
|
||||||
|
{
|
||||||
|
users.users = lib.mapAttrs (
|
||||||
|
_: userCfg:
|
||||||
|
{
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [ "wheel" ];
|
||||||
|
}
|
||||||
|
// userCfg
|
||||||
|
) siteConfig.adminUsers;
|
||||||
|
|
||||||
|
security.sudo.wheelNeedsPassword = false;
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -25,6 +25,8 @@
|
||||||
time.timeZone = "America/Los_Angeles";
|
time.timeZone = "America/Los_Angeles";
|
||||||
i18n.defaultLocale = "en_US.UTF-8";
|
i18n.defaultLocale = "en_US.UTF-8";
|
||||||
|
|
||||||
|
services.journald.storage = "persistent";
|
||||||
|
|
||||||
services.timesyncd.enable = true;
|
services.timesyncd.enable = true;
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
|
|
|
||||||
29
modules/deploy-ssh.nix
Normal file
29
modules/deploy-ssh.nix
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
@ -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";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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} <<SQL
|
|
||||||
$(< ${bootstrapSql})
|
|
||||||
SQL
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.mediawiki-init = {
|
|
||||||
after = [ "mysql-bootstrap.service" ];
|
|
||||||
requires = [ "mysql-bootstrap.service" ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
services.mediawiki.extraConfig = lib.mkAfter ''
|
|
||||||
$wgReadOnly = "This wiki is a read-only replica.";
|
|
||||||
$wgEnableUploads = false;
|
|
||||||
'';
|
|
||||||
|
|
||||||
systemd.services.mediawiki-init.wantedBy = lib.mkForce [ ];
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
siteConfig,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
replicationUser = siteConfig.database.replicationUser;
|
|
||||||
mysqlCli = "${pkgs.mariadb}/bin/mysql -u root";
|
|
||||||
initReplicaSql = pkgs.writeText "mysql-init-replica.sql" ''
|
|
||||||
STOP SLAVE;
|
|
||||||
SET @repl_pass = FROM_BASE64('$repl_pass_b64');
|
|
||||||
SET @change_master = CONCAT(
|
|
||||||
"CHANGE MASTER TO ",
|
|
||||||
"MASTER_HOST='${siteConfig.hosts.primary.tailscaleName}', ",
|
|
||||||
"MASTER_USER='${replicationUser}', ",
|
|
||||||
"MASTER_PASSWORD=", QUOTE(@repl_pass), ", ",
|
|
||||||
"MASTER_USE_GTID=slave_pos"
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @change_master;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
START SLAVE;
|
|
||||||
SHOW SLAVE STATUS\G
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
|
||||||
services.mysql = {
|
|
||||||
enable = true;
|
|
||||||
package = pkgs.mariadb;
|
|
||||||
|
|
||||||
settings.mysqld = {
|
|
||||||
bind-address = "127.0.0.1";
|
|
||||||
server-id = 2;
|
|
||||||
relay_log = "relay-bin";
|
|
||||||
log_slave_updates = 1;
|
|
||||||
read_only = 1;
|
|
||||||
super_read_only = 1;
|
|
||||||
gtid_strict_mode = 1;
|
|
||||||
innodb_file_per_table = 1;
|
|
||||||
innodb_buffer_pool_size = "2G";
|
|
||||||
innodb_log_file_size = "256M";
|
|
||||||
innodb_flush_method = "O_DIRECT";
|
|
||||||
innodb_flush_neighbors = 0;
|
|
||||||
innodb_io_capacity = 500;
|
|
||||||
innodb_io_capacity_max = 1000;
|
|
||||||
max_connections = 40;
|
|
||||||
thread_cache_size = 50;
|
|
||||||
table_open_cache = 2000;
|
|
||||||
tmp_table_size = "32M";
|
|
||||||
max_heap_table_size = "32M";
|
|
||||||
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";
|
|
||||||
};
|
|
||||||
|
|
||||||
services.rsyncd = {
|
|
||||||
enable = true;
|
|
||||||
settings = {
|
|
||||||
globalSection = {
|
|
||||||
"use chroot" = false;
|
|
||||||
};
|
|
||||||
sections.mediawiki-images = {
|
|
||||||
path = "/var/lib/mediawiki/images";
|
|
||||||
comment = "MediaWiki upload mirror target";
|
|
||||||
"read only" = false;
|
|
||||||
list = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
(pkgs.writeShellScriptBin "init-replica" ''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
repl_pass_b64="$(tr -d '\n' < ${config.age.secrets.mysql-replication.path} | base64 -w0)"
|
|
||||||
|
|
||||||
${mysqlCli} <<SQL
|
|
||||||
$(< ${initReplicaSql})
|
|
||||||
SQL
|
|
||||||
'')
|
|
||||||
];
|
|
||||||
}
|
|
||||||
144
scripts/bootstrap-host.sh
Normal file
144
scripts/bootstrap-host.sh
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage:
|
||||||
|
nix run .#bootstrap-host -- <main-wiki|replica-wiki> <target-host> [ssh-identity-file]
|
||||||
|
nix run .#bootstrap-host -- <main-target-host> <replica-target-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'
|
||||||
Binary file not shown.
|
|
@ -1,5 +1,11 @@
|
||||||
age-encryption.org/v1
|
age-encryption.org/v1
|
||||||
-> ssh-ed25519 Ziw7aw dohDDxsPMp1TRbvNh2qAPUFmW4cuMLnjislpRleHEHI
|
-> ssh-ed25519 Ziw7aw 5sI+R2UT4nP6T6WTm1EJ4elU8KXBlI7jzgTxVOXi/Sg
|
||||||
9MEq670uN4CzO8U/2HZAXr8MpI/vte/5pC2yQPKWemg
|
pQcdU7F0xoTiZQMyA+8L8bAUgg+7ftk1cQPCP5UOWvc
|
||||||
--- 2npxqFr1cEdGtDhZlb4zVpy04F4Xsfb2NAu3eTDUTYg
|
-> X25519 PRy6mfo+WzqtakJny8K9A0M8ilnEmkpREpa0wuMGNVI
|
||||||
»æØ¹LT<EFBFBD>
LÜt2là!q>Àý0"ËH5ò^Ìè¼—Ó ^˜ƒj5ÁQRwš×^à?MVon«iĵ,dD愘ž‹S-q¢€
|
Szmhsy/l6Qs5pX6TwcjPg3gxgQtNT1y0KTXEMPG/wZg
|
||||||
|
-> X25519 ja3QNUKZfQIkTCt8O41f0+3oh1dXk/v86ZKHUcvvKxI
|
||||||
|
6Qb1KMzfbyZMT2OJsckGRiWrcxS3xgCL7jXz0QWucZw
|
||||||
|
--- 7d03+3jqRYD4cII07DMedPieU+qIyl+b3KL+xcEUwro
|
||||||
|
þ¿Â휫5Dh?G^|ƤJd`üg4ïU¥ÒÀ'¯ä«:«t
|
||||||
|
–
LNÍIIÂfå˜cÕ›3²rFÚRå9€”¥êƒÄ
|
||||||
|
vŒD6•7ã’
|
||||||
Binary file not shown.
|
|
@ -14,7 +14,6 @@ let
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
"tailscale-auth.age".publicKeys = adminKeys ++ allHosts;
|
|
||||||
"mysql-mediawiki.age".publicKeys = adminKeys ++ allHosts;
|
"mysql-mediawiki.age".publicKeys = adminKeys ++ allHosts;
|
||||||
"mysql-replication.age".publicKeys = adminKeys ++ allHosts;
|
"mysql-replication.age".publicKeys = adminKeys ++ allHosts;
|
||||||
"mediawiki-admin-password.age".publicKeys = adminKeys ++ allHosts;
|
"mediawiki-admin-password.age".publicKeys = adminKeys ++ allHosts;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
age-encryption.org/v1
|
|
||||||
-> ssh-ed25519 Ziw7aw Fa0iIVw0anW/5eEZeeHjPQub7dSfowddlBHGnv/zZ2E
|
|
||||||
cXCR7bmRgwlrRGvJxAXmeceG8IZeEikBYpcK23WPn/c
|
|
||||||
--- tE9ZGzyHyQpZoBGr8m3YySYeSbpNlISsxKG7fIgqdwg
|
|
||||||
<EFBFBD>?Õ\ƒ\‹Ótîr-¼Dÿã{)‹ßÍ>þJ¼—r
Xz‡àñŸZ]¤“Eîáÿ¶šßeë1±œÝWúý÷§c[ÔQ†Æ³2²¢€Å<E282AC>š÷cfaf9Ž5äÚ
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue