feat: init
Some checks failed
CI / check (push) Has been cancelled
CI / deploy (push) Has been cancelled

This commit is contained in:
Jet 2026-03-17 04:07:14 -07:00
commit 8cfede9f57
No known key found for this signature in database
28 changed files with 2129 additions and 0 deletions

View 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
}
'';
};
};
};
}

View 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 -"
];
}

View 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";
};
}