Skip to content

Set physical machine as Jenkins slave using Wireguard

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 use Wireguard to create a VPN tunnel between the subnet containing the physical machine(s) and GCP.

This is a representation of what we would like to achieve:

jenkins-slave.drawio.png

Create Virtual Machine

The first step would be to create a resource inside GCP that would act as VPN server, being the resource a Virtual Machine. The VPN connection will be created between this VM and the subnet that contains the physical machine(s).

The VM should be ubuntu based, as this will ease the setup with the tools we need. You can see an example here, which created this virtual machine.

After applying changes, this VM will have an external IP, which we will use to accept incoming connections from the subnet containing the VPN clients.

Enable IP forwarding

By default, linux kernels don't allow forwarding traffic from one subnet to another one. In our case, this VM should actually do that, so it is important that we enable IP forwarding. This can be done when creating the standalone instance.

...
inputs = {
  ...
  can_ip_forward = true
  ...
}

Install VPN server

To create a VPN server we are going to use PiVPN, which let's you use both Wireguard and OpenVPN protocols. This VPN server is not only designed for Raspberry Pi, but also for Ubuntu/Debian distributions, which makes it suitable for our VM. In our setup we are going to do the connection with Wireguard.

For easiness in this step, we will directly create a configuration file and tell the PiVPN installer to use that file, so that we don't have to take decisions interactively.

First, we will create a file, let's call it pivpnSetup.conf , and include the following config:

IPv4dev=ens4
install_user=jesus_nuno
VPN=wireguard
pivpnNET=10.63.199.0
subnetClass=24
pivpnforceipv6route=1
pivpnforceipv6=0
pivpnenableipv6=0
ALLOWED_IPS="0.0.0.0/0, ::0/0"
pivpnMTU=1420
pivpnPORT=51820
pivpnDNS1=1.1.1.1
pivpnDNS2=1.0.0.1
UNATTUPG=1
EOL

A few things to remark about this configuration:

  • The install_user could be any user doing the setup. Make sure that it matches the user with which you would ssh  to the Virtual Machine.
  • The pivpnNET would be any subnet that would eventually contain the machine(s) to connect to Jenkins. Make sure that this subnet is not overlapping some other subnets.
  • The chosen DNS is Cloudflare, but feel free to adjust it to your needs.

There is an example you could check here.

Then run the following script

sudo apt-get update
sudo hostname vpn-server
curl -L https://install.pivpn.io > install.sh
chmod +x install.sh
./install.sh --unattended pivpnSetup.conf
sudo reboot

After rebooting, we need to add the VPN clients to the VPN server. For example, we can add some mac mini by running:

pivpn -a --name "mac-mini-1"

This will generate a configuration file under ~/configs/mac-mini-1.conf. This configuration file should be placed into the machine that will be connected to Jenkins, under ~/.wireguard folder.

Set Firewall rules

Since our VM above has an external IP and the firewall blocks everything by default, we should add some firewall rules to allow some connections to it. We are going to add two:

Allow VPN and SSH connections to the VPN server

If we want to be able to SSH or connect through VPN to the VM from a subnet that is located at a particular office, we need to let the connections from that office to pass the firewall.

{
  name                    = "fw-office-to-wireguard-vpn-server-allow"
  description             = "Allow VPN and SSH connections to VPN server from a particular office"
  direction               = "INGRESS"
  priority                = null
  ranges                  = ["XXX.XXX.XXX.XXX"]
  source_tags             = null
  source_service_accounts = null
  target_tags             = ["vm-jenkins-macos-vpn-server-useast1"]
  target_service_accounts = null
  allow = [
    {
      protocol = "tcp"
      ports    = ["22"]
    },
    {
      protocol = "udp"
      ports    = ["51820"]
    }
  ]
  deny       = []
  log_config = null
}

This assumes that the VM that hosts the VPN server has the vm-jenkins-macos-vpn-server-useast1 tag.

The ranges should be fulfilled with the source IPs from the offices that host the physical machines, for example 82.80.46.146  for Israel or 62.96.69.90 for Berlin.

Allow subnets connections to the VPN server

We need to take into account that Jenkins is deployed in GKE. Since Jenkins will have to connect to the slave machines through the VPN server, then all the subnets should be able to pass the firewall.

{
  name                    = "fw-subnets-to-vpn-server-allow"
  description             = "Allow connection from all subnets to VPN server"
  direction               = "INGRESS"
  priority                = null
  ranges                  = ["XXX.XXX.XXX.XXX/YY"]
  source_tags             = null
  source_service_accounts = null
  target_tags             = ["vm-jenkins-macos-vpn-server-useast1"]
  target_service_accounts = null
  allow = [
    {
      protocol = "all"
      ports    = []
    }
  ]
  deny       = []
  log_config = null
}

This assumes that the VM that hosts the VPN server has the vm-jenkins-macos-vpn-server-useast1 tag.

The ranges should be fulfilled with the CIDR blocks for the subnets in that particular VPN. For example, "10.39.0.0/24", "10.39.32.0/19" for agp-shared-svpc-prod-ew or "10.187.0.0/22", "172.20.0.0/16", "172.21.0.0/18", "10.187.4.0/24" for agp-fairbid-svpc-prod-p7 project.

Create routes

The next step is to create a custom route that says "hey, all the packets that are going to certain CIDR block, they need to go through our custom VM". This will target the subnet that contains the physical machines, therefore, all traffic going to that subnet should go through our standalone instance. Let's not forget that our VM already knows about this subnet because we configured it when installing the VPN server just a few steps above.

...

dependency "vpn-server" {
  config_path = "../../vpn-server/us-east1"
}

...

inputs {
  ...

  routes = [
    {
      name        = "static-route-to-physical-machine-subnet-through-wireguard-vpn-server"
      description = "Route for traffic to physical machines in office via Wireguard VPN server inside GCP"
      dest_range  = "10.63.199.0/24"
      next_hop_ip = values(dependency.vpn-server.outputs.instances_ip)[0]
      priority    = 1000
    }
  ]

}

The configuration above routes all packets that are going to 10.63.199.0/24 subnet, in our example, this would be the subnet that we configured above to contain the physical machines.

This configuration assumes that the standalone instance was deployed in a folder named as indicated above in the config_path.

Set DNS

Since we don't want to be dealing with the IP created above, assigned to the physical machine, let's create a DNS record for it.

"physical-machine-slave.fyber.com" = {
  name   = "physical-machine-slave"
  type   = "private"
  domain = "physical-machine-slave.fyber.com."
  labels = "${local.common_locals.labels}"
  recordsets = [
    {
      name = "physical-machine-1"
      type = "A"
      ttl  = 300
      records = [
        "10.63.199.2"
      ]
    }
  ]
}

This will let us access the machine with a more user friendly name, you can see an example here.

We can confirm this works through

nslookup physical-machine-1.physical-machine-slave.fyber.com

Note that physical-machine is just a placeholder for anything you want to set up, for example, mac-mini or windows-desk would be valid options.

Prepare physical machine

Configuring the machine is very straight forward. We just need to install Wireguard client. If you are configuring a MacOS machine, you can download it from here. After installing it, install a tunnel from a file

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

and use the file that you placed under ~/configs/mac-mini-1.conf in one of the steps above, when configuring the clients in the VPN server.

After completing this step and connecting to the VPN, make sure that data is flowing (check "Data received" and "Data sent")

image-2024-12-13_15-6-7.png

and that Wireguard is always connected by enabling "On Demand": Click on "Edit" button and enable both "Ethernet" and "Wi-Fi".

image-2024-12-13_15-7-25.png

Don't forget to 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.

Set up split tunneling

If you are worried about the amount of traffic that your physical machine is sending through the VPN tunnel, you could enable split tunneling so that only Jenkins traffic is transmitted through wireguard. Check the official documentation about how to configure that in the wireguard configuration file in the physical machines.

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-mini-1"
              launcher:
                ssh:
                  credentialsId: "macosx_slave"
                  host: "10.63.199.2"
                  port: 22
                  javaPath: "/Users/jenkins/.jenv/shims/java"
                  sshHostKeyVerificationStrategy: "nonVerifyingKeyVerificationStrategy"
              mode: NORMAL
              name: "mac_mini_slave_1"
              nodeDescription: "Mac Mini in 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.

You can see an example here.

Configure OpenVPN

Even though the current setup is already working, it would be nice to be able to connect to the machines ourselves, not only Jenkins. We can do this through OpenVPN, after configuring a couple of things.

Since OpenVPN is deployed in the Transit VPC project, we need to advertise the subnet that contains the physical machines. This is important to make this subnet accessible through OpenVPN (in Transit VPC).

inputs {
  ...

  bgp {
    ...
    advertised_ip_ranges = [
      ...,
      {
        description = "Subnet for physical machines in office"
        range       = "10.63.199.0/24"
      },
      ...
    ]
    ...
  }

  ...
}

Note that the IP above is the one we are using as an example throughout this document.

You can see how & where to do this here.

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.

  "physical-machine-slave" = {
    "dns_name"           = "physical-machine-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 10.63.199.0/24  should be accessible through OpenVPN. As a reminder, this is the network that contains the physical machines that we want to connect to Jenkins. Note that this CIDR block is the example that we are using throughout this document, but you can adjust it to your needs.