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):

yaml
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_all
Diagram

Bot Security Flow

📱 Inbound DM

🔍 Security Check

✅ Allow

❌ Block

🤖 Agent

Quick Start

  1. 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
  2. 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

Diagram
AgentSecurityBotUserAgentSecurityBotUseralt[Allowed][Blocked]Send MessageCheck AuthorizationAllowed/BlockedProcess MessageResponseReplyAccess Denied
ComponentPurposeStatus
AllowlistControl accessConceptual
DM PolicyMessage filteringConceptual
PairingChannel authorizationConceptual

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.

Diagram

Unknown sender

In allowlist

Paired channel

Approved

Denied

Inbound DM

Sender Check

Block/Ignore ❌

Process ✅

Tool Approval

Execute Tool

Block Tool ❌

Safe Defaults by Channel

Telegram

Recommended production config:

yaml
# 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_all

Security 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_users now defers to unknown_user_policy (parity with Discord/Slack, PR #1885).

Discord

Recommended production config:

yaml
# 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:

yaml
# 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)

WhatsApp

Recommended production config:

yaml
# 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.

Command Access Control

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_SECRET to the same value on every host.
  • Explicit control: setting PRAISONAI_CALLBACK_SECRET overrides the persisted secret — useful for rotation or shared-vault-managed environments.

2. Configure Unknown User Policy

python
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:

  1. Bot generates a pairing code
  2. Owner receives DM with inline Approve/Deny buttons
  3. Owner clicks Approve → User is permanently approved
  4. 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 abc12345

4. Manual Approval (CLI)

Owners can approve pairing requests manually:

bash
# 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 ghi13579

Choosing unknown_user_policy

Pick the policy that matches how open your bot should be:

ScenarioPolicyRationale
Public bot with self-service onboardingpair + owner_user_idOwner approves each new user; audit trail; safe default for growing communities.
Internal-only bot on trusted networkallow + empty allowed_usersEveryone on the network is trusted; skips friction. Never use on a public token.
Fixed team / private botdeny + explicit allowed_usersWhitelist is the security boundary; policy is defense-in-depth.
Default / uncertaindeny (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

python
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 bot

Approve Pairing Requests

When unknown users DM your bot, they receive pairing codes. Approve them via CLI:

bash
# User receives: "Your pairing code: ABCD1234"
# Owner approves:
praisonai pairing approve telegram ABCD1234 --label "alice"

Manage Pairings

bash
# List all paired channels
praisonai pairing list

# Revoke access for specific channel
praisonai pairing revoke telegram 987654321

# Clear all pairings
praisonai pairing clear --confirm

List Pending Requests

List all pending pairing codes waiting for approval:

python
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:

KeyTypeSourceNotes
codestrcanonical8-char pairing code
channel_typestrcanonicale.g. "telegram", "discord", "slack", "whatsapp"
channel_id`str \None`canonicalBound channel id if code was generated with one
created_atfloatcanonicalUnix timestamp (seconds) when code was generated
channelstrUI aliasSame value as channel_type, kept for UI banner compatibility
user_idstrUI aliasCurrently equals code (see note in approve() docstring)
user_namestrUI aliasFormatted as "User {code}"
age_secondsintUI aliasint(now - created_at)

CLI Commands

Use the praisonai pairing commands to manage pairings from the command line:

bash
# 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 clear

Available Commands:

CommandPurposeRequired Args
praisonai pairing listList all paired channels—
praisonai pairing approve PLATFORM CODE [CHANNEL_ID]Approve an 8-char pairing codeplatform, code
praisonai pairing revoke PLATFORM CHANNEL_IDRevoke a paired channelplatform, channel_id
praisonai pairing clearClear all paired channels—

Platform values: telegram, discord, slack, whatsapp

Pairing Flow:

Diagram
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 bot

7. REST API

The gateway exposes REST endpoints for pairing management:

MethodPathBody / QueryResponseAuth 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:

bash
# 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/revoke

Doctor Security Check

Use the built-in doctor to audit your bot security configuration:

bash
praisonai doctor --category bots

The 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_all in 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 policy

Self-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, or observe
  • [ ] Blocklist configured for known spam sources
  • [ ] Built-in commands (/help, /status, /new) verified to respect allowlist
  • [ ] admin_users set for owner/admin user IDs
  • [ ] user_allowed_commands configured to limit regular users to read-only commands
  • [ ] Empty allowed_users is intentional and paired with unknown_user_policy: "allow", or allowed_users is 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_SECRET set to a shared value only across multiple hosts/replicas
  • [ ] unknown_user_policy configured (deny/pair/allow)
  • [ ] If using praisonai bot start with a bot.yaml, confirm the unknown_user_policy line 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_policy now fails closed at load time in both bot.yaml (PR #5111) and multi-channel gateway.yaml (PR #5112). Fix the typo — do not disable the validator.
  • [ ] owner_user_id set for inline approvals
  • [ ] Pairing workflow tested and verified
  • [ ] Revocation process documented
  • [ ] At-rest hashing is on by default from PraisonAI #3974 — pairing.json never 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() — returns before_tool + before_agent hook 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:

yaml
# 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 minute

2. Multi-Channel Security

Maintain consistent security across channels with unique tokens and role-specific access:

Unique token per channel (CRITICAL):

yaml
# ❌ 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:

yaml
# 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-users

Role-specific access control:

yaml
# 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 team

3. Environment-Based Security

Different security levels per environment:

yaml
# 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 approval

Security Headers & API Protection

When running bot gateways, enable security headers:

python
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:

python
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_users

Troubleshooting

Pairing Issues

Problem: Pairing codes not working

Solution:

  1. Check PRAISONAI_GATEWAY_SECRET is set
  2. Verify code hasn’t expired (5 min default)
  3. Ensure code typed exactly (case sensitive)

Problem: Pairing lost after restart

Solution:

  1. Set PRAISONAI_GATEWAY_SECRET env var
  2. 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:

  1. Upgrade to the latest praisonai version (fix included in 2026-04-22 release)
  2. Older builds had a duplicate internal method that stripped the canonical code key when the UI pairing banner was loaded

Allowlist Issues

Problem: Bot not responding to allowed users

Solution:

  1. Check exact user ID format (username vs numeric ID)
  2. Verify allowlist syntax in YAML
  3. Run praisonai doctor for validation

Problem: Bot responding to blocked users

Solution:

  1. Check allowlist is configured (not just blocklist)
  2. Verify group_policy setting
  3. 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


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.