Bot Security & DM Policy
Cross-channel DM pairing, inbound trust defaults, and safe configuration patterns for production bot deployments.
Bot security enables safe deployment of PraisonAI agents across messaging channels with built-in protection against abuse and unauthorized access.
Secure baseline for new configs (passes gateway_security):
channels:
telegram:
token: ${TELEGRAM_BOT_TOKEN}
allowed_users: # without it, gateway_security WARNs under the default deny policy
- "${TELEGRAM_OWNER_ID}"
# group_policy: mention_only # default since #2038 — omit unless you need respond_allQuick Start
- Basic Security Setup
python from praisonaiagents import Agent from praisonaiagents.bots import BotConfig # Secure bot with allowlist config = BotConfig( allowed_users=["@your_username", "123456789"], unknown_user_policy="deny" # Default secure behavior ) agent = Agent( instructions="You are a helpful assistant", # Bot configuration handled by adapter )In YAML configuration:
yaml channels: telegram_cfo: platform: telegram token: ${TELEGRAM_CFO_BOT_TOKEN} allowed_users: ${TELEGRAM_ALLOWED_USERS} # comma-separated env var routes: default: cfo - Advanced Security Config
python from praisonaiagents import Agent from praisonaiagents.bots import BotConfig # Production security setup with pairing config = BotConfig( allowed_users=["@admin_user"], unknown_user_policy="pair", # Secure pairing flow auto_approve_tools=True, # For bot environments group_policy="mention_only" # Only respond when mentioned ) agent = Agent( instructions="Secure production assistant", # Configure with your bot adapter )
How It Works
| Component | Purpose | Status |
|---|---|---|
| Allowlist | Control access | Conceptual |
| DM Policy | Message filtering | Conceptual |
| Pairing | Channel authorization | Conceptual |
Security Model
PraisonAI treats inbound DMs as untrusted input by default. Production deployments should use explicit pairing and allowlists to prevent abuse, spam, and prompt injection from unknown senders.
Safe Defaults by Channel
Telegram
Recommended production config:
# bot.yaml
channels:
telegram:
token: ${TELEGRAM_BOT_TOKEN}
allowed_users:
- "@your_username"
- "123456789" # User ID
admin_users: ${TELEGRAM_ADMIN_USERS}
user_allowed_commands: "help,status,whoami"
# group_policy defaults to mention_only — set explicitly only if you need respond_allSecurity features:
- ✅ Prefer named access groups over inline ids when more than one channel shares a sender list — one revoke point, no drift.
- ✅ User allowlist by platform-native user ID (Telegram: numeric ID; Discord: snowflake; Slack: U…)
- ✅ Group mention-only policy
- ✅ Built-in command filtering — /help, /status, /new and custom commands respect
allowed_users(PR #1835) - ✅ Empty
allowed_usersnow defers tounknown_user_policy(parity with Discord/Slack, PR #1885).
Discord
Recommended production config:
# bot.yaml
channels:
discord:
token: ${DISCORD_BOT_TOKEN}
allowed_users:
- "your_user_id"
- "guild:server_id" # Specific server only
group_policy: "mention_only"Security features:
- ✅ User/guild allowlist support (user ID format: Discord snowflake)
- ✅ Role-based restrictions
- ✅ Thread-safe message handling
- ⚠️ DMs from unknown users are processed by default
Slack
Recommended production config:
# bot.yaml
channels:
slack:
token: ${SLACK_BOT_TOKEN}
app_token: ${SLACK_APP_TOKEN}
allowed_users:
- "U0123456789" # User ID
- "C9876543210" # Channel ID
group_policy: "mention_only"Security features:
- ✅ User/channel allowlist (user ID format: Slack U…)
- ✅ Enterprise Grid support
- ✅ Socket mode security
- ✅ Built-in DM filtering (mentions required)
Recommended production config:
# bot.yaml
channels:
whatsapp:
allowed_users:
- "+1234567890" # Phone numbers
- "group123@g.us" # Group IDs
blocklist:
- "+spam_number"Security features:
- ✅ Strong default security – allowlist required for DMs
- ✅ Phone number + group allowlists
- ✅ Built-in self-chat detection
- ✅ Automatic spam filtering
Admin Users & Per-Command Access
Layer command-level restrictions on top of user allowlists: admin_users grants full command access; user_allowed_commands limits everyone else to a comma-separated list. /help and /whoami are always permitted.
Full guide to admin_users and user_allowed_commands
Owner-DM Pairing
The pairing system is now shipped and enables owner-approval for unknown users with inline Approve/Deny buttons sent directly to your DM.
For production deployments, use owner-DM pairing to authorize unknown users dynamically:
1. Callback Signing (default is safe)
The signing key resolves in this order (pairing codes and callback buttons alike):
- Single host, default install: nothing to do — inline buttons and pairing codes just work across restarts.
- Multiple hosts / replicas: set
PRAISONAI_CALLBACK_SECRETto the same value on every host. - Explicit control: setting
PRAISONAI_CALLBACK_SECREToverrides the persisted secret — useful for rotation or shared-vault-managed environments.
2. Configure Unknown User Policy
from praisonaiagents import Agent
from praisonaiagents.bots import BotConfig
config = BotConfig(
token="your-bot-token",
unknown_user_policy="pair", # Enable owner-approval workflow
owner_user_id="123456789", # Your platform user ID
)
agent = Agent(
name="Support Bot",
instructions="Help users with their questions",
)3. Owner Approval Workflow
When an unknown user messages your bot:
- Bot generates a pairing code
- Owner receives DM with inline Approve/Deny buttons
- Owner clicks Approve → User is permanently approved
- Owner clicks Deny → Request is rejected
CLI Fallback: If owner_user_id is not set, the bot replies:
Your pairing code: abc12345. Ask the owner to run: praisonai pairing approve telegram abc123454. Manual Approval (CLI)
Owners can approve pairing requests manually:
# Approve a specific pairing code
praisonai pairing approve telegram abc12345
# Approve for Discord
praisonai pairing approve discord def67890
# Approve for Slack
praisonai pairing approve slack ghi13579Choosing unknown_user_policy
Pick the policy that matches how open your bot should be:
| Scenario | Policy | Rationale |
|---|---|---|
| Public bot with self-service onboarding | pair + owner_user_id | Owner approves each new user; audit trail; safe default for growing communities. |
| Internal-only bot on trusted network | allow + empty allowed_users | Everyone on the network is trusted; skips friction. Never use on a public token. |
| Fixed team / private bot | deny + explicit allowed_users | Whitelist is the security boundary; policy is defense-in-depth. |
| Default / uncertain | deny (leave unset) | Secure by default — nothing surprising leaks through. |
All three values are read per-channel from gateway.yaml — see Bot Gateway → Channel Security.
Gateway Pairing
For production deployments, use gateway pairing to authorize channels dynamically with the shipped pairing system:
Enable Pairing Policy
from praisonaiagents.bots import BotConfig
config = BotConfig(
allowed_users=["@owner"],
unknown_user_policy="pair" # Enable pairing for unknown users
)
# Unknown users will automatically receive pairing codes when they DM the botApprove Pairing Requests
When unknown users DM your bot, they receive pairing codes. Approve them via CLI:
# User receives: "Your pairing code: ABCD1234"
# Owner approves:
praisonai pairing approve telegram ABCD1234 --label "alice"Manage Pairings
# List all paired channels
praisonai pairing list
# Revoke access for specific channel
praisonai pairing revoke telegram 987654321
# Clear all pairings
praisonai pairing clear --confirmList Pending Requests
List all pending pairing codes waiting for approval:
from praisonai.gateway.pairing import PairingStore
store = PairingStore()
# All pending codes across every channel
for req in store.list_pending():
print(req["code"], req["channel_type"], req["channel_id"], req["age_seconds"])
# Filter by channel
telegram_only = store.list_pending(channel_type="telegram")Response Schema:
| Key | Type | Source | Notes | |
|---|---|---|---|---|
code | str | canonical | 8-char pairing code | |
channel_type | str | canonical | e.g. "telegram", "discord", "slack", "whatsapp" | |
channel_id | `str \ | None` | canonical | Bound channel id if code was generated with one |
created_at | float | canonical | Unix timestamp (seconds) when code was generated | |
channel | str | UI alias | Same value as channel_type, kept for UI banner compatibility | |
user_id | str | UI alias | Currently equals code (see note in approve() docstring) | |
user_name | str | UI alias | Formatted as "User {code}" | |
age_seconds | int | UI alias | int(now - created_at) |
CLI Commands
Use the praisonai pairing commands to manage pairings from the command line:
# List all paired channels
praisonai pairing list
# Approve a pairing code (this is the exact command shown to users)
praisonai pairing approve telegram abc12345
# Revoke a paired channel
praisonai pairing revoke telegram @username
# Clear all paired channels
praisonai pairing clearAvailable Commands:
| Command | Purpose | Required Args |
|---|---|---|
praisonai pairing list | List all paired channels | — |
praisonai pairing approve PLATFORM CODE [CHANNEL_ID] | Approve an 8-char pairing code | platform, code |
praisonai pairing revoke PLATFORM CHANNEL_ID | Revoke a paired channel | platform, channel_id |
praisonai pairing clear | Clear all paired channels | — |
Platform values: telegram, discord, slack, whatsapp
Pairing Flow:
sequenceDiagram
participant User
participant Bot
participant PairingStore
participant CLI
User->>Bot: unknown message (triggers pairing)
Bot->>PairingStore: generate_code(channel_type, channel_id)
PairingStore-->>Bot: abc12345
Bot->>User: Ask owner to run: praisonai pairing approve telegram abc12345
CLI->>PairingStore: approve(telegram, abc12345)
PairingStore->>PairingStore: verify_and_pair()
PairingStore-->>CLI: success
User->>Bot: now authorised
Bot-->>User: response
classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
classDef bot fill:#189AB4,stroke:#7C90A0,color:#fff
classDef success fill:#10B981,stroke:#7C90A0,color:#fff
class User user
class Bot,PairingStore,CLI bot7. REST API
The gateway exposes REST endpoints for pairing management:
| Method | Path | Body / Query | Response | Auth Required |
|---|---|---|---|---|
GET | /api/pairing/pending | — | list_pending() schema | ✅ |
POST | /api/pairing/approve | { "channel": str, "code": str } | { "approved": true, ... } | ✅ |
POST | /api/pairing/revoke | { "channel": str, "user_id": str } | { "revoked": true, ... } | ✅ |
Example Usage:
# List pending requests
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/pairing/pending
# Approve a code
curl -X POST -H "Authorization: Bearer $TOKEN" \
-d '{"channel":"telegram","code":"abc12345"}' \
http://localhost:8000/api/pairing/approve
# Revoke a channel
curl -X POST -H "Authorization: Bearer $TOKEN" \
-d '{"channel":"telegram","user_id":"@username"}' \
http://localhost:8000/api/pairing/revokeDoctor Security Check
Use the built-in doctor to audit your bot security configuration:
praisonai doctor --category botsThe security check flags:
- ❌ Open bot – channel has no allowlist/blocklist AND
unknown_user_policy: "allow"(FAIL) - ⚠️ Dropped DMs – channel has no allowlist/blocklist under the default
unknown_user_policy: "deny"(WARN; unknown DMs are dropped) - ⚠️ Permissive group policies –
respond_allin production - ⚠️ Missing gateway secret – pairing codes won’t persist
- ✅ Secure configuration – allowlists + mention-only policies
Example output:
Bot Security Config: WARN
Security recommendations: 2 channel(s) could use stricter defaults
Details:
telegram: No allowed_users and unknown_user_policy=deny - DMs from unknown users are dropped
discord: group_policy='respond_all' - consider 'mention_only' for security
Remediation: Consider allowlists for DM security and 'mention_only' group policySelf-Hoster Security Checklist
Before going public with your bot:
✅ DM Policy Configured
- [ ] Allowlist configured for each channel
- [ ] Unknown sender behavior defined (block/ignore/process)
- [ ] Group policies set to
mention_only,command_only, orobserve - [ ] Blocklist configured for known spam sources
- [ ] Built-in commands (/help, /status, /new) verified to respect allowlist
- [ ]
admin_usersset for owner/admin user IDs - [ ]
user_allowed_commandsconfigured to limit regular users to read-only commands - [ ] Empty
allowed_usersis intentional and paired withunknown_user_policy: "allow", orallowed_usersis non-empty.
✅ Gateway Pairing Active
- [ ] Callback/pairing signing works by default via
~/.praisonai/gateway/.gateway_secret(0600) — no action needed on a single host - [ ]
PRAISONAI_CALLBACK_SECRETset to a shared value only across multiple hosts/replicas - [ ]
unknown_user_policyconfigured (deny/pair/allow) - [ ] If using
praisonai bot startwith abot.yaml, confirm theunknown_user_policyline in the startup banner matches what you configured. A typo will now fail closed at load time — treat that as intended, not a bug. - [ ] A typo in
unknown_user_policynow fails closed at load time in bothbot.yaml(PR #5111) and multi-channelgateway.yaml(PR #5112). Fix the typo — do not disable the validator. - [ ]
owner_user_idset for inline approvals - [ ] Pairing workflow tested and verified
- [ ] Revocation process documented
- [ ] At-rest hashing is on by default from PraisonAI #3974 —
pairing.jsonnever contains a plaintext code - [ ] A per-channel-type brute-force lockout defends every approval surface (HTTP, in-chat button, CLI). Tunable via
PairingStore(max_failures=, lockout_window=, lockout_cooldown=)
✅ Tool Approval Enabled
- [ ] Dangerous tools require approval (not auto-approved)
- [ ] Approval backend configured (Slack/Telegram/HTTP)
- [ ] Tool risk levels reviewed and appropriate
- [ ] Approval timeout configured
✅ Monitoring & Alerts
- [ ] Bot security doctor check passing
- [ ] Audit logging enabled (
praisonai.security.enable_audit_log) — sensitive keys (api keys, tokens, passwords) in tool inputs are redacted by default; see Audit Logging to customise - [ ] Injection defense active (
enable_injection_defense()— returnsbefore_tool+before_agenthook IDs) - [ ] Rate limiting configured for API calls
✅ Infrastructure Security
- [ ] Bot tokens stored securely (not in code)
- [ ] Environment variables encrypted at rest
- [ ] Network access restricted (firewall rules)
- [ ] Regular security updates scheduled
Common Security Patterns
1. Staged Rollout
Start with restrictive settings and gradually open access:
# Stage 1: Internal testing
channels:
telegram:
allowed_users: ["@internal_team"]
group_policy: "command_only"
# Stage 2: Trusted users
channels:
telegram:
allowed_users: ["@internal_team", "@trusted_users"]
group_policy: "mention_only"
# Stage 3: Public (with safety nets)
channels:
telegram:
# Remove allowlist for open access
group_policy: "mention_only"
rate_limit: 10 # messages per minute2. Multi-Channel Security
Maintain consistent security across channels with unique tokens and role-specific access:
Unique token per channel (CRITICAL):
# ❌ WRONG - Same token reused (security violation)
channels:
telegram_cfo:
token: ${TELEGRAM_BOT_TOKEN} # DON'T REUSE TOKENS
telegram_ops:
token: ${TELEGRAM_BOT_TOKEN} # This will FAIL validation
# ✅ CORRECT - Unique tokens per channel
channels:
telegram_cfo:
token: ${TELEGRAM_CFO_BOT_TOKEN}
allowed_users: ${FINANCE_TEAM_USERS}
telegram_ops:
token: ${TELEGRAM_OPS_BOT_TOKEN}
allowed_users: ${OPS_TEAM_USERS}Shared allowlists across platforms:
# Shared allowed users
x-allowlist: &shared-users
- "admin_user_1"
- "admin_user_2"
- "trusted_group_1"
channels:
telegram:
allowed_users: *shared-users
discord:
allowed_users: *shared-users
slack:
allowed_users: *shared-usersRole-specific access control:
# Different teams access different bots
channels:
telegram_cfo:
token: ${TELEGRAM_CFO_BOT_TOKEN}
allowed_users: ${FINANCE_TEAM_USERS} # Only finance team
telegram_ops:
token: ${TELEGRAM_OPS_BOT_TOKEN}
allowed_users: ${OPS_TEAM_USERS} # Only ops team3. Environment-Based Security
Different security levels per environment:
# development.yaml - loose security
channels:
telegram:
# No allowlist for dev testing
group_policy: "respond_all"
# staging.yaml - moderate security
channels:
telegram:
allowed_users: ["@staging_team"]
group_policy: "mention_only"
# production.yaml - strict security
channels:
telegram:
allowed_users: ["@verified_users"]
group_policy: "command_only"
approval: true # All tools need approvalSecurity Headers & API Protection
When running bot gateways, enable security headers:
from praisonai.gateway import GatewayServer
server = GatewayServer(
host="0.0.0.0",
port=8765,
security_headers=True, # Add CORS, CSP, etc.
rate_limit=True, # Enable rate limiting
require_https=True, # Redirect HTTP to HTTPS
)Advanced: Custom Security Hooks
Implement custom security logic with hooks:
from praisonaiagents.hooks import add_hook, HookResult
@add_hook('before_tool')
def channel_security_check(event_data):
"""Custom security check based on channel type"""
channel = event_data.context.get('channel_type')
sender = event_data.context.get('sender_id')
tool_name = event_data.tool_name
# Block file operations from Telegram DMs
if channel == 'telegram' and tool_name in ['write_file', 'delete_file']:
if not is_verified_user(sender):
return HookResult.block("File operations not allowed from unverified Telegram users")
# Require approval for shell commands from all channels
if tool_name == 'execute_command':
return HookResult.request_approval(f"Shell command from {channel}: {sender}")
return HookResult.allow()
def is_verified_user(user_id: str) -> bool:
"""Check if user is in verified allowlist"""
verified_users = os.environ.get('VERIFIED_USERS', '').split(',')
return user_id in verified_usersTroubleshooting
Pairing Issues
Problem: Pairing codes not working
Solution:
- Check
PRAISONAI_GATEWAY_SECRETis set - Verify code hasn’t expired (5 min default)
- Ensure code typed exactly (case sensitive)
Problem: Pairing lost after restart
Solution:
- Set
PRAISONAI_GATEWAY_SECRETenv var - Codes without persistent secret are temporary
Problem: praisonai pairing approve reports “Invalid or expired code” even though a code was just generated from the UI
Solution:
- Upgrade to the latest
praisonaiversion (fix included in 2026-04-22 release) - Older builds had a duplicate internal method that stripped the canonical
codekey when the UI pairing banner was loaded
Allowlist Issues
Problem: Bot not responding to allowed users
Solution:
- Check exact user ID format (username vs numeric ID)
- Verify allowlist syntax in YAML
- Run
praisonai doctorfor validation
Problem: Bot responding to blocked users
Solution:
- Check allowlist is configured (not just blocklist)
- Verify
group_policysetting - Check if user has alternate access path
Problem: My bot silently ignores every DM from users I haven’t listed in allowed_users and I don’t know why.
Solution: Since PR #5112, every deny-policy drop leaves a WARNING in the bot log:
Dropped message from unknown user_id=... platform=... conversation=...: unknown_user_policy=deny and user is not in allowed_users.
Grep your bot log for unknown_user_policy=deny. If you see the WARNING, either add the printed user_id to allowed_users, or set unknown_user_policy=allow (or pair with an owner_user_id). If you don’t see the WARNING, the message isn’t reaching the admission pipeline — check the token, network, and platform adapter first.
Problem: Unauthorized users can still invoke /help, /status, or /new
Solution: Upgrade to the PraisonAI release that includes PR #1835. Earlier versions enforced the allowlist only on plain-text messages, not on command handlers.
Problem: After upgrading PraisonAI my Telegram bot stopped replying to everyone, including the owner.
Solution: PR #1885 fixed a Telegram bug where empty allowed_users bypassed unknown_user_policy. Either add your user ID to allowed_users, or set unknown_user_policy: "allow" if you actually want the bot to respond to anyone. Default is "deny". Since PR #5112, a dropped DM now leaves a WARNING with the sender’s user_id — the fastest way to confirm this is what’s happening.
Best Practices
✅ Allowlist Management
- Use explicit allowlists for all channels
- Regularly review and update allowed users
- Implement role-based access where possible
- Log all access attempts for audit trails
🔐 Secret Management
- Store tokens in environment variables
- Rotate secrets regularly
- Use encrypted storage for sensitive data
- Never commit secrets to version control
⚡ Rate Limiting
- Implement per-user rate limits
- Set global request quotas
- Monitor for unusual activity patterns
- Implement exponential backoff for failed requests
📊 Monitoring
- Enable audit logging
- Monitor security events
- Set up alerting for suspicious activity
- Regular security reviews
Related
Core agent setup and configuration
Gateway SetupMulti-channel gateway configuration
By following these security practices, your PraisonAI bots will operate safely in production while maintaining the flexibility to serve legitimate users. Regular security audits help ensure your configuration stays secure over time.