- Python 53.9%
- Nix 46.1%
| nixos/tests | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| configuration.nix.example | ||
| flake.lock | ||
| flake.nix | ||
| overlay.nix | ||
| pyproject.toml | ||
| README.md | ||
| xmpp-cli-bot-module.nix | ||
| xmpp-cli-bot.py | ||
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
allowedJidsis required — the bot will refuse to start without at least one allowed JID.- The service runs as a dedicated unprivileged
xmpp-cli-botuser. - 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 tosudoerswith 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:
-
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 -
Check your Prosody
authenticationmethod. It must match the SASL mechanism slixmpp negotiates:Prosody setting Works with authentication = "internal_plain"PLAINauthentication = "internal_hashed"SCRAM-SHA-1,SCRAM-SHA-256 -
Force a specific SASL mechanism if auto-negotiation fails:
services.xmppCliBot = { saslMech = "PLAIN"; # or "SCRAM-SHA-1" debug = true; }; -
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.targetis 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.