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
|
|
@ -13,10 +13,16 @@ import { AutotaskWebhookPayload } from '@/lib/types/webhook';
|
|||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Extract source IP and user agent for logging
|
||||
const sourceIp = request.headers.get('x-forwarded-for')?.split(',')[0].trim()
|
||||
|| request.headers.get('x-real-ip')
|
||||
|| 'unknown';
|
||||
const userAgent = request.headers.get('user-agent') || 'unknown';
|
||||
|
||||
// Parse webhook payload
|
||||
const payload: AutotaskWebhookPayload = await request.json();
|
||||
|
||||
console.log(`[WEBHOOK API] Received ${payload.eventType} event for ${payload.entityType} #${payload.entityId}`);
|
||||
console.log(`[WEBHOOK API] Received ${payload.eventType} event for ${payload.entityType} #${payload.entityId} from IP: ${sourceIp}`);
|
||||
|
||||
// Validate required fields
|
||||
if (!payload.eventId || !payload.eventType || !payload.entityType || !payload.entityId) {
|
||||
|
|
@ -29,7 +35,7 @@ export async function POST(request: NextRequest) {
|
|||
// Process the webhook asynchronously
|
||||
// Note: We return 200 immediately to Autotask, then process in background
|
||||
// This prevents timeouts for slow processing
|
||||
const result = await webhookService.processWebhook(payload);
|
||||
const result = await webhookService.processWebhook(payload, sourceIp, userAgent);
|
||||
|
||||
if (result.success) {
|
||||
return NextResponse.json({
|
||||
|
|
|
|||
77
app/api/webhooks/ips/route.ts
Normal file
77
app/api/webhooks/ips/route.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Webhook Source IPs API
|
||||
* View unique source IPs from webhook logs for whitelisting
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { postgresClient } from '@/lib/services/postgres-client';
|
||||
|
||||
/**
|
||||
* GET /api/webhooks/ips
|
||||
* Get unique source IPs from webhook logs
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const hours = parseInt(searchParams.get('hours') || '168'); // Default 7 days
|
||||
const entityType = searchParams.get('entityType') || undefined;
|
||||
|
||||
// Build query to get unique IPs with counts
|
||||
let query = `
|
||||
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,
|
||||
COUNT(*) FILTER (WHERE status = 'failed') as failed_count,
|
||||
array_agg(DISTINCT entity_type) as entity_types
|
||||
FROM webhook_logs
|
||||
WHERE received_at >= NOW() - INTERVAL '${hours} hours'
|
||||
AND source_ip IS NOT NULL
|
||||
`;
|
||||
|
||||
const params: any[] = [];
|
||||
|
||||
if (entityType) {
|
||||
query += ` AND entity_type = $1`;
|
||||
params.push(entityType);
|
||||
}
|
||||
|
||||
query += `
|
||||
GROUP BY source_ip, user_agent
|
||||
ORDER BY request_count DESC
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, params);
|
||||
|
||||
// Format the results
|
||||
const ips = result.rows.map((row: any) => ({
|
||||
ip: row.source_ip,
|
||||
userAgent: row.user_agent,
|
||||
requestCount: parseInt(row.request_count),
|
||||
successfulCount: parseInt(row.successful_count),
|
||||
failedCount: parseInt(row.failed_count),
|
||||
firstSeen: row.first_seen,
|
||||
lastSeen: row.last_seen,
|
||||
entityTypes: row.entity_types,
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ips,
|
||||
count: ips.length,
|
||||
period: `${hours} hours`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('[WEBHOOK IPS API] Error fetching IPs:', errorMessage);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch webhook IPs', details: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
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
|
||||
|
|
@ -13,12 +13,12 @@ export class WebhookService {
|
|||
/**
|
||||
* Process an incoming webhook from Autotask
|
||||
*/
|
||||
async processWebhook(payload: AutotaskWebhookPayload): Promise<WebhookProcessingResult> {
|
||||
async processWebhook(payload: AutotaskWebhookPayload, sourceIp?: string, userAgent?: string): Promise<WebhookProcessingResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Log the webhook event
|
||||
await this.logWebhookEvent(payload, 'pending');
|
||||
await this.logWebhookEvent(payload, 'pending', sourceIp, userAgent);
|
||||
|
||||
console.log(`[WEBHOOK] Processing ${payload.eventType} event for ${payload.entityType} #${payload.entityId}`);
|
||||
|
||||
|
|
@ -169,10 +169,10 @@ export class WebhookService {
|
|||
/**
|
||||
* Log webhook event to database
|
||||
*/
|
||||
private async logWebhookEvent(payload: AutotaskWebhookPayload, status: 'pending' | 'processed' | 'failed'): Promise<void> {
|
||||
private async logWebhookEvent(payload: AutotaskWebhookPayload, status: 'pending' | 'processed' | 'failed', sourceIp?: string, userAgent?: string): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO webhook_logs (event_id, entity_type, entity_id, event_type, status, payload)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
INSERT INTO webhook_logs (event_id, entity_type, entity_id, event_type, status, source_ip, user_agent, payload)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (event_id) DO NOTHING
|
||||
`;
|
||||
|
||||
|
|
@ -182,6 +182,8 @@ export class WebhookService {
|
|||
payload.entityId,
|
||||
payload.eventType,
|
||||
status,
|
||||
sourceIp || null,
|
||||
userAgent || null,
|
||||
JSON.stringify(payload),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ export interface WebhookLog {
|
|||
event_type: WebhookEventType;
|
||||
status: 'pending' | 'processed' | 'failed';
|
||||
error_message?: string;
|
||||
source_ip?: string;
|
||||
user_agent?: string;
|
||||
received_at: Date;
|
||||
processed_at?: Date;
|
||||
processing_time_ms?: number;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ CREATE TABLE IF NOT EXISTS webhook_logs (
|
|||
event_type VARCHAR(50) NOT NULL, -- create, update, delete
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'pending', -- pending, processed, failed
|
||||
error_message TEXT,
|
||||
source_ip VARCHAR(45), -- IPv4 or IPv6 address
|
||||
user_agent TEXT, -- User agent string from request
|
||||
received_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
processed_at TIMESTAMP,
|
||||
processing_time_ms INTEGER,
|
||||
|
|
@ -36,6 +38,7 @@ CREATE INDEX IF NOT EXISTS idx_webhook_logs_entity ON webhook_logs(entity_type,
|
|||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_status ON webhook_logs(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_received_at ON webhook_logs(received_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_entity_type ON webhook_logs(entity_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_source_ip ON webhook_logs(source_ip);
|
||||
|
||||
-- Indexes for webhook_configs
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_configs_entity_type ON webhook_configs(entity_type);
|
||||
|
|
|
|||
35
migrations/005_add_webhook_ip_logging.sql
Normal file
35
migrations/005_add_webhook_ip_logging.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- Migration: Add IP logging to webhook_logs
|
||||
-- Description: Add source_ip and user_agent columns for IP whitelisting
|
||||
|
||||
-- Add source_ip column if it doesn't exist
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'webhook_logs' AND column_name = 'source_ip'
|
||||
) THEN
|
||||
ALTER TABLE webhook_logs ADD COLUMN source_ip VARCHAR(45);
|
||||
COMMENT ON COLUMN webhook_logs.source_ip IS 'Source IP address of webhook request (IPv4 or IPv6)';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add user_agent column if it doesn't exist
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'webhook_logs' AND column_name = 'user_agent'
|
||||
) THEN
|
||||
ALTER TABLE webhook_logs ADD COLUMN user_agent TEXT;
|
||||
COMMENT ON COLUMN webhook_logs.user_agent IS 'User agent string from webhook request';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add index on source_ip for efficient IP-based queries
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_source_ip ON webhook_logs(source_ip);
|
||||
|
||||
-- Verify columns were added
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'webhook_logs'
|
||||
AND column_name IN ('source_ip', 'user_agent');
|
||||
Loading…
Add table
Add a link
Reference in a new issue