<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nate Howard</title>
    <description>The latest articles on DEV Community by Nate Howard (@nhowardtli).</description>
    <link>https://dev.to/nhowardtli</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3838414%2F45a03c7e-ddbe-4832-862b-05df02382c4c.jpeg</url>
      <title>DEV Community: Nate Howard</title>
      <link>https://dev.to/nhowardtli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nhowardtli"/>
    <language>en</language>
    <item>
      <title>I Embedded Cryptographic Signatures Inside a BGP Daemon — Here’s Why Automated Networks Need This published</title>
      <dc:creator>Nate Howard</dc:creator>
      <pubDate>Sun, 22 Mar 2026 13:25:07 +0000</pubDate>
      <link>https://dev.to/nhowardtli/i-embedded-cryptographic-signatures-inside-a-bgp-daemon-heres-why-automated-networks-need-this-4n85</link>
      <guid>https://dev.to/nhowardtli/i-embedded-cryptographic-signatures-inside-a-bgp-daemon-heres-why-automated-networks-need-this-4n85</guid>
      <description>&lt;p&gt; The Problem Nobody’s Solving &lt;br&gt;
We’re automating production networks at an accelerating pace. Cisco, Fortinet, Juniper — everyone’s shipping programmable operations, intent-based&lt;br&gt;
networking, and closed-loop remediation. But there’s a fundamental question nobody has answered: &lt;br&gt;
How do you prove that what your automation reports about your network is actually true? &lt;/p&gt;

&lt;p&gt;BGPsec validates that a route came from where it claims. RPKI validates origin ASes. Neither one validates what a router actually did or what state it’s&lt;br&gt;
actually in. When an orchestration platform reports “BGP peer 10.1.1.1 is in Established state,” there’s no cryptographic proof behind that claim. The&lt;br&gt;
system could be working from cached data. It could be reading stale output. It could be compromised. &lt;br&gt;
This is the observation trust gap — and it gets dangerous fast when automated systems start making routing decisions based on unverified state. &lt;/p&gt;

&lt;p&gt;VIRP: Signing at the Source &lt;/p&gt;

&lt;p&gt;VIRP (Verified Infrastructure Response Protocol) solves this by moving cryptographic attestation inside the network daemon itself. Not beside it. Not&lt;br&gt;
after the fact. Inside the process space where BGP events fire. &lt;br&gt;
I wrote a VIRP module for FRRouting that hooks directly into bgpd’s finite state machine and RIB processing. When a peer transitions state, when a route&lt;br&gt;
is announced or withdrawn, when a best-path recalculation fires — the observation is HMAC-SHA256 signed in the same execution context, before it ever&lt;br&gt;
leaves the daemon. &lt;br&gt;
The architecture: &lt;br&gt;
┌─────────────────────────────────────────────────┐&lt;br&gt;
│ FRR bgpd process │&lt;br&gt;
│ │&lt;br&gt;
│ BGP FSM ──► peer_status_changed ──┐ │&lt;br&gt;
│ BGP FSM ──► peer_backward_trans ──┤ │&lt;br&gt;
│ RIB ──► bgp_process ─────────┤ bgp_virp │&lt;br&gt;
│ RIB ──► bgp_route_update ────┘ module │&lt;br&gt;
│ │ │&lt;br&gt;
│ HMAC-SHA256 sign │&lt;br&gt;
│ │ │&lt;br&gt;
└──────────────────┼───────────────────────────────┘&lt;br&gt;
│ Unix socket&lt;br&gt;
▼&lt;br&gt;
┌──────────────┐&lt;br&gt;
│ O-Node │&lt;br&gt;
│ chain.db │&lt;br&gt;
└──────────────┘&lt;/p&gt;

&lt;p&gt;Four hooks. Four observation types. Each one signed at collection time with a key that no external system touches. &lt;br&gt;
What Gets Signed — and Why It Matters &lt;br&gt;
Every observation carries a trust tier assigned at the source, not at analysis time:Hook Obs Type Tier Data Capturedpeer_status_changed Type 1 GREEN Peer IP/AS, old→new FSM state, router-idpeer_backward_transition Type 2 YELLOW Peer dropped from Established — automatic alert gradebgp_route_update Type 4 GREEN Old/new best-path with full attrs (AS path, NH, MED, LP)bgp_process Type 3 GREEN Prefix announce/withdraw, peer, AFI/SAFI &lt;br&gt;
Notice that peer_backward_transition automatically gets YELLOW tier. When a BGP peer drops from Established, that’s not normal operations —&lt;br&gt;
the observation itself carries the severity classification. Any consumer of VIRP observations doesn’t get to decide whether a peer loss is important. The&lt;br&gt;
daemon already told it. &lt;br&gt;
The trust tiers follow a four-level model: &lt;br&gt;
GREEN — Normal operations. Observation verified, no anomalies.&lt;br&gt;
YELLOW — Elevated concern. Something happened that warrants attention.RED — Active threat or failure. Human approval required before action.&lt;br&gt;
BLACK — Observation channel compromised. Full stop. &lt;br&gt;
The Wire Format &lt;br&gt;
Every observation hits the wire in a compact binary format designed for speed and unambiguous parsing: &lt;br&gt;
Bytes 0-3: Magic (0x56495250 — "VIRP" in ASCII)&lt;br&gt;
Byte 4: Version (1)&lt;br&gt;
Byte 5: Observation type (1-4)&lt;br&gt;
Byte 6: Trust tier (0=GREEN, 1=YELLOW, 2=RED, 3=BLACK)&lt;br&gt;
Byte 7: Reserved&lt;br&gt;
Bytes 8-15: Timestamp (nanosecond epoch)&lt;br&gt;
Bytes 16-19: Sequence number&lt;br&gt;
Bytes 20-23: Payload length&lt;br&gt;
Bytes 24-N: JSON payload (variable)&lt;br&gt;
Bytes N-N+32: HMAC-SHA256 (covers bytes 0 through end of payload)&lt;/p&gt;

&lt;p&gt;24-byte fixed header. Variable JSON payload. 32-byte HMAC covering everything from the magic bytes through the last byte of payload. Any modification&lt;br&gt;
— a changed timestamp, a flipped bit in the tier field, a rewritten payload — invalidates the signature. &lt;br&gt;
The magic bytes spelling “VIRP” in hex (0x56495250) aren’t just aesthetic. They let any receiver immediately reject non-VIRP traffic on shared sockets&lt;br&gt;
without parsing further. &lt;br&gt;
Building It: Step by Step &lt;br&gt;
Prerequisites &lt;br&gt;
You need an FRR installation (or source tree), OpenSSL development headers, and a Unix-like environment. The VIRP module compiles as either a&lt;br&gt;
loadable module or linked directly into bgpd. &lt;br&gt;
Step 1: Validate Signing Logic (Standalone) &lt;br&gt;
Before touching FRR, confirm the cryptographic primitives work in isolation: &lt;/p&gt;

&lt;h1&gt;
  
  
  Compile the standalone test harness — no FRR dependency
&lt;/h1&gt;

&lt;p&gt;gcc -o virp_bgp_test virp_bgp_test.c -lssl -lcrypto -Wall -O2&lt;br&gt;
 # Generate a test key&lt;br&gt;
echo -n "your-hmac-key-here" &amp;gt; /tmp/virp-test.key&lt;br&gt;
 # Run it&lt;br&gt;
./virp_bgp_test --key /tmp/virp-test.key&lt;/p&gt;

&lt;p&gt;Expected output: 5/5 PASS — signing, verification, tamper detection, sequence ordering, tier assignment. If any test fails here, you have an OpenSSL&lt;br&gt;
issue, not a VIRP issue. &lt;br&gt;
Step 2: Add to FRR Build System &lt;br&gt;
Option A: Loadable Module (Recommended) &lt;br&gt;
Add to bgpd/subdir.am: &lt;/p&gt;

&lt;h1&gt;
  
  
  VIRP observation module
&lt;/h1&gt;

&lt;p&gt;if BGPD&lt;br&gt;
module_LTLIBRARIES += bgpd/bgpd_virp.la&lt;br&gt;
endif&lt;br&gt;
 bgpd_bgpd_virp_la_SOURCES = bgpd/bgp_virp.c&lt;br&gt;
bgpd_bgpd_virp_la_LIBADD = -lssl -lcrypto&lt;br&gt;
bgpd_bgpd_virp_la_LDFLAGS = -avoid-version -module -shared -export-dynamic&lt;/p&gt;

&lt;p&gt;Then rebuild FRR: &lt;br&gt;
cd /path/to/frr&lt;br&gt;
./bootstrap.sh&lt;br&gt;
./configure --enable-bgp-virp&lt;br&gt;
make -j$(nproc)&lt;br&gt;
sudo make install&lt;/p&gt;

&lt;p&gt;Option B: Compile Into bgpdAdd bgpd/bgp_virp.c to bgpd_bgpd_SOURCES and -lssl -lcrypto to bgpd_bgpd_LDADD in bgpd/subdir.am. &lt;br&gt;
Step 3: Key Management &lt;br&gt;
VIRP uses channel-separated key binding — the Observation Key (O-Key) used for signing is completely separate from any key used for intent execution.&lt;br&gt;
A compromised management station cannot forge observations. &lt;/p&gt;

&lt;h1&gt;
  
  
  Generate HMAC key on the O-Node (your trust anchor)
&lt;/h1&gt;

&lt;p&gt;openssl rand -hex 32 &amp;gt; /etc/virp/bgp.key&lt;br&gt;
chmod 600 /etc/virp/bgp.key&lt;br&gt;
 # Distribute to FRR host (same key, shared secret)&lt;br&gt;
scp /etc/virp/bgp.key frr-host:/etc/virp/bgp.key&lt;/p&gt;

&lt;p&gt;Step 4: Configure FRR &lt;br&gt;
Inside your BGP configuration: &lt;br&gt;
router bgp 65000&lt;br&gt;
virp hmac-key /etc/virp/bgp.key&lt;br&gt;
virp onode-socket /tmp/virp-onode.sock&lt;br&gt;
virp enable&lt;br&gt;
!&lt;/p&gt;

&lt;p&gt;Three lines. That’s the entire VIRP configuration on the router side. The module handles hook registration, signing, serialization, and socket transmission&lt;br&gt;
internally. &lt;br&gt;
Step 5: Verify &lt;br&gt;
frr# show bgp virp status&lt;br&gt;
 VIRP Module Status:&lt;br&gt;
State: ACTIVE&lt;br&gt;
HMAC Key: loaded (/etc/virp/bgp.key)&lt;br&gt;
O-Node: connected (/tmp/virp-onode.sock)&lt;br&gt;
Observations: 47 signed, 47 delivered, 0 failed&lt;br&gt;
 frr# show bgp virp statistics&lt;br&gt;
  Type 1 (peer_status): 12&lt;br&gt;
Type 2 (backward_trans): 2&lt;br&gt;
Type 3 (bgp_process): 18&lt;br&gt;
Type 4 (route_update): 15&lt;br&gt;
Signing errors: 0&lt;br&gt;
Socket errors: 0&lt;/p&gt;

&lt;p&gt;Step 6: Watch It Work &lt;br&gt;
On a neighbor router, shut and re-enable a BGP peer. Then watch the signed observations flow: &lt;/p&gt;

&lt;h1&gt;
  
  
  On the FRR host
&lt;/h1&gt;

&lt;p&gt;journalctl -u frr -f | grep VIRP&lt;br&gt;
 # You'll see:&lt;/p&gt;

&lt;h1&gt;
  
  
  VIRP[1]: type=1 tier=0 peer=10.1.1.2 AS=65001 state=Established
&lt;/h1&gt;

&lt;h1&gt;
  
  
  VIRP[2]: type=2 tier=1 peer=10.1.1.2 AS=65001 BACKWARD_TRANSITION
&lt;/h1&gt;

&lt;h1&gt;
  
  
  VIRP[3]: type=4 tier=0 prefix=10.0.0.0/24 bestpath_changed=yes
&lt;/h1&gt;

&lt;p&gt;Type 2 with tier=1 — that’s the backward transition. Automatically YELLOW. The daemon classified it before any external system touched it. &lt;br&gt;
The O-Node: Your Trust Anchor &lt;br&gt;
The O-Node is a separate process (ideally on a separate host or container) that receives, verifies, and chains observations. In my production deployment,&lt;br&gt;
the O-Node runs on an isolated LXC container with its own IP, its own SQLite chain database, and no automation code anywhere on it. &lt;br&gt;
Here’s a minimal receiver for testing: &lt;/p&gt;

&lt;h1&gt;
  
  
  !/usr/bin/env python3
&lt;/h1&gt;

&lt;p&gt;"""Minimal VIRP O-Node receiver for testing."""&lt;br&gt;
import socket, struct, hmac, hashlib, json, os&lt;br&gt;
 SOCK_PATH = "/tmp/virp-onode.sock"KEY_PATH = "/etc/virp/bgp.key"&lt;br&gt;
HDR_SIZE = 24&lt;br&gt;
HMAC_LEN = 32&lt;br&gt;
 TIERS = {0: "GREEN", 1: "YELLOW", 2: "RED", 3: "BLACK"}&lt;br&gt;
 def main():&lt;br&gt;
with open(KEY_PATH) as f:&lt;br&gt;
key = f.read().strip().encode()&lt;br&gt;
  if os.path.exists(SOCK_PATH):&lt;br&gt;
os.unlink(SOCK_PATH)&lt;br&gt;
  sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)&lt;br&gt;
sock.bind(SOCK_PATH)&lt;br&gt;
sock.listen(5)&lt;br&gt;
print(f"O-Node listening on {SOCK_PATH}")&lt;br&gt;
  while True:&lt;br&gt;
conn, _ = sock.accept()&lt;br&gt;
try:&lt;br&gt;
header = conn.recv(HDR_SIZE)&lt;br&gt;
if len(header) &amp;lt; HDR_SIZE:&lt;br&gt;
continue&lt;br&gt;
  magic = struct.unpack('!I', header[0:4])[0]&lt;br&gt;
if magic != 0x56495250:&lt;br&gt;
continue&lt;br&gt;
  obs_type = header[5]&lt;br&gt;
trust_tier = header[6]&lt;br&gt;
sequence = struct.unpack('I', header[16:20])[0]&lt;br&gt;
payload_len = struct.unpack('I', header[20:24])[0]&lt;br&gt;
  remaining = conn.recv(payload_len + HMAC_LEN)&lt;br&gt;
payload = remaining[:payload_len]&lt;br&gt;
received_hmac = remaining[payload_len:payload_len + HMAC_LEN]&lt;br&gt;
  computed = hmac.new(key, header + payload, hashlib.sha256).digest()&lt;br&gt;
valid = hmac.compare_digest(computed, received_hmac)&lt;br&gt;
  status = "VERIFIED" if valid else "** INVALID **"&lt;br&gt;
print(f"[{status}] seq={sequence} type={obs_type} "&lt;br&gt;
f"tier={TIERS.get(trust_tier, '?')} "&lt;br&gt;
f"payload={payload.decode()}")&lt;br&gt;
except Exception as e:&lt;br&gt;
print(f"Error: {e}")&lt;br&gt;
finally:&lt;br&gt;
conn.close()&lt;br&gt;
 if &lt;strong&gt;name&lt;/strong&gt; == "&lt;strong&gt;main&lt;/strong&gt;":&lt;br&gt;
main()&lt;/p&gt;

&lt;p&gt;In production, the O-Node writes verified observations to chain.db (SQLite), forming a cryptographic audit trail. Each observation references the&lt;br&gt;
previous one’s hash, creating a tamper-evident chain. If someone deletes or modifies a historical observation, the chain breaks and every subsequent&lt;br&gt;
entry becomes suspect. &lt;br&gt;
Why This Isn’t Just Another Monitoring Tool &lt;br&gt;
This is the part most people miss on first read. VIRP isn’t collecting telemetry. It’s creating evidence. &lt;br&gt;
When an automation platform consumes VIRP observations, it’s not reading SNMP counters or parsing CLI output that it generated itself. It’s receiving&lt;br&gt;
cryptographically signed attestations from inside the daemon — signed with a key no external system holds. The automation cannot fabricate an&lt;br&gt;
observation. It cannot backdate one. It cannot upgrade a YELLOW to a GREEN. &lt;br&gt;
This creates what I call evidence-gated operations: any system consuming observations can only act on what it can prove. If it wants to claim a peer is&lt;br&gt;
down, there must be a signed Type 2 observation in the chain. If there isn’t, the system refuses the conclusion. No evidence, no claim. &lt;br&gt;
I tested this by running autonomous remediation workflows against VIRP-protected infrastructure. The orchestration layer identified 10 different bypass&lt;br&gt;
vectors and attempted to rewrite source code, recompile binaries, and set immutable flags — all without approval. VIRP’s trust tier system caught every&lt;br&gt;
attempt. RED-tier actions couldn’t execute without human approval, and the system couldn’t forge the observations needed to justify GREEN-tier&lt;br&gt;
confidence.How This Differs from BGPsec and RPKIBGPsec / RPKI VIRPPrimary goal Route origin and path validation Device state and observation attestationTrigger UPDATE message received from peer Local event inside the daemonWhat’s signed Routing table entries, ROAs Device-level telemetry, FSM state transitionsAutomation role Passive filtering of invalid routes Active evidence chain for automated decision-makingPerformance Computationally expensive (BGPsec adoption bottleneck) HMAC-SHA256, hardware-accelerated on modern CPUs &lt;br&gt;
VIRP doesn’t replace BGPsec or RPKI. They validate the routing control plane. VIRP validates what the device itself is doing. They’re complementary —&lt;br&gt;
and both are necessary once automated systems start making routing decisions. &lt;br&gt;
The Universal Argument &lt;br&gt;
The wire format and signing mechanism are completely protocol-agnostic. The same 24-byte header, the same HMAC, the same O-Node receiver works&lt;br&gt;
for BGP, DNS, IPsec, or anything else that generates observable events. The only thing that changes per domain is the JSON payload schema and the&lt;br&gt;
hook points. &lt;br&gt;
I’ve already built collectors for Cisco IOS (SSH), FortiGate (SSH via C executor), and Palo Alto PAN-OS. The FRR integration goes further — it’s not&lt;br&gt;
collecting from the outside, it’s signing from the inside. But the observation format is identical across all of them. &lt;br&gt;
This is the core argument in the IETF RFC draft (draft-howard-virp-01, submitted to the RATS working group): VIRP is a domain-agnostic trust primitive for&lt;br&gt;
infrastructure operations. If it works for BGP, it works for DNS. If it works for DNS, it works for IoT telemetry. The protocol doesn’t care what it’s attesting&lt;br&gt;
— it cares that the attestation is cryptographically sound. &lt;br&gt;
What This Proves &lt;br&gt;
With this running on a 35-router GNS3 lab across 13 autonomous systems, the implementation demonstrates five things: &lt;br&gt;
VIRP signs at collection time. The HMAC happens inside bgpd’s process space, at the instant the event fires.&lt;br&gt;
Observation-only. The module registers read-only hooks. It cannot modify routes, peer state, or configuration. Pure attestation.&lt;br&gt;
Protocol-agnostic framing. The observation structure works identically whether the source is BGP, DNS, or IPsec.&lt;br&gt;
Trust tiers at the source. Backward transitions are automatically YELLOW. Normal operations are GREEN. Classification happens in the daemon,&lt;br&gt;
not in the consumer.&lt;br&gt;
Tamper detection. Any modification between bgpd and the O-Node is caught by HMAC verification.&lt;/p&gt;

&lt;p&gt;Try It Yourself &lt;br&gt;
The full implementation — bgp_virp.h, bgp_virp.c, standalone test harness, and this integration guide — is available on GitHub: &lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nhowardtli" rel="noopener noreferrer"&gt;
        nhowardtli
      &lt;/a&gt; / &lt;a href="https://github.com/nhowardtli/virp" rel="noopener noreferrer"&gt;
        virp
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      VIRP Protocol: Cryptographic Trust Primitives and Network Trust Anchor consumable by any system
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;
 

&lt;p&gt;Licensed under Apache 2.0. The RFC draft (draft-howard-virp-01) is under review with the IETF RATS working group. A DOI-registered version is archived&lt;br&gt;
on Zenodo for citation. &lt;/p&gt;

&lt;p&gt;If you’re building automated network operations and you don’t have a trust layer, you’re building on faith. VIRP replaces faith with evidence&lt;/p&gt;

</description>
      <category>security</category>
      <category>bgp</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
