<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>article Azure Databricks — Reverse SSH Tunnel for On-Premises Connectivity in Technical Blog</title>
    <link>https://community.databricks.com/t5/technical-blog/azure-databricks-reverse-ssh-tunnel-for-on-premises-connectivity/ba-p/155969</link>
    <description>&lt;H2 id="b3b7" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;Introduction&lt;/H2&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;Azure Databricks users often need to access on-premises resources, such as databases, that reside in their corporate networks. In most cases, the right network path, like ExpressRoute or a Site-to-Site VPN combined with a private endpoint or a load-balanced proxy, is enough to get traffic through. However, in some enterprise environments, the on-premises database sits behind a corporate firewall that blocks all inbound traffic, and the database or network team cannot open inbound ports, even for internal traffic. This is where most of the standard patterns stop working.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;Previously, we published blogs on&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;A class="z nh" href="https://medium.com/databricks-platform-sme/azure-databricks-serverless-private-connectivity-to-customers-resources-part-2-b1c43ed8fcd4" target="_blank" rel="noopener" data-discover="true"&gt;Standard Load Balancer based connectivity&lt;/A&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;and&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;A class="z nh" href="https://medium.com/databricks-platform-sme/azure-databricks-serverless-private-connectivity-to-customer-resources-through-azure-application-32ea023b2b26" target="_blank" rel="noopener" data-discover="true"&gt;Azure Application Gateway based connectivity&lt;/A&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;for Azure Databricks Serverless. Both assume a reachable target in the customer’s Azure tenant or a reachable on-premises target over the existing network connectivity. In this post, we look at a different model, a reverse SSH tunnel proxy hub that works when inbound access to the on-premises network is not allowed.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;In this blog, we walk through the architecture and present options to build a reverse-tunnel proxy hub with high availability. We will also show how this new connectivity pattern supports both Azure Databricks classic and serverless compute, without relaxing any on-premises inbound firewall restrictions. This is particularly useful for Lakeflow Connect customers ingesting from on-premises databases, where the connection must initiate from the customer’s network.&lt;/P&gt;
&lt;BLOCKQUOTE class="ads adt adu"&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;Note:&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;We use MySQL on port 3306 as the running example throughout this post. The same pattern works for any TCP-based database — PostgreSQL on 5432, Oracle on 1521, SQL Server on 1433, and so on.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="3b9e" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;Why a reverse SSH tunnel?&lt;/H2&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;In most enterprise networks, outbound traffic from on-premises to the cloud is permitted over ExpressRoute, a Site-to-Site VPN, or SD-WAN, while inbound traffic from the cloud into the on-premises network is restricted by the corporate firewall. A conventional connectivity model, where Azure Databricks initiates the connection to the on-premises database, requires inbound firewall rules to be configured. This may not be allowed in some environments, or may require a compliance review. A reverse SSH tunnel inverts this flow — the on-premises side initiates the connection, dialing out to a proxy virtual machine (VM) in the cloud over SSH. The proxy VM only needs to accept an outbound SSH connection, which the corporate firewall already permits.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;The word “reverse” can be misleading. The SSH connection itself is a normal outbound connection from on-premises to the cloud. What is “reverse” is the direction of the application traffic that rides on top of it. When the on-premises host runs ssh -R against the proxy VM, it opens a listener on the proxy that forwards any incoming connection back over the SSH session to the on-premises database. Azure Databricks, sitting on the same VNet as the proxy VM, connects to that listener and reaches the database as if it were local to the cloud.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;This direction aligns cleanly with the firewall policy in most enterprises. Outbound SSH (port 22) from on-premises to the cloud is almost always permitted for internal traffic. Inbound SSH is almost never permitted. The reverse tunnel takes advantage of this asymmetry and gives us a secure, authenticated path for database traffic without relaxing any firewall restrictions.&lt;/P&gt;
&lt;P class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="d1f9" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;When to use this?&lt;/H2&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;The reverse SSH tunnel pattern fills a specific gap in the connectivity options for Azure Databricks. This pattern is the right fit when:&lt;/P&gt;
&lt;UL class=""&gt;
&lt;LI id="0417" class="abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;Your on-premises target is reachable from Azure over an existing network path (ExpressRoute, Site-to-Site VPN, or SD-WAN), but the corporate firewall blocks inbound traffic from the cloud.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;This pattern is&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG class="abz pf"&gt;not&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;the right fit when:&lt;/P&gt;
&lt;UL class=""&gt;
&lt;LI id="3275" class="abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;Your on-premises target is reachable, and inbound access is already permitted. Simpler patterns (private endpoint, load-balanced proxy, or direct connection) cover the case more directly.&lt;/LI&gt;
&lt;LI id="3cc1" class="abx aby xg abz b aca adz acc acd ace aea acg ach aci aeb ack acl acm aec aco acp acq aed acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;No network path exists between Azure and your on-premises network. Establish ExpressRoute, VPN, or SD-WAN connectivity first before considering any cloud-to-on-premises pattern.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="7bca" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;Prerequisites&lt;/H2&gt;
&lt;TABLE&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;STRONG&gt;Component&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;STRONG&gt;Notes&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;On-premises Tunnel host&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Runs “autossh”. Needs outbound port 22 to the proxy VMs and network access to the on-premises database.&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Proxy Hub VNet in Azure&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Connectivity to on-premises via ExpressRoute, Site-to-Site VPN, or SD-WAN.&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Proxy VMs in the Proxy Hub VNet&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;At least one; two for high availability. Run “socat” and the HTTP health check.&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Standard Load Balancer&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Required for high availability (multi-VM). Optional for development or single-VM setups.&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Network path to Databricks&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;VNet peering (classic compute) or NCC + Private Endpoint + Private Link Service (serverless compute).&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="93dd" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;Architecture&lt;/H2&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;The solution consists of three zones — the on-premises network, the proxy hub VNet in Azure, and the Azure Databricks compute plane. Traffic flows from Databricks, through the proxy hub, and over the reverse SSH tunnels into the on-premises database.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="ReverseTunnel-ArchDiagram.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/26572iB214C89AC136B3A8/image-size/large?v=v2&amp;amp;px=999" role="button" title="ReverseTunnel-ArchDiagram.png" alt="ReverseTunnel-ArchDiagram.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;The proxy hub VNet in the middle is the core segment or zone of this architecture. We will mostly focus on building out the following components:&lt;/P&gt;
&lt;UL class=""&gt;
&lt;LI id="f731" class="abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;Proxy VMs (two or more for high availability)&lt;/STRONG&gt;. Each proxy VM accepts an incoming reverse SSH connection from the on-premises tunnel host and runs two additional services:&lt;BR /&gt;— socat, which bridges the VM’s network interface to the tunnel listener&lt;BR /&gt;— A lightweight HTTP health check endpoint that the load balancer probes.&lt;/LI&gt;
&lt;LI id="d108" class="abx aby xg abz b aca adz acc acd ace aea acg ach aci aeb ack acl acm aec aco acp acq aed acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;Standard Load Balancer&lt;/STRONG&gt;. An internal Standard Load Balancer (SLB) sits in front of the proxy VMs. It provides a stable frontend IP, distributes traffic across the backend pool, and removes unhealthy VMs when the HTTP health probe fails. This is what gives us automatic failover when a tunnel fails.&lt;/LI&gt;
&lt;LI id="0f94" class="abx aby xg abz b aca adz acc acd ace aea acg ach aci aeb ack acl acm aec aco acp acq aed acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;Private Link Service (PLS)&lt;/STRONG&gt;. The PLS exposes the SLB frontend to Azure Databricks serverless compute. Serverless compute lives in a Databricks-managed VNet and cannot use VNet peering to reach customer resources, so it needs the PLS combined with a Private Endpoint (PE) rule in a Network Connectivity Configuration (NCC) on the Databricks side.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;On the on-premises side, a single Linux host (physical or virtual) establishes the reverse tunnels. This tunnel host runs one autossh process per proxy VM, and each autossh process carries one or more -R port forwards that map a high port on the proxy VM to the on-premises database. The tunnel host does not need anything special beyond network access to the database and outbound SSH to the proxy VMs.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;On the Azure Databricks side, classic compute reaches the SLB frontend through VNet peering, and serverless compute reaches it through the NCC Private Endpoint that targets the PLS. Both paths converge on the same SLB, so we serve both compute types with one proxy hub.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="3613" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;How to make the tunnel reachable?&lt;/H2&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;By default, ssh -R binds its listener on the remote side to the loopback interface (&lt;EM class="adv"&gt;localhost:13306&lt;/EM&gt;). This is controlled by the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;GatewayPorts&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;setting in&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;sshd_config&lt;/EM&gt;, which ships as no on most Linux distributions. The default is deliberate — a reverse-forwarded port is not automatically exposed to the rest of the network. In our tests, we keep this default, which means the tunnel sits safely on the proxy VM’s loopback and is not reachable directly from the NIC.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;So, for the default model, we need a way to bridge traffic from the NIC to the tunnel on the loopback interface. That is where socat comes in. On each proxy VM, we run a small socat process that listens on the network interface (&lt;EM class="adv"&gt;0.0.0.0:3306&lt;/EM&gt;) and forwards whatever it receives to&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;localhost:13306&lt;/EM&gt;, where the tunnel is waiting. Traffic from Databricks flows into the NIC, through socat, into the tunnel, and out to the on-premises database.&lt;/P&gt;
&lt;BLOCKQUOTE class="ads adt adu"&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;Note:&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;You can skip socat by setting GatewayPorts yes on the proxy VM’s SSH server and binding the -R forward to 0.0.0.0 directly. The tunnel then listens on the NIC itself. Compared to the default model, this removes the loopback boundary as an extra defense-in-depth layer — access control falls entirely on NSG rules, source IP filtering, and database authentication. We keep the sshd default and let socat handle the bridge explicitly.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="9a4b" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;How to make the tunnel resilient?&lt;/H2&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;With two or more proxy VMs behind the SLB, we already get redundancy for many common failures. If a proxy VM crashes or becomes unreachable, a simple TCP health probe lets the SLB detect it and stop sending traffic to it, and the remaining VM keeps serving. But what if just the SSH tunnel on a VM dies, while the VM itself and the socat service on it stay healthy? In that case, a plain TCP health probe on port&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;3306&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;detects that socat is listening and marks the VM as healthy, even though no traffic actually reaches the database. Databricks connections start failing intermittently while the SLB reports the VM as healthy.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;To overcome this challenge and detect that state, we run a small HTTP health check service on each proxy VM. The HTTP service listens on port 8080, and for every incoming request, it attempts a TCP connection to the tunnel port on loopback (&lt;EM class="adv"&gt;localhost:13306&lt;/EM&gt;). If the connection succeeds, the tunnel is alive, and we return&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;HTTP 200&lt;/EM&gt;. If the connection fails, the tunnel is dead, and we return&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;HTTP 503&lt;/EM&gt;. The SLB is configured to probe this HTTP endpoint instead of probing port 3306 directly.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;With this setup, the SLB reflects the actual health of the tunnel, not just the listener in front of it. When a tunnel dies on one proxy VM, the health probe starts returning&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;503&lt;/EM&gt;, the SLB removes the VM from rotation within a few seconds (about 10), and traffic flows entirely through the remaining VM. When the tunnel recovers (autossh reconnects), the probe returns&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;200&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;again, and the VM rejoins the pool automatically.&lt;/P&gt;
&lt;BLOCKQUOTE class="ads adt adu"&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;Note:&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;SLB failover redirects new connections only. Queries already in flight on the failed proxy VM may be dropped and need to be retried by the Databricks client (JDBC driver, foreign catalog query, or Lakeflow Connect gateway). Once retried, the connection is routed to the healthy VM.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="b2d8" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;Building the tunnel&lt;/H2&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;With the proxy hub designed, we now build the tunnel itself. This has two ends — the cloud side, where each proxy VM accepts the reverse SSH connection, and the on-premises side, where the tunnel host initiates the connection out to the cloud. We walk through both in turn.&lt;/P&gt;
&lt;BLOCKQUOTE class="ads adt adu"&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;Note:&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;The commands and sample scripts/configs in this section assume Ubuntu. Adapt package manager commands, file paths, service names, and parameters as needed for your environment.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H3 id="d8c1" class="aer acw xg bb acx aes aet aeu nu aev aew aex nx aci aey aez afa acm afb afc afd acq afe aff afg afh bg" data-selectable-paragraph=""&gt;Cloud side — the proxy VM&lt;/H3&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;Each proxy VM runs a stock Linux OS with three pieces added — the socat service that bridges the NIC to the tunnel, a small Python-based HTTP health check that the SLB probes, and no changes to the default&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;sshd&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;configuration.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;1.&lt;/STRONG&gt; Install socat from the package repository:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;sudo apt-get install -y socat&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;2.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;A sample systemd unit for socat (runs at boot and restarts if it ever exits):&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# /etc/systemd/system/socat-db-proxy.service
[Unit]
Description=socat DB proxy (NIC to SSH tunnel)
After=network.target

[Service]
ExecStart=/usr/bin/socat TCP-LISTEN:3306,bind=0.0.0.0,fork,reuseaddr TCP:localhost:13306
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;The&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;TCP-LISTEN:3306,bind=0.0.0.0,fork,reuseaddr&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/EM&gt;binds on port 3306 across all network interfaces, handles each incoming connection in a separate child process, and allows the port to be reused quickly if the service restarts.&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;TCP:localhost:13306&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;is the forward target — the tunnel listener on the loopback interface.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acc acd ace acg ach aci ack acl acm aco acp acq acs act afr acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;3.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Next, the health check. We implement it as a small Python HTTP server. On every incoming GET request, it attempts a TCP connection to the tunnel port on the loopback interface. If the connection succeeds, it returns&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;HTTP 200&lt;/EM&gt;; otherwise,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;503&lt;/EM&gt;. A sample implementation:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# /usr/local/bin/tunnel-health-check.py
#!/usr/bin/env python3
import socket
import http.server

TUNNEL_PORT = 13306

class HealthHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            s = socket.create_connection(("127.0.0.1", TUNNEL_PORT), timeout=3)
            s.close()
            self.send_response(200)
        except Exception:
            self.send_response(503)
        self.end_headers()

    def log_message(self, *args):
        pass

http.server.HTTPServer(("0.0.0.0", 8080), HealthHandler).serve_forever()&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acc acd ace acg ach aci ack acl acm aco acp acq acs act afr acu vi bg" data-selectable-paragraph=""&gt;Make it executable:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;sudo chmod +x /usr/local/bin/tunnel-health-check.py&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;A sample systemd unit for the health check:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# /etc/systemd/system/tunnel-health-check.service
[Unit]
Description=Tunnel health check HTTP endpoint
After=network.target

[Service]
ExecStart=/usr/bin/python3 /usr/local/bin/tunnel-health-check.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;The standard&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;sshd&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;on the proxy VM accepts the reverse SSH connection from the on-premises tunnel host. We leave GatewayPorts to no as the default, which keeps the tunnel listener safely on the loopback interface, as explained earlier. No changes to&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;sshd_config&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;are needed for the reverse tunnel to work.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;Finally, enable and start both services:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;sudo systemctl daemon-reload
sudo systemctl enable socat-db-proxy tunnel-health-check
sudo systemctl start socat-db-proxy tunnel-health-check&lt;/LI-CODE&gt;
&lt;BLOCKQUOTE class="ads adt adu"&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;Security Note:&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Apply NSG rules on the proxy VM to limit access as suggested below.&lt;/P&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;— Inbound SSH (port 22): Restrict to the on-premises CIDR or specific tunnel host IPs.&lt;/P&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;— Inbound database port (e.g., 3306): Restrict to the Databricks VNet CIDR (classic) and the PLS NAT subnet (serverless).&lt;/P&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;— Inbound health probe port (e.g., 8080): Restrict to the Azure load balancer probe source 168.63.129.16/32.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;The proxy VM is now ready to accept reverse SSH tunnels and serve traffic to Databricks through the SLB.&lt;/P&gt;
&lt;H2 id="c90f" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;On-premises side — the tunnel host&lt;/H2&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;The on-premises tunnel host is a Linux host (physical or virtual) with network access to the database and outbound SSH (port 22) to the proxy VMs. Because the tunnel needs to stay up through network blips, proxy VM restarts, and other connection drops, we use autossh, a wrapper around ssh that automatically reconnects when the SSH session dies. On the tunnel host, we set up three pieces: autossh, an SSH key pair for authenticating to each proxy VM, and one systemd service per proxy VM so the tunnel comes up at boot and stays alive.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;1.&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;Install&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;autossh&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;from the package repository:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;sudo apt-get install -y autossh&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;2.&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;Generate an SSH key pair for the tunnel and copy the public key to each proxy VM:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_tunnel -N ""
ssh-copy-id -i ~/.ssh/id_rsa_tunnel &amp;lt;user&amp;gt;@&amp;lt;proxy-vm-1-ip&amp;gt;
ssh-copy-id -i ~/.ssh/id_rsa_tunnel &amp;lt;user&amp;gt;@&amp;lt;proxy-vm-2-ip&amp;gt;&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;3.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Wrap&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;autossh&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;in a systemd service so it starts at boot and restarts if it ever exits. Because each autossh process maintains exactly one outbound SSH connection, we create one systemd service per proxy VM. A sample unit for the first proxy VM:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# /etc/systemd/system/ssh-tunnel-proxy1.service
[Unit]
Description=SSH Reverse Tunnel to Proxy VM 1
After=network-online.target
Wants=network-online.target

[Service]
User=&amp;lt;user&amp;gt;
ExecStart=/usr/bin/autossh -M 0 -N \
  -R 13306:&amp;lt;db-ip&amp;gt;:3306 \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -o ExitOnForwardFailure=yes \
  -o StrictHostKeyChecking=accept-new \
  -i /home/&amp;lt;user&amp;gt;/.ssh/id_rsa_tunnel \
  &amp;lt;user&amp;gt;@&amp;lt;proxy-vm-1-ip&amp;gt;
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;Let’s walk through the flags:&lt;/P&gt;
&lt;UL class=""&gt;
&lt;LI id="e9b9" class="abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;&lt;EM class="adv"&gt;-M 0&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/EM&gt;—&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;disables autossh’s built-in monitoring port. By itself, that means autossh would never detect a dead connection. We get that detection from SSH’s own keepalive mechanism, which is why -M 0 is always paired with ServerAliveInterval and ServerAliveCountMax.&lt;/LI&gt;
&lt;LI id="3b07" class="abx aby xg abz b aca adz acc acd ace aea acg ach aci aeb ack acl acm aec aco acp acq aed acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;&lt;EM class="adv"&gt;ServerAliveInterval=30&lt;/EM&gt;&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— sends a keepalive every 30 seconds.&lt;/LI&gt;
&lt;LI id="80dc" class="abx aby xg abz b aca adz acc acd ace aea acg ach aci aeb ack acl acm aec aco acp acq aed acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;&lt;EM class="adv"&gt;ServerAliveCountMax=3&lt;/EM&gt;&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— means after three missed keepalives (about 90 seconds), SSH treats the connection as dead and exits. At that point, systemd restarts autossh, which establishes a fresh tunnel.&lt;/LI&gt;
&lt;LI id="ada2" class="abx aby xg abz b aca adz acc acd ace aea acg ach aci aeb ack acl acm aec aco acp acq aed acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;&lt;EM class="adv"&gt;ExitOnForwardFailure=yes&lt;/EM&gt;&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— exits SSH immediately if the remote port forward cannot be bound, which is useful when a proxy VM is briefly unreachable at connect time.&lt;/LI&gt;
&lt;LI id="340d" class="abx aby xg abz b aca adz acc acd ace aea acg ach aci aeb ack acl acm aec aco acp acq aed acs act acu adw adx ady bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;&lt;EM class="adv"&gt;-R 13306:&amp;lt;db-ip&amp;gt;:3306&lt;/EM&gt;&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— is the reverse port forward — open a listener on localhost:13306 on the proxy VM and forward everything to the on-premises database at &amp;lt;db-ip&amp;gt;:3306, which the tunnel host reaches over the local network.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;Create a matching&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;ssh-tunnel-proxy2.service&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;for the second proxy VM (change the service name, description, and target IP). Then enable and start both:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;sudo systemctl daemon-reload
sudo systemctl enable ssh-tunnel-proxy1 ssh-tunnel-proxy2
sudo systemctl start ssh-tunnel-proxy1 ssh-tunnel-proxy2&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;For a multi-database setup, extend each&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;autossh&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;command with additional -R forwards, mapping a different high-port tunnel port to each database:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;autossh ... \
  -R 13306:&amp;lt;mysql-ip&amp;gt;:3306 \
  -R 15432:&amp;lt;postgres-ip&amp;gt;:5432 \
  -R 11433:&amp;lt;sqlserver-ip&amp;gt;:1433&lt;/LI-CODE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;Each proxy VM’s socat configuration then bridges the corresponding NIC port to each tunnel port.&lt;/P&gt;
&lt;BLOCKQUOTE class="ads adt adu"&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;STRONG class="abz pf"&gt;Security Note:&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Lock down the on-premises tunnel host as well.&lt;/P&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;— Outbound port 22: Restrict the on-premises firewall to allow port 22 outbound only to the specific proxy VM IPs.&lt;/P&gt;
&lt;P class="abx aby adv abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;— SSH key hardening: Restrict the tunnel SSH key to forwarding only. On each proxy VM, find the entry for the tunnel public key in `~/.ssh/authorized_keys` (the one added by `ssh-copy-id` earlier) and prefix it with `restrict,permitlisten="13306"` (add a `permitlisten` for each forwarded port). This blocks shell access, PTY allocation, and agent forwarding.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;With both ends in place, the tunnel is live — the on-premises host maintains one&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM class="adv"&gt;autossh&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;process per proxy VM, each proxy VM has the tunnel listener on loopback plus socat bridging to the NIC, and the SLB probes the health check to detect and route around any tunnel failures.&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu vi bg" data-selectable-paragraph=""&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="7346" class="acv acw xg bb acx ns acy nt nu nv acz nw nx ada adb adc add ade adf adg adh adi adj adk adl adm bg" data-selectable-paragraph=""&gt;Failover testing&lt;/H2&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;To validate the HA design end-to-end, we ran a set of controlled failure tests against a two-VM proxy hub. The setup mirrored the architecture described above — two proxy VMs behind an SLB, a Databricks foreign catalog for JDBC queries, and a Lakeflow Connect CDC pipeline for a long-lived connection test. The goal was to confirm that a tunnel failure is detected quickly, does not interrupt Databricks traffic, and recovers automatically. We also ran a few negative tests. As shown in the table below, the results of these tests validate the specific design choices we made, such as the HTTP health check and pointing the connection at the SLB for CDC.&lt;/P&gt;
&lt;TABLE&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;STRONG&gt;#&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;STRONG&gt;Test scenario&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;STRONG&gt;Expected result&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;STRONG&gt;Observed result&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;1&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;JDBC query through SLB, both VMs are healthy&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Query succeeds (baseline)&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Query succeeded&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;2&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;JDBC query, one tunnel stop&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;ped, plain TCP probe (no HTTP health check)&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;TCP probe sees socat as healthy; SLB keeps routing to dead path; queries fail intermittently&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;As expected — SLB kept routing to the dead path, queries failed intermittently&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;3&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;JDBC query, one tunnel stopped, HTTP health check enabled&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;SLB detects and fails over; queries continue to succeed&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;SLB detected within a few seconds, routed traffic to the remaining VM, and queries continued to succeed&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;4&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Foreign catalog query, classic compute over VNet peering&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Query succeeds (baseline)&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Query succeeded&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;5&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Foreign catalog query, serverless compute over PE → PLS → SLB&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Query succeeds (baseline)&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Query succeeded&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;6&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Lakeflow CDC pipeline, connection points at a specific proxy VM, that VM's tunnel stopped&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Gateway keeps retrying the dead VM; pipeline fails persistently until the connection is changed&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;As expected — gateway kept retrying the same dead VM, pipeline failed persistently until we reconnected at the SLB&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;7&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Lakeflow CDC pipeline, connection points at SLB, one tunnel stopped&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Gateway reconnects through SLB to a healthy VM; pipeline resumes without data loss&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Existing gateway connection broke, retry reconnected through SLB, pipeline resumed from the last binlog position, no data loss&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;SPAN&gt;The most important takeaway from all these tests — point Databricks connections at the SLB frontend rather than a specific proxy VM. The SLB is what carries the failover through.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Conclusion&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;The reverse SSH tunnel proxy hub we presented in this post is one of many patterns to connect Azure Databricks classic and serverless compute to on-premises resources. We have previously published blogs and detailed reference architectures for several such connectivity patterns. What makes this model different is the direction of connection initiation — the on-premises host initiates the connection outbound (on-premises to cloud) to avoid relaxing firewall restrictions, and the return traffic flows back (cloud to on-premises) over the established path.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;If your on-premises security policy prohibits inbound connections from the cloud, and changing that policy isn't on the table, the reverse SSH tunnel proxy hub gives you a documented, validated path to reach your databases from both Azure Databricks classic and serverless compute. The architecture requires no inbound firewall changes on-premises, recovers automatically from tunnel failures, and supports multiple databases over a single hub.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class="pw-post-body-paragraph abx aby xg abz b aca adn acc acd ace ado acg ach aci adp ack acl acm adq aco acp acq adr acs act acu vi bg" data-selectable-paragraph=""&gt;&lt;SPAN&gt;For the Databricks-side configuration, refer to the official Azure Databricks documentation on &lt;/SPAN&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/databricks/security/network/serverless-network-security/ssh-reverse-tunnel" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Connect to on-premises databases using an SSH reverse tunnel.&lt;/SPAN&gt;&lt;/A&gt;&lt;/P&gt;</description>
    <pubDate>Fri, 01 May 2026 20:38:44 GMT</pubDate>
    <dc:creator>KiranAnand</dc:creator>
    <dc:date>2026-05-01T20:38:44Z</dc:date>
    <item>
      <title>Azure Databricks — Reverse SSH Tunnel for On-Premises Connectivity</title>
      <link>https://community.databricks.com/t5/technical-blog/azure-databricks-reverse-ssh-tunnel-for-on-premises-connectivity/ba-p/155969</link>
      <description>&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="ReverseTunnel-ArchDiagram.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/26574iB746558C68733DBA/image-size/medium?v=v2&amp;amp;px=400" role="button" title="ReverseTunnel-ArchDiagram.png" alt="ReverseTunnel-ArchDiagram.png" /&gt;&lt;/span&gt;&lt;/P&gt;</description>
      <pubDate>Fri, 01 May 2026 20:38:44 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/azure-databricks-reverse-ssh-tunnel-for-on-premises-connectivity/ba-p/155969</guid>
      <dc:creator>KiranAnand</dc:creator>
      <dc:date>2026-05-01T20:38:44Z</dc:date>
    </item>
  </channel>
</rss>

