Skip to content

Step 1 of 7 in the DevOps journey · see the full journey

2025

Server Sorcery 101

A hand-configured VM network built with Vagrant and VirtualBox.

Overview

I built a local VM network to practise Linux administration, access control and monitoring before deploying an application. Vagrant created the machines; I configured the services and investigated failures in the management path.

context
Coursework
status
Archived learning project
role
Solo
started
Apr 2025
setting
Local Vagrant and VirtualBox VM lab; infrastructure configuration without an application deployment.
built with
  • Vagrant
  • VirtualBox
  • Ubuntu 22.04
  • Nginx
  • WireGuard
  • Prometheus
  • +4

Key decisions

Establish the network before adding an application
The documented baseline separates the load balancer, web tier and app host, with private addressing and a VPN management path.
Harden access without disabling provisioning
Key-based SSH, firewall rules and Fail2Ban had to coexist with Vagrant's own user and NAT management interface.

Demonstrated results

Verified an SSH ban and its expiry
The journal records failed-login testing, a reported Fail2Ban ban and access returning after the configured ten-minute interval.
Recorded a substantially faster boot after resizing
Increasing the load balancer to 2 GB reduced recorded startup time from about 7 minutes 24 seconds to 1 minute 29 seconds in this lab.

Known limitations

Configuration was still largely manual
This stage establishes the baseline rather than an Ansible-reproducible fleet. The following automation project addresses repeated service configuration.
Provisioning depended on the host environment
Windows and WSL integration, adapter state and the NAT management path all needed attention. These results describe this local setup.

No application here

This stage configures infrastructure without deploying the application. It is the baseline for comparing the later automation and deployment work.

What is here instead is the substrate every later stage inherits: the private addressing, the hardening posture, and a monitoring stack. Stage 3 will eventually make load-balancer decisions using it. The four-host topology describes that baseline; the archived Vagrantfile also contains a later backup host, noted in the reference figures.

Access controls, checked

I configured SSH keys, a dedicated devops account, UFW rules and an sshd jail. Restricting AllowUsers to devops initially locked out Vagrant's provisioning user, so that account had to be retained too. The NAT-forwarded SSH connection also needed a rule matching the guest-visible gateway address, 10.0.2.2.

The Fail2Ban test first revealed that the service was not running. After starting it, repeated failed logins produced a ban visible in fail2ban-client status sshd. The journal records access returning after ten minutes, and a later change enables the service at boot. That is a functional check of this jail, not a full security audit.

Boot-time comparison

The load balancer's recorded startup time was 7min 23.627s. After increasing its memory allocation to 2 GB, the next recorded result was 1min 28.996s. These observations helped me investigate local resource allocation; they are not a general benchmark for Nginx or virtual machines.

The seven hours

The dominant entry in the journal is a single afternoon lost to Vagrant being unable to reach VirtualBox from WSL:

Vagrant is unable to use the VirtualBox provider from the Windows Subsystem for
Linux without access to the Windows environment.

That was only the first of five distinct failures. Each looked unrelated, and together they consumed more than seven hours:

  • the Windows PATH was not shared with WSL, so cmd.exe was not found
  • shared folders had to be disabled because host paths must be on DrvFs
  • the VirtualBox driver service was not running
  • the guest's serial port had to be disconnected to avoid a raw-file error

The serial-port workaround was carried into the next project's Vagrantfile.

The one worth remembering

The most instructive failure is not a tooling problem. It is the interface that was hardened away:

The NAT adapter was disabled in order to force all egress through a central proxy. This broke Vagrant's management channel. I investigated firewall rules, bans and interface configuration, recording which hypotheses the checks supported or rejected.

The connection still timed out. This was a critical finding. It proved the issue was not a UFW rule.

The cause turned out to be a line I had changed earlier:

# /etc/netplan/50-cloud-init.yaml
dhcp4: false

The development record describes the management-channel conflict:

Flaw: This was a good security idea in principle, but it broke Vagrant's fundamental management channel, which relies on enp0s3 being active. For a Vagrant-managed environment, this approach is not viable.

The revised policy keeps the interface and uses ufw default deny outgoing with explicit allow rules instead. Same intent, no lockout.

Stack and source figures

how it fits together

provisioning
  • Vagrant + VirtualBox The documented baseline uses four main VMs on jammy64 with static addressing. This setup required Windows-side provisioning and explicit Windows/WSL integration.
  • Host-only network 192.168.56.0/24 with .10–.13 assigned. The secondary adapter has to be brought up by provisioning, or the management path used later in the journey does not exist.
access
  • WireGuard on UDP 51820 VPN-only management access, with sshd reachable but the management path never public.
  • SSH hardening Key-only auth, a dedicated devops user, a restricted umask. AllowUsers lists vagrant as well as devops, because hardening that locks provisioning out is a self-inflicted outage.
edge
  • Nginx load balancer The edge tier that terminates client traffic. Needed 2 GB to boot in 1m29s instead of 7m24s.
monitoring
  • Prometheus + Grafana The first metrics pipeline in the journey. It is the point where the fleet became observable rather than merely configured.
hardening
  • UFW + Fail2Ban Deny-by-default inbound rules. The SSH allow rule has to name the VirtualBox NAT gateway (10.0.2.2), not the host-only range, or it never matches.
VMs
4 The Vagrantfile defines 5. The fifth is a backup server added late and documented nowhere else.
commits
35
journal
2,057 lines
Full issue record · 7 entries

what broke, and what happened

  • 7 fixed / 7

Every entry below says what broke and what became of it. An unresolved one is listed as unresolved rather than dropped.

  1. fixed

    Vagrant could not drive VirtualBox from WSL at all. More than seven hours were lost before it worked

    `Vagrant is unable to use the VirtualBox provider from the Windows Subsystem for Linux without access to the Windows environment.`

    then: The VMs were built on Windows proper and then driven from WSL over the host-only network. The whole `192.168.56.0/24` topology exists to support that path.

  2. fixed

    The management interface was left DOWN, so Vagrant could never reach the host-only network it was supposed to configure

    `enp0s8` was in state DOWN with no IPv4 address, while sshd listened correctly on `0.0.0.0:22`.

    then: The secondary adapter was brought up in the Vagrant provisioning. That let the later stages address the VMs by their `192.168.56.x` addresses at all.

  3. fixed

    A firewall rule that could never match, because Vagrant reaches the guest through the NAT port forward

    The guest sees `10.0.2.2`, so `ALLOW IN from 192.168.56.0/24` never applies. Fixed by allowing from `10.0.2.2` to port 22.

    then: The rule allows the VirtualBox NAT gateway instead of the host-only range. Applied by hand rather than through Ansible, because this stage is Vagrant and manual configuration only.

  4. fixed

    Hardening locked Vagrant out of its own machines

    `AllowUsers devops` → `vagrant@192.168.56.10: Permission denied (publickey)`. Fixed: `AllowUsers devops vagrant`.

    then: The SSH configuration lists `vagrant` alongside `devops` in `AllowUsers`, preserving provisioning access in this manually configured stage.

  5. fixed

    Disabling the NAT interface to force egress through a proxy broke Vagrant entirely. Three hypotheses were tested and disproven before the cause surfaced

    `50-cloud-init.yaml` contained `dhcp4: false`, which I had set during the earlier proxy work. The VM had lost its IPv4 address on `enp0s3`.

    then: A config the earlier proxy work had introduced, reverted so the interface keeps its DHCP lease. The three wrong hypotheses cost more than the fourth would have.

  6. fixed

    Fail2Ban was not enabled at boot, so the sshd jail silently did nothing

    `Failed to access socket path: /var/run/fail2ban/fail2ban.sock. Is fail2ban running?` The jail was configured correctly and enforcing nothing.

    then: Started the service, then proved it rather than assuming it: six failed logins produced `Connection refused`, and `fail2ban-client status sshd` reported the ban.

  7. fixed

    The load balancer took over seven minutes to become SSH-reachable

    Startup finished in `7min 23.627s`. After raising its RAM: `1min 28.996s`.

    then: Raised to 2 GB in the Vagrantfile (`lb_vb.memory = "2048"`), which brought startup under a minute and a half.