Problem Solved This guide shows you how to receive Rhombus webhooks on servers behind firewalls or NAT without requiring a public IP address or complex VPN setup.
When working with webhook integrations, it’s common to require a publicly accessible endpoint. However, some environments—particularly on-premises or secured networks—do not allow direct public IP exposure. This guide walks through how to use reverse SSH tunneling to expose a webhook listener running on a private server.
Use Cases
This method is ideal for various scenarios where direct public access isn’t available or desired:
Security Requirements Your server is behind a NAT/firewall and cannot have a public IP
Webhook Integration You need to receive webhook POST requests from Rhombus
Secure & Simple You want a secure way to forward traffic to your local webhook listener
Enterprise Networks Corporate firewalls prevent direct inbound connections
Architecture Overview
How It Works The reverse SSH tunnel connects the public relay back to your local server, forwarding external traffic securely to your webhook listener.
Components
Component Role Examples
Private Server Runs the webhook listener localhost:8080Public Relay Small public cloud instance EC2, Linode, DigitalOcean Webhook Sender Sends HTTP POST requests Rhombus Cloud Services
Step-by-Step Implementation
Provision a Public Relay Server
Set up a lightweight Linux server (e.g., Ubuntu) on a cloud provider like AWS, GCP, or DigitalOcean. Requirements:
Assign a public IP or domain name (e.g., relay.yourdomain.com)
Open inbound ports (80 or 443 ) for HTTP/HTTPS traffic
Minimal specs: 1 CPU, 512MB RAM is sufficient
AWS EC2
DigitalOcean
Google Cloud
# Example AWS EC2 instance setup
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--count 1 \
--instance-type t2.micro \
--key-name my-key-pair \
--security-groups my-security-group
# Example DigitalOcean droplet setup
doctl compute droplet create relay-server \
--size s-1vcpu-512mb-10gb \
--image ubuntu-20-04-x64 \
--region nyc3
# Example Google Cloud VM setup
gcloud compute instances create relay-server \
--zone=us-central1-a \
--machine-type=e2-micro \
--image-family=ubuntu-2004-lts \
--image-project=ubuntu-os-cloud
Configure SSH for Remote Tunneling
On the relay server , modify the SSH daemon configuration to allow remote port forwarding: sudo nano /etc/ssh/sshd_config
Ensure the following options are set: # Enable gateway ports for remote forwarding
GatewayPorts yes
# Allow TCP forwarding
AllowTcpForwarding yes
# Allow opening any port
PermitOpen any
Security Note These settings allow remote port forwarding. Only enable on dedicated relay servers and secure with proper firewall rules.
Restart the SSH service: sudo systemctl restart ssh
# Verify SSH is running
sudo systemctl status ssh
Set Up SSH Key Authentication
On your private server (where the webhook listener runs), generate SSH keys and copy them to the relay: # Generate SSH key pair
ssh-keygen -t rsa -b 4096 -C "[email protected] "
# Copy public key to relay server
ssh-copy-id -i ~/.ssh/id_rsa.pub user@ < RELAY_PUBLIC_I P >
Verify passwordless SSH access: # Test connection (should not prompt for password)
ssh user@ < RELAY_PUBLIC_I P >
Use a dedicated SSH key for the tunnel to make key rotation easier and improve security isolation.
Establish the Reverse SSH Tunnel
Run the following command on your private server : Basic Tunnel
Background Process
Persistent Tunnel
# Basic reverse SSH tunnel
ssh -R 80:localhost:8080 user@ < RELAY_PUBLIC_I P >
Explanation:
80 - External port exposed by the relay server
localhost:8080 - Your local webhook listener address
Connection stays active in foreground
# Run tunnel in background
ssh -Nf -R 80:localhost:8080 user@ < RELAY_PUBLIC_I P >
Options:
-N - Don’t execute remote commands
-f - Go to background after authentication
-R - Remote port forwarding
Persistent Tunnel with autossh
# Install autossh for persistent tunneling
sudo apt install autossh
# Run persistent tunnel
autossh -M 0 -Nf -R 80:localhost:8080 user@ < RELAY_PUBLIC_I P >
Benefits:
Auto-reconnects if connection drops
Built-in monitoring and recovery
Ideal for production environments
Test the Setup
Now test that your tunnel is working correctly: # 1. Start your webhook listener locally
# Example: Node.js server on port 8080
node webhook-server.js
# 2. Test from external source
curl -X POST http:// < RELAY_PUBLIC_I P > /webhook \
-H "Content-Type: application/json" \
-d '{"test": "webhook payload"}'
# 3. Check your local server logs for the request
Webhook URL for Rhombus: http://<RELAY_PUBLIC_IP>/your-webhook-endpoint
Success Indicator If you see the request in your local webhook listener logs, the tunnel is working correctly!
Security Enhancements
HTTPS with NGINX
For production environments, add HTTPS support:
NGINX Setup
SSL Certificate
Configuration
# Install NGINX on relay server
sudo apt update
sudo apt install nginx
# Create basic configuration
sudo nano /etc/nginx/sites-available/webhook-tunnel
# Install Certbot for Let's Encrypt
sudo apt install certbot python3-certbot-nginx
# Generate SSL certificate
sudo certbot --nginx -d your-domain.com
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / {
proxy_pass http://localhost:80;
proxy_set_header Host $ host ;
proxy_set_header X-Real-IP $ remote_addr ;
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
proxy_set_header X-Forwarded-Proto $ scheme ;
}
}
Security Best Practices
Key-Based Authentication:
Use key-based SSH authentication only
Disable password authentication
Rotate SSH keys regularly
# Disable password authentication
sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart ssh
Firewall Configuration:
Restrict traffic with firewall rules
Use fail2ban for SSH protection
Monitor tunnel connections
# Example firewall rules (UFW)
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Restrict Access:
Limit SSH access to specific IPs when possible
Use non-standard SSH ports
Enable two-factor authentication for SSH
# Allow SSH only from specific IP
sudo ufw allow from YOUR_IP to any port 22
Production Deployment
Systemd Service for Auto-Start
Create a systemd service to automatically start the tunnel on boot:
# Create service file
sudo nano /etc/systemd/system/webhook-tunnel.service
Systemd Service Configuration
[Unit]
Description =Webhook SSH Tunnel
After =network.target
[Service]
Type =simple
User =your-username
ExecStart =/usr/bin/autossh -M 0 -N -R 80:localhost:8080 [email protected]
Restart =always
RestartSec =5
Environment = "AUTOSSH_GATETIME=0"
Environment = "AUTOSSH_POLL=30"
[Install]
WantedBy =multi-user.target
# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable webhook-tunnel
sudo systemctl start webhook-tunnel
# Check status
sudo systemctl status webhook-tunnel
Monitoring and Logging
# Monitor tunnel status
ps aux | grep autossh
# Check tunnel logs
journalctl -u webhook-tunnel -f
# Test tunnel health
curl -s http:// < RELAY_PUBLIC_I P > /health || echo "Tunnel down"
Benefits of This Approach
Works Behind Firewalls Functions perfectly behind NAT or corporate firewalls without any inbound rules
No VPN Required Eliminates complex VPN setup and maintenance overhead
Easy Automation Simple to automate with autossh and systemd for production reliability
Enterprise Safe Fully outbound connection—safe for most enterprise network policies
Cost Effective Uses minimal resources on a small cloud instance ($5-10/month)
Highly Reliable Auto-reconnects and self-heals with proper configuration
Troubleshooting
Problem: Cannot establish SSH connection to relay serverSolutions: # Check SSH service status
sudo systemctl status ssh
# Verify firewall allows SSH
sudo ufw status
# Test with verbose logging
ssh -v user@ < RELAY_PUBLIC_I P >
# Check SSH configuration
sudo sshd -t
Problem: Tunnel established but webhook requests don’t reach local serverSolutions: # Check if port is bound on relay
ss -tlnp | grep :80
# Test local webhook server
curl localhost:8080/test
# Verify GatewayPorts setting
sudo grep GatewayPorts /etc/ssh/sshd_config
# Check relay server logs
sudo journalctl -u ssh -f
Problem: Port 80 already in use on relay serverSolutions: # Find what's using port 80
sudo lsof -i :80
# Use alternative port
ssh -R 8080:localhost:8080 user@ < RELAY_PUBLIC_I P >
# Stop conflicting service (e.g., Apache)
sudo systemctl stop apache2
Connection Drops Frequently
Problem: Tunnel disconnects regularlySolutions:
Use autossh instead of regular ssh
Add keep-alive settings to SSH config
Check network stability between servers
Increase timeout values
# Add to ~/.ssh/config
Host relay-server
HostName < RELAY_PUBLIC_I P >
ServerAliveInterval 60
ServerAliveCountMax 3
Example Webhook Implementations
// webhook-server.js
const express = require ( 'express' );
const app = express ();
app . use ( express . json ());
// Rhombus webhook endpoint
app . post ( '/rhombus-webhook' , ( req , res ) => {
console . log ( 'Received webhook:' , req . body );
// Process webhook payload
const { eventType , deviceId , timestamp } = req . body ;
// Your business logic here
console . log ( `Event: ${ eventType } from device ${ deviceId } at ${ timestamp } ` );
// Respond to acknowledge receipt
res . status ( 200 ). json ({ status: 'received' });
});
// Health check endpoint
app . get ( '/health' , ( req , res ) => {
res . status ( 200 ). json ({ status: 'healthy' });
});
const PORT = 8080 ;
app . listen ( PORT , () => {
console . log ( `Webhook server running on port ${ PORT } ` );
});
Installation: npm install express
node webhook-server.js
# webhook_server.py
from flask import Flask, request, jsonify
import logging
app = Flask( __name__ )
logging.basicConfig( level = logging. INFO )
@app.route ( '/rhombus-webhook' , methods = [ 'POST' ])
def webhook ():
data = request.get_json()
app.logger.info( f 'Received webhook: { data } ' )
# Process webhook payload
event_type = data.get( 'eventType' )
device_id = data.get( 'deviceId' )
timestamp = data.get( 'timestamp' )
# Your business logic here
app.logger.info( f 'Event: { event_type } from device { device_id } at { timestamp } ' )
# Respond to acknowledge receipt
return jsonify({ 'status' : 'received' }), 200
@app.route ( '/health' , methods = [ 'GET' ])
def health ():
return jsonify({ 'status' : 'healthy' }), 200
if __name__ == '__main__' :
app.run( host = 'localhost' , port = 8080 , debug = True )
Installation: pip install flask
python webhook_server.py
// WebhookListener.cs
using Microsoft . AspNetCore . Mvc ;
[ ApiController ]
[ Route ( "/" )]
public class WebhookController : ControllerBase
{
private readonly ILogger < WebhookController > _logger ;
public WebhookController ( ILogger < WebhookController > logger )
{
_logger = logger ;
}
[ HttpPost ( "rhombus-webhook" )]
public IActionResult ReceiveWebhook ([ FromBody ] dynamic payload )
{
_logger . LogInformation ( $"Received webhook: { payload }" );
// Process webhook payload
string eventType = payload . eventType ;
string deviceId = payload . deviceId ;
string timestamp = payload . timestamp ;
// Your business logic here
_logger . LogInformation ( $"Event: { eventType } from device { deviceId } at { timestamp }" );
// Respond to acknowledge receipt
return Ok ( new { status = "received" });
}
[ HttpGet ( "health" )]
public IActionResult Health ()
{
return Ok ( new { status = "healthy" });
}
}
Run: // webhook-server.go
package main
import (
" encoding/json "
" fmt "
" log "
" net/http "
)
type WebhookPayload struct {
EventType string `json:"eventType"`
DeviceID string `json:"deviceId"`
Timestamp string `json:"timestamp"`
}
func webhookHandler ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodPost {
http . Error ( w , "Method not allowed" , http . StatusMethodNotAllowed )
return
}
var payload WebhookPayload
err := json . NewDecoder ( r . Body ). Decode ( & payload )
if err != nil {
http . Error ( w , "Bad request" , http . StatusBadRequest )
return
}
log . Printf ( "Received webhook: %+v " , payload )
log . Printf ( "Event: %s from device %s at %s " ,
payload . EventType , payload . DeviceID , payload . Timestamp )
// Respond to acknowledge receipt
w . Header (). Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ). Encode ( map [ string ] string { "status" : "received" })
}
func healthHandler ( w http . ResponseWriter , r * http . Request ) {
w . Header (). Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ). Encode ( map [ string ] string { "status" : "healthy" })
}
func main () {
http . HandleFunc ( "/rhombus-webhook" , webhookHandler )
http . HandleFunc ( "/health" , healthHandler )
port := ":8080"
fmt . Printf ( "Webhook server running on port %s \n " , port )
log . Fatal ( http . ListenAndServe ( port , nil ))
}
Run:
Configuring Webhooks in Rhombus
Once your listener is running and tunnel is established:
Get Your Webhook URL
Your public webhook URL will be: http://<RELAY_PUBLIC_IP>/rhombus-webhook
or with HTTPS: https://your-domain.com/rhombus-webhook
Configure in Rhombus Console
Log in to Rhombus Console
Navigate to Settings → Third Party Integrations →Webhooks
Click Add Webhook
Enter your public webhook URL
Select the events you want to receive
Save configuration
Monitor Events
Watch your webhook server logs to see incoming events in real-time.
Next Steps
Success! You now have a secure, reliable way to receive Rhombus webhooks on your private infrastructure without exposing your servers to the internet or requiring complex VPN setups.
Additional Resources
Webhook Security : Always validate webhook signatures and use HTTPS in production
Rate Limiting : Implement rate limiting on your webhook endpoint to prevent abuse
Logging : Maintain comprehensive logs of webhook events for debugging and auditing
Monitoring : Set up monitoring and alerts for webhook delivery failures
For production deployments, consider implementing webhook signature verification to ensure webhooks are genuinely from Rhombus.