Connecting Jenkins and its agents over Azure Relay

My Jenkins controller runs on my LAN with no public IP. I wanted a build agent that runs in Azure – close to the resources it builds, and able to authenticate to Azure as a managed identity instead of carrying a service-principal secret. The problem: an agent in Azure cannot reach a controller on my LAN, and I did not want to expose Jenkins to the internet, punch an inbound hole in my firewall, or stand up a VPN just for a build agent.

Azure Relay solves exactly this. It is a managed service whose whole job is to let two parties that both sit behind NAT reach each other by each making an outbound connection to a rendezvous point in Azure. Pair it with azbridge (the Azure Relay Bridge – it turns a relay hybrid connection into a plain TCP tunnel) and the Jenkins agent connection rides over the relay. No public Jenkins, no inbound firewall change, no VPN.

A Jenkins agent on an Azure VM reaches a private LAN Jenkins controller through an Azure Relay hybrid connection using azbridge sender and listener tunnels; builds authenticate to Azure with the VM managed identity
The agent dials out to the relay; the controller listens on the relay. Nothing accepts an inbound connection from the internet.

How the tunnel is built

One hybrid connection named jenkins-agent on a Relay namespace, with two SAS keys: a listener key for the controller side and a sender key for the agent side. Each side runs one azbridge process, the same way you would use ssh -L / -R:

# On the LAN Jenkins controller - publish port 8080 onto the relay (listener)
azbridge -T jenkins-agent:8080 -x "$LISTENER_SAS"

# On the Azure VM agent - bind localhost:8080 to the relay (sender)
azbridge -L 8080:jenkins-agent -x "$SENDER_SAS"

Now the agent’s localhost:8080 is the controller’s 8080, tunnelled over the relay. Both azbridge processes made outbound connections to Azure; neither side is listening for the internet.

Attaching the agent

With the tunnel up, this is just an ordinary Jenkins inbound (JNLP over WebSocket) agent pointed at the tunnel:

java -jar agent.jar   -url http://localhost:8080/   -name azure-agent -secret "$AGENT_SECRET"   -webSocket -workDir /home/jenkins/agent

Two gotchas worth recording. First, a modern Jenkins controller ships remoting compiled for Java 21; an agent on Java 17 fails with UnsupportedClassVersionError class 65.0 vs 61.0. Second, run both the azbridge tunnel and the agent as systemd services with Restart=always, and order the agent unit After=azbridge.service – otherwise a reboot leaves you with an agent that starts before its tunnel exists. Keep the SAS strings and the agent secret in root-only environment files, out of the unit files.

What is and is not credential-free

The reason to put the agent in Azure at all is that the builds authenticate to Azure as the VM’s managed identity – Terraform and the az CLI just work, with no service principal and no client secret anywhere on the runner. That is the part that matters: the thing doing real work in your subscription holds no long-lived Azure credential.

Being honest about the plumbing, though: the relay tunnel still uses the two SAS keys, and the agent still uses its JNLP secret. So the accurate claim is “no Azure credentials on the runner,” not “no credentials at all.” Azure Relay does support Azure AD / RBAC auth in place of SAS, which would let the tunnel use the same managed identity – a worthwhile next step to close that gap.

Why this shape

The controller stays private. The firewall stays closed inbound. There is no VPN to operate. The agent runs next to the resources it manages, holds an identity rather than a secret, and the relay hybrid connection itself is a few lines of Terraform. For a build agent that has to live in a different network than its controller, it is a lot less machinery than the alternatives.


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *