Fil-C on Nix

Filnix wiki

mbrock/filnix

nixos/demo-wiki.nix

https://filc.less.rest: a public Fil-C server on EC2, booted from the filnix AMI (nixos/amazon.nix). MediaWiki runs on Fil-C PHP-FPM behind Fil-C lighttpd, with SQLite; TLS comes from Let's Encrypt over HTTP-01 on the machine itself. scripts/launch-demo-wiki launches it.

It is meant to survive being broken into without cost: no IAM instance profile, IMDSv2 with a hop limit of 1, no tokens or private keys but its own TLS key, a read-only wiki without account creation, and updates pulled from the latest filnix release, with nothing that could push to it. /status/ shows what filc-ps finds running.

λfilnix
λconfig, lib, pkgs, …
let
in
24imports=
import./amazon.nix inherit filnix;
26filnix.configuration=
"demo-wiki"
27networking.hostName=
lib.mkForce"filc-wiki"

lego (ACME) and minica (its bootstrap certificate) are Go.

30filc.native=
native: inherit native lego minica;

A public server: no console autologin; root logs in with the key pair.

33services.getty.autologinUser=
lib.mkForcenull
34users.motd=
lib.mkForce
filc.less.rest: MediaWiki on Fil-C PHP-FPM and lighttpd, on a Fil-C
NixOS (nixos/demo-wiki.nix in github:mbrock/filnix). It updates itself
from the latest filnix release (filnix-update.timer).
42networking.firewall.allowedTCPPorts=
80 443
47services.mediawiki=
48enable=
true
49name=
"Fil-C wiki"
50url=
"https://${domain}"
51webserver=
"none"
52database.type=
"sqlite"

The module's default is PHP 8.3, which is not ported.

54phpPackage=
pkgs.php84.buildEnvextensions = all, enabled: enabled ++ all.apcu;

Nobody logs in, so nobody needs to know it.

58passwordFile=
"/var/lib/mediawiki/admin-password"

A Fil-C php-fpm worker grows to 150-250 MB; t3.medium has 4 GiB.

60poolConfig=
61"pm"=
"static"
62"pm.max_children"=
4
63"pm.max_requests"=
500
64"listen.mode"=
"0660"
66extraConfig=
# Read-only to the world.
$wgGroupPermissions['*']['edit'] = false;
$wgGroupPermissions['*']['createaccount'] = false;
$wgGroupPermissions['*']['createpage'] = false;
$wgGroupPermissions['user']['edit'] = false;
$wgEnableUploads = false;
$wgMainCacheType = CACHE_NONE;
uses domain
77systemd.services.mediawiki-admin-password=
78description=

Generate the MediaWiki admin password

79wantedBy=
"mediawiki-init.service"
80before=
"mediawiki-init.service"
81serviceConfig.Type=
"oneshot"
82script=
f=${wiki.passwordFile}
if [ ! -s "$f" ]; then
install -d -m 0750 -o mediawiki -g mediawiki "$(dirname "$f")"
(umask 077; head -c 24 /dev/urandom | base64 > "$f")
chown mediawiki "$f"
fi
uses wiki

lighttpd reaches PHP-FPM through the pool's socket, owned by mediawiki.

93users.users.lighttpd.extraGroups=
"mediawiki"
95services.lighttpd=
96enable=
true
97document-root=
"${wiki.finalPackage}/share/mediawiki"
98enableModules=
"mod_access" "mod_alias" "mod_fastcgi" "mod_openssl" "mod_redirect" "mod_setenv"
106extraConfig=
index-file.names += ( "index.php" )
url.access-deny = ( "~", ".inc" )
fastcgi.server = ( ".php" => ((
"socket" => "${config.services.phpfpm.pools.mediawiki.socket}",
"check-local" => "enable",
)))
alias.url += (
"/.well-known/acme-challenge/" => "/var/lib/acme/acme-challenge/.well-known/acme-challenge/",
"/status/" => "${status}/",
)
$HTTP["url"] =~ "^/status/" {
index-file.names = ( "index.txt" )
setenv.add-response-header = ( "Content-Type" => "text/plain; charset=utf-8" )
}
$SERVER["socket"] == ":443" {
ssl.engine = "enable"
ssl.pemfile = "/var/lib/acme/${domain}/full.pem"
}
$SERVER["socket"] == "[::]:443" {
ssl.engine = "enable"
ssl.pemfile = "/var/lib/acme/${domain}/full.pem"
}
$SERVER["socket"] == "[::]:80" { }
$HTTP["scheme"] == "http" {
$HTTP["url"] !~ "^/.well-known/acme-challenge/" {
url.redirect = ( "" => "https://${domain}''${url.path}''${qsa}" )
}
}
139security.acme=
140acceptTerms=
true
141certs.${domain}=
142webroot=
"/var/lib/acme/acme-challenge"
143group=
"lighttpd"
144reloadServices=
"lighttpd"
uses domain

lighttpd starts with the self-signed certificate that the acme unit makes first, and reloads when Let's Encrypt's arrives.

149systemd.services.lighttpd=
150wants=
"acme-${domain}.service"
151after=
"acme-${domain}.service"
uses domain

What runs here, every 5 minutes, at https://filc.less.rest/status/.

155systemd.services.filc-status=
156description=

Write the filc-ps status page

157path=
"/run/current-system/sw"
158serviceConfig.Type=
"oneshot"
159script=
install -d -m 0755 ${status}
{
echo "filc.less.rest at $(date -u '+%Y-%m-%d %H:%M UTC'), up $(awk '{ printf "%d days %d hours", $1 / 86400, $1 % 86400 / 3600 }' /proc/uptime)"
echo "system: $(readlink /run/current-system)"
# The revision this system was built from, not the updater's tag
# file, which lags when a switch fails before writing it.
echo "filnix: ${filnix.rev or "local"}$(tag=$(cat /var/lib/filnix-update/tag 2>/dev/null) && echo " (filnix-update: $tag)")"
echo "kernel: $(uname -r)"
echo
echo "Every running process, by whether it maps libpizlo, the Fil-C runtime."
echo "Native ones must be in the closure of an allowed exception"
echo "(lib/native-exceptions.nix, or this machine's filc.native: lego, minica)."
echo
filc-ps || echo "filc-ps exited $?"
} > ${status}/index.txt.new
mv ${status}/index.txt.new ${status}/index.txt
uses status
178systemd.timers.filc-status=
179wantedBy=
"timers.target"
180timerConfig=
181OnBootSec=
"2min"
182OnUnitActiveSec=
"5min"

Pull updates: switch to this configuration at the newest filnix release, substituting everything (max-jobs 0 refuses to build here).

188systemd.services.filnix-update=
189description=

Switch to demo-wiki at the latest filnix release

190path=
with pkgs; config.nix.package config.system.build.nixos-rebuild curl jq coreutils config.systemd.package
198serviceConfig.Type=
"oneshot"
199serviceConfig.StateDirectory=
"filnix-update"

The switch changes this unit too; restarting it would kill the switch's caller before it writes the tag, and the rerun collides with the switch.

202restartIfChanged=
false
203script=
tag=$(curl -fsS https://api.github.com/repos/mbrock/filnix/releases/latest | jq -er .tag_name)
case $tag in v*) ;; *) echo "unexpected tag: $tag"; exit 1 ;; esac
if [ "$tag" = "$(cat /var/lib/filnix-update/tag 2>/dev/null)" ]; then
echo "at $tag already"
exit 0
fi
echo "switching to $tag"
nixos-rebuild switch --flake "github:mbrock/filnix/$tag#demo-wiki" \
--option max-jobs 0 --option builders ""
echo "$tag" > /var/lib/filnix-update/tag
systemctl start --no-block filc-status.service
217systemd.timers.filnix-update=
218wantedBy=
"timers.target"
219timerConfig=
220OnBootSec=
"15min"
221OnUnitActiveSec=
"6h"
222RandomizedDelaySec=
"30min"