Step 1 of 7 in the DevOps journey · see the full journey
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.
- 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.exewas 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
enp0s3being 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 + VirtualBoxThe documented baseline uses four main VMs on jammy64 with static addressing. This setup required Windows-side provisioning and explicit Windows/WSL integration. -
Host-only network192.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 51820VPN-only management access, with sshd reachable but the management path never public. -
SSH hardeningKey-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 balancerThe edge tier that terminates client traffic. Needed 2 GB to boot in 1m29s instead of 7m24s.
-
- monitoring
-
-
Prometheus + GrafanaThe first metrics pipeline in the journey. It is the point where the fleet became observable rather than merely configured.
-
- hardening
-
-
UFW + Fail2BanDeny-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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.