Moves the Prosody server, xmpp-web frontends (chat/plaza), and Forgejo's wiki-update webhook notifier from pinix to blunix, co-located with Traefik/ACME so TLS comes straight from Traefik's cert store instead of the old manual copy process. pinix's prosody/xmpp-web/webhook modules stay in place but deactivated as a fallback, matching the earlier Forgejo migration's pattern. Implements docs/intent/xmpp-umzug-pinix-blunix.md. Deployed and verified live on blunix (nixos-rebuild switch, chat/plaza returning 200, prosody healthy, agent XMPP listeners reconnected). Two first-bootstrap bugs found and fixed during that verification, both specific to bringing Prosody up on a host it never ran on before: - prosody-certs-sync.service used to call a blocking `systemctl reload-or-restart prosody.service` whenever the cert file differed from what's installed -- including on first activation, where prosody.service's own start job is still queued behind this unit (Requires+After), deadlocking forever. Now only reloads an already-running Prosody; ordering alone brings up a fresh one with the cert just installed. - hosts/blunix/localServices/secrets.nix force-owned the global `bindPW` secret to root with the (previously correct) rationale that it belongs to a pinix-only service. Prosody now runs on blunix too and needs to read it directly as its own LDAP bind config include, so that override no longer applies to it. pinix's own deploy (data migration, DNS/webhook cutover) is separate follow-up work, not part of this commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANg2L2xUmNJ45t9mcv6HVT
180 lines
6 KiB
Nix
180 lines
6 KiB
Nix
# Moved from hosts/pinix/localServices/prosody/setup-prosody.nix on
|
|
# 2026-09-06 (docs/intent/xmpp-umzug-pinix-blunix.md): same pinix-outage
|
|
# resilience motivation as the earlier Forgejo move. Prosody is now
|
|
# co-located with Traefik/ACME, LLDAP, and the agent-xmpp-listener that
|
|
# already ran here, instead of reaching all three over the Tailnet.
|
|
{ config, pkgs, ... }:
|
|
{
|
|
services.prosody = {
|
|
enable = true;
|
|
|
|
# The included agenix files only exist at runtime.
|
|
checkConfig = false;
|
|
|
|
package = pkgs.prosody.override {
|
|
withExtraLuaPackages = luaPackages: [
|
|
luaPackages.lualdap
|
|
];
|
|
withOnlyInstalledCommunityModules = [
|
|
"muc_moderation"
|
|
];
|
|
};
|
|
|
|
c2sRequireEncryption = true;
|
|
s2sRequireEncryption = true;
|
|
|
|
httpFileShare = {
|
|
domain = "upload.${config.domain}";
|
|
http_external_url = "https://upload.${config.domain}/";
|
|
};
|
|
|
|
httpPorts = [ 5280 ];
|
|
httpsPorts = [ 5281 ];
|
|
|
|
extraModules = [
|
|
"http"
|
|
"presence"
|
|
];
|
|
|
|
extraConfig = ''
|
|
-- Advertises the address for SOCKS5 Bytestream (out-of-band file
|
|
-- transfer) negotiation. This must be an address the two
|
|
-- negotiating clients can actually reach -- now that c2s is bound
|
|
-- directly on blunix's public interface, that is the public IPv4,
|
|
-- not a Tailnet address a federated/remote client could never
|
|
-- route to.
|
|
proxy65_address = "${config.publicIPv4}"
|
|
trusted_proxies = {
|
|
"127.0.0.1",
|
|
"::1",
|
|
}
|
|
prosodyctl_service_warnings = false
|
|
Include "/run/agenix/${config.domain}.cfg.lua"
|
|
Include "/run/agenix/motd.cfg.lua"
|
|
'';
|
|
|
|
virtualHosts."${config.domain}" = {
|
|
enabled = true;
|
|
domain = config.domain;
|
|
extraConfig = ''
|
|
authentication = "ldap";
|
|
ldap_base = "ou=people,${config.ldapBaseDN}"
|
|
ldap_server = "127.0.0.1:3890"
|
|
ldap_rootdn = "uid=admin,ou=people,${config.ldapBaseDN}"
|
|
filter = "(uid=$user)"
|
|
|
|
-- Registered-account web client (hosts/blunix/localServices/xmpp-web.nix)
|
|
modules_enabled = {
|
|
"websocket";
|
|
}
|
|
http_host = "chat.${config.domain}"
|
|
http_external_url = "https://chat.${config.domain}/"
|
|
http_paths = {
|
|
websocket = "/xmpp-websocket";
|
|
}
|
|
cross_domain_websocket = { "https://chat.${config.domain}"; }
|
|
'';
|
|
ssl = {
|
|
cert = "/var/lib/prosody/certs/${config.domain}.crt.pem";
|
|
key = "/var/lib/prosody/certs/${config.domain}.key.pem";
|
|
};
|
|
};
|
|
|
|
muc = [
|
|
{
|
|
domain = "muc.${config.domain}";
|
|
name = "0ff.space public chatrooms";
|
|
restrictRoomCreation = "local";
|
|
moderation = true;
|
|
roomDefaultLanguage = "de";
|
|
roomDefaultPublic = false;
|
|
roomDefaultMembersOnly = true;
|
|
extraConfig = ''
|
|
muc_room_default_persistent = true
|
|
muc_log_by_default = true
|
|
muc_log_expires_after = "30d"
|
|
muc_log_cleanup_interval = 4 * 60 * 60
|
|
'';
|
|
}
|
|
];
|
|
|
|
virtualHosts."bot.${config.domain}" = {
|
|
enabled = true;
|
|
domain = "bot.${config.domain}";
|
|
extraConfig = ''
|
|
modules_enabled = { "admin_shell"; }
|
|
authentication = "internal_hashed";
|
|
'';
|
|
ssl = {
|
|
cert = "/var/lib/prosody/certs/${config.domain}.crt.pem";
|
|
key = "/var/lib/prosody/certs/${config.domain}.key.pem";
|
|
};
|
|
};
|
|
};
|
|
|
|
# Replaces the old manual SSH copy process
|
|
# (hosts/pinix/localServices/prosody/README.md, "Refresh the TLS
|
|
# certificate"), which existed only because Prosody and Traefik/ACME ran
|
|
# on different hosts. Now that both run on blunix, this unit reads the
|
|
# same certificate Traefik already obtained straight out of its ACME
|
|
# store and installs it where Prosody expects it -- no key ever leaves
|
|
# this host over the network.
|
|
systemd.services.prosody-certs-sync = {
|
|
description = "Sync Prosody's TLS certificate from Traefik's ACME store";
|
|
after = [ "traefik.service" ];
|
|
path = [ pkgs.jq pkgs.coreutils ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
set -eu
|
|
dir=/var/lib/prosody/certs
|
|
install -d -o prosody -g prosody -m 0750 "$dir"
|
|
|
|
tmp_crt=$(mktemp)
|
|
tmp_key=$(mktemp)
|
|
trap 'rm -f "$tmp_crt" "$tmp_key"' EXIT
|
|
|
|
jq -r '.cloudflare.Certificates[] | select(.domain.main == "${config.domain}") | .certificate' \
|
|
/var/lib/traefik/acme.json | base64 -d > "$tmp_crt"
|
|
jq -r '.cloudflare.Certificates[] | select(.domain.main == "${config.domain}") | .key' \
|
|
/var/lib/traefik/acme.json | base64 -d > "$tmp_key"
|
|
|
|
cert="$dir/${config.domain}.crt.pem"
|
|
key="$dir/${config.domain}.key.pem"
|
|
|
|
# Only touch the installed files -- and only reload Prosody -- when
|
|
# the certificate actually changed (normally once per ACME renewal,
|
|
# every ~60-90 days), not on every timer tick.
|
|
if ! cmp -s "$tmp_crt" "$cert" 2>/dev/null; then
|
|
install -o prosody -g prosody -m 0644 "$tmp_crt" "$cert"
|
|
install -o prosody -g prosody -m 0640 "$tmp_key" "$key"
|
|
# Prosody requires this unit, so on first activation (no cert
|
|
# installed yet) prosody.service isn't up yet and its own start
|
|
# job is still queued behind this one -- calling reload-or-restart
|
|
# here would block waiting on a job that can't proceed until this
|
|
# script returns, deadlocking forever. Only reload an
|
|
# already-running Prosody; ordering alone brings up a fresh one
|
|
# with the cert just installed above.
|
|
if systemctl is-active --quiet prosody.service; then
|
|
systemctl reload-or-restart prosody.service
|
|
fi
|
|
fi
|
|
'';
|
|
};
|
|
|
|
systemd.timers.prosody-certs-sync = {
|
|
description = "Periodic Prosody TLS certificate sync from Traefik";
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig = {
|
|
OnCalendar = "daily";
|
|
Persistent = true;
|
|
};
|
|
};
|
|
|
|
systemd.services.prosody = {
|
|
after = [ "prosody-certs-sync.service" ];
|
|
requires = [ "prosody-certs-sync.service" ];
|
|
};
|
|
}
|