Target Server Security Guide

This guide is for the developer who owns a target Linode behind *.eng.sttark.com, such as hvac.eng.sttark.com.

It is written so a human or AI agent can complete the server-side work without using this repository's setup scripts.

Goal

The target server must be reachable like this:

browser or API client -> machine broker -> target server over VPC

The target server must not be directly reachable from the public internet on its app ports.

Target Outcome

When this guide is complete:

  • https://hvac.eng.sttark.com is protected by broker-side auth
  • the HVAC app is only reachable from the broker over the private VPC
  • direct public access to the target app ports is blocked
  • SSH remains available for administration

Current Broker Values

Unless the broker owner tells you otherwise, use these values:

  • Broker public IP: 172.238.218.19
  • Broker VPC IP: 10.0.1.2
  • VPC subnet: 10.0.1.0/24
  • Proxy base domain: eng.sttark.com

What The Target Developer Must Deliver

You are responsible for:

  1. Putting your Linode in the same VPC as the broker.
  2. Making your application listen on a private address and port the broker can reach.
  3. Blocking direct public access to that application.
  4. Giving the broker owner the final private target URL, for example http://10.0.1.5:3000.
  5. Verifying the app works correctly when accessed through https://<subdomain>.eng.sttark.com.

Preconditions

Before you start, you need:

  • the Linode API token if you are using the API
  • the Linode ID of the target server
  • the config profile ID for the target server if modifying network interfaces by API
  • the broker owner to confirm the correct subdomain and target port
  • shell access to the target machine as root or another sudo-capable user

Step 1: Put The Linode On The Broker VPC

The target Linode must be on the same Linode VPC as the broker, with the VPC interface configured as the only interface in the config profile and NAT 1:1 enabled.

Do not keep a separate public interface alongside the VPC interface. That causes routing issues.

Required Linode network shape

Use a config equivalent to:

{
  "interfaces": [
    {
      "purpose": "vpc",
      "subnet_id": "<SUBNET_ID>",
      "ipv4": { "nat_1_1": "any" }
    }
  ],
  "helpers": { "network": true }
}

Manual API workflow

Power the Linode off before changing interfaces:

curl -X POST \
  -H "Authorization: Bearer $LINODE_API_KEY" \
  "https://api.linode.com/v4/linode/instances/<LINODE_ID>/shutdown"

List config profiles:

curl -s \
  -H "Authorization: Bearer $LINODE_API_KEY" \
  "https://api.linode.com/v4/linode/instances/<LINODE_ID>/configs" | jq

Update the chosen config profile:

curl -X PUT \
  -H "Authorization: Bearer $LINODE_API_KEY" \
  -H "Content-Type: application/json" \
  "https://api.linode.com/v4/linode/instances/<LINODE_ID>/configs/<CONFIG_ID>" \
  -d '{
    "interfaces": [
      {
        "purpose": "vpc",
        "subnet_id": <SUBNET_ID>,
        "ipv4": { "nat_1_1": "any" }
      }
    ],
    "helpers": { "network": true }
  }'

Boot the Linode again:

curl -X POST \
  -H "Authorization: Bearer $LINODE_API_KEY" \
  "https://api.linode.com/v4/linode/instances/<LINODE_ID>/boot"

Verify VPC assignment

On the Linode:

ip addr show eth0
ip route

You should see:

  • a private address in 10.0.1.0/24
  • normal outbound internet access through NAT 1:1

Step 2: Make The App Reachable On The Private Network

Your app must listen on a port that the broker can reach over the VPC.

Good examples:

  • http://10.0.1.5:3000
  • http://10.0.1.5:8080
  • http://127.0.0.1:3000 behind a local reverse proxy that listens on 10.0.1.5:80

Requirements

  • The app must respond on the target URL you plan to register.
  • If the app uses WebSockets, they must work through a reverse proxy.
  • The app should tolerate the external hostname hvac.eng.sttark.com.
  • The app should prefer relative URLs for assets and API calls.

Quick checks on the target host

Replace the port with your app port:

ss -ltnp | grep ':3000'
curl -I http://127.0.0.1:3000
curl -I http://10.0.1.5:3000

From the broker host, the broker owner should also be able to run:

curl -I http://10.0.1.5:3000

Step 3: Apply Host Firewall Rules On The Target

The target host firewall must be default-deny for inbound traffic.

Allow only:

  • 22/tcp from anywhere for SSH
  • all traffic from 10.0.1.0/24 so the broker and other VPC peers can reach the app

For Ubuntu or Debian with UFW:

sudo apt-get update
sudo apt-get install -y ufw fail2ban

sudo ufw --force reset
sudo ufw default deny incoming
sudo ufw default allow outgoing

sudo ufw allow 22/tcp comment 'Allow SSH'
sudo ufw allow from 10.0.1.0/24 comment 'Allow broker and VPC peers'

sudo ufw --force enable
sudo ufw status verbose

Recommended fail2ban setup:

sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
EOF

sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban --no-pager

Required firewall result

The server must behave like this:

  • ssh from the public internet works
  • curl http://<public-ip>:<app-port> fails from the public internet
  • curl http://<vpc-ip>:<app-port> works from the broker or another host on the VPC

Step 4: Apply Linode Cloud Firewall Rules

Host firewall is not enough. The Linode Cloud Firewall should also block direct public app access before traffic reaches the VM.

Create or update a firewall with these inbound rules:

  • allow 22/tcp from 0.0.0.0/0 and ::/0
  • allow 1-65535/tcp from 172.238.218.19/32
  • drop all other inbound traffic

Outbound policy can remain allow.

Example create call

curl -X POST \
  -H "Authorization: Bearer $LINODE_API_KEY" \
  -H "Content-Type: application/json" \
  "https://api.linode.com/v4/networking/firewalls" \
  -d '{
    "label": "sttark-target-firewall",
    "rules": {
      "inbound_policy": "DROP",
      "outbound_policy": "ACCEPT",
      "inbound": [
        {
          "label": "allow-ssh",
          "action": "ACCEPT",
          "protocol": "TCP",
          "ports": "22",
          "addresses": {
            "ipv4": ["0.0.0.0/0"],
            "ipv6": ["::/0"]
          }
        },
        {
          "label": "allow-broker",
          "action": "ACCEPT",
          "protocol": "TCP",
          "ports": "1-65535",
          "addresses": {
            "ipv4": ["172.238.218.19/32"]
          }
        }
      ],
      "outbound": []
    }
  }'

Attach it to the Linode:

curl -X POST \
  -H "Authorization: Bearer $LINODE_API_KEY" \
  -H "Content-Type: application/json" \
  "https://api.linode.com/v4/networking/firewalls/<FIREWALL_ID>/devices" \
  -d '{
    "type": "linode",
    "id": <LINODE_ID>
  }'

Step 5: Verify Broker Reachability Over VPC

From the target server:

ping -c 1 10.0.1.2

From the broker, the broker owner should verify your app endpoint:

curl -I http://<target-vpc-ip>:<target-port>

If this fails, do not register the proxy route yet.

Step 6: Hand Off The Final Target URL

When your server is ready, send the broker owner:

  • subdomain, for example hvac
  • target URL, for example http://10.0.1.5:3000
  • confirmation that public app ports are blocked
  • confirmation that broker reachability over VPC was tested

The broker owner will register a route like:

{
  "type": "register_proxy",
  "subdomain": "hvac",
  "target": "http://10.0.1.5:3000"
}

Step 7: Validate End-To-End Behavior

After the route is registered, verify all of these:

Browser path

  • visiting https://hvac.eng.sttark.com redirects to Google sign-in if you are not authenticated
  • after sign-in with a @sttark.com account, the HVAC app loads

Programmatic path

With a valid broker API key:

curl -I \
  -H "X-API-Key: <BROKER_API_KEY>" \
  https://hvac.eng.sttark.com/

Expected result:

  • the request succeeds
  • the app content comes from your HVAC server, not the broker health page

Public-direct path

From a machine outside the VPC:

curl -I http://<public-ip>:<app-port>
curl -I https://<public-ip>:<app-port>

Expected result:

  • both direct public checks fail or time out

App Compatibility Notes

Your application should assume it is behind a reverse proxy.

Required behaviors

  • respect Host, X-Forwarded-Host, and X-Forwarded-Proto
  • support HTTPS at the public edge even if the local target is plain HTTP
  • not hardcode the machine broker domain into app redirects
  • not require direct public exposure for static assets, APIs, or WebSockets

Common issues

  • absolute redirects to the wrong host
  • asset URLs pointing at a raw IP or localhost
  • WebSocket endpoints hardcoded to local addresses
  • CSRF or origin checks that do not allow https://hvac.eng.sttark.com

AI Agent Success Criteria

If you are giving this document to an AI agent, the task is complete only when all of these are true:

  • the Linode is on the broker VPC using a VPC-only interface with NAT 1:1
  • the target app is reachable on a private VPC URL
  • UFW or equivalent is default-deny, with only SSH and 10.0.1.0/24 allowed inbound
  • Linode Cloud Firewall is attached and only allows SSH plus broker-originated traffic
  • direct public access to the app port is blocked
  • the broker owner has the final target URL to register
  • end-to-end access through https://<subdomain>.eng.sttark.com works after registration

Troubleshooting

The server becomes unreachable after adding the VPC

Cause:

  • the config profile contains both public and vpc interfaces
  • or Network Helper is disabled

Fix:

  • power off the Linode
  • set the VPC interface as the only interface
  • enable Network Helper
  • boot the Linode again

The broker cannot reach the target over 10.0.1.x

Check:

  • both machines are in the same Linode region
  • both machines are in the same VPC
  • the target firewall allows 10.0.1.0/24
  • the app is actually listening on the expected port

The app loads by direct public IP

Your target is not secured yet. Fix one or both of:

  • host firewall rules
  • Linode Cloud Firewall rules

The app loads through the broker but assets or WebSockets fail

Fix the app to behave correctly behind a reverse proxy:

  • use the public hostname
  • use forwarded headers
  • avoid hardcoded internal URLs

Related Docs

  • docs/PROXY_INTEGRATION.md
  • docs/VPC_SETUP.md