Cherwood Corporation — Division 03

CHERWOOD
NETWORK
SOLUTIONS

The internal MSP division building and operating the automation that manages, backs up, and secures every other Cherwood property.

03
Devices managed
01
System live nightly
00
Shared credentials
04
Bugs root-caused

Build log

Done
VLAN 60 + MSP-scoped firewall policy

Isolated segment, narrow SSH+SNMP policies, explicit logged DENY backstop.

Done
Dedicated service account

svc-automation — independently revocable, no shared admin login. Also caught and fixed a real gap: it briefly shared a password hash with the admin account on the Cisco devices, closed with unique credentials.

Done
Netmiko/Cisco IOS SSH bug

12-year-old open-source compatibility bug, root-caused and fixed.

Done
Git version history + drift detection

Every pull committed, real changes flagged accurately.

Done
Live status dashboard

Internal Flask page, Trusted-VLAN-only access.

Done
Public deployment — repo, Pages, self-hosted site

Pushed to GitHub, GitHub Pages mirror enabled, self-hosted at this domain via the existing DMZ container and Cloudflare Tunnel. Two real deployment bugs found and fixed along the way — see below.

Done
Automated Topology Discovery

LLDP-based mapping across Cisco IOS and FortiOS, reconciled into one graph and rendered nightly via Graphviz. See the live diagram →

The real engineering story

Bug 01 — a 12-year-old, still-open Paramiko bug

Legacy Cisco IOS SSH servers don't strictly follow the public-key handshake spec — asking for the signature twice, even after it was already sent. Traced via debug-level analysis and a decade of GitHub issue history. Fixed with ios_compat_patch.py, not a workaround.

Bug 02 — false alerts, every single night

FortiGate re-encrypts fields with a random salt on every pull — real ciphertext, zero real change. Would have buried every genuine alert forever. Fixed with normalization before comparison, verified against an empty diff.

Bug 03 — the site silently served the wrong content

After deploying this site, the public URL intermittently showed Cherwood Health's page instead. DNS, Cloudflare's cache, the file on disk, and nginx's config all checked out individually correct — the real cause was localhost resolving to either 127.0.0.1 or ::1 depending on the connecting process, and this site's nginx block only listened on the IPv4 address. Whichever loopback path the tunnel happened to use decided which site got served. Fixed by adding listen [::]:80; alongside the existing listener — found the identical gap on a second, unrelated port serving tarunc.com shortly after.

Bug 04 — DNS filtering blocked the domain, twice, for different reasons

The FortiGate's DNS filter redirected this domain to its own block page. A brand-new throwaway test hostname hit the identical block, ruling out anything specific to this project's name. The category grid didn't show an age-based rule either — the exact trigger stayed unconfirmed. What's proven: static domain-filter allow entries fix it, but the GUI silently failed to save them twice; only a CLI write followed by a CLI read-back reliably worked.

Stack

PYTHON 3.11NETMIKOPARAMIKOGRAPHVIZGITFLASKCRONCISCO IOSFORTIOSNGINXCLOUDFLARE TUNNEL