This site uses third-party cookies, learn more or accept

Set up a Linux testing environment on Windows

Did you know you can set up a Ubuntu virtual machine on Windows and communicate between the two?
Written by Maxwell Pelic,

For a while I have been running an Apache testing server on my Windows 11 machine, but I realized the differences between it and my Ubuntu production servers might cause issues in the future. I had already encountered some differences when integration Python and PHP in the same server, so I figured my best bet was to set up a Linux testing server on Windows.

It turns out, with a little googling and trying things out, it is pretty simple to integrate the two systems using Windows’ WSL (Windows Subsystem for Linux). To save you the time and research I had to go through, here is the process I made.

WSL Installation & Setup

First, you can view a list of Linux distributions by running the PowerShell command wsl --list --online. Once you select a distribution (you probably want the same distro that your production server is using), you can install it using wsl --install -d <distro>. For example, I am using Ubuntu 20.04, so I ran wsl --install -d Ubuntu-20.04.

You’ll now be able to launch the distro by running wsl -d <distro>, and you can communicate with it using the PowerShell wsl command.

You can learn more about WSL here.

Apache Installation & Setup

I’ll leave the details of the Apache setup to you, but I installed apache2, python, PHP, and MySQL. Since I have multiple sites I’m working on, I created virtual hosts for each one and came up with a testing domain name to distinguish them.

Creating Certificates

I wanted to simplify the process of creating locally signed certificates so I could access my sites using HTTPS. I created a PowerShell script that takes the domain name(s) the virtualhost will use and creates and copies certificates to the Ubuntu VM.

First, you’ll need to make a Certificate Authority to sign the certificates, and install it as a trusted root certificate on both machines. Navigate to the folder where you want to store the CA and run the following commands:

openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout RootCA.key -out RootCA.pem -subj "/C=US/CN=Example-Root-CA"
openssl x509 -outform pem -in RootCA.pem -out RootCA.crt

Once you create the certificates, you’ll need to install them on each system. For windows, you can just open the certificate, click “Install”, and choose the “Trusted Root Certification Authorities” store. For Ubuntu, you’ll need to copy the RootCA.crt file to /usr/local/share/ca-certificates and run sudo update-ca-certificates to install it.

Now that you have the RootCA files, we can create a script to automate the certificate creation process. I created a file called MakeCert.ps1 and put it in the folder with the root certificate, and created a file called .domains.txt to list the domains I want to secure. The script ended up looking like this (you can change the certificate details to whatever you want):

#write default values
$file_path = '' + (Get-Location) + '\.domains.txt'
Set-Content -Path $file_path -Value "authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]"

#if no args, exit
if ( $args.count -eq 0 ) {
    Write-Host "No arguments supplied"
    exit
}

#add each domain to the file
for ( $i = 0; $i -lt $args.count; $i++ ) {
    Add-Content -Path $file_path -Value "DNS.$i = $($args[$i])"
} 
$file_names = $args[0]

#the certificates will be in their own folder to keep things organized
#delete folder if it already exists
if (Test-Path ".\$file_names") {
    Remove-Item -Path ".\$file_names" -Recurse -Force
}
#make cert
New-Item -Path ".\$file_names" -ItemType Directory

openssl req -new -nodes -newkey rsa:2048 -keyout ".\$file_names\cert_export.key" -out ".\$file_names\cert_export.csr" -subj "/C=US/ST=Michigan/L=LocalDevelopment/O=Local-Certs/CN=LocalPowerShellScript"
openssl x509 -req -sha256 -days 1024 -in ".\$file_names\cert_export.csr" -CA RootCA.pem -CAkey RootCA.key -CAcreateserial -extfile $file_path -out ".\$file_names\cert_export.crt"

#copy the cert to wsl
$ubuntu_cert_path = '/var/www/certs/' + $file_names
"ubuntu" | wsl -d Ubuntu -- sudo -S mkdir $ubuntu_cert_path
"ubuntu" | wsl -d Ubuntu -- sudo -S cp -r ./$file_names/* $ubuntu_cert_path

#get the data to add to the config file
Write-Host "

Add to config file: 

SSLEngine on
SSLCertificateFile $ubuntu_cert_path/cert_export.crt
SSLCertificateKeyFile $ubuntu_cert_path/cert_export.key"

You can run the file by passing the domain names you want to secure as arguments. For example, if I wanted to secure example.com and www.example.com, I would run .\MakeCert.ps1 example.com www.example.com. The script will create a folder with the first domain name you pass, and copy the certificates to the Ubuntu VM. It will also print the data you need to add to the Apache config file.

I based the certificate creation on this helpful article.

Connecting to the VM

Connecting to the Virtual Machine from Windows and vice-versa is a little difficult, because the IP addresses of both machines can change. I ended up adapting an answer from this question to make a PowerShell script to update the hosts file on Windows and run a startup script on Linux.

$runningDirectory = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition

$hostsFile        = "c:\windows\system32\drivers\etc\hosts"
#make this file in the same directory as the script with a list of hostnames you want to use
$wslHostnamesFile = "$runningDirectory\Ubuntu-hostnames.txt"
$tmpFile          = "$runningDirectory\.new-hostnames.txt"
$distroName       = "Ubuntu"

# Get IP address of WSL distro
$wslIpAddr = wsl -d $distroName -- ip addr
$match = [System.Text.RegularExpressions.Regex]::Match($wslIpAddr, "(?<ip>(172|192\.168)\.[\d\.]*)\/")
$ip = $match.Groups["ip"]

Write-Host "$distroName is available at $ip"


# read hosts file and change the IPs
$host_file_contents = Get-Content $hostsFile -Encoding UTF8 -Raw
if($null -eq $host_file_contents) { $host_file_contents = "" }
$hostnames = Get-Content $wslHostnamesFile -Encoding UTF8

#loop through the hosts names and either add them or update the IP
foreach ($hostname in $hostnames) {

    Write-Host "Adding $ip $hostname to hosts file"
    if($host_file_contents -like "*`t$hostname*"){
        $host_file_contents = [System.Text.RegularExpressions.Regex]::Replace($host_file_contents, "(\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b)\s+$hostname", "$ip`t$hostname") 
        continue
    }
    $host_file_contents += "`n$ip`t$hostname"
    
}

#trim the file
$host_file_contents = $host_file_contents.trim()

# Save hosts file
$host_file_contents | Set-Content -Path $tmpFile -Encoding UTF8 

# Compare two files to avoid unnecessary popups
$result = $true
$old_hosts_content = (Get-Content $hostsFile)
if($null -ne $old_hosts_content -and ($old_hosts_content).trim() -ne "") {
    $result = Compare-Object -ReferenceObject ($old_hosts_content).trim() -DifferenceObject ((Get-Content $tmpFile).trim())
}
if ($result)
{
    # Prepare and execute encoded command (note that we use 'pwsh' instead of 'powershell')
    $command = "Get-Content -Path ""$tmpFile"" | Set-Content -Path ""$hostsFile"""
    Write-Host "Updating hosts file with: $command"
    $encodedCmd = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
    Start-Process -FilePath powershell -Verb RunAs -ArgumentList "-encodedcommand $encodedCmd"
}
else 
{
    Write-Host "Hosts file is the same, modification is unnecessary"
}

# Start services inside the WSL
wsl -d $distroName -e echo ubuntu | wsl -d $distroName -e sudo -S /root/start_services.sh

Start-Sleep -Seconds 5

# Remove temporary file
Remove-Item $tmpFile

In addition to updating the hosts file, the script runs a bash script located at /root/start_services.sh on the Ubuntu VM. This script starts the Apache server. I run MySQL on Windows, so I also added windows as a hostname for the Windows computer. You can add any other commands you want to run on startup to this script.

#!/bin/bash

source ~/.bash_profile

#start ubuntu server

echo password | sudo -S service apache2 start

#start anything else here

#update hosts file for windows
IP=$(dig +short HostComputer.local | head -n1)

echo ubuntu | sudo -S sed -i "/windows/ s/.*/$IP\twindows/g" /etc/hosts

Note: you’ll need to replace HostComputer with the name of your Windows computer. You’ll need to replace “password” with the password for the Ubuntu user.

Conclusion

That’s it, you can run the MakeCert.ps1 script to create a certificate, and then connect to the VM from Windows. You can use the startup script to start (or restart) the Ubuntu server.

Previous Article: I Made a Chess game in JavaScript