feat: init
This commit is contained in:
commit
8cfede9f57
28 changed files with 2129 additions and 0 deletions
129
modules/wiki-replica/caddy.nix
Normal file
129
modules/wiki-replica/caddy.nix
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
let
|
||||
botPattern = ''(?i)(ClaudeBot|GPTBot|CCBot|Bytespider|AhrefsBot|SemrushBot|MJ12bot|DotBot|PetalBot|Amazonbot|anthropic-ai|ChatGPT-User|cohere-ai|FacebookBot|Google-Extended|PerplexityBot)'';
|
||||
|
||||
robotsTxt = ''
|
||||
User-agent: ClaudeBot
|
||||
Disallow: /
|
||||
User-agent: GPTBot
|
||||
Disallow: /
|
||||
User-agent: CCBot
|
||||
Disallow: /
|
||||
User-agent: Bytespider
|
||||
Disallow: /
|
||||
User-agent: anthropic-ai
|
||||
Disallow: /
|
||||
User-agent: ChatGPT-User
|
||||
Disallow: /
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://www.noisebridge.net/sitemap.xml
|
||||
'';
|
||||
|
||||
# Shared clearnet config for www and readonly vhosts
|
||||
commonConfig = ''
|
||||
# Health check endpoint
|
||||
handle /health {
|
||||
respond "ok" 200
|
||||
}
|
||||
|
||||
# Bot blocking
|
||||
@bots header_regexp User-Agent "${botPattern}"
|
||||
respond @bots 403
|
||||
|
||||
# robots.txt
|
||||
handle /robots.txt {
|
||||
respond "${robotsTxt}"
|
||||
}
|
||||
|
||||
# Rate limiting for anonymous users
|
||||
# {client_ip} works with or without a reverse proxy in front
|
||||
@anon {
|
||||
not header_regexp Cookie "nb_wiki_session="
|
||||
}
|
||||
rate_limit @anon {
|
||||
zone replica_zone {
|
||||
key {client_ip}
|
||||
events 60
|
||||
window 1m
|
||||
}
|
||||
}
|
||||
|
||||
# Cache headers (read-only replica — everything is public-cacheable)
|
||||
header Cache-Control "public, max-age=7200"
|
||||
|
||||
# Security headers
|
||||
header {
|
||||
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
X-Wiki-Mode "read-only"
|
||||
}
|
||||
|
||||
php_fastcgi unix//run/phpfpm/mediawiki.sock {
|
||||
root ${config.services.mediawiki.finalPackage}/share/mediawiki
|
||||
}
|
||||
|
||||
file_server {
|
||||
root ${config.services.mediawiki.finalPackage}/share/mediawiki
|
||||
}
|
||||
'';
|
||||
in
|
||||
{
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
package = pkgs.caddy-custom;
|
||||
|
||||
globalConfig = ''
|
||||
order rate_limit before basicauth
|
||||
servers {
|
||||
# Trust Cloudflare's edge IPs so {client_ip} resolves to the real visitor
|
||||
trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
|
||||
metrics
|
||||
}
|
||||
'';
|
||||
|
||||
virtualHosts = {
|
||||
"readonly.noisebridge.net" = {
|
||||
extraConfig = commonConfig;
|
||||
};
|
||||
|
||||
# ── Tor .onion vhost ──
|
||||
# Tor daemon forwards port 80 → localhost:8080
|
||||
# HTTP only, no TLS (.onion v3 is end-to-end encrypted)
|
||||
# Read-only (same as clearnet replica)
|
||||
":8080" = {
|
||||
extraConfig = ''
|
||||
# Bot blocking
|
||||
@bots header_regexp User-Agent "${botPattern}"
|
||||
respond @bots 403
|
||||
|
||||
# robots.txt — block everything on .onion
|
||||
handle /robots.txt {
|
||||
respond "User-agent: *
|
||||
Disallow: /
|
||||
"
|
||||
}
|
||||
|
||||
# Security headers (no HSTS — no TLS over .onion)
|
||||
header {
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "no-referrer"
|
||||
X-Wiki-Mode "read-only"
|
||||
X-Wiki-Access "tor"
|
||||
}
|
||||
|
||||
php_fastcgi unix//run/phpfpm/mediawiki.sock {
|
||||
root ${config.services.mediawiki.finalPackage}/share/mediawiki
|
||||
}
|
||||
|
||||
file_server {
|
||||
root ${config.services.mediawiki.finalPackage}/share/mediawiki
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
46
modules/wiki-replica/mediawiki.nix
Normal file
46
modules/wiki-replica/mediawiki.nix
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
{
|
||||
services.mediawiki.extraConfig = lib.mkAfter ''
|
||||
# ----- Read-only mode -----
|
||||
$wgReadOnly = "This is a read-only mirror of the Noisebridge wiki.";
|
||||
$wgEnableUploads = false;
|
||||
$wgEnableEmail = false;
|
||||
|
||||
# ----- File cache (still useful for read-only serving) -----
|
||||
$wgUseFileCache = true;
|
||||
$wgFileCacheDirectory = "/var/cache/mediawiki";
|
||||
$wgShowIPinHeader = false;
|
||||
|
||||
# ----- No account creation on replica -----
|
||||
$wgGroupPermissions['*']['createaccount'] = false;
|
||||
'';
|
||||
|
||||
# Smaller PHP-FPM pool for replica
|
||||
# Use individual mkForce to override defaults without clobbering
|
||||
# required settings (listen, user, group) set by the mediawiki module
|
||||
services.phpfpm.pools.mediawiki.settings = {
|
||||
"pm" = lib.mkForce "dynamic";
|
||||
"pm.max_children" = lib.mkForce 8;
|
||||
"pm.start_servers" = lib.mkForce 2;
|
||||
"pm.min_spare_servers" = lib.mkForce 1;
|
||||
"pm.max_spare_servers" = lib.mkForce 4;
|
||||
"pm.max_requests" = lib.mkForce 500;
|
||||
"request_terminate_timeout" = lib.mkForce "30s";
|
||||
"pm.status_path" = "/fpm-status";
|
||||
|
||||
# OPcache
|
||||
"php_admin_value[opcache.enable]" = 1;
|
||||
"php_admin_value[opcache.memory_consumption]" = 128;
|
||||
"php_admin_value[opcache.max_accelerated_files]" = 10000;
|
||||
"php_admin_value[opcache.revalidate_freq]" = 60;
|
||||
"php_admin_value[opcache.jit]" = 1255;
|
||||
"php_admin_value[opcache.jit_buffer_size]" = "32M";
|
||||
|
||||
"php_admin_value[memory_limit]" = "128M";
|
||||
"php_admin_value[max_execution_time]" = 30;
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/cache/mediawiki 0755 mediawiki mediawiki -"
|
||||
];
|
||||
}
|
||||
106
modules/wiki-replica/mysql.nix
Normal file
106
modules/wiki-replica/mysql.nix
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
{
|
||||
services.mysql = {
|
||||
enable = true;
|
||||
package = pkgs.mariadb;
|
||||
dataDir = "/var/lib/mysql";
|
||||
|
||||
settings.mysqld = {
|
||||
bind-address = "127.0.0.1";
|
||||
|
||||
# InnoDB (smaller for replica)
|
||||
innodb_buffer_pool_size = "128M";
|
||||
innodb_log_file_size = "64M";
|
||||
innodb_flush_log_at_trx_commit = 2;
|
||||
innodb_file_per_table = 1;
|
||||
|
||||
# GTID replication (replica)
|
||||
server-id = 2;
|
||||
read_only = 1;
|
||||
relay_log = "relay-bin";
|
||||
log_slave_updates = 1;
|
||||
gtid_strict_mode = 1;
|
||||
|
||||
# Performance
|
||||
max_connections = 50;
|
||||
table_open_cache = 200;
|
||||
|
||||
# Character set
|
||||
character-set-server = "binary";
|
||||
collation-server = "binary";
|
||||
};
|
||||
|
||||
# Do NOT use ensureDatabases or ensureUsers on the replica —
|
||||
# they require writes, but read_only=1 blocks writes.
|
||||
# The database, tables, and users arrive via replication from the primary.
|
||||
};
|
||||
|
||||
# Node exporter (scraped by primary over Tailscale)
|
||||
# Includes textfile collector for failover metrics
|
||||
services.prometheus.exporters.node = {
|
||||
enable = true;
|
||||
port = 9100;
|
||||
enabledCollectors = [
|
||||
"cpu"
|
||||
"diskstats"
|
||||
"filesystem"
|
||||
"loadavg"
|
||||
"meminfo"
|
||||
"netdev"
|
||||
"stat"
|
||||
"time"
|
||||
"vmstat"
|
||||
"systemd"
|
||||
"textfile"
|
||||
];
|
||||
extraFlags = [
|
||||
"--collector.textfile.directory=/var/lib/prometheus-node-exporter/textfile"
|
||||
];
|
||||
};
|
||||
|
||||
# mysqld exporter (scraped by primary over Tailscale for replication metrics)
|
||||
# Key metrics: mysql_slave_status_seconds_behind_master,
|
||||
# mysql_slave_status_slave_io_running, mysql_slave_status_slave_sql_running,
|
||||
# mysql_global_status_queries (read-only query volume)
|
||||
services.prometheus.exporters.mysqld = {
|
||||
enable = true;
|
||||
port = 9104;
|
||||
runAsLocalSuperUser = true;
|
||||
};
|
||||
|
||||
# Memcached exporter (scraped by primary over Tailscale)
|
||||
# Key metrics: hit ratio, evictions, memory usage
|
||||
services.prometheus.exporters.memcached = {
|
||||
enable = true;
|
||||
port = 9150;
|
||||
extraFlags = [ "--memcached.address=localhost:11211" ];
|
||||
};
|
||||
|
||||
# Helper script to initialize replication (run once after provisioning)
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeShellScriptBin "init-replication" ''
|
||||
set -euo pipefail
|
||||
REPL_PASS=$(cat ${config.age.secrets.mysql-replication.path})
|
||||
echo "Configuring GTID-based replication from wiki (primary)..."
|
||||
${pkgs.mariadb}/bin/mysql -u root <<SQL
|
||||
SET GLOBAL read_only = 0;
|
||||
STOP SLAVE;
|
||||
CHANGE MASTER TO
|
||||
MASTER_HOST='wiki',
|
||||
MASTER_USER='repl',
|
||||
MASTER_PASSWORD='$REPL_PASS',
|
||||
MASTER_USE_GTID=slave_pos;
|
||||
START SLAVE;
|
||||
SET GLOBAL read_only = 1;
|
||||
SHOW SLAVE STATUS\G
|
||||
SQL
|
||||
echo "Replication initialized. Check SHOW SLAVE STATUS for errors."
|
||||
'')
|
||||
];
|
||||
|
||||
age.secrets.mysql-replication = {
|
||||
file = ../../secrets/mysql-replication.age;
|
||||
owner = "mysql";
|
||||
group = "mysql";
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue