feat: add IP address logging to webhooks for whitelisting
Implements comprehensive IP logging for webhook requests to enable IP whitelisting and security monitoring. Features: - Capture source IP from webhook requests (x-forwarded-for, x-real-ip) - Capture user agent for identification - Store in webhook_logs table - New API endpoint: GET /api/webhooks/ips - View unique IPs with request counts and statistics - Identify Autotask IPs for whitelisting Database Changes: - Added source_ip column (VARCHAR 45) to webhook_logs - Added user_agent column (TEXT) to webhook_logs - Added index on source_ip for efficient queries - Migration 005 for existing installations API Endpoints: - GET /api/webhooks/ips?hours=168&entityType=Tickets Returns unique IPs with: * Request counts (total, successful, failed) * First/last seen timestamps * Entity types accessed * User agent strings Use Cases: 1. Identify Autotask webhook IPs 2. Configure IP whitelist in nginx/Pangolin/Cloudflare 3. Monitor for unauthorized webhook attempts 4. Audit webhook sources 5. Detect IP changes from Autotask Security Benefits: - Enable IP whitelisting for webhook endpoint - Block unauthorized webhook attempts - Monitor for suspicious activity - Audit trail of webhook sources Documentation: - Complete IP whitelisting guide (WEBHOOK_IP_WHITELISTING.md) - Configuration examples for nginx, Pangolin, Cloudflare - Monitoring queries and best practices - Troubleshooting guide Files Modified: - migrations/004_webhook_support.sql - Added IP columns - migrations/005_add_webhook_ip_logging.sql - Migration for existing installs - lib/types/webhook.ts - Added IP fields to WebhookLog - lib/services/webhook-service.ts - Capture and log IPs - app/api/webhooks/autotask/route.ts - Extract IP from headers - app/api/webhooks/ips/route.ts - New IP viewing endpoint - docs/WEBHOOK_IP_WHITELISTING.md - Complete guide Next Steps: 1. Run migration (004 for new, 005 for existing) 2. Deploy updated code 3. Receive webhooks from Autotask 4. View IPs via /api/webhooks/ips 5. Configure IP whitelist in proxy/tunnel
This commit is contained in:
parent
8e83347138
commit
a093f8787c
7 changed files with 672 additions and 7 deletions
540
docs/WEBHOOK_IP_WHITELISTING.md
Normal file
540
docs/WEBHOOK_IP_WHITELISTING.md
Normal file
|
|
@ -0,0 +1,540 @@
|
|||
# Webhook IP Whitelisting Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to identify Autotask's webhook source IPs and configure IP whitelisting to secure your webhook endpoint.
|
||||
|
||||
---
|
||||
|
||||
## Why IP Whitelisting?
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Only allow webhooks from Autotask's servers
|
||||
- ✅ Block unauthorized webhook attempts
|
||||
- ✅ Additional security layer beyond HTTPS
|
||||
- ✅ Prevent abuse and DDoS attacks
|
||||
|
||||
**When to Use:**
|
||||
- Production environments
|
||||
- High-security requirements
|
||||
- After identifying Autotask's IP ranges
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Identify Autotask IPs
|
||||
|
||||
### **Method 1: View Webhook Logs (Recommended)**
|
||||
|
||||
After receiving a few webhooks from Autotask, check the logged IPs:
|
||||
|
||||
**Via API:**
|
||||
```bash
|
||||
curl https://your-domain.com/api/webhooks/ips?hours=168
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"ips": [
|
||||
{
|
||||
"ip": "52.1.2.3",
|
||||
"userAgent": "Autotask-Webhook/1.0",
|
||||
"requestCount": 150,
|
||||
"successfulCount": 148,
|
||||
"failedCount": 2,
|
||||
"firstSeen": "2026-01-20T10:00:00Z",
|
||||
"lastSeen": "2026-01-24T15:00:00Z",
|
||||
"entityTypes": ["Tickets", "Companies", "Tasks"]
|
||||
},
|
||||
{
|
||||
"ip": "52.5.6.7",
|
||||
"userAgent": "Autotask-Webhook/1.0",
|
||||
"requestCount": 75,
|
||||
"successfulCount": 75,
|
||||
"failedCount": 0,
|
||||
"firstSeen": "2026-01-22T08:00:00Z",
|
||||
"lastSeen": "2026-01-24T14:30:00Z",
|
||||
"entityTypes": ["TimeEntries", "Projects"]
|
||||
}
|
||||
],
|
||||
"count": 2,
|
||||
"period": "168 hours"
|
||||
}
|
||||
```
|
||||
|
||||
**Via Database:**
|
||||
```sql
|
||||
-- Get unique IPs with request counts
|
||||
SELECT
|
||||
source_ip,
|
||||
user_agent,
|
||||
COUNT(*) as request_count,
|
||||
MIN(received_at) as first_seen,
|
||||
MAX(received_at) as last_seen,
|
||||
COUNT(*) FILTER (WHERE status = 'processed') as successful,
|
||||
COUNT(*) FILTER (WHERE status = 'failed') as failed
|
||||
FROM webhook_logs
|
||||
WHERE received_at >= NOW() - INTERVAL '7 days'
|
||||
AND source_ip IS NOT NULL
|
||||
GROUP BY source_ip, user_agent
|
||||
ORDER BY request_count DESC;
|
||||
```
|
||||
|
||||
**Identify Autotask IPs:**
|
||||
- Look for IPs with `Autotask-Webhook` user agent
|
||||
- High request counts (if you have active webhooks)
|
||||
- Multiple entity types
|
||||
- Consistent activity pattern
|
||||
|
||||
### **Method 2: Contact Autotask Support**
|
||||
|
||||
Request official IP ranges from Autotask:
|
||||
- Open support ticket
|
||||
- Ask for "Webhook source IP ranges"
|
||||
- They may provide CIDR blocks (e.g., `52.1.0.0/16`)
|
||||
|
||||
### **Method 3: Check Autotask Documentation**
|
||||
|
||||
Some Autotask zones publish their IP ranges:
|
||||
- Check Autotask developer documentation
|
||||
- Look for "Webhook IP ranges" or "API IP addresses"
|
||||
- May vary by zone (ww1, ww5, ww15, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Configure IP Whitelist
|
||||
|
||||
### **Option A: Nginx**
|
||||
|
||||
Edit your nginx configuration:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name webhooks.yourdomain.com;
|
||||
|
||||
# ... SSL configuration ...
|
||||
|
||||
location = /api/webhooks/autotask {
|
||||
# IP Whitelist - Autotask webhook IPs
|
||||
allow 52.1.2.3; # Autotask IP 1
|
||||
allow 52.5.6.7; # Autotask IP 2
|
||||
allow 52.10.0.0/16; # Autotask IP range (CIDR)
|
||||
deny all; # Block everything else
|
||||
|
||||
# Rate limiting
|
||||
limit_req zone=webhook_limit burst=20 nodelay;
|
||||
|
||||
# Proxy to Pulse app
|
||||
proxy_pass http://pulse_app;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Test and reload:**
|
||||
```bash
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### **Option B: Pangolin**
|
||||
|
||||
Update your Pangolin configuration:
|
||||
|
||||
```yaml
|
||||
# /etc/pangolin/config.yml
|
||||
|
||||
name: pulse-webhooks
|
||||
|
||||
ingress:
|
||||
- hostname: webhooks.yourdomain.com
|
||||
path: /api/webhooks/autotask
|
||||
service: http://localhost:3100
|
||||
|
||||
security:
|
||||
# IP Whitelist
|
||||
ipWhitelist:
|
||||
- "52.1.2.3" # Autotask IP 1
|
||||
- "52.5.6.7" # Autotask IP 2
|
||||
- "52.10.0.0/16" # Autotask IP range
|
||||
|
||||
# Rate limiting
|
||||
rateLimit:
|
||||
enabled: true
|
||||
requestsPerMinute: 100
|
||||
burstSize: 20
|
||||
```
|
||||
|
||||
**Restart tunnel:**
|
||||
```bash
|
||||
sudo systemctl restart pangolin-tunnel
|
||||
```
|
||||
|
||||
### **Option C: Cloudflare Tunnel**
|
||||
|
||||
Use Cloudflare Access rules:
|
||||
|
||||
1. **Go to Cloudflare Dashboard** → Zero Trust → Access → Applications
|
||||
2. **Create Application:**
|
||||
- Name: `Pulse Webhooks`
|
||||
- Domain: `webhooks.yourdomain.com`
|
||||
- Path: `/api/webhooks/autotask`
|
||||
3. **Add Policy:**
|
||||
- Name: `Autotask IPs Only`
|
||||
- Action: `Allow`
|
||||
- Rule: `IP ranges`
|
||||
- Values: `52.1.2.3`, `52.5.6.7`, `52.10.0.0/16`
|
||||
4. **Save**
|
||||
|
||||
### **Option D: Firewall (UFW)**
|
||||
|
||||
If using UFW on the server:
|
||||
|
||||
```bash
|
||||
# Allow specific IPs to port 443
|
||||
sudo ufw allow from 52.1.2.3 to any port 443
|
||||
sudo ufw allow from 52.5.6.7 to any port 443
|
||||
sudo ufw allow from 52.10.0.0/16 to any port 443
|
||||
|
||||
# Deny all other traffic to port 443 (if not already blocked)
|
||||
# Be careful with this - ensure you have other access methods!
|
||||
# sudo ufw deny 443
|
||||
|
||||
# Check rules
|
||||
sudo ufw status numbered
|
||||
```
|
||||
|
||||
**Warning:** Be careful with firewall rules. Ensure you don't lock yourself out!
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Test IP Whitelist
|
||||
|
||||
### **1. Test from Allowed IP (Autotask)**
|
||||
|
||||
Trigger a webhook from Autotask:
|
||||
- Create a test ticket or company
|
||||
- Check webhook logs to verify it was received
|
||||
|
||||
```bash
|
||||
# Check recent webhooks
|
||||
curl https://your-domain.com/api/webhooks/logs?limit=5
|
||||
```
|
||||
|
||||
### **2. Test from Blocked IP**
|
||||
|
||||
From a different IP (your computer):
|
||||
|
||||
```bash
|
||||
curl -X POST https://webhooks.yourdomain.com/api/webhooks/autotask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"eventId":"test","eventType":"create","entityType":"Tickets","entityId":1}'
|
||||
```
|
||||
|
||||
**Expected result:**
|
||||
- **Nginx:** `403 Forbidden`
|
||||
- **Pangolin/Cloudflare:** Connection refused or 403
|
||||
- **Firewall:** Connection timeout
|
||||
|
||||
### **3. Verify Logs**
|
||||
|
||||
Check that only Autotask IPs are getting through:
|
||||
|
||||
```sql
|
||||
-- Recent webhook IPs
|
||||
SELECT source_ip, COUNT(*) as count
|
||||
FROM webhook_logs
|
||||
WHERE received_at >= NOW() - INTERVAL '1 hour'
|
||||
GROUP BY source_ip;
|
||||
```
|
||||
|
||||
Should only show Autotask IPs.
|
||||
|
||||
---
|
||||
|
||||
## Monitoring IP Changes
|
||||
|
||||
### **Set Up Alerts for New IPs**
|
||||
|
||||
Create a monitoring query to detect new source IPs:
|
||||
|
||||
```sql
|
||||
-- Find IPs seen in last 24 hours that weren't seen before
|
||||
SELECT DISTINCT wl.source_ip, wl.user_agent, MIN(wl.received_at) as first_seen
|
||||
FROM webhook_logs wl
|
||||
WHERE wl.received_at >= NOW() - INTERVAL '24 hours'
|
||||
AND wl.source_ip NOT IN (
|
||||
SELECT DISTINCT source_ip
|
||||
FROM webhook_logs
|
||||
WHERE received_at < NOW() - INTERVAL '24 hours'
|
||||
AND received_at >= NOW() - INTERVAL '30 days'
|
||||
)
|
||||
GROUP BY wl.source_ip, wl.user_agent;
|
||||
```
|
||||
|
||||
### **Regular IP Audit**
|
||||
|
||||
Run weekly to check for IP changes:
|
||||
|
||||
```bash
|
||||
# Get IPs from last 7 days
|
||||
curl https://your-domain.com/api/webhooks/ips?hours=168 > webhook-ips-$(date +%Y%m%d).json
|
||||
|
||||
# Compare with previous week
|
||||
diff webhook-ips-20260117.json webhook-ips-20260124.json
|
||||
```
|
||||
|
||||
If new IPs appear:
|
||||
1. Verify they're legitimate Autotask IPs
|
||||
2. Check user agent matches `Autotask-Webhook`
|
||||
3. Update whitelist configuration
|
||||
4. Reload/restart proxy or tunnel
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### **Webhooks Stopped Working After Whitelisting**
|
||||
|
||||
**Check 1: Verify Autotask IP**
|
||||
```sql
|
||||
-- Check recent failed webhooks
|
||||
SELECT source_ip, user_agent, error_message, received_at
|
||||
FROM webhook_logs
|
||||
WHERE status = 'failed'
|
||||
AND received_at >= NOW() - INTERVAL '1 hour'
|
||||
ORDER BY received_at DESC;
|
||||
```
|
||||
|
||||
If you see legitimate Autotask IPs being blocked, add them to whitelist.
|
||||
|
||||
**Check 2: Proxy Logs**
|
||||
|
||||
**Nginx:**
|
||||
```bash
|
||||
tail -f /var/log/nginx/pulse-webhooks-error.log | grep 403
|
||||
```
|
||||
|
||||
**Pangolin:**
|
||||
```bash
|
||||
sudo journalctl -u pangolin-tunnel -f | grep denied
|
||||
```
|
||||
|
||||
**Check 3: Test Without Whitelist**
|
||||
|
||||
Temporarily disable IP whitelist to confirm that's the issue:
|
||||
|
||||
**Nginx:** Comment out `allow`/`deny` lines and reload
|
||||
**Pangolin:** Remove `ipWhitelist` section and restart
|
||||
|
||||
If webhooks work without whitelist, the issue is IP configuration.
|
||||
|
||||
### **Autotask Changed IPs**
|
||||
|
||||
Autotask may change their webhook source IPs:
|
||||
|
||||
1. **Check webhook logs** for new IPs
|
||||
2. **Verify** they're legitimate (user agent, request pattern)
|
||||
3. **Update whitelist** with new IPs
|
||||
4. **Keep old IPs** for a few days (in case of rollback)
|
||||
5. **Remove old IPs** after confirming they're no longer used
|
||||
|
||||
### **Getting Blocked Yourself**
|
||||
|
||||
If you accidentally block legitimate traffic:
|
||||
|
||||
**Quick Fix (Nginx):**
|
||||
```bash
|
||||
# Edit config to remove IP whitelist
|
||||
sudo nano /etc/nginx/sites-available/pulse-webhooks
|
||||
|
||||
# Comment out the deny rules
|
||||
# deny all;
|
||||
|
||||
# Reload
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
**Quick Fix (Pangolin):**
|
||||
```bash
|
||||
# Edit config
|
||||
sudo nano /etc/pangolin/config.yml
|
||||
|
||||
# Remove or comment ipWhitelist section
|
||||
|
||||
# Restart
|
||||
sudo systemctl restart pangolin-tunnel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### **1. Use CIDR Blocks When Possible**
|
||||
|
||||
Instead of individual IPs:
|
||||
```nginx
|
||||
# Better
|
||||
allow 52.10.0.0/16;
|
||||
|
||||
# Less flexible
|
||||
allow 52.10.1.1;
|
||||
allow 52.10.1.2;
|
||||
allow 52.10.1.3;
|
||||
```
|
||||
|
||||
### **2. Document Your Whitelist**
|
||||
|
||||
Keep a record of:
|
||||
- Which IPs are whitelisted
|
||||
- When they were added
|
||||
- Source of information (Autotask support, logs, etc.)
|
||||
- Last verified date
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
# IP Whitelist Documentation
|
||||
# Last updated: 2026-01-24
|
||||
|
||||
ipWhitelist:
|
||||
- "52.1.2.3" # Added: 2026-01-20, Source: Webhook logs
|
||||
- "52.5.6.7" # Added: 2026-01-22, Source: Webhook logs
|
||||
- "52.10.0.0/16" # Added: 2026-01-24, Source: Autotask support ticket #12345
|
||||
```
|
||||
|
||||
### **3. Monitor Regularly**
|
||||
|
||||
- Weekly: Check for new IPs in logs
|
||||
- Monthly: Audit whitelist against active IPs
|
||||
- After Autotask updates: Verify webhooks still work
|
||||
|
||||
### **4. Combine with Other Security**
|
||||
|
||||
IP whitelisting is one layer. Also use:
|
||||
- ✅ HTTPS/TLS encryption
|
||||
- ✅ Rate limiting
|
||||
- ✅ Path-based access control
|
||||
- ✅ Request validation
|
||||
- ✅ Monitoring and alerting
|
||||
|
||||
### **5. Have a Rollback Plan**
|
||||
|
||||
Keep configuration backups:
|
||||
```bash
|
||||
# Backup before changes
|
||||
sudo cp /etc/nginx/sites-available/pulse-webhooks \
|
||||
/etc/nginx/sites-available/pulse-webhooks.backup-$(date +%Y%m%d)
|
||||
|
||||
# Or for Pangolin
|
||||
sudo cp /etc/pangolin/config.yml \
|
||||
/etc/pangolin/config.yml.backup-$(date +%Y%m%d)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### **GET /api/webhooks/ips**
|
||||
|
||||
Get unique source IPs from webhook logs.
|
||||
|
||||
**Parameters:**
|
||||
- `hours` (optional): Number of hours to look back (default: 168 = 7 days)
|
||||
- `entityType` (optional): Filter by entity type
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Last 7 days
|
||||
curl https://your-domain.com/api/webhooks/ips?hours=168
|
||||
|
||||
# Last 24 hours, tickets only
|
||||
curl https://your-domain.com/api/webhooks/ips?hours=24&entityType=Tickets
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"ips": [
|
||||
{
|
||||
"ip": "52.1.2.3",
|
||||
"userAgent": "Autotask-Webhook/1.0",
|
||||
"requestCount": 150,
|
||||
"successfulCount": 148,
|
||||
"failedCount": 2,
|
||||
"firstSeen": "2026-01-20T10:00:00Z",
|
||||
"lastSeen": "2026-01-24T15:00:00Z",
|
||||
"entityTypes": ["Tickets", "Companies"]
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
"period": "168 hours"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Queries
|
||||
|
||||
### **Get All Unique IPs**
|
||||
```sql
|
||||
SELECT DISTINCT source_ip, user_agent
|
||||
FROM webhook_logs
|
||||
WHERE source_ip IS NOT NULL
|
||||
ORDER BY source_ip;
|
||||
```
|
||||
|
||||
### **Get IP Activity Summary**
|
||||
```sql
|
||||
SELECT
|
||||
source_ip,
|
||||
COUNT(*) as total_requests,
|
||||
COUNT(DISTINCT entity_type) as entity_types_count,
|
||||
MIN(received_at) as first_seen,
|
||||
MAX(received_at) as last_seen,
|
||||
ROUND(AVG(processing_time_ms), 2) as avg_processing_ms
|
||||
FROM webhook_logs
|
||||
WHERE source_ip IS NOT NULL
|
||||
GROUP BY source_ip
|
||||
ORDER BY total_requests DESC;
|
||||
```
|
||||
|
||||
### **Detect Suspicious IPs**
|
||||
```sql
|
||||
-- IPs with high failure rates
|
||||
SELECT
|
||||
source_ip,
|
||||
user_agent,
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE status = 'failed') as failed,
|
||||
ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'failed') / COUNT(*), 2) as failure_rate
|
||||
FROM webhook_logs
|
||||
WHERE source_ip IS NOT NULL
|
||||
AND received_at >= NOW() - INTERVAL '24 hours'
|
||||
GROUP BY source_ip, user_agent
|
||||
HAVING COUNT(*) FILTER (WHERE status = 'failed') > 5
|
||||
ORDER BY failure_rate DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Steps to Enable IP Whitelisting:**
|
||||
|
||||
1. ✅ **Run migration** to add IP logging columns
|
||||
2. ✅ **Deploy updated code** with IP capture
|
||||
3. ✅ **Receive webhooks** from Autotask (let it run for a day)
|
||||
4. ✅ **Identify Autotask IPs** via `/api/webhooks/ips` endpoint
|
||||
5. ✅ **Configure whitelist** in your proxy/tunnel
|
||||
6. ✅ **Test** that webhooks still work
|
||||
7. ✅ **Monitor** for new IPs regularly
|
||||
|
||||
**Result:**
|
||||
- Only Autotask's IPs can send webhooks
|
||||
- Unauthorized requests are blocked
|
||||
- Additional security layer for your webhook endpoint
|
||||
- Logged IP data for audit and troubleshooting
|
||||
Loading…
Add table
Add a link
Reference in a new issue