init
This commit is contained in:
commit
642869ce9b
27 changed files with 1414 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
57
.github/workflows/ci.yml
vendored
Normal file
57
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: nix flake check
|
||||
run: nix flake check --print-build-logs
|
||||
|
||||
- name: Build primary host
|
||||
run: nix build .#nixosConfigurations.main-wiki.config.system.build.toplevel --print-build-logs
|
||||
|
||||
- name: Build replica host
|
||||
run: nix build .#nixosConfigurations.replica-wiki.config.system.build.toplevel --print-build-logs
|
||||
|
||||
deploy:
|
||||
needs: check
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: deploy
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -t ed25519 "$(nix eval --raw .#deploy.nodes.\"main-wiki\".hostname)" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
ssh-keyscan -t ed25519 "$(nix eval --raw .#deploy.nodes.\"replica-wiki\".hostname)" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: Deploy all hosts
|
||||
run: nix run .#deploy
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.bootstrap/
|
||||
.direnv/
|
||||
81
README.md
Normal file
81
README.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Noisebridge Wiki Infra
|
||||
|
||||
This repo manages the Noisebridge MediaWiki primary and replica on NixOS.
|
||||
|
||||
## Commands
|
||||
|
||||
Bootstrap a brand new VPS into NixOS and seed its stable agenix host key:
|
||||
|
||||
```sh
|
||||
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]
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
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
|
||||
```
|
||||
|
||||
What bootstrap does:
|
||||
|
||||
- generates or reuses `.bootstrap/<host>/host.age`
|
||||
- writes the matching public recipient to `secrets/hosts/<host>.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
|
||||
|
||||
Deploy all already-bootstrapped hosts:
|
||||
|
||||
```sh
|
||||
nix run .#deploy
|
||||
```
|
||||
|
||||
Deploy one host only:
|
||||
|
||||
```sh
|
||||
nix run .#deploy -- .#main-wiki
|
||||
nix run .#deploy -- .#replica-wiki
|
||||
```
|
||||
|
||||
Check the flake:
|
||||
|
||||
```sh
|
||||
nix flake check 'path:.' --accept-flake-config
|
||||
```
|
||||
|
||||
## Secret Model
|
||||
|
||||
- admin keys stay in `secrets/secrets.nix`
|
||||
- host recipients live in `secrets/hosts/*.age.pub`
|
||||
- host private age keys stay local in `.bootstrap/` and are gitignored
|
||||
- hosts decrypt agenix secrets with `/var/lib/agenix/host.age`
|
||||
- host SSH keys are separate and can rotate without breaking agenix
|
||||
|
||||
## Normal Lifecycle
|
||||
|
||||
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.
|
||||
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`
|
||||
34
disk-config.nix
Normal file
34
disk-config.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
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 = "/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
356
flake.lock
generated
Normal file
356
flake.lock
generated
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
{
|
||||
"nodes": {
|
||||
"agenix": {
|
||||
"inputs": {
|
||||
"darwin": "darwin",
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770165109,
|
||||
"narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=",
|
||||
"owner": "ryantm",
|
||||
"repo": "agenix",
|
||||
"rev": "b027ee29d959fda4b60b57566d64c98a202e0feb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ryantm",
|
||||
"repo": "agenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"darwin": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"agenix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1744478979,
|
||||
"narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=",
|
||||
"owner": "lnl7",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "43975d782b418ebf4969e9ccba82466728c2851b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "lnl7",
|
||||
"ref": "master",
|
||||
"repo": "nix-darwin",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"deploy-rs": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"utils": "utils"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770019181,
|
||||
"narHash": "sha256-hwsYgDnby50JNVpTRYlF3UR/Rrpt01OrxVuryF40CFY=",
|
||||
"owner": "serokell",
|
||||
"repo": "deploy-rs",
|
||||
"rev": "77c906c0ba56aabdbc72041bf9111b565cdd6171",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "serokell",
|
||||
"repo": "deploy-rs",
|
||||
"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": {
|
||||
"lastModified": 1733328505,
|
||||
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"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": [
|
||||
"agenix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1745494811,
|
||||
"narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"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,
|
||||
"narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"agenix": "agenix",
|
||||
"deploy-rs": "deploy-rs",
|
||||
"disko": "disko",
|
||||
"nixos-anywhere": "nixos-anywhere",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"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"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
318
flake.nix
Normal file
318
flake.nix
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
{
|
||||
description = "Basic MediaWiki primary + replica deployment";
|
||||
|
||||
nixConfig = {
|
||||
max-jobs = "auto";
|
||||
cores = 0;
|
||||
};
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
agenix = {
|
||||
url = "github:ryantm/agenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
deploy-rs = {
|
||||
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 =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
agenix,
|
||||
deploy-rs,
|
||||
disko,
|
||||
nixos-anywhere,
|
||||
...
|
||||
}:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
lib = nixpkgs.lib;
|
||||
|
||||
siteConfig = rec {
|
||||
wikiName = "Noisebridge";
|
||||
baseDomain = "noisebridge.net";
|
||||
replicaSubdomain = "replica";
|
||||
sshUser = "root";
|
||||
primaryHostName = "main-wiki";
|
||||
replicaHostName = "replica-wiki";
|
||||
|
||||
database = {
|
||||
name = "noisebridge_mediawiki";
|
||||
mediawikiUser = "wiki";
|
||||
replicationUser = "repl";
|
||||
};
|
||||
|
||||
hosts = {
|
||||
primary = {
|
||||
nixosName = primaryHostName;
|
||||
tailscaleName = primaryHostName;
|
||||
};
|
||||
replica = {
|
||||
nixosName = replicaHostName;
|
||||
tailscaleName = replicaHostName;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mkPublicDomain =
|
||||
role:
|
||||
if role == "primary" then
|
||||
siteConfig.baseDomain
|
||||
else
|
||||
"${siteConfig.replicaSubdomain}.${siteConfig.baseDomain}";
|
||||
|
||||
mkHostMeta =
|
||||
role:
|
||||
siteConfig.hosts.${role}
|
||||
// {
|
||||
inherit role;
|
||||
publicDomain = mkPublicDomain role;
|
||||
};
|
||||
|
||||
mkHost =
|
||||
{
|
||||
hostMeta,
|
||||
hostModule,
|
||||
roleModules,
|
||||
}:
|
||||
lib.nixosSystem {
|
||||
inherit system;
|
||||
specialArgs = {
|
||||
inherit agenix siteConfig hostMeta;
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
||||
primaryMeta = mkHostMeta "primary";
|
||||
replicaMeta = mkHostMeta "replica";
|
||||
in
|
||||
{
|
||||
nixosConfigurations = {
|
||||
main-wiki = mkHost {
|
||||
hostMeta = primaryMeta;
|
||||
hostModule = ./hosts/main-wiki;
|
||||
roleModules = [
|
||||
./modules/wiki-primary/mysql.nix
|
||||
./modules/wiki-primary/mediawiki.nix
|
||||
];
|
||||
};
|
||||
|
||||
replica-wiki = mkHost {
|
||||
hostMeta = replicaMeta;
|
||||
hostModule = ./hosts/replica-wiki;
|
||||
roleModules = [
|
||||
./modules/wiki-replica/mysql.nix
|
||||
./modules/wiki-replica/mediawiki.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
deploy.nodes = {
|
||||
main-wiki = {
|
||||
hostname = primaryMeta.tailscaleName;
|
||||
profiles.system = {
|
||||
user = siteConfig.sshUser;
|
||||
sshUser = siteConfig.sshUser;
|
||||
path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.main-wiki;
|
||||
};
|
||||
};
|
||||
|
||||
replica-wiki = {
|
||||
hostname = replicaMeta.tailscaleName;
|
||||
profiles.system = {
|
||||
user = siteConfig.sshUser;
|
||||
sshUser = siteConfig.sshUser;
|
||||
path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.replica-wiki;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
checks = builtins.mapAttrs (_: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
|
||||
|
||||
apps.${system} = {
|
||||
deploy = {
|
||||
type = "app";
|
||||
program = "${pkgs.writeShellScript "deploy-noisebridge" ''
|
||||
if [ "$#" -eq 0 ] || [ "''${1#-}" != "$1" ]; then
|
||||
exec ${deploy-rs.packages.${system}.default}/bin/deploy \
|
||||
--auto-rollback true \
|
||||
--magic-rollback true \
|
||||
path:.# \
|
||||
"$@"
|
||||
fi
|
||||
|
||||
exec ${deploy-rs.packages.${system}.default}/bin/deploy \
|
||||
--auto-rollback true \
|
||||
--magic-rollback true \
|
||||
"$@"
|
||||
''}";
|
||||
meta.description = "Deploy all Noisebridge wiki hosts by default";
|
||||
};
|
||||
|
||||
bootstrap-host = {
|
||||
type = "app";
|
||||
program = "${pkgs.writeShellScript "bootstrap-host" ''
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
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
|
||||
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";
|
||||
};
|
||||
};
|
||||
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
agenix.packages.${system}.default
|
||||
deploy-rs.packages.${system}.default
|
||||
nixos-anywhere.packages.${system}.default
|
||||
mariadb.client
|
||||
rsync
|
||||
curl
|
||||
jq
|
||||
age
|
||||
openssl
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
11
hosts/main-wiki/default.nix
Normal file
11
hosts/main-wiki/default.nix
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{ hostMeta, siteConfig, ... }:
|
||||
{
|
||||
imports = [
|
||||
../shared/hardware-configuration.nix
|
||||
];
|
||||
|
||||
networking.hostName = hostMeta.nixosName;
|
||||
networking.domain = siteConfig.baseDomain;
|
||||
|
||||
system.stateVersion = "24.11";
|
||||
}
|
||||
11
hosts/replica-wiki/default.nix
Normal file
11
hosts/replica-wiki/default.nix
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{ hostMeta, siteConfig, ... }:
|
||||
{
|
||||
imports = [
|
||||
../shared/hardware-configuration.nix
|
||||
];
|
||||
|
||||
networking.hostName = hostMeta.nixosName;
|
||||
networking.domain = siteConfig.baseDomain;
|
||||
|
||||
system.stateVersion = "24.11";
|
||||
}
|
||||
18
hosts/shared/hardware-configuration.nix
Normal file
18
hosts/shared/hardware-configuration.nix
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{ lib, modulesPath, ... }:
|
||||
{
|
||||
imports = [
|
||||
(modulesPath + "/virtualisation/digital-ocean-config.nix")
|
||||
];
|
||||
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
fileSystems."/" = {
|
||||
device = lib.mkDefault "/dev/disk/by-label/nixos";
|
||||
fsType = "ext4";
|
||||
};
|
||||
|
||||
swapDevices = [ ];
|
||||
|
||||
networking.useDHCP = lib.mkDefault true;
|
||||
}
|
||||
31
modules/caddy.nix
Normal file
31
modules/caddy.nix
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{ 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
|
||||
}
|
||||
'';
|
||||
};
|
||||
}
|
||||
43
modules/common.nix
Normal file
43
modules/common.nix
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{ pkgs, ... }:
|
||||
{
|
||||
age.identityPaths = [ "/var/lib/agenix/host.age" ];
|
||||
|
||||
nix.settings = {
|
||||
experimental-features = [
|
||||
"nix-command"
|
||||
"flakes"
|
||||
];
|
||||
trusted-users = [
|
||||
"root"
|
||||
"@wheel"
|
||||
];
|
||||
auto-optimise-store = true;
|
||||
max-jobs = "auto";
|
||||
cores = 0;
|
||||
};
|
||||
|
||||
nix.gc = {
|
||||
automatic = true;
|
||||
dates = "weekly";
|
||||
options = "--delete-older-than 14d";
|
||||
};
|
||||
|
||||
time.timeZone = "America/Los_Angeles";
|
||||
i18n.defaultLocale = "en_US.UTF-8";
|
||||
|
||||
services.timesyncd.enable = true;
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/agenix 0700 root root -"
|
||||
"z /var/lib/agenix/host.age 0400 root root -"
|
||||
];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
vim
|
||||
git
|
||||
curl
|
||||
jq
|
||||
rsync
|
||||
mariadb.client
|
||||
];
|
||||
}
|
||||
109
modules/mediawiki-base.nix
Normal file
109
modules/mediawiki-base.nix
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
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";
|
||||
};
|
||||
}
|
||||
18
modules/security.nix
Normal file
18
modules/security.nix
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{ ... }:
|
||||
{
|
||||
networking.firewall = {
|
||||
enable = true;
|
||||
allowedTCPPorts = [ 80 443 ];
|
||||
};
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
openFirewall = false;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
PermitRootLogin = "prohibit-password";
|
||||
X11Forwarding = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
32
modules/tailscale.nix
Normal file
32
modules/tailscale.nix
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
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";
|
||||
}
|
||||
29
modules/wiki-primary/mediawiki.nix
Normal file
29
modules/wiki-primary/mediawiki.nix
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{ 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";
|
||||
};
|
||||
};
|
||||
}
|
||||
123
modules/wiki-primary/mysql.nix
Normal file
123
modules/wiki-primary/mysql.nix
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
{
|
||||
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" ];
|
||||
};
|
||||
}
|
||||
9
modules/wiki-replica/mediawiki.nix
Normal file
9
modules/wiki-replica/mediawiki.nix
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{ lib, ... }:
|
||||
{
|
||||
services.mediawiki.extraConfig = lib.mkAfter ''
|
||||
$wgReadOnly = "This wiki is a read-only replica.";
|
||||
$wgEnableUploads = false;
|
||||
'';
|
||||
|
||||
systemd.services.mediawiki-init.wantedBy = lib.mkForce [ ];
|
||||
}
|
||||
92
modules/wiki-replica/mysql.nix
Normal file
92
modules/wiki-replica/mysql.nix
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
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
|
||||
'')
|
||||
];
|
||||
}
|
||||
1
secrets.nix
Normal file
1
secrets.nix
Normal file
|
|
@ -0,0 +1 @@
|
|||
import ./secrets/secrets.nix
|
||||
1
secrets/hosts/main-wiki.age.pub
Normal file
1
secrets/hosts/main-wiki.age.pub
Normal file
|
|
@ -0,0 +1 @@
|
|||
age19ex268xfn6wh8c56lrrmtsrlg9ft66y3d9hhqyn08cxczgd09snqhdz0q4
|
||||
1
secrets/hosts/replica-wiki.age.pub
Normal file
1
secrets/hosts/replica-wiki.age.pub
Normal file
|
|
@ -0,0 +1 @@
|
|||
age1hgje5jk7vzudh9ytad2rfrpn3ynwwec3n4r0jdtrz4t7pn8nhv3qk6gjnu
|
||||
5
secrets/mediawiki-admin-password.age
Normal file
5
secrets/mediawiki-admin-password.age
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 Ziw7aw 0yT2caPg64EyERo1cFIGmOo8lzVzOe3aoRyjz7JnhQA
|
||||
NbKUAH7m/tCA4J3ICwOBx9OQJVUrc8lkqHzI5vWqFnc
|
||||
--- efr0t2OzlcLkLPGc77FUJSheExslTUlAFOOWO1bJhx4
|
||||
ȼF Y ?=‘ó·2ojz6~®’g¯xýMäVN¤®•ާÑß÷).eé´íwʬ©óÕ¡EÀ¦ló’ãáG§³!R/¦³Œ^ºüf[\ØâJ
|
||||
5
secrets/mysql-mediawiki.age
Normal file
5
secrets/mysql-mediawiki.age
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 Ziw7aw dohDDxsPMp1TRbvNh2qAPUFmW4cuMLnjislpRleHEHI
|
||||
9MEq670uN4CzO8U/2HZAXr8MpI/vte/5pC2yQPKWemg
|
||||
--- 2npxqFr1cEdGtDhZlb4zVpy04F4Xsfb2NAu3eTDUTYg
|
||||
»æØ¹LT<EFBFBD>
LÜt2là!q>Àý0"ËH5ò^Ìè¼—Ó ^˜ƒj5ÁQRwš×^à?MVon«iĵ,dD愘ž‹S-q¢€
|
||||
BIN
secrets/mysql-replication.age
Normal file
BIN
secrets/mysql-replication.age
Normal file
Binary file not shown.
21
secrets/secrets.nix
Normal file
21
secrets/secrets.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
let
|
||||
readRecipient = path: builtins.replaceStrings [ "\n" ] [ "" ] (builtins.readFile path);
|
||||
|
||||
adminKeys = [
|
||||
# Jet
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu jetthomaspham@gmail.com"
|
||||
];
|
||||
mainWiki = readRecipient ./hosts/main-wiki.age.pub;
|
||||
replicaWiki = readRecipient ./hosts/replica-wiki.age.pub;
|
||||
|
||||
allHosts = [
|
||||
mainWiki
|
||||
replicaWiki
|
||||
];
|
||||
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;
|
||||
}
|
||||
5
secrets/tailscale-auth.age
Normal file
5
secrets/tailscale-auth.age
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
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