<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Architecture on network-notes</title><link>https://blog2.network-notes.com/tags/architecture/</link><description>Recent content in Architecture on network-notes</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>brett@network-notes.com (Brett Lykins)</managingEditor><webMaster>brett@network-notes.com (Brett Lykins)</webMaster><copyright>© 2015-2026 Brett Lykins</copyright><lastBuildDate>Wed, 20 May 2026 10:00:00 -0500</lastBuildDate><atom:link href="https://blog2.network-notes.com/tags/architecture/feed.xml" rel="self" type="application/rss+xml"/><item><title>Stop Picking Tools, Start Picking Functions: The NAF Framework</title><link>https://blog2.network-notes.com/posts/2026/naf-framework/</link><pubDate>Wed, 20 May 2026 10:00:00 -0500</pubDate><author>brett@network-notes.com (Brett Lykins)</author><dc:creator>Brett Lykins</dc:creator><guid>https://blog2.network-notes.com/posts/2026/naf-framework/</guid><description>&lt;h2 id="the-tool-first-trap"&gt;The Tool-First Trap&lt;/h2&gt;
&lt;p&gt;Every network automation conversation I&amp;rsquo;ve been part of starts the same way: &amp;ldquo;Should we use Ansible or Nornir?
NetBox or Nautobot?
Terraform or Pulumi?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;These are the wrong first questions.
They&amp;rsquo;re implementation details masquerading as architecture decisions.
You end up picking a tool, building around it, then discovering six months later that you&amp;rsquo;ve solved 30% of the problem and created three new ones.&lt;/p&gt;
&lt;p&gt;The result is what Damien Garros describes as the &amp;ldquo;Frankenstack&amp;rdquo;: a pile of point tools stitched together with glue scripts, each solving a narrow problem but none composing into a coherent system.
I built these early in my career.
I spent years at Network to Code and OpsMill helping customers untangle them.
You&amp;rsquo;ve probably built or inherited one yourself.
They work until they don&amp;rsquo;t, and when they break, nobody can reason about the whole thing because there was never a whole thing to reason about.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s missing isn&amp;rsquo;t better tools.
It&amp;rsquo;s a shared vocabulary for the &lt;em&gt;functions&lt;/em&gt; your automation system needs to perform.
The &lt;a href="https://reference.networkautomation.forum/Framework/Framework/"&gt;NAF Reference Framework&lt;/a&gt; provides exactly that.&lt;/p&gt;
&lt;h2 id="the-six-building-blocks"&gt;The Six Building Blocks&lt;/h2&gt;
&lt;p&gt;The Network Automation Forum (NAF) published a reference architecture that breaks network automation into six functional building blocks.
It&amp;rsquo;s not a product.
It&amp;rsquo;s not a standard.
It&amp;rsquo;s a blueprint, a way to think about what your automation system needs to do before you decide how to do it.&lt;/p&gt;
&lt;p&gt;The six blocks answer four questions:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Building Block(s)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What do I want the network to look like?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Intent&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What does the network actually look like?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do I read from and write to the network?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Collector&lt;/strong&gt; (read) / &lt;strong&gt;Executor&lt;/strong&gt; (write)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do I coordinate all of this?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Orchestrator&lt;/strong&gt; / &lt;strong&gt;Presentation&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Here&amp;rsquo;s what each block does:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Intent&lt;/strong&gt; stores and manages the desired state of your network.
This is your source of truth: IP addressing, topology, service definitions, configuration templates, validation rules.
It exposes an API, supports CRUD operations, and should provide versioning and validation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Observability&lt;/strong&gt; stores and processes the &lt;em&gt;actual&lt;/em&gt; state.
It persists what the Collector retrieves, runs analytics against it, and generates events when actual state diverges from intended state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Orchestrator&lt;/strong&gt; coordinates workflows across the other blocks.
It doesn&amp;rsquo;t touch the network directly.
It responds to events, schedules tasks, chains operations together, and handles rollback when something fails.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Executor&lt;/strong&gt; pushes changes to the network. Configuration deploys, software upgrades, device reboots.
It speaks SSH, NETCONF, gNMI, REST, whatever the device supports.
Operations should be idempotent and support dry-run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Collector&lt;/strong&gt; pulls state from the network: show commands, SNMP polls, streaming telemetry, syslog, flow data.
It normalizes vendor-specific output into structured data that Observability can consume.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Presentation&lt;/strong&gt; is how humans (and external systems) interact with everything else. Dashboards, CLIs, ChatOps, ITSM integrations, API gateways.&lt;/p&gt;
&lt;p&gt;The architecture has a deliberate symmetry: the left side is the &lt;em&gt;read path&lt;/em&gt; (Observability and Collector reading state from infrastructure), the right side is the &lt;em&gt;write path&lt;/em&gt; (Intent and Executor pushing state to infrastructure), and the Orchestrator sits in the middle coordinating both.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog2.network-notes.com/img/2026/naf-building-blocks.32d88a56a88fad51b22df642aef555ebf1318ac46e51df804c4af5c5c5ca4756.svg" alt="NAF Reference Architecture - six building blocks arranged in four layers with read/write symmetry" loading="lazy" /&gt;&lt;/p&gt;
&lt;h2 id="why-functions-before-tools"&gt;Why Functions Before Tools&lt;/h2&gt;
&lt;p&gt;Thinking in building blocks separates &lt;em&gt;what you need&lt;/em&gt; from &lt;em&gt;how you implement it&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;One tool can fill multiple blocks.
Nautobot, for example, covers Intent (it&amp;rsquo;s a source of truth), Orchestrator (its Jobs framework coordinates workflows), and Presentation (it has a web UI and API).
That&amp;rsquo;s fine.
The framework doesn&amp;rsquo;t prescribe how many tools you use. What matters is that the functions are covered.&lt;/p&gt;
&lt;p&gt;Conversely, one block might need multiple tools.
Your Collector might be Telegraf for metrics, a streaming telemetry receiver for gNMI, and a custom script for legacy SNMP devices.
Three tools, one function.&lt;/p&gt;
&lt;p&gt;This is why starting with tools fails.
If you pick Ansible because someone on the team knows it, you&amp;rsquo;ve filled part of the Executor block and maybe part of the Orchestrator block.
But you haven&amp;rsquo;t thought about Intent, Observability, or how the pieces connect.
Six months later you&amp;rsquo;re writing a wrapper script that queries a spreadsheet (your accidental Intent block) and pipes it into an Ansible playbook (your Executor), with no Observability, no Collector feeding back actual state, and no Orchestrator handling failures.&lt;/p&gt;
&lt;p&gt;The framework makes these gaps visible before you start building.&lt;/p&gt;
&lt;h2 id="mapping-real-tools"&gt;Mapping Real Tools&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s how I map tools I&amp;rsquo;ve used to the framework.
This isn&amp;rsquo;t exhaustive. It&amp;rsquo;s meant to show how the mental model works in practice.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog2.network-notes.com/img/2026/naf-tool-mapping.5f8aacdaa71f304ce4e3e496264b73368db5ab29b07fd3a84aae2081c0394484.svg" alt="Tools mapped to NAF building blocks - showing how Infrahub, Nautobot, NetBox, NAAS, CI/CD, and Prometheus each cover different blocks" loading="lazy" /&gt;&lt;/p&gt;
&lt;h3 id="intent"&gt;Intent&lt;/h3&gt;
&lt;p&gt;The Intent block is where most network automation projects should start, because everything downstream depends on having reliable desired-state data.&lt;/p&gt;
&lt;p&gt;Three platforms dominate this space right now: &lt;a href="https://netboxlabs.com/"&gt;NetBox&lt;/a&gt;, &lt;a href="https://nautobot.com/"&gt;Nautobot&lt;/a&gt;, and &lt;a href="https://opsmill.com/infrahub-platform/"&gt;Infrahub&lt;/a&gt;.
All three serve as network sources of truth.
All three expose APIs for automation to consume.
They differ in architecture, schema flexibility, and how far they extend beyond pure data storage, but any of them can fill the Intent block effectively.
I&amp;rsquo;ll have a detailed comparison of all three in a forthcoming post.&lt;/p&gt;
&lt;p&gt;My preference is Infrahub, and I should be transparent about why: I was Director of Product at OpsMill during its development, and I worked with several of the NAF Framework contributors at Network to Code before that.
It&amp;rsquo;s schema-first with a versioned graph database, and it reaches into Orchestrator territory (CI/CD integration, proposed changes with approval workflows) and Presentation (web UI, GraphQL API).
Having built automation systems with all three platforms, I think Infrahub covers more of the Intent block&amp;rsquo;s requirements in a single platform than the alternatives do today.&lt;/p&gt;
&lt;p&gt;That said, NetBox has the largest community and plugin ecosystem by a wide margin, and Nautobot extends further into Orchestrator (its Jobs framework) and Presentation than NetBox does.
Both are proven at scale in ways that Infrahub, being newer, is still proving.
Your team&amp;rsquo;s requirements should drive the decision.&lt;/p&gt;
&lt;p&gt;At Amazon, we use internal platforms that serve the same Intent function. They store desired state for network infrastructure and expose it through APIs that automation consumes.
The tools are different, but the function is identical.&lt;/p&gt;
&lt;h3 id="executor-and-collector"&gt;Executor and Collector&lt;/h3&gt;
&lt;p&gt;These are the transport layer, the blocks that actually touch your network devices.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/lykinsbd/naas"&gt;NAAS&lt;/a&gt; (Netmiko-as-a-Service) is a project I maintain that wraps Netmiko behind a REST API.
It handles both read and write operations: you POST a command or configuration payload, NAAS manages the SSH session, and returns structured results.
It fills both the Executor block (config pushes, device operations) and the Collector block (show commands, state retrieval).&lt;/p&gt;
&lt;p&gt;Wrapping device interaction in a service means your Orchestrator doesn&amp;rsquo;t need to know how to SSH into a Juniper vs. an Arista.
It just calls an API.
The transport complexity is encapsulated in one place.&lt;/p&gt;
&lt;p&gt;Other tools that fill these blocks: Nornir (Python-native, multi-threaded), Ansible (push-mode config management), NAPALM (multi-vendor abstraction), and for collection specifically, Telegraf, streaming telemetry receivers, or SNMP-based collectors.&lt;/p&gt;
&lt;h3 id="orchestrator"&gt;Orchestrator&lt;/h3&gt;
&lt;p&gt;This is where workflows live.
CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins) are the most common Orchestrator in practice. They respond to events (a merge to main), coordinate steps (validate, generate config, deploy, verify), and handle failures.&lt;/p&gt;
&lt;p&gt;Nautobot Jobs serve this function within the Nautobot ecosystem.
Tools like Prefect, Temporal, or Apache Airflow work here too, especially for complex multi-step workflows with retry logic and rollback.&lt;/p&gt;
&lt;p&gt;In my experience, you almost always end up with a dedicated orchestration platform at scale because the coordination logic gets complex enough to warrant its own system.&lt;/p&gt;
&lt;h3 id="observability"&gt;Observability&lt;/h3&gt;
&lt;p&gt;Prometheus and Grafana are the default answer for metrics.
Elasticsearch or Loki for logs.
For network-specific observability, tools like &lt;a href="https://github.com/netenglabs/suzieq"&gt;Suzieq&lt;/a&gt; or &lt;a href="https://www.batfish.org/"&gt;Batfish&lt;/a&gt; provide deeper network state analysis.&lt;/p&gt;
&lt;p&gt;The key requirement from the framework: Observability should generate events when actual state diverges from intended state.
That feedback loop (Collector reads state, Observability detects drift, Orchestrator triggers remediation via Executor) is where automation becomes closed-loop rather than fire-and-forget.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog2.network-notes.com/img/2026/naf-closed-loop.439a1adfd1d091efdaf75481dd018d266b59768ca1f850de3b9dcfad49a03172.svg" alt="The closed-loop feedback cycle - Intent deploys via Executor, Collector reads state, Observability detects drift, Orchestrator remediates" loading="lazy" /&gt;&lt;/p&gt;
&lt;h3 id="presentation"&gt;Presentation&lt;/h3&gt;
&lt;p&gt;ServiceNow, Slack bots, custom dashboards, CLI tools, API gateways.
This block is the most organization-specific because it depends entirely on how your users (network engineers, NOC staff, other teams requesting changes) prefer to interact with the system.&lt;/p&gt;
&lt;h2 id="how-to-use-this"&gt;How to Use This&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re starting a new automation project, use the framework as a checklist:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Identify which blocks you need first.&lt;/strong&gt; Not every project needs all six on day one. If your immediate pain is &amp;ldquo;we don&amp;rsquo;t know what the network should look like,&amp;rdquo; start with Intent. If it&amp;rsquo;s &amp;ldquo;we can&amp;rsquo;t deploy changes reliably,&amp;rdquo; start with Executor.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Audit your existing stack.&lt;/strong&gt; Map every tool you currently use to a block. You&amp;rsquo;ll likely find gaps (no Observability feeding back to Intent) and overlaps (three different tools all partially filling Orchestrator, none of them well).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Evaluate new tools against the framework.&lt;/strong&gt; When someone pitches you a product, ask: &amp;ldquo;Which block does this fill? Do I already have something there? Does it compose with what I have in adjacent blocks?&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Look for the missing feedback loops.&lt;/strong&gt; The framework&amp;rsquo;s read/write symmetry implies closed loops: Intent defines desired state, Executor pushes it, Collector reads actual state, Observability compares them, Orchestrator remediates drift. If any link in that chain is missing, your automation is open-loop. It pushes changes but can&amp;rsquo;t verify or correct them.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The framework is a thinking tool, not a solution.
But it gives you a shared language for talking about what you&amp;rsquo;re building and a way to spot what&amp;rsquo;s missing before you&amp;rsquo;re six months into a project that can&amp;rsquo;t close the loop.&lt;/p&gt;
&lt;p&gt;The full framework documentation is at &lt;a href="https://reference.networkautomation.forum/Framework/Framework/"&gt;reference.networkautomation.forum&lt;/a&gt;.
The community discussion happens in the &lt;a href="https://networkautomationfrm.slack.com"&gt;NAF Slack&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>CLI Over HTTPS Part 4: Where Do We Go from Here?</title><link>https://blog2.network-notes.com/posts/2026/cli-over-https-4/</link><pubDate>Thu, 07 May 2026 09:00:00 +0000</pubDate><author>brett@network-notes.com (Brett Lykins)</author><dc:creator>Brett Lykins</dc:creator><guid>https://blog2.network-notes.com/posts/2026/cli-over-https-4/</guid><description>&lt;p&gt;This is the last post in the series. &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-1/"&gt;Part 1&lt;/a&gt; explained why SSH is slow for automation. &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-2/"&gt;Part 2&lt;/a&gt; measured it. HTTPS batch is up to 17x faster at real-world latencies. &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-3/"&gt;Part 3&lt;/a&gt; showed that an edge proxy and a transparent tunnel capture most of that improvement even when devices don&amp;rsquo;t speak HTTPS natively.&lt;/p&gt;
&lt;p&gt;This post is the practical takeaway: when to use what, how to deploy it, and what the industry should build next.&lt;/p&gt;
&lt;h2 id="the-decision-framework"&gt;The Decision Framework&lt;/h2&gt;
&lt;p&gt;Not every network needs a proxy. Not every automation run is latency-sensitive. Here&amp;rsquo;s how to think about it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog2.network-notes.com/img/2026/cli-over-https-decision-framework.58aab159b9be4b27cc56cdae4b50f2416b19c9bb2a7f7a86026a080df6ecefcb.svg" alt="Decision framework: choose SSH direct for under 5ms RTT, a proxy or tunnel for WAN latency, or native HTTPS when the vendor supports it" loading="lazy" /&gt;&lt;/p&gt;
&lt;h3 id="ssh-direct-is-fine-when"&gt;SSH Direct Is Fine When&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Your automation server is co-located with the devices (same DC, same site)&lt;/li&gt;
&lt;li&gt;Round-trip time to the devices is under ~5ms&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re managing fewer than a few hundred devices&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re already using SSH multiplexing (ControlMaster) or persistent connections&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At local latency, SSH overhead is measured in single-digit milliseconds. The protocol tax from &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-1/"&gt;Part 1&lt;/a&gt; is real but negligible. Don&amp;rsquo;t add architectural complexity to save 3ms per device.&lt;/p&gt;
&lt;h3 id="deploy-a-proxy-when"&gt;Deploy a Proxy When&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Your automation runs over a WAN (30ms+ RTT to the devices)&lt;/li&gt;
&lt;li&gt;You manage devices across multiple sites, regions, or continents&lt;/li&gt;
&lt;li&gt;Automation run time is operationally painful (the Rackspace problem from Part 1)&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re already running regional infrastructure (jump hosts, bastion servers, Ansible Tower nodes)&lt;/li&gt;
&lt;li&gt;You can modify your automation to speak HTTPS instead of SSH&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The proxy pattern from &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-3/"&gt;Part 3&lt;/a&gt; showed 5.3-14.7x improvement at real WAN latencies (14.7x with connection reuse at 150ms RTT). If you already have bastion hosts in each region, you&amp;rsquo;re halfway there. The proxy is a bastion that speaks HTTPS instead of (or in addition to) SSH.&lt;/p&gt;
&lt;h3 id="deploy-a-tunnel-when"&gt;Deploy a Tunnel When&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You need WAN optimization but can&amp;rsquo;t change your automation tooling&lt;/li&gt;
&lt;li&gt;Your team has years of Ansible playbooks, Nornir scripts, or Netmiko wrappers that must speak SSH&lt;/li&gt;
&lt;li&gt;You want a migration path: tunnel first, proxy later&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The tunnel from &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-3/"&gt;Part 3&lt;/a&gt; is transparent to both sides: automation speaks SSH to the headend, the device sees SSH from the site proxy. With command batching, it hits 12.7x speedup at intercontinental latency. Without batching, it&amp;rsquo;s still 3.0x faster than SSH direct.&lt;/p&gt;
&lt;h3 id="deploy-naas-when"&gt;Deploy NAAS When&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You want a production-ready proxy with multi-vendor support out of the box&lt;/li&gt;
&lt;li&gt;You need connection pooling, async jobs, and circuit breakers&lt;/li&gt;
&lt;li&gt;You manage devices across 100+ platforms (everything Netmiko supports)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://github.com/lykinsbd/naas"&gt;NAAS (Netmiko as a Service)&lt;/a&gt; implements the proxy pattern with production concerns handled. Deploy an instance per region, point your automation at it, and the SSH stays local. More on NAAS below.&lt;/p&gt;
&lt;h3 id="push-for-native-https-when"&gt;Push for Native HTTPS When&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;re evaluating new platforms or vendors&lt;/li&gt;
&lt;li&gt;You have influence over vendor roadmaps (large deployments, design partners)&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re building internal tooling that could expose CLI over HTTPS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Native HTTPS eliminates the proxy entirely. The ~17x batch improvement from &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-2/"&gt;Part 2&lt;/a&gt; is the ceiling. No intermediate hop, no backend SSH overhead. If a vendor offers it, use it.&lt;/p&gt;
&lt;h2 id="naas-the-production-proxy"&gt;NAAS: The Production Proxy&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/lykinsbd/naas"&gt;NAAS (Netmiko as a Service)&lt;/a&gt; is what the proxy pattern looks like when you build it for production. Written in Python, it wraps &lt;a href="https://github.com/ktbyers/netmiko"&gt;Netmiko&lt;/a&gt; behind a REST API, which means it supports &lt;a href="https://github.com/ktbyers/netmiko/blob/develop/PLATFORMS.md"&gt;100+ device platforms&lt;/a&gt;: Cisco IOS, NX-OS, ASA, Juniper Junos, Arista EOS, Palo Alto, and everything else Netmiko handles.&lt;/p&gt;
&lt;p&gt;You POST a JSON payload with the device address, platform, credentials, and commands. NAAS opens the SSH session, runs the commands, and returns the output in the HTTP response:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -k -X POST https://naas.dc1.example.com:8443/v1/send_command &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -u &lt;span class="s2"&gt;&amp;#34;automation:token&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;Content-Type: application/json&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -d &lt;span class="s1"&gt;&amp;#39;{&amp;#34;host&amp;#34;: &amp;#34;10.1.1.1&amp;#34;, &amp;#34;platform&amp;#34;: &amp;#34;cisco_ios&amp;#34;, &amp;#34;commands&amp;#34;: [&amp;#34;show version&amp;#34;, &amp;#34;show ip route&amp;#34;]}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;What NAAS handles that a minimal proxy doesn&amp;rsquo;t:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-vendor connection pooling.&lt;/strong&gt; Persistent SSH connections with health checks and automatic reconnection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async job queue.&lt;/strong&gt; Long-running commands (&lt;code&gt;show tech-support&lt;/code&gt;, bulk config pushes) run in a Redis-backed queue. Your automation gets a job ID back immediately and polls for results.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Circuit breaker and observability.&lt;/strong&gt; Stops hammering unreachable devices, exposes Prometheus metrics for connection pool health and per-device latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Deploy a NAAS instance in each data center or region, and your automation talks HTTPS to the nearest one. The SSH sessions stay local. The architecture is the same as what the benchmarks in &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-3/"&gt;Part 3&lt;/a&gt; measured: HTTPS over the WAN, SSH on the last hop.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/lykinsbd/naas.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; naas
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -k -X POST https://localhost:8443/v1/send_command &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -u &lt;span class="s2"&gt;&amp;#34;username:password&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;Content-Type: application/json&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -d &lt;span class="s1"&gt;&amp;#39;{&amp;#34;host&amp;#34;: &amp;#34;10.1.1.1&amp;#34;, &amp;#34;platform&amp;#34;: &amp;#34;cisco_ios&amp;#34;, &amp;#34;commands&amp;#34;: [&amp;#34;show version&amp;#34;]}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;See the &lt;a href="https://naas.readthedocs.io/en/latest/installation/"&gt;NAAS getting started guide&lt;/a&gt; for full setup and configuration.&lt;/p&gt;
&lt;h2 id="what-exists-and-whats-missing"&gt;What Exists and What&amp;rsquo;s Missing&lt;/h2&gt;
&lt;p&gt;Some of the pieces are already in place.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://aristanetworks.github.io/EosSdk/docs/2.22.1.5/ref/eapi.html"&gt;Arista&amp;rsquo;s eAPI&lt;/a&gt; accepts CLI commands via JSON-RPC over HTTPS. It wraps everything in JSON, but the core pattern is there: send commands over HTTPS, get output back. The ASA interface and eAPI have been in production for years. NAAS (described above) brings the proxy pattern to the 100+ platforms Netmiko supports. The &lt;a href="https://github.com/lykinsbd/clibench"&gt;clibench&lt;/a&gt; tunnel mode demonstrates the transparent SSH-to-HTTP approach for teams that can&amp;rsquo;t change their automation tooling.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s missing:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A standard CLI-over-HTTPS interface.&lt;/strong&gt; Not RESTCONF, not gNMI. Those are structured data interfaces for a different use case. A simple, standardized way to send CLI commands over HTTPS and get text output back. The ASA pattern is a reasonable starting point: &lt;code&gt;GET /cli/exec/{command}&lt;/code&gt; for show commands, &lt;code&gt;POST /cli/config&lt;/code&gt; for configuration. Basic auth or token auth over TLS. &lt;code&gt;Content-Type: text/plain&lt;/code&gt;. No JSON wrapping unless the client asks for it. Arista&amp;rsquo;s eAPI is the closest thing to this, but it&amp;rsquo;s vendor-specific and JSON-only.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Proxy support in the automation ecosystem.&lt;/strong&gt; Ansible could ship a connection plugin that talks HTTPS to a proxy like NAAS instead of SSH to the device. Nornir could support an HTTP transport alongside Paramiko and Netmiko. NAAS works today as a standalone API, and a native connection plugin would make adoption even easier: a configuration option instead of a code change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Broader vendor adoption.&lt;/strong&gt; Every network OS already has an HTTPS server for its web UI. Exposing the CLI through that same server is not a large engineering effort. The ASA proves the concept. A plain-text CLI endpoint alongside the structured API would cover both use cases.&lt;/p&gt;
&lt;p&gt;None of this requires abandoning SSH. SSH remains the right tool for interactive sessions, for out-of-band recovery, for environments where HTTPS infrastructure doesn&amp;rsquo;t exist. The argument isn&amp;rsquo;t &amp;ldquo;replace SSH.&amp;rdquo; It&amp;rsquo;s &amp;ldquo;stop using SSH for the thing it&amp;rsquo;s worst at.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="the-numbers-one-more-time"&gt;The Numbers, One More Time&lt;/h2&gt;
&lt;p&gt;For reference, here&amp;rsquo;s the speedup picture from the series. Most automation tools (Netmiko, Ansible, Scrapli) use PTY mode, so SSH PTY is the realistic baseline:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Speedup vs SSH PTY&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS batch (native)&lt;/td&gt;
&lt;td&gt;~17x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy (reused connection)&lt;/td&gt;
&lt;td&gt;~14.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tunnel batch&lt;/td&gt;
&lt;td&gt;~12.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy (new connection)&lt;/td&gt;
&lt;td&gt;~5.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS keep-alive (native)&lt;/td&gt;
&lt;td&gt;~3.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tunnel per-command&lt;/td&gt;
&lt;td&gt;~3.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH with ControlMaster&lt;/td&gt;
&lt;td&gt;~1.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src="https://blog2.network-notes.com/img/2026/cli-over-https-speedup-comparison.02343e29dd2272328fcd4e01b59870866db2b5d661afd35e764b5a7f221c1b6d.svg" alt="Bar chart comparing speedup vs SSH PTY: ControlMaster 1.7x, tunnel per-cmd 3.0x, HTTPS keep-alive 3.4x, proxy new conn 5.3x, tunnel batch 12.7x, proxy reused conn 14.7x, HTTPS batch 17x" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;The proxy with connection reuse gets you most of the native HTTPS improvement without requiring any changes to the devices. The tunnel with batching is close behind, and requires zero changes to your automation tooling either.&lt;/p&gt;
&lt;h2 id="try-it-yourself"&gt;Try It Yourself&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://github.com/lykinsbd/clibench"&gt;benchmark tool&lt;/a&gt; supports all scenarios:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# All transports (SSH, HTTPS, HTTP/3, proxy, tunnel)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./bin/clibench bench --latency regional --iterations &lt;span class="m"&gt;20&lt;/span&gt; --commands &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Proxy pattern (HTTPS + HTTP/3 variants)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./bin/clibench bench --latency regional --iterations &lt;span class="m"&gt;20&lt;/span&gt; --commands &lt;span class="m"&gt;5&lt;/span&gt; --transport proxy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tunnel (transparent SSH-to-HTTP WAN optimization)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./bin/clibench bench --latency intercontinental --iterations &lt;span class="m"&gt;20&lt;/span&gt; --commands &lt;span class="m"&gt;5&lt;/span&gt; --transport tunnel-https
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The code is MIT licensed. Run it on your own infrastructure, with your own latency profiles, and see what the numbers look like for your network.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;My take:&lt;/strong&gt; The network automation community has treated SSH as a given for fifteen years. It was the right default when automation meant one engineer scripting against a handful of devices. At the scale most organizations operate today, SSH&amp;rsquo;s protocol overhead is a measurable, avoidable cost. Native HTTPS CLI is the right long-term direction. The proxy and tunnel patterns are deployable today. I built &lt;a href="https://github.com/lykinsbd/naas"&gt;NAAS&lt;/a&gt; so you can start today. Contributions welcome.&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>CLI Over HTTPS Part 3: The Proxy Pattern</title><link>https://blog2.network-notes.com/posts/2026/cli-over-https-3/</link><pubDate>Tue, 05 May 2026 09:00:00 +0000</pubDate><author>brett@network-notes.com (Brett Lykins)</author><dc:creator>Brett Lykins</dc:creator><guid>https://blog2.network-notes.com/posts/2026/cli-over-https-3/</guid><description>&lt;p&gt;In &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-1/"&gt;Part 1&lt;/a&gt; I showed that SSH burns 10-15 round trips before delivering a single byte of command output. In &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-2/"&gt;Part 2&lt;/a&gt; I proved it. HTTPS batch is ~17x faster than SSH at real-world latencies when the device supports it natively. Even HTTPS keep-alive, with no batching, is 3.4x faster.&lt;/p&gt;
&lt;p&gt;The obvious objection: most devices don&amp;rsquo;t support it natively. Your Cisco IOS switches, your Juniper routers, your Arista leaf nodes, they speak SSH. And while some of them have other interfaces, SSH is not changing anytime soon.&lt;/p&gt;
&lt;p&gt;So the question isn&amp;rsquo;t &amp;ldquo;how do I get my switches to speak HTTPS.&amp;rdquo; The question is: &lt;strong&gt;where does the SSH happen?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There are two answers. The &lt;strong&gt;proxy&lt;/strong&gt; requires your automation to speak HTTPS, but it&amp;rsquo;s architecturally simple: one hop, one translation. The &lt;strong&gt;tunnel&lt;/strong&gt; keeps SSH on both ends and optimizes only the WAN segment, so existing tooling works unchanged. Both relocate the expensive SSH round trips to a local link where they cost almost nothing.&lt;/p&gt;
&lt;h2 id="the-proxy-replace-ssh-on-the-wan"&gt;The Proxy: Replace SSH on the WAN&lt;/h2&gt;
&lt;p&gt;SSH is slow because of round trips. Round trips are slow because of distance. If you move the SSH session closer to the device, the round trips get cheap.&lt;/p&gt;
&lt;p&gt;A proxy co-located with the devices, in the same data center or local network, talks SSH to the devices over a 1-2ms link where the protocol overhead is negligible. Your automation platform talks HTTPS to the proxy over the WAN, where the round-trip savings from &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-1/"&gt;Part 1&lt;/a&gt; actually matter.&lt;/p&gt;
&lt;p&gt;The device never knows the difference, it sees an SSH session from a local IP. Your automation never touches SSH directly, it sends an HTTP request and gets CLI output back in the response body.&lt;/p&gt;
&lt;h2 id="the-architecture"&gt;The Architecture&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://blog2.network-notes.com/img/2026/cli-over-https-proxy-architecture.a680c9de0edef0e2a287b403abe4bc1a33f193079c789073a817090ba9ab3277.svg" alt="Edge proxy architecture: automation talks HTTPS over the WAN to a co-located proxy, which talks SSH to devices over a 1-2ms local link" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;The proxy is the only component that touches SSH. Everything upstream is HTTPS: connection pooling, TLS 1.3, request batching, proper &lt;code&gt;Content-Length&lt;/code&gt; framing. Everything downstream is SSH, but over a link where it doesn&amp;rsquo;t matter.&lt;/p&gt;
&lt;h2 id="proving-it"&gt;Proving It&lt;/h2&gt;
&lt;p&gt;I added a proxy mode to the &lt;a href="https://github.com/lykinsbd/clibench"&gt;benchmark tool from Part 2&lt;/a&gt;. The proxy is an HTTPS server that receives commands via the same ASA-style endpoints (&lt;code&gt;/admin/exec/&lt;/code&gt;, &lt;code&gt;/admin/config&lt;/code&gt;), then opens an SSH session to a backend device and returns the output.&lt;/p&gt;
&lt;p&gt;The test setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Backend device:&lt;/strong&gt; SSH listener with 2ms RTT (local latency)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proxy:&lt;/strong&gt; HTTPS frontend with WAN latency, SSH client to backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Benchmark client:&lt;/strong&gt; Talks HTTPS to the proxy, same as it would to a native HTTPS device&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Four proxy modes tested with a new WAN connection per request (cold start, first request of an automation run):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;fresh-ssh:&lt;/strong&gt; New WAN connection + new SSH to backend per request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pooled-ssh:&lt;/strong&gt; New WAN connection, reuses one SSH connection on the backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;h3-fresh-ssh:&lt;/strong&gt; Same as fresh-ssh, but the WAN leg uses HTTP/3 (QUIC)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;h3-pooled-ssh:&lt;/strong&gt; QUIC on WAN, pooled SSH on backend&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plus two connection-reuse modes (steady state, what a running automation platform does):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;keep-alive:&lt;/strong&gt; Persistent HTTPS connection to proxy, pooled SSH on backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;h3-keep-alive:&lt;/strong&gt; Persistent QUIC connection to proxy, pooled SSH on backend&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The proxy&amp;rsquo;s WAN-facing listener gets the same latency injection as the direct SSH and HTTPS tests from Part 2. The backend SSH link gets a fixed 2ms RTT. All transports experience the same WAN conditions. The only difference is what happens on the last hop.&lt;/p&gt;
&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;All runs: 20 iterations, 5 commands per iteration (batched in one POST). SSH direct numbers from Part 2 for comparison. The &amp;ldquo;SSH direct&amp;rdquo; column uses PTY/shell mode, what Netmiko and Ansible actually do.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;WAN RTT&lt;/th&gt;
&lt;th&gt;SSH direct (PTY)&lt;/th&gt;
&lt;th&gt;Proxy (new conn)&lt;/th&gt;
&lt;th&gt;Proxy (reused conn)&lt;/th&gt;
&lt;th&gt;Speedup (reused vs SSH PTY)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;td&gt;528ms&lt;/td&gt;
&lt;td&gt;124ms&lt;/td&gt;
&lt;td&gt;56ms&lt;/td&gt;
&lt;td&gt;9.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70ms&lt;/td&gt;
&lt;td&gt;1,208ms&lt;/td&gt;
&lt;td&gt;248ms&lt;/td&gt;
&lt;td&gt;95ms&lt;/td&gt;
&lt;td&gt;12.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150ms&lt;/td&gt;
&lt;td&gt;2,571ms&lt;/td&gt;
&lt;td&gt;489ms&lt;/td&gt;
&lt;td&gt;175ms&lt;/td&gt;
&lt;td&gt;14.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src="https://blog2.network-notes.com/img/2026/cli-over-https-proxy-results.158ed5e2994a247307eda2406debb6658d28e4dc01f5014b2b871c975bdb7376.svg" alt="Bar chart comparing SSH direct (PTY) vs proxy new connection vs proxy reused connection at 30ms, 70ms, and 150ms WAN latency" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;The new-connection proxy (full TLS handshake per request) is 4.3-5.3x faster than SSH direct. With connection reuse, the proxy hits 9.4-14.7x, and the advantage grows with latency because reusing the connection eliminates the TLS handshake entirely, paying only 1 round trip per request.&lt;/p&gt;
&lt;p&gt;At 150ms RTT (a US NOC managing devices in Hong Kong) SSH direct (PTY) takes 2.6 seconds per device. The proxy with a persistent connection does it in 175ms.&lt;/p&gt;
&lt;h2 id="why-it-works"&gt;Why It Works&lt;/h2&gt;
&lt;p&gt;SSH direct (PTY mode) at 150ms RTT pays the full protocol tax on every round trip over the WAN:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TCP handshake: 1 RT × 150ms&lt;/li&gt;
&lt;li&gt;SSH version exchange: 1 RT × 150ms&lt;/li&gt;
&lt;li&gt;Key exchange: 2 RT × 150ms&lt;/li&gt;
&lt;li&gt;Auth + channel + PTY + shell: 4 RT × 150ms&lt;/li&gt;
&lt;li&gt;Session prep: 2 RT × 150ms&lt;/li&gt;
&lt;li&gt;5 commands with echo verification: 5 RT × 150ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s ~15 round trips × 150ms = &lt;strong&gt;~2,250ms&lt;/strong&gt; of protocol overhead, plus processing time.&lt;/p&gt;
&lt;p&gt;The proxy with a new connection splits that cost across two links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WAN leg (HTTPS, new connection):&lt;/strong&gt; TCP + TLS 1.3 + HTTP request = ~3 RT × 150ms = &lt;strong&gt;~450ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local leg (SSH):&lt;/strong&gt; The same ~15 SSH round trips, but at 2ms = &lt;strong&gt;~30ms&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Total: ~480ms. That&amp;rsquo;s the cold-start cost when your automation opens a new connection to the proxy.&lt;/p&gt;
&lt;p&gt;The proxy with a reused connection eliminates the handshake entirely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WAN leg (HTTPS, reused connection):&lt;/strong&gt; HTTP request/response = ~1 RT × 150ms = &lt;strong&gt;~150ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local leg (SSH):&lt;/strong&gt; Same ~30ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Total: ~180ms. Measured: 175ms.&lt;/p&gt;
&lt;p&gt;This is the steady-state performance. Once your automation has an open connection to the proxy (which any HTTP client maintains by default), every subsequent request costs exactly one WAN round trip plus the local SSH work. The SSH overhead is still there. It&amp;rsquo;s just happening on a link where 15 round trips cost 30ms instead of 2,250ms.&lt;/p&gt;
&lt;h2 id="fresh-vs-pooled-does-it-matter"&gt;Fresh vs Pooled: Does It Matter?&lt;/h2&gt;
&lt;p&gt;At local latency, not much.
The gap between fresh-ssh (132ms) and pooled-ssh (119ms) at 30ms WAN RTT is 13ms, the cost of one SSH handshake at 2ms RTT.
In production you&amp;rsquo;d pool connections anyway for resource efficiency, but the performance argument for pooling is modest when the backend latency is low.&lt;/p&gt;
&lt;p&gt;The operational argument matters more.
A pooled connection means fewer SSH sessions on the device, and Network devices have finite session limits.
An ASA might handle 5 concurrent SSH sessions, a catalyst might allow 16.
If your proxy is serving 50 requests per second, fresh connections will exhaust those limits instantly.
Pooling keeps one session open per device and multiplexes commands through it.&lt;/p&gt;
&lt;p&gt;The pooling logic in clibench is simple.
&lt;code&gt;getSSH()&lt;/code&gt; returns an existing connection if one is pooled, or dials a new one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getSSH&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pooled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;tcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backendAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sshCfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;tcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backendAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sshCfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The tradeoff is stale connections; devices reboot, sessions time out, firewalls drop idle flows.
The proxy needs to detect dead connections and reconnect, the same problem as HTTP connection pooling or database connection pooling.
In clibench, a failed session operation clears the pool so the next request gets a fresh connection.
In production, you&amp;rsquo;d add periodic health checks and a circuit breaker for unreachable devices; which is what &lt;a href="https://github.com/lykinsbd/naas"&gt;NAAS&lt;/a&gt; does, for example.&lt;/p&gt;
&lt;h2 id="the-tunnel-keep-ssh-on-both-ends"&gt;The Tunnel: Keep SSH on Both Ends&lt;/h2&gt;
&lt;p&gt;The proxy requires changing your automation client. What if you can&amp;rsquo;t?&lt;/p&gt;
&lt;p&gt;Many teams have years of Ansible playbooks, Nornir scripts, and Netmiko wrappers that all speak SSH. Rewriting them to speak HTTPS is a project, not a config change. The tunnel solves this: both your automation and the device speak SSH. The WAN segment in between uses HTTPS or HTTP/3, but neither endpoint knows or cares.&lt;/p&gt;
&lt;h3 id="architecture"&gt;Architecture&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog2.network-notes.com/img/2026/cli-over-https-tunnel-architecture.6587a549d1a13214b49157b53d51b5ea8a213ed452a9e45a3b6c084fc7e5dce6.svg" alt="Tunnel architecture: automation speaks SSH to a headend, which converts to HTTPS/H3 over the WAN, then a site proxy converts back to SSH for the device" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;headend&lt;/strong&gt; sits near your automation server. It accepts SSH connections, parses the exec command, and forwards it as an HTTP request over the WAN to the &lt;strong&gt;site proxy&lt;/strong&gt;. The site proxy is the same component from the proxy pattern above. It receives the HTTP request and talks SSH to the device on a local link.&lt;/p&gt;
&lt;p&gt;Your automation runs &lt;code&gt;ssh headend &amp;quot;show version&amp;quot;&lt;/code&gt; and gets back the device output. Under the hood, the WAN segment used HTTPS with 2-3 round trips instead of SSH&amp;rsquo;s 15+.&lt;/p&gt;
&lt;h3 id="results-1"&gt;Results&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;WAN RTT&lt;/th&gt;
&lt;th&gt;SSH direct (PTY)&lt;/th&gt;
&lt;th&gt;Tunnel (per-cmd)&lt;/th&gt;
&lt;th&gt;Tunnel (batch)&lt;/th&gt;
&lt;th&gt;Speedup (batch vs SSH PTY)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;td&gt;528ms&lt;/td&gt;
&lt;td&gt;228ms&lt;/td&gt;
&lt;td&gt;82ms&lt;/td&gt;
&lt;td&gt;6.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70ms&lt;/td&gt;
&lt;td&gt;1,208ms&lt;/td&gt;
&lt;td&gt;429ms&lt;/td&gt;
&lt;td&gt;121ms&lt;/td&gt;
&lt;td&gt;10.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150ms&lt;/td&gt;
&lt;td&gt;2,571ms&lt;/td&gt;
&lt;td&gt;856ms&lt;/td&gt;
&lt;td&gt;202ms&lt;/td&gt;
&lt;td&gt;12.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two things jump out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Without batching, the tunnel is slower than the proxy.&lt;/strong&gt; The per-command tunnel mode (ssh-https-ssh) pays SSH overhead on &lt;em&gt;both&lt;/em&gt; ends: the automation-to-headend SSH handshake, plus the site proxy-to-device SSH handshake. That&amp;rsquo;s two sets of SSH round trips at campus latency (~2ms each), plus the WAN HTTP request per command. At 150ms, 856ms is still 3.0x faster than SSH direct, but much worse than the proxy&amp;rsquo;s 175ms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With batching, the tunnel approaches proxy performance.&lt;/strong&gt; The batch mode sends all 5 commands in a single SSH exec payload to the headend. The headend forwards them as one HTTP POST. The site proxy runs them all in one SSH session. At 150ms, that&amp;rsquo;s 202ms vs the proxy&amp;rsquo;s 175ms. The tunnel pays a small penalty for the extra SSH hop on the automation side, but it&amp;rsquo;s close.&lt;/p&gt;
&lt;p&gt;The tunnel&amp;rsquo;s value isn&amp;rsquo;t raw speed. It&amp;rsquo;s that you get 12.7x improvement with zero changes to your automation code or your devices.&lt;/p&gt;
&lt;h2 id="proxy-vs-tunnel-when-to-use-which"&gt;Proxy vs Tunnel: When to Use Which&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Use the proxy&lt;/strong&gt; when you can modify your automation to speak HTTPS. It&amp;rsquo;s faster (14.7x with connection reuse vs 12.7x for the tunnel), simpler (one hop instead of two), and has lower per-request overhead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use the tunnel&lt;/strong&gt; when you can&amp;rsquo;t change the automation client. If your tooling must speak SSH (connection plugins, credential management, or organizational inertia) the tunnel gives you WAN optimization transparently. The batch mode requires that your SSH client sends multiple commands in one exec call (which tools like &lt;code&gt;ssh host &amp;quot;cmd1 &amp;amp;&amp;amp; cmd2&amp;quot;&lt;/code&gt; do naturally), but even per-command mode is 3.0x faster than SSH direct at high latency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use both&lt;/strong&gt; in a migration. Deploy the tunnel first for immediate wins with no code changes, then migrate automation to speak HTTPS to the proxy directly as you refactor.&lt;/p&gt;
&lt;h2 id="what-this-looks-like-in-practice"&gt;What This Looks Like in Practice&lt;/h2&gt;
&lt;p&gt;If you have an internal API that accepts &amp;ldquo;run this command on this device&amp;rdquo; requests and returns the output, you&amp;rsquo;re already running a version of this.&lt;/p&gt;
&lt;p&gt;Examples in the ecosystem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Salt proxy minions with NAPALM behind the Salt REST API&lt;/li&gt;
&lt;li&gt;AWX execution environments co-located with devices&lt;/li&gt;
&lt;li&gt;Oxidized&amp;rsquo;s web interface&lt;/li&gt;
&lt;li&gt;The Rackspace Go microservices from &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-1/"&gt;Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lykinsbd/naas"&gt;NAAS (Netmiko as a Service)&lt;/a&gt;: wraps Netmiko behind a REST API with connection pooling, async jobs, and circuit breakers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of these co-locate SSH with the devices (good), but don&amp;rsquo;t expose a clean HTTPS interface upstream, or they bury it under job queues, inventory sync, and YAML sprawl. The core pattern is simpler than any of those tools. The proxy in &lt;a href="https://github.com/lykinsbd/clibench"&gt;clibench&lt;/a&gt; proves the concept in ~180 lines of Go. A production deployment adds multi-vendor support, health checks, and credential management on top.&lt;/p&gt;
&lt;h2 id="security"&gt;Security&lt;/h2&gt;
&lt;p&gt;The proxy doesn&amp;rsquo;t make things more or less secure. It changes the trust model.&lt;/p&gt;
&lt;p&gt;With SSH direct, your automation server holds the SSH keys and authenticates directly to every device.
With the proxy pattern, the trust boundary splits in two: your automation authenticates to the proxy (over HTTPS, using API tokens, mTLS, or whatever your org uses for service-to-service auth), and the proxy authenticates to the devices (over SSH, using keys that are available to the proxy itself).&lt;/p&gt;
&lt;p&gt;What actually changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Where the SSH keys live.&lt;/strong&gt; They move from the automation server to the proxy. The private keys never cross the WAN in either model (SSH public key auth sends a signature, not the key), but the proxy pattern puts the keys physically closer to the devices they unlock.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The WAN-side auth mechanism.&lt;/strong&gt; Your automation no longer speaks SSH to devices. It speaks HTTPS to the proxy. That&amp;rsquo;s not inherently better or worse. It&amp;rsquo;s a different credential type (API token or client cert vs SSH key) managed through whatever system your organization already runs for service authentication.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The blast radius of a compromised proxy.&lt;/strong&gt; The proxy has access to the SSH keys for every device it manages. Compromise the proxy, and you have access to the fleet. This is the same risk profile as an SSH bastion host, which most organizations already operate and already know how to harden: minimal attack surface, restricted network access, key rotation, session logging, and monitoring. The proxy deserves the same care you&amp;rsquo;d give a bastion.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="when-the-proxy-doesnt-help"&gt;When the Proxy Doesn&amp;rsquo;t Help&lt;/h2&gt;
&lt;p&gt;The proxy pattern assumes the WAN latency between your automation and the devices is the bottleneck. If your automation server is already co-located with the devices (same rack, same DC), there&amp;rsquo;s no WAN leg to optimize. SSH at 1-2ms RTT is fast enough.&lt;/p&gt;
&lt;p&gt;It also doesn&amp;rsquo;t help if your bottleneck is device processing time rather than transport overhead. If a &lt;code&gt;show tech-support&lt;/code&gt; takes 30 seconds to generate on the device, the transport saves you a few hundred milliseconds on a 30-second operation. Still worth it at scale, but the relative improvement is smaller.&lt;/p&gt;
&lt;p&gt;And the proxy adds operational complexity. It&amp;rsquo;s another service to deploy, monitor, and maintain. For a team managing 50 devices in one location, the overhead isn&amp;rsquo;t justified. For a team managing thousands of devices across multiple continents, which is where SSH overhead actually hurts, the proxy pays for itself on the first automation run.&lt;/p&gt;
&lt;h2 id="try-it"&gt;Try It&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://github.com/lykinsbd/clibench"&gt;benchmark code&lt;/a&gt; includes all modes. Run it yourself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# All transports at 150ms WAN&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./bin/clibench bench --latency intercontinental --iterations &lt;span class="m"&gt;20&lt;/span&gt; --commands &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Proxy only (HTTPS + HTTP/3 variants)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./bin/clibench bench --latency intercontinental --iterations &lt;span class="m"&gt;20&lt;/span&gt; --commands &lt;span class="m"&gt;5&lt;/span&gt; --transport proxy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tunnel mode (SSH-to-HTTPS transparent WAN tunnel)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./bin/clibench bench --latency intercontinental --iterations &lt;span class="m"&gt;20&lt;/span&gt; --commands &lt;span class="m"&gt;5&lt;/span&gt; --transport tunnel-https
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In &lt;a href="https://blog2.network-notes.com/posts/2026/cli-over-https-4/"&gt;Part 4&lt;/a&gt;, I&amp;rsquo;ll lay out a decision framework for choosing between SSH direct, a proxy, a tunnel, and native HTTPS, and dig into &lt;a href="https://github.com/lykinsbd/naas"&gt;NAAS&lt;/a&gt; as a production deployment of the proxy pattern.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;My take:&lt;/strong&gt; The proxy pattern isn&amp;rsquo;t a workaround. It&amp;rsquo;s the right architecture for managing geographically distributed network infrastructure. SSH is fine for the last hop. HTTPS (or QUIC) is better for everything upstream.&lt;/p&gt;
&lt;/blockquote&gt;</description></item></channel></rss>