Skip to content

Remote-Pulse Telegram Approval Flow Setup

F4-6 implementation: Defense-in-depth human-in-the-loop approval for destructive commands in sensitive groups (prod, iarq).

Overview

When Remote-Pulse server receives a destructive command (e.g., reboot, pkg_install, exec_shell) for a host in a sensitive group, it triggers a Telegram approval flow:

  1. Server creates command record with approval_token
  2. Server fires webhook to n8n
  3. n8n sends Telegram message with inline keyboard [Approve / Reject]
  4. Admin clicks button within 5 minutes
  5. n8n calls server callback endpoint (/v1/admin/commands/approve/{token} or /reject/{token})
  6. Server marks command as human_approved=true or rejected_reason="..."
  7. Agent polls and executes approved command (or skips rejected)

TTL: 5 minutes. After timeout, command auto-rejects with rejected_reason="approval_timeout".

Prerequisites

PRE-1: Telegram Bot

Decision pending (ADR-0008 Open Question #12): - Option A: Reuse @Veraclawd_bot (token 8597804826:AAEW...XJQ_LCA) - Option B: Create dedicated @RemotePulse_bot

Current recommendation: Reuse Veraclawd bot to avoid bot proliferation. Alerts already go there; approval requests fit same persona.

Token storage: n8n credential telegram-bot-credentials (type "Telegram API")

PRE-2: Telegram Chat ID

Admin chat ID for approval messages: 730947207 (Ramon's Telegram user ID).

Environment variable: TELEGRAM_CHAT_ID_ADMINS=730947207 in n8n LXC 200.

PRE-3: Server Public URL

n8n needs to call server callback endpoints. Server must be reachable from n8n LXC 200.

Options: - Public URL: https://rp.monxas.casa (via Caddy HA LXC 270/271) - Internal URL: http://192.168.0.196:8080 (direct to LXC 280)

Recommendation: Use internal URL for callbacks (faster, no TLS overhead). Caddy only needed for external enrollment.

Environment variable: RP_SERVER_PUBLIC_URL=http://192.168.0.196:8080 in n8n.

PRE-4: n8n Webhook URL

Server needs to fire approval request webhook to n8n.

Webhook path: /webhook/rp-approval (configured in n8n workflow)

Full URL: http://192.168.0.200:5678/webhook/rp-approval

Environment variable in server LXC 280:

RP_N8N_APPROVAL_WEBHOOK_URL=http://192.168.0.200:5678/webhook/rp-approval

Setup Steps

Step 1: Import n8n Workflow

  1. Copy workflow JSON to n8n host:

    scp homelab-infra/n8n/workflows/rp-approval-flow.json [email protected]:/tmp/
    

  2. Import via n8n UI:

  3. Navigate to http://192.168.0.200:5678
  4. Workflows → Import from File
  5. Select /tmp/rp-approval-flow.json
  6. Click "Import"

  7. Verify workflow has 8 nodes:

  8. Webhook Trigger
  9. Format Telegram Message
  10. Send Telegram Message
  11. Respond to Webhook
  12. Telegram Callback Trigger
  13. Parse Callback Data
  14. Call Server Endpoint
  15. Edit Telegram Message

Step 2: Configure n8n Credentials

  1. Create Telegram Bot credential (if not exists):
  2. Settings → Credentials → Add Credential
  3. Type: "Telegram API"
  4. Name: telegram-bot-credentials
  5. Access Token: 8597804826:AAEW...XJQ_LCA (or new bot token)
  6. Save

  7. Set environment variables in n8n LXC 200:

    ssh [email protected]
    
    # Edit /etc/systemd/system/n8n.service or docker-compose.yml
    # Add:
    Environment="TELEGRAM_CHAT_ID_ADMINS=730947207"
    Environment="RP_SERVER_PUBLIC_URL=http://192.168.0.196:8080"
    
    systemctl daemon-reload
    systemctl restart n8n  # or docker-compose restart n8n
    

Step 3: Configure Server Environment

In Remote-Pulse server LXC 280:

ssh [email protected]

# Edit /etc/rp/server.env or systemd unit
cat >> /etc/rp/server.env <<EOF
RP_N8N_APPROVAL_WEBHOOK_URL=http://192.168.0.200:5678/webhook/rp-approval
EOF

systemctl restart remote-pulse-server

Step 4: Activate Workflow

  1. In n8n UI, open "Remote-Pulse Approval Flow"
  2. Click "Activate" toggle (top right)
  3. Verify webhook is listening: http://192.168.0.200:5678/webhook/rp-approval

Step 5: Test End-to-End

Test 1: Webhook Delivery

# From any host with curl
curl -X POST http://192.168.0.200:5678/webhook/rp-approval \
  -H "Content-Type: application/json" \
  -d '{
    "event": "approval_required",
    "command_id": "test-123",
    "command_type": "reboot",
    "command_payload": {"delay_s": 30},
    "host_hostname": "test-host",
    "host_group": "prod",
    "issued_by": "admin",
    "issued_at": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
    "approval_token": "00000000-0000-0000-0000-000000000001",
    "approval_url_accept": "http://192.168.0.196:8080/v1/admin/commands/approve/00000000-0000-0000-0000-000000000001",
    "approval_url_reject": "http://192.168.0.196:8080/v1/admin/commands/reject/00000000-0000-0000-0000-000000000001",
    "expires_at": "'$(date -u -v+5M +%Y-%m-%dT%H:%M:%SZ)'"
  }'

Expected: Telegram message appears in chat 730947207 with inline buttons.

Test 2: Approval Click

  1. Click "✓ Approve" button in Telegram
  2. Check server logs:
    ssh [email protected]
    journalctl -u remote-pulse-server -f | grep approval
    
  3. Verify Telegram message edits to "✅ Approved by @username at ..."

Test 3: Full Integration

# Issue a destructive command from server CLI
rp admin exec --host=test-host-prod --cmd="echo test"

# Expected flow:
# 1. Command created with approval_token
# 2. Telegram message sent
# 3. Click Approve
# 4. Command marked human_approved=true
# 5. Agent polls and executes

Configuration Reference

Server Settings

Setting Default Description
RP_N8N_APPROVAL_WEBHOOK_URL None (required) n8n webhook endpoint for approval requests
RP_SERVER_PUBLIC_URL https://rp.monxas.casa Public URL for callback endpoints
RP_APPROVAL_TTL_MINUTES 5 Approval token TTL before auto-reject

n8n Environment Variables

Variable Default Description
TELEGRAM_CHAT_ID_ADMINS None (required) Admin chat ID for approval messages
RP_SERVER_PUBLIC_URL https://rp.monxas.casa Server base URL for callbacks

Telegram Bot Setup (if creating new bot)

  1. Message @BotFather on Telegram
  2. /newbot
  3. Name: RemotePulse (display name)
  4. Username: RemotePulse_bot (must end in _bot)
  5. Copy token: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz
  6. Get chat ID:
    # Send a message to the bot, then:
    curl https://api.telegram.org/bot<TOKEN>/getUpdates
    # Extract "chat": {"id": 730947207}
    

Webhook Security

Defense Layers

  1. Single-use tokens: approval_token is UUID v4, cleared after first use
  2. TTL enforcement: Tokens expire 5min after approval_requested_at
  3. IP allowlist (TODO F5): n8n LXC 200 IP 192.168.0.200 allowlisted in server firewall
  4. HMAC signing (TODO F8): Optional HMAC-SHA256 signature on callbacks for cryptographic verification

IP Allowlist Configuration (Future)

# On server LXC 280
ufw allow from 192.168.0.200 to any port 8000 comment "n8n approval callbacks"

HMAC Signing (Future)

If paranoid, add HMAC verification:

  1. Generate shared secret:

    openssl rand -hex 32
    

  2. Configure in both server and n8n:

    # Server
    RP_APPROVAL_HMAC_SECRET=<secret>
    
    # n8n
    RP_APPROVAL_HMAC_SECRET=<secret>
    

  3. n8n signs callback payload; server verifies before accepting.

Troubleshooting

No Telegram message appears

Symptoms: Server logs "approval webhook delivered", but no Telegram message.

Checks: 1. n8n workflow active? Check UI toggle. 2. Telegram credentials valid? Test with /getMe API call. 3. Chat ID correct? Send test message manually. 4. n8n logs: docker logs n8n or journalctl -u n8n.

Fix:

# Test Telegram bot manually
curl -X POST "https://api.telegram.org/bot<TOKEN>/sendMessage" \
  -d "chat_id=730947207&text=Test"

Callback endpoint returns 404

Symptoms: Telegram button click, but server returns "Approval request not found".

Checks: 1. approval_token in database? Query:

SELECT id, approval_token, human_approved FROM commands WHERE approval_token IS NOT NULL;
2. Token already used? Check approval_token is NULL after first callback. 3. Token expired? Check approval_requested_at + 5min < now().

Fix: Re-issue command, click faster.

Callback endpoint returns 403 "expired"

Symptoms: Click approve after 5+ minutes.

Fix: Commands auto-reject after 5min. Re-issue command and approve faster.

Server webhook fails to reach n8n

Symptoms: Server logs "approval webhook failed".

Checks: 1. n8n reachable? curl http://192.168.0.200:5678/webhook/rp-approval 2. Firewall blocking? Check LXC 280 → LXC 200 connectivity. 3. Webhook URL correct in server config?

Fix:

# Test connectivity
ssh [email protected]
curl -v http://192.168.0.200:5678/webhook/rp-approval

Workflow executes but callback fails

Symptoms: Telegram message sent, button clicked, but server never receives callback.

Checks: 1. RP_SERVER_PUBLIC_URL correct in n8n? 2. Server API running? curl http://192.168.0.196:8080/health 3. n8n execution logs: Check "Call Server Endpoint" node output.

Fix: Update RP_SERVER_PUBLIC_URL in n8n environment, restart workflow.

Maintenance

Cleanup Expired Tokens

Server runs automatic cleanup every 1 minute (scheduled task):

# Auto-rejects commands with approval_token older than 5min
UPDATE commands
SET rejected_reason = 'approval_timeout',
    approval_token = NULL
WHERE approval_token IS NOT NULL
  AND approval_requested_at < NOW() - INTERVAL '5 minutes'
  AND human_approved = FALSE
  AND rejected_reason IS NULL;

Manual cleanup:

rp admin cleanup-expired-approvals

Monitoring

Prometheus metrics (TODO F5-5): - rp_approval_requested_total (Counter) - rp_approval_approved_total (Counter, label: approver_id) - rp_approval_rejected_total (Counter) - rp_approval_timeout_total (Counter)

Grafana alert (TODO F5): - rp_approval_timeout_total > 3 in 1h → warning ("High approval timeout rate")

Audit Trail

All approval decisions logged in: 1. Postgres: commands table with approved_by and rejected_reason 2. Loki: Server structured logs with approval_token, command_id, approver_id

Query audit trail:

SELECT
  id,
  command_type,
  issued_by,
  approved_by,
  rejected_reason,
  approval_requested_at,
  approval_responded_at
FROM commands
WHERE approval_requested_at IS NOT NULL
ORDER BY approval_requested_at DESC
LIMIT 50;

Integration with Local Policy

Approval flow complements agent local policy enforcement (F4-5):

  1. Server-side: Command requires Telegram approval (per ADR-0008 table)
  2. Agent-side: Even with human_approved=true, agent still checks /etc/rp/allow-remote-exec flag

Example: exec_shell on prod host: - Server: requires Telegram approval → sends to n8n → admin approves → human_approved=true - Agent: receives signed command with human_approved=true → checks local flag /etc/rp/allow-remote-exec → if missing, rejects with local_policy_deny

Defense-in-depth: Server compromise + Telegram compromise still can't execute destructive commands on agents without local flags.

Future Enhancements

F5: Multi-admin approval

Support multiple admins with role-based approval: - TELEGRAM_CHAT_ID_ADMINS=730947207,987654321 (comma-separated) - n8n sends to group chat, first responder wins

F8: Audit log secondary sink

Replicate approval decisions to Loki for forensics:

logger.info("command_approved", command_id=..., approver=..., extra=...)

F8: Approval history dashboard

Grafana panel showing: - Approval rate by command_type - Median approval latency (time to click) - Top approvers by count

References

  • ADR-0008 §13: Telegram approval flow specification
  • F4-5 local-policy: Agent-side enforcement docs
  • F4-4 signing: Ed25519 command signing
  • n8n workflow: /Users/ramonkamibayashicarrera/homelab-infra/n8n/workflows/rp-approval-flow.json
  • Server router: monxas-remote-pulse/server/src/rp_server/routers/approvals.py