No description
  • Python 53.9%
  • Nix 46.1%
Find a file
2026-05-07 23:42:43 -07:00
nixos/tests Fix OMEMO storage: implement JSONFileStorage using omemo 2.0.0 API 2026-05-05 16:50:40 -07:00
tests Format bot reply output in markdown code blocks with shell syntax highlighting 2026-05-07 23:42:43 -07:00
.gitignore Don't track .direnv files 2026-04-30 10:21:10 -07:00
AGENTS.md Document nix fmt and black formatting conventions in AGENTS.md 2026-05-05 21:22:42 -07:00
configuration.nix.example Added OMEMO encryption support. 2026-05-05 09:52:35 -07:00
flake.lock Nix flake update. 2026-05-07 23:18:24 -07:00
flake.nix Add xmlschema to python-env, required by oldmemo.etree at runtime 2026-05-05 15:59:23 -07:00
overlay.nix Fix OMEMO storage: implement JSONFileStorage using omemo 2.0.0 API 2026-05-05 16:50:40 -07:00
pyproject.toml Fixed testing. Added fixes to module as well. 2026-04-30 10:23:54 -07:00
README.md Add supplementaryGroups option for bot user group membership 2026-05-07 23:12:50 -07:00
xmpp-cli-bot-module.nix Add supplementaryGroups option for bot user group membership 2026-05-07 23:12:50 -07:00
xmpp-cli-bot.py Format bot reply output in markdown code blocks with shell syntax highlighting 2026-05-07 23:42:43 -07:00

xmpp-cli-bot

A NixOS service that joins an XMPP MUC room and executes shell commands sent to it, replying with the output.

Quick Start

1. Add the flake input

In your flake.nix:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    xmpp-cli-bot.url = "git+https://git.p4p4j0hn.ca/john/xmpp-cli-bot.git";
  };

  outputs = { self, nixpkgs, xmpp-cli-bot }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
        xmpp-cli-bot.nixosModules.default
      ];
    };
  };
}

2. Configure

In your configuration.nix:

{ config, pkgs, ... }: {
  services.xmppCliBot = {
    enable          = true;
    jid             = "bot@xmpp.example.com";
    passwordFile    = "/run/secrets/xmpp-bot-password";
    room            = "ops@conference.xmpp.example.com";
    nick            = "server-bot";
    allowedJids     = [ "alice@xmpp.example.com" ];
    maxOutputBytes  = 16384;
    rateLimit       = 10;
    extraSystemPackages = with pkgs; [ jq curl htop ];
  };
}

3. Create the password file

sudo install -dm 0750 /run/secrets
echo -n 'yourpassword' | sudo tee /run/secrets/xmpp-bot-password
sudo chown xmpp-cli-bot /run/secrets/xmpp-bot-password
sudo chmod 0400 /run/secrets/xmpp-bot-password

For production, use agenix or sops-nix — see configuration.nix.example.

4. Apply

sudo nixos-rebuild switch --flake .#myhost

5. Use it

In your XMPP client, join the MUC room and send:

! systemctl status nginx
! journalctl -n 50 -u myapp
! df -h
! nixos-rebuild switch --flake .#myhost

Options Reference

Option Default Description
jid Bot's XMPP account (user@domain)
passwordFile Path to file with XMPP password
room MUC room JID (room@conference.domain)
nick "cli-bot" Bot's MUC nickname
prefix "!" Message prefix triggering execution
allowedJids Non-empty list of allowed bare JIDs (required)
timeout 30 Command timeout (seconds)
maxOutputBytes 16384 Hard cap on command output in bytes
rateLimit 10 Max commands per JID per 60-second sliding window
shell bash Shell for executing commands
debug false Verbose logging
saslMech null Force a SASL mechanism (PLAIN, SCRAM-SHA-1, SCRAM-SHA-256, DIGEST-MD5)
extraSystemPackages [] Extra tools on the bot's PATH
supplementaryGroups ["systemd-journal", "proc"] Supplementary groups for the bot user
systemdExtraConfig {} Merge into systemd unit

OMEMO Encryption

The bot supports OMEMO end-to-end encryption for both MUC and 1:1 messages. When enabled:

  • Incoming OMEMO-encrypted commands are decrypted and executed
  • Replies are encrypted back to the sender (or all MUC participants)
  • Falls back to plaintext if the recipient doesn't support OMEMO

Enable OMEMO

services.xmppCliBot = {
  # ... your existing config ...
  omemo = {
    enable = true;
    dataDir = "/var/lib/xmpp-cli-bot/omemo";
    blindTrustBeforeVerification = true;
  };
};

Trust Management

The bot uses Blind Trust Before Verification (BTBV) by default — new devices are automatically trusted on first contact. Set blindTrustBeforeVerification = false to require manual trust decisions (logged but not acted upon automatically).

Key Backup

OMEMO keys are stored in dataDir as a JSON file. This directory contains:

  • Your bot's OMEMO identity keypair
  • Pre-keys and signed pre-keys
  • Session state for each contact

Important: Do NOT include this in regular backups — OMEMO's forward secrecy means restoring old keys breaks sessions. Only use it for migration to a new host.

Security Notes

  • allowedJids is required — the bot will refuse to start without at least one allowed JID.
  • The service runs as a dedicated unprivileged xmpp-cli-bot user.
  • systemd hardening is enabled by default (ProtectSystem, PrivateTmp, NoNewPrivileges, etc.).
  • Commands are run with a restricted PATH; add tools via extraSystemPackages.
  • For privileged operations (e.g. nixos-rebuild switch), add the bot user to sudoers with fine-grained rules rather than widening the systemd sandbox.

Supplementary Groups

The bot user is added to systemd-journal and proc groups by default, granting read access to journalctl and /proc. Customize with:

services.xmppCliBot.supplementaryGroups = [ "systemd-journal" "proc" "docker" ];

Giving the Bot sudo Access (Optional)

security.sudo.extraRules = [{
  users = [ "xmpp-cli-bot" ];
  commands = [
    { command = "${pkgs.nixos-rebuild}/bin/nixos-rebuild"; options = [ "NOPASSWD" ]; }
    { command = "/run/current-system/sw/bin/systemctl restart *"; options = [ "NOPASSWD" ]; }
  ];
}];

Viewing Logs

journalctl -u xmpp-cli-bot -f

Troubleshooting

Authentication failed: not-authorized

If you see Authentication failed: not-authorized in the logs:

  1. Check the password file has no trailing newline. The service strips newlines automatically, but if testing manually ensure you used echo -n:

    printf '%s' 'yourpassword' > /run/secrets/xmpp-bot-password
    
  2. Check your Prosody authentication method. It must match the SASL mechanism slixmpp negotiates:

    Prosody setting Works with
    authentication = "internal_plain" PLAIN
    authentication = "internal_hashed" SCRAM-SHA-1, SCRAM-SHA-256
  3. Force a specific SASL mechanism if auto-negotiation fails:

    services.xmppCliBot = {
      saslMech = "PLAIN";  # or "SCRAM-SHA-1"
      debug    = true;
    };
    
  4. Enable debug logging (debug = true) to see the full SASL mechanism negotiation in the journal.

Checking available SASL mechanisms

To see what mechanisms your Prosody server offers:

echo "" | openssl s_client -connect your.server.com:5222 -starttls xmpp -quiet 2>/dev/null | \
  grep -o '<mechanism>[^<]*</mechanism>'

Connection refused or timeout

  • Ensure network-online.target is reached before the service starts (handled automatically).
  • Verify the JID domain resolves and port 5222 is reachable.
  • Check TLS certificate validity — an expired cert will cause connection failure.