DEV Community

the bomber
the bomber

Posted on

Securing Webhooks Between Netlify and a Home Automation Server

Tired of random webhook spam hitting your home-automation server?

If you run an automation server at home to handle form submissions or data from a public-facing site like Netlify, you quickly realize that the open internet is loud. Anyone can scan your public IP or guess your webhook URL and flood your logs with garbage data—or worse, trigger unintended routines.

Running my own website and automation server for Dream Futures Tech here in Pompano Beach, Florida, I needed a way to ensure that incoming HTTP requests actually originated from my Netlify deployment and not an automated bot scraping the web.

In my judgment, the most reliable lightweight solution without reaching for heavy enterprise infrastructure is combining a SHA-256 HMAC signature with a strict request timestamp.

Here is how I implemented it using plain JavaScript on both ends.

Step 1: Generate the Signature on Netlify

When a user triggers an action on the site, a Netlify Function prepares the payload. Before sending it to my home server’s dynamic DNS endpoint, the function generates a cryptographic signature using a shared secret key stored in Netlify's environment variables.

We also attach the current Unix timestamp. This prevents replay attacks, where a malicious actor intercepts a legitimate payload and replays it hours later.

const crypto = require('crypto');

exports.handler = async function(event) {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  const secret = process.env.WEBHOOK_SECRET;
  const timestamp = Date.now().toString();
  const payload = event.body;

  // Create the HMAC signature
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(`${timestamp}.${payload}`);
  const signature = hmac.digest('hex');

  try {
    const response = await fetch('https://my-home-server.duckdns.org/webhook', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Signature': signature,
        'X-Timestamp': timestamp
      },
      body: payload
    });

    return { statusCode: 200, body: 'Webhook dispatched securely.' };
  } catch (error) {
    return { statusCode: 500, body: 'Dispatch failed.' };
  }
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Verify the Signature on the Home Server

On my home automation server running a lightweight Node.js and Express setup, the incoming request must pass two strict validation checks before any business logic executes.

First, it checks if the timestamp is older than five minutes. If it is, the request is dropped immediately. Second, it recalculates the HMAC using the shared secret and compares it against the incoming X-Signature header using crypto.timingSafeEqual to prevent timing attacks.

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.text({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-signature'];
  const timestamp = req.headers['x-timestamp'];
  const payload = req.body;

  if (!signature || !timestamp) {
    return res.status(401).send('Missing security headers.');
  }

  // Check timestamp drift (5 minute window)
  const fiveMinutes = 5 * 60 * 1000;
  if (Math.abs(Date.now() - parseInt(timestamp, 10)) > fiveMinutes) {
    return res.status(401).send('Timestamp expired.');
  }

  // Recalculate HMAC
  const secret = process.env.WEBHOOK_SECRET;
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(`${timestamp}.${payload}`);
  const expectedSignature = hmac.digest('hex');

  // Constant-time comparison
  const sigBuffer = Buffer.from(signature, 'hex');
  const expectedBuffer = Buffer.from(expectedSignature, 'hex');

  if (sigBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
    return res.status(403).send('Invalid signature.');
  }

  // Parse payload and run local automation
  const data = JSON.parse(payload);
  console.log('Valid webhook received:', data);

  res.status(200).send('Received');
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Why This Matters for Self-Hosted Setups

Exposing a home server to the internet always carries inherent risks. Relying solely on obscure routing paths or firewall rules is not enough because public DNS leaks happen. By forcing cryptographic verification with a rolling timestamp, you ensure that even if someone sniffs the webhook URL, they cannot forge valid payloads or replay old ones.

It takes less than an hour to set up, requires zero paid third-party tools, and instantly cleans up your server logs.

I build sites and automations for small businesses at dreamfuturestech.com.

Top comments (0)