Building a Shared Security Node on a Nano Linode (Without a Static IP)

Building a Shared Security Node on a Nano Linode (Without a Static IP)

I recently spun up a nano Linode to act as a single, shared security and compliance utility node for all my other infrastructure — a central CrowdSec engine protecting multiple sites, plus a lightweight consent-logging endpoint for GDPR/state privacy compliance. One box, one blocklist, one audit trail, instead of duplicating security tooling across every server.

The catch: this Linode doesn't have a static IP available on its plan, and it's not the only server it needs to talk to — some traffic comes from infrastructure I control, some comes from managed hosting (Pantheon) that I don't. Those two paths need two different solutions.

The setup

The node runs two things:

  • CrowdSec central engine + LAPI — the shared decision engine every other site's bouncer reports to and pulls blocklist decisions from
  • A tiny consent-logging API — SQLite-backed, records consent events from client sites for audit purposes

Everything else — bot detection logic, blocklist enforcement — happens on the individual sites via lightweight bouncers/agents. This box is the brain, not a proxy for traffic.

Step 1: VLAN for anything you control

If you own the other servers, don't route this traffic over the public internet at all. A private VLAN removes the IP problem entirely for that path.

  • In the Linode Cloud Manager, create a VLAN in the same region as the nano box and the other Linodes (VLANs don't span regions)
  • Attach the nano Linode's config profile to the VLAN — this adds a private interface (e.g. eth1)
  • Attach each other Linode's config profile to the same VLAN
  • Reboot every attached Linode — VLAN interfaces need a reboot to activate, config changes don't hot-apply
  • On the nano box, bind CrowdSec's LAPI to the private VLAN IP only, not 0.0.0.0:
# /etc/crowdsec/local_api_credentials.yaml
url: http://192.168.x.x:8080/
  • Point bouncer configs on your other Linodes at that private IP
  • Firewall the public interface to drop the LAPI port entirely — it should never be reachable from outside the VLAN

Done. No public exposure, no dependency on any IP that might change, for anything you control.

Step 2: DDNS for everything you don't control

Managed hosting (Pantheon, in my case) still needs a stable public hostname to reach the LAPI. With no static IP available, the fix is to keep the DNS record current automatically, and point everything at the hostname, never the raw IP.

  • Use the Linode DNS Manager API rather than a third-party DDNS service — one token, one thing to secure, stays inside infrastructure you already trust
  • Create an A record now (e.g. crowdsec.yourdomain.com) pointing at the current IP, and set its TTL to 300 seconds
  • Drop this script on the nano box and cron it every 5 minutes
#!/usr/bin/env bash
# update-dns.sh — updates a Linode DNS A record when the public IP changes

set -euo pipefail

LINODE_TOKEN="your-linode-api-token"
DOMAIN_ID="your-domain-id"        # linode-cli domains list
RECORD_ID="your-a-record-id"      # linode-cli domains records-list $DOMAIN_ID
LAST_IP_FILE="/var/local/last_public_ip"

CURRENT_IP=$(curl -s https://ifconfig.co)
LAST_IP=$(cat "$LAST_IP_FILE" 2>/dev/null || echo "")

if [ "$CURRENT_IP" != "$LAST_IP" ]; then
  curl -s -X PUT \
    -H "Authorization: Bearer $LINODE_TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"target\": \"$CURRENT_IP\"}" \
    "https://api.linode.com/v4/domains/$DOMAIN_ID/records/$RECORD_ID"

  echo "$CURRENT_IP" > "$LAST_IP_FILE"
  echo "$(date): IP changed to $CURRENT_IP, DNS updated"
else
  echo "$(date): IP unchanged ($CURRENT_IP)"
fi
# crontab -e
*/5 * * * * /usr/local/bin/update-dns.sh >> /var/log/update-dns.log 2>&1
  • Point Pantheon-side bouncer configs at crowdsec.yourdomain.com, never a raw IP — this is what makes the whole scheme resilient
  • Add an external healthcheck (UptimeRobot's free tier is enough) pinging the LAPI port on that hostname — a silent DNS mismatch here means every client site quietly loses bot protection, and you want to know before they do

A note on TLS

Because certs get issued against the hostname, not the IP, Let's Encrypt renewals keep working through IP changes with no extra handling. Just make sure nothing in your renewal config has a hardcoded IP address anywhere.

Why not just pay for a static IP?

I would have, but it wasn't offered on this plan/region — allocation constraints happen. DDNS-via-provider-API with a low TTL and an external healthcheck is a reasonable substitute once you accept it as the primary design rather than a stopgap: cron every 5 minutes, not hourly, and monitor it like the piece of security infrastructure it now is.

The result

  • Internal traffic (my own Linodes): private VLAN, no public exposure, no IP dependency at all
  • External traffic (Pantheon): stable hostname backed by a self-healing DNS record, monitored externally
  • One shared CrowdSec blocklist and one consent-audit log serving every property, instead of duplicated tooling on each box