Skip to content

Set physical machine as Jenkins slave using OpenVPN

Imported from Confluence

Content may be outdated. Verify before following any procedures. View original | Last updated: October 2025

Sometimes there is a team in need for a Jenkins slave, which is a physical machine located somewhere. We created a custom setup to achieve this, which consists of the steps described in this section. We will leverage the use of OpenVPN to route traffic between Jenkins and these machines.

Heads up

This setup has configs that we used to set up three mac laptops connected to FairBid Jenkins. If you need to replicate this setup in a different project, please be mindful about replacing the FairBid configurations with the corresponding ones.

Install OpenVPN client

The first step would be to make sure that the target machine is permanently connected to OpenVPN. In order to do that, we need to request an OpenVPN package with a profile already embedded. We can ask for it within a CIT ticket. They will:

  • Create the host device and generate config
  • Define access permissions - who has access to it? By default is not accessible

After getting the package and installing it, we need to make sure that the machine is always connected, therefore we need to adjust the settings so that OpenVPN client automatically connects even on reboot.

Set firewall rule

If we want to be able to connect from Kubernetes (where Jenkins is installed) to OpenVPN, we need to set up a firewall rule that allows this connection. It's important to remember that OpenVPN is installed inside Transit VPC, therefore this following rule has to be added to agp-transit-vpc  project:

resource "google_compute_firewall" "fairbid-gke-static-subnet-to-openvpn" {
  description        = null
  destination_ranges = []
  direction          = "INGRESS"
  disabled           = false
  name               = "fairbid-gke-static-subnet-to-openvpn"
  network            = "https://www.googleapis.com/compute/v1/projects/agp-transit-network-prod-lw/global/networks/transit-svpc-prod-01"
  priority           = 1000
  project            = "agp-transit-network-prod-lw"
  source_ranges      = ["172.21.64.0/18"]
  source_tags        = []
  target_tags        = ["openvpn"]
  allow {
    protocol = "all"
  }
  timeouts {
    create = null
    delete = null
    update = null
  }
}

Please note that the source_ranges CIDR block corresponds to the one where Kubernetes pods are created. This example corresponds to FairBid.

The target_tags is openvpn because that's the tag of the VM where OpenVPN is running.

In order to make Jenkins be able to connect to the mac laptops, FairBid needs to know about their IPs. These laptops are connected through OpenVPN to Transit-VPC, so Transit-VPC needs to tell FairBid about their subnet.

The three laptops IPs are:

100.96.1.162
100.96.1.178
100.96.1.194

The smallest subnet that covers them is

100.96.1.128/25

Create routes

Now we need to configure Transit VPC project and create a route that basically says "every time that we have traffic with destination IP corresponding to the subnet containing the laptops, please send it to the OpenVPN VM". Please note that we have two routes because we have two OpenVPN VMs, for high availability.

resource "google_compute_route" "to-berlin-mac-laptop-1" {
  description            = "Route destination to macbooks in Berlin office via openvpn agent 1"
  dest_range             = "100.96.1.128/25"
  name                   = "to-berlin-mac-laptop-1"
  network                = "https://www.googleapis.com/compute/v1/projects/agp-transit-network-prod-lw/global/networks/transit-svpc-prod-01"
  next_hop_gateway       = null
  next_hop_ilb           = null
  next_hop_instance      = "vm-openvpn-agp-prod-useast1-1"
  next_hop_instance_zone = "us-east1-b"
  next_hop_ip            = null
  next_hop_vpn_tunnel    = null
  priority               = 1000
  project                = "agp-transit-network-prod-lw"
  tags                   = []
  timeouts {
    create = null
    delete = null
  }
}

resource "google_compute_route" "to-berlin-mac-laptop-2" {
  description            = "Route destination to macbooks in Berlin office via openvpn agent 2"
  dest_range             = "100.96.1.128/25"
  name                   = "to-berlin-mac-laptop-2"
  network                = "https://www.googleapis.com/compute/v1/projects/agp-transit-network-prod-lw/global/networks/transit-svpc-prod-01"
  next_hop_gateway       = null
  next_hop_ilb           = null
  next_hop_instance      = "vm-openvpn-agp-prod-useast1-2"
  next_hop_instance_zone = "us-east1-c"
  next_hop_ip            = null
  next_hop_vpn_tunnel    = null
  priority               = 1000
  project                = "agp-transit-network-prod-lw"
  tags                   = []
  timeouts {
    create = null
    delete = null
  }
}

This config belongs to FairBid project, hence the naming. We have three configured laptops that act as Jenkins slaves. The network containing all them three matches the IP in dest_range and these IPs can be obtained from OpenVPN client UI.

Create DNS

Since we don't want to be dealing with the physical machines IPs, the ones assigned by OpenVPN connection, let's create DNS records for them:

"macosx-slave.fyber.com" = {
  name   = "macosx-slave"
  type   = "private"
  domain = "macosx-slave.fyber.com."
  labels = "${local.common_locals.labels}"
  recordsets = [
    {
      name = "intel-1"
      type = "A"
      ttl  = 300
      records = [
        "100.96.1.162",
      ]
    },
    {
      name = "m1-1"
      type = "A"
      ttl  = 300
      records = [
        "100.96.1.178",
      ]
    },
    {
      name = "m1-2"
      type = "A"
      ttl  = 300
      records = [
        "100.96.1.194",
      ]
    }
  ]
}

You can adjust this to your setup, depending on the machines you're preparing, since this block above corresponds to FairBid SDK setup.

We can confirm this works through

nslookup intel-1.macosx-slave.fyber.com
nslookup m1-1.macosx-slave.fyber.com
nslookup m1-2.macosx-slave.fyber.com

The responses should return the OpenVPN IPs.

Prepare physical machine

Enable SSH in your physical machine

image-2024-12-13_15-12-3.png

Tips & tricks

Keeping physical machines stable is a real challenge, especially when they're locked up in an office that not everybody has access to. Moreover, during days in which one cannot go to the office but needs to solve some issue, keeping these machines healthy is a must. Here you have some recommendations.

Enable remote login

You can do this in a MacOS machine in the settings

image-2024-12-13_15-15-29.png

Enable "Log in automatically"

This will make the system log in once the machine is rebooted, which is essential if the machine restarts and you need the SSH server to be booted up.

image-2024-12-13_15-16-55.png

Remember

If the user does not log in after rebooting, the SSH server will NOT be started and, therefore, you won't be able to connect

We understand the security concerns of this option but, to mitigate this, enable the lock screen with password after few minutes inactive

image-2024-12-13_15-19-58.png

which will greatly reduce the exposed risk.

Don't put the machine to sleep

Set these options if the machine is connected to power socket and not on battery.

image-2024-12-13_15-21-50.png

This will prevent killing any session after the machine is inactive for some time.

Set up rebooting schedule

Sometimes we adjust some configs on the fly and forget to save them. If we reboot the machine without having written those configurations somewhere, they will be lost.

By rebooting the machine periodically, we ensure that we have a more reliable environment. On top of this, if the machine gets frozen because of whatever reason and we don't have physical access to it, we will just have to wait until next reboot. Combining this with auto-login, guarantees that we will have a more reliable remote connection to it.

Configure Jenkins

Now it's the time in which we will add a new agent to Jenkins. This setup assumes that you are using Jenkins Configuration-as-Code.

JCasC:
  configScripts:
    ...
    nodes: |
      jenkins:
        nodes:
          - permanent:
              labelString: "mac-intel-1"
              launcher:
                ssh:
                  credentialsId: "macosx_slave"
                  host: "100.96.1.162"
                  javaPath: "/Users/jenkins/.jenv/shims/java"
                  port: 22
                  sshHostKeyVerificationStrategy: "nonVerifyingKeyVerificationStrategy"
              mode: EXCLUSIVE
              name: "macosx_slave_intel_1"
              nodeDescription: "MacBook Pro in Berlin office"
              numExecutors: 1
              remoteFS: "/Users/jenkins"
              retentionStrategy:
                demand:
                  idleDelay: 120
                  inDemandDelay: 1
          - permanent:
              labelString: "mac-m1-1"
              launcher:
                ssh:
                  credentialsId: "macosx_slave"
                  host: "100.96.1.178"
                  javaPath: "/Users/jenkins/.jenv/shims/java"
                  port: 22
                  sshHostKeyVerificationStrategy: "nonVerifyingKeyVerificationStrategy"
              mode: EXCLUSIVE
              name: "macosx_slave_m1_1"
              nodeDescription: "MacBook Pro in Berlin office"
              numExecutors: 1
              remoteFS: "/Users/jenkins"
              retentionStrategy:
                demand:
                  idleDelay: 120
                  inDemandDelay: 1
          - permanent:
              labelString: "mac-m1-2"
              launcher:
                ssh:
                  credentialsId: "macosx_slave"
                  host: "100.96.1.194"
                  javaPath: "/Users/jenkins/.jenv/shims/java"
                  port: 22
                  sshHostKeyVerificationStrategy: "nonVerifyingKeyVerificationStrategy"
              mode: EXCLUSIVE
              name: "macosx_slave_m1_2"
              nodeDescription: "MacBook Pro in Berlin office"
              numExecutors: 1
              remoteFS: "/Users/jenkins"
              retentionStrategy:
                demand:
                  idleDelay: 120
                  inDemandDelay: 1
    ...

This assumes that you have an user called jenkins  inside your MacOS machine. It also assumes that you're using jenv  to manage your Java environment. If this is not the case, please adjust accordingly.

The code above is using some credentials that we need to configure, referred as macosx_slave. In our case, we will use an SSH key:

  • The public key will be added to the physical machine and its content will be placed inside ~/.ssh/authorized_keys file.
  • The private key will be placed as a secret inside your Jenkins Configuration-as-Code.
JCasC:
  configScripts:
    ...
    credentials-conf: |
      credentials:
        system:
          domainCredentials:
            - credentials:
              - basicSSHUserPrivateKey:
                  scope:    GLOBAL
                  id:       macosx_slave
                  username: jenkins
                  privateKeySource:
                    directEntry:
                      privateKey: "${MACOSX_SLAVE}"
    ...

Now just place the content of the private key in a secrets file.

Hey!

The configuration with credentials might be different depending on your setup. In fact, we replaced classic credentials with GCP Secrets connection, but we won't write that whole setup here since it's out of scope.

Enable DNS peering

This step is important because the DNS we created above won't be accessible unless we enable DNS peering. This is because the DNS created is private and created under a different project. We should let the Transit VPC know how to resolve it, which can be achieved through DNS peering.

  "macosx-slave" = {
    "dns_name"           = "macosx-slave.fyber.com."
    "target_network_url" = "projects/agp-shared-svpc-prod-ew/global/networks/agp-shared-svpc-prod-01"
  }

The target_network_url is the network that contains the DNS that OpenVPN should resolve.

You can see an example here.

Configure OpenVPN subnet

The last step would be to create a CIT ticket and let them know that the subnet 172.21.0.0/16  should be accessible through OpenVPN. As a reminder, this is the network that contains the Jenkins running pods.