From 42df7b5618d2d8500d814e7c1839260a38844559 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sun, 19 Apr 2026 01:11:37 -0700 Subject: [PATCH] Run Zulip on host-managed services --- ...ntik-backed-team-chat-and-workspace-sso.md | 4 + nixos/hosts/burrow-forge/default.nix | 8 - nixos/modules/burrow-zulip.nix | 290 ++++++++++-------- 3 files changed, 170 insertions(+), 132 deletions(-) diff --git a/evolution/proposals/BEP-0008-authentik-backed-team-chat-and-workspace-sso.md b/evolution/proposals/BEP-0008-authentik-backed-team-chat-and-workspace-sso.md index ff6e63d..0ce03a6 100644 --- a/evolution/proposals/BEP-0008-authentik-backed-team-chat-and-workspace-sso.md +++ b/evolution/proposals/BEP-0008-authentik-backed-team-chat-and-workspace-sso.md @@ -49,6 +49,10 @@ across vendor-native Google auth flows when Burrow already operates an IdP. - Add a Burrow-managed Zulip workload on the forge host at `chat.burrow.net`. The deployment should be repo-owned and rebuildable from Nix, even if the runtime uses vendor-supported container images internally. +- Prefer host-managed NixOS services for Zulip's stateful dependencies + (PostgreSQL, Redis, RabbitMQ, memcached, backups) so Burrow owns the + operational surface directly rather than composing a container-side service + mesh. - Zulip should authenticate through Authentik SAML rather than local passwords as the primary path. Initial bootstrap may still keep an operational escape hatch while the deployment is being validated. diff --git a/nixos/hosts/burrow-forge/default.nix b/nixos/hosts/burrow-forge/default.nix index 2464672..be97661 100644 --- a/nixos/hosts/burrow-forge/default.nix +++ b/nixos/hosts/burrow-forge/default.nix @@ -170,13 +170,6 @@ in mode = "0400"; }; - age.secrets.burrowZulipMemcachedPassword = { - file = ../../../secrets/infra/zulip-memcached-password.age; - owner = "root"; - group = "root"; - mode = "0400"; - }; - age.secrets.burrowZulipRabbitmqPassword = { file = ../../../secrets/infra/zulip-rabbitmq-password.age; owner = "root"; @@ -275,7 +268,6 @@ in enable = true; administratorEmail = identities.contact.canonicalEmail; postgresPasswordFile = config.age.secrets.burrowZulipPostgresPassword.path; - memcachedPasswordFile = config.age.secrets.burrowZulipMemcachedPassword.path; rabbitmqPasswordFile = config.age.secrets.burrowZulipRabbitmqPassword.path; redisPasswordFile = config.age.secrets.burrowZulipRedisPassword.path; secretKeyFile = config.age.secrets.burrowZulipSecretKey.path; diff --git a/nixos/modules/burrow-zulip.nix b/nixos/modules/burrow-zulip.nix index ee6d6c7..b5e72b7 100644 --- a/nixos/modules/burrow-zulip.nix +++ b/nixos/modules/burrow-zulip.nix @@ -5,99 +5,30 @@ let yamlFormat = pkgs.formats.yaml { }; composeFile = yamlFormat.generate "burrow-zulip-compose.yaml" { services = { - database = { - image = "zulip/zulip-postgresql:14"; - restart = "unless-stopped"; - secrets = [ "zulip__postgres_password" ]; - environment = { - POSTGRES_DB = "zulip"; - POSTGRES_USER = "zulip"; - POSTGRES_PASSWORD_FILE = "/run/secrets/zulip__postgres_password"; - }; - volumes = [ "postgresql-14:/var/lib/postgresql/data:rw" ]; - attach = false; - }; - memcached = { - image = "memcached:alpine"; - restart = "unless-stopped"; - command = [ - "sh" - "-euc" - '' - echo 'mech_list: plain' > "$SASL_CONF_PATH" - echo "zulip@$HOSTNAME:$(cat /run/burrow/memcached-password)" > "$MEMCACHED_SASL_PWDB" - echo "zulip@localhost:$(cat /run/burrow/memcached-password)" >> "$MEMCACHED_SASL_PWDB" - exec memcached -S - '' - ]; - environment = { - SASL_CONF_PATH = "/home/memcache/memcached.conf"; - MEMCACHED_SASL_PWDB = "/home/memcache/memcached-sasl-db"; - }; - volumes = [ "./secrets/memcached-password:/run/burrow/memcached-password:ro" ]; - attach = false; - }; - rabbitmq = { - image = "rabbitmq:4.2"; - restart = "unless-stopped"; - volumes = [ - "rabbitmq:/var/lib/rabbitmq:rw" - "./rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro" - ]; - attach = false; - }; - redis = { - image = "redis:alpine"; - restart = "unless-stopped"; - command = [ - "sh" - "-euc" - "/usr/local/bin/docker-entrypoint.sh --requirepass \"$(cat \"$REDIS_PASSWORD_FILE\")\"" - ]; - secrets = [ "zulip__redis_password" ]; - environment = { - REDIS_PASSWORD_FILE = "/run/secrets/zulip__redis_password"; - }; - volumes = [ "redis:/data:rw" ]; - attach = false; - }; zulip = { image = "ghcr.io/zulip/zulip-server:11.6-1"; restart = "unless-stopped"; + network_mode = "host"; secrets = [ "zulip__postgres_password" - "zulip__memcached_password" "zulip__rabbitmq_password" "zulip__redis_password" "zulip__secret_key" "zulip__email_password" ]; environment = { - SETTING_REMOTE_POSTGRES_HOST = "database"; - SETTING_MEMCACHED_LOCATION = "memcached:11211"; - SETTING_RABBITMQ_HOST = "rabbitmq"; - SETTING_REDIS_HOST = "redis"; + SETTING_REMOTE_POSTGRES_HOST = "127.0.0.1"; + SETTING_MEMCACHED_LOCATION = "127.0.0.1:11211"; + SETTING_RABBITMQ_HOST = "127.0.0.1"; + SETTING_REDIS_HOST = "127.0.0.1"; }; - volumes = [ "zulip:/data:rw" ]; + volumes = [ "${cfg.dataDir}/data:/data:rw" ]; ulimits.nofile = { soft = 1000000; hard = 1048576; }; - depends_on = [ - "database" - "memcached" - "rabbitmq" - "redis" - ]; }; }; - - volumes = { - zulip = { }; - postgresql-14 = { }; - rabbitmq = { }; - redis = { }; - }; }; in { @@ -157,11 +88,6 @@ in description = "File containing the Zulip PostgreSQL password."; }; - memcachedPasswordFile = lib.mkOption { - type = lib.types.str; - description = "File containing the Zulip memcached password."; - }; - rabbitmqPasswordFile = lib.mkOption { type = lib.types.str; description = "File containing the Zulip RabbitMQ password."; @@ -184,6 +110,49 @@ in pkgs.podman-compose ]; + services.postgresql = { + ensureDatabases = [ "zulip" ]; + ensureUsers = [ + { + name = "zulip"; + ensureDBOwnership = true; + } + ]; + settings = { + listen_addresses = lib.mkDefault "127.0.0.1"; + password_encryption = lib.mkDefault "scram-sha-256"; + }; + authentication = lib.mkAfter '' + host zulip zulip 127.0.0.1/32 scram-sha-256 + ''; + }; + + services.postgresqlBackup = { + enable = true; + backupAll = false; + databases = [ "zulip" ]; + }; + + services.memcached = { + enable = true; + listen = "127.0.0.1"; + port = 11211; + extraOptions = [ "-U 0" ]; + }; + + services.redis.servers.zulip = { + enable = true; + bind = "127.0.0.1"; + port = 6379; + requirePassFile = cfg.redisPasswordFile; + }; + + services.rabbitmq = { + enable = true; + listenAddress = "127.0.0.1"; + port = 5672; + }; + services.caddy.virtualHosts."${cfg.domain}".extraConfig = '' encode gzip zstd reverse_proxy 127.0.0.1:${toString cfg.port} @@ -191,18 +160,114 @@ in systemd.tmpfiles.rules = [ "d ${cfg.dataDir} 0755 root root - -" + "d ${cfg.dataDir}/data 0755 root root - -" + "d ${cfg.dataDir}/data/logs 0755 root root - -" + "d ${cfg.dataDir}/data/logs/emails 0755 root root - -" + "d ${cfg.dataDir}/data/secrets 0700 root root - -" "d ${cfg.dataDir}/secrets 0700 root root - -" "d ${cfg.dataDir}/logs 0755 root root - -" ]; + systemd.services.burrow-zulip-postgres-bootstrap = { + description = "Bootstrap PostgreSQL role for Burrow Zulip"; + after = [ "postgresql.service" ]; + wants = [ "postgresql.service" ]; + requiredBy = [ "burrow-zulip.service" ]; + before = [ "burrow-zulip.service" ]; + path = [ + config.services.postgresql.package + pkgs.bash + pkgs.coreutils + pkgs.python3 + ]; + serviceConfig = { + Type = "oneshot"; + User = "root"; + Group = "root"; + }; + script = '' + set -euo pipefail + + db_password="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.postgresPasswordFile})" + db_password_sql="$(printf '%s' "$db_password" | python3 -c "import sys; print(sys.stdin.read().replace(chr(39), chr(39) * 2), end=\"\")")" + setup_sql="$(mktemp)" + trap 'rm -f "$setup_sql"' EXIT + + cat > "$setup_sql" < ${lib.escapeShellArg "${cfg.dataDir}/secrets/email-password"} chmod 0600 ${lib.escapeShellArg "${cfg.dataDir}/secrets/email-password"} - install -m 0444 ${lib.escapeShellArg cfg.memcachedPasswordFile} ${lib.escapeShellArg "${cfg.dataDir}/secrets/memcached-password"} - cat > ${lib.escapeShellArg "${cfg.dataDir}/rabbitmq.conf"} </dev/null 2>&1; do - attempts=$((attempts + 1)) - if [ "$attempts" -ge 90 ]; then - echo "error: RabbitMQ did not become ready for Zulip bootstrap" >&2 - exit 1 - fi - sleep 2 - done - } + ensure_zulip_data_layout() { + local zulip_data_dir=${lib.escapeShellArg "${cfg.dataDir}/data"} - ensure_zulip_volume_layout() { - local zulip_volume_mount - zulip_volume_mount="$(podman volume inspect burrow-zulip_zulip --format '{{.Mountpoint}}')" - install -d -m 0755 "$zulip_volume_mount/logs" - install -d -m 0755 "$zulip_volume_mount/logs/emails" - install -d -m 0700 "$zulip_volume_mount/secrets" - chown 1000:1000 "$zulip_volume_mount/logs" "$zulip_volume_mount/logs/emails" "$zulip_volume_mount/secrets" + install -d -m 0755 "$zulip_data_dir/logs" + install -d -m 0755 "$zulip_data_dir/logs/emails" + install -d -m 0700 "$zulip_data_dir/secrets" + chown 1000:1000 "$zulip_data_dir/logs" "$zulip_data_dir/logs/emails" "$zulip_data_dir/secrets" - if [ ! -s "$zulip_volume_mount/secrets/bootstrap-owner-password" ]; then + if [ ! -s "$zulip_data_dir/secrets/bootstrap-owner-password" ]; then umask 077 - openssl rand -base64 24 > "$zulip_volume_mount/secrets/bootstrap-owner-password" + openssl rand -base64 24 > "$zulip_data_dir/secrets/bootstrap-owner-password" fi - chown 1000:1000 "$zulip_volume_mount/secrets/bootstrap-owner-password" - chmod 0600 "$zulip_volume_mount/secrets/bootstrap-owner-password" + chown 1000:1000 "$zulip_data_dir/secrets/bootstrap-owner-password" + chmod 0600 "$zulip_data_dir/secrets/bootstrap-owner-password" } bootstrap_realm_if_needed() { @@ -415,15 +461,11 @@ EOF if [ ! -e .initialized ]; then compose pull - compose up -d database memcached rabbitmq redis - wait_for_rabbitmq compose run --rm -T zulip app:init touch .initialized fi - compose up -d database memcached rabbitmq redis - wait_for_rabbitmq - ensure_zulip_volume_layout + ensure_zulip_data_layout compose up -d zulip bootstrap_realm_if_needed '';