Some projects start with a clear plan. This one started with a question: if I spend my days managing infrastructure for clients, why am I not running any of my own?
Layer 8 is the answer. This is the story of how it was built โ from a blank screen to a live, publicly accessible, self-hosted blog with real security monitoring โ and everything that happened along the way.
The starting point
The infrastructure was already there. A Proxmox hypervisor running a Ryzen 9 7950X with 96GB RAM, a UniFi Cloud Gateway Fibre managing the network, TrueNAS for block storage, a Synology NAS for file shares and backups. Plenty of headroom. What was missing was something worth hosting.
The goal was a blog โ somewhere to document homelab projects, write up technical walkthroughs, and keep a record of what I build and break. The secondary goal was to do it properly: not a cloud-hosted WordPress install, not a managed service, but a genuinely self-hosted stack running on my own hardware, behind my own firewall, with full control over every layer.
Choosing the stack
The first decision was the platform. Ghost was tempting โ polished, capable, good editor โ but it brings Node.js, MySQL, and ongoing dependency management. For a blog that’s primarily static content, that’s a lot of moving parts to maintain and a larger attack surface to manage.
Hugo won. It generates pure HTML files โ no database, no runtime, no API surface. Nginx serves static files. The only code running is the web server itself, which is about as minimal an attack surface as you can get for a public-facing service.
For hosting, an LXC container on Proxmox rather than a full VM. Lightweight, fast to spin up, easy to snapshot. Two cores, 2GB RAM, 20GB disk โ the hypervisor barely notices it’s running.
Network architecture: the DMZ
Before writing a single line of code, the network architecture needed to be right. Putting a public-facing server on the same network as your main LAN is asking for trouble โ if the server is compromised, the attacker has a foothold inside your network.
The solution is a DMZ VLAN. A separate network segment that the public server lives in, isolated from everything else by firewall rules.
On Proxmox, a second Linux bridge (vmbr1) was created with no IP address assigned at the host level, connected to a dedicated physical NIC (nic0). On UniFi, that NIC’s switch port was configured as an access port on VLAN 30 โ the DMZ.
The critical firewall rules in UniFi:
Allow WAN โ 172.20.30.10:443 (HTTPS)
Allow WAN โ 172.20.30.10:80 (HTTP โ redirect)
Allow DMZ โ WAN (outbound: updates, Let's Encrypt)
Block DMZ โ LAN (isolation โ the important one)
Block DMZ โ Management VLAN
That last rule is the one that matters most. Even if the blog server is fully compromised, the attacker cannot reach the rest of the network. The DMZ is a dead end.
The LXC container was given a static IP of 172.20.30.10/24 with a gateway of 172.20.30.254 โ the UniFi firewall. From inside the container, the main LAN is completely unreachable.
Designing the site
With the infrastructure in place, the design came next. The brief was simple: minimalist, dark theme that follows system preferences, clean typography, nothing that looks like a generic blog template.
After several iterations, the final design settled on:
- Instrument Serif for display text โ elegant, editorial, with enough personality to stand out without being loud
- Inter for body text โ clean, modern, highly readable
- Blue accent (
#2563eblight /#5b8df5dark) used sparingly throughout - Dark background (
#0e1117) with subtle card backgrounds and fine borders
The homepage splits into two columns: a hero section on the left with the title and tagline, and a live information panel on the right showing a clock, service status indicators, and news feeds. Below that, expandable project cards โ click any card to see a synopsis and summary, with a “Read full โ” link to the complete writeup.
The name came fairly naturally once the concept was clear. The OSI model has 7 layers โ physical through application. Layer 8 is the unofficial one: the human. Most interesting problems in tech eventually come back to it.
The live panel
The homepage panel pulls real-time data from multiple sources via a Python script running as a cron job every 15 minutes:
Service status โ Microsoft 365, Google Workspace, Azure, AWS, Cloudflare, and GitHub, each linking to their official status pages. Green dot means operational, amber means degraded.
News โ RSS feeds from The Register, Bleeping Computer, IT Pro UK, Infosecurity Magazine, Graham Cluley, NCSC UK, Sky News Tech, and BBC Tech. UK-focused where possible.
Product updates โ Changelog and blog feeds from Microsoft 365, Google Workspace, Tailscale, Proxmox, and UniFi. The distinction matters: news is things that happened, updates are things that changed.
The script writes everything to a single feed.json file in the static directory. Hugo serves it as a static file, and the homepage JavaScript reads it on page load and refreshes every 30 seconds.
output = {
"updated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"status": [m365_status(), google_status(), azure_status(), ...],
"news": news,
"updates": updates,
"proxmox": get_proxmox_stats()
}
The Proxmox stats โ CPU usage, memory, and uptime โ come from the Proxmox API directly, authenticated with an API token scoped to read-only node statistics. A specific firewall rule allows the LXC to reach the Proxmox management interface on port 8006, and only on that port.
The About page
The About page ended up being one of the more interesting parts of the build. Rather than a static wall of text, it has two interactive elements:
Live lab stats โ three cards showing real-time CPU usage, memory consumption, and uptime pulled from the Proxmox API. These update every 30 seconds from the same feed.json the homepage uses.
An interactive terminal โ a fake shell where visitors can type commands and get responses about the setup:
luke@layer8:~$ whoami
luke
IT professional. Home lab enthusiast. I genuinely enjoy what I do.
I work across networking, infrastructure, cloud administration and
identity management โ supporting clients from sole traders to
enterprise. Day to day that means Microsoft 365 tenants, Entra ID,
Intune & Autopilot, Exchange Online, VoIP provisioning and fibre
installs. No two days look the same.
Available commands include whoami, neofetch, ls projects, cat about.txt, uptime, stack, ping, and clear. It’s a small touch, but it sets the tone for what the site is about.
Hardening the server
With the site built and serving, the security hardening layer went on top.
UFW was configured with a default deny-incoming policy, allowing only ports 22, 80, and 443 over TCP. IPv6 was disabled entirely โ not in use, no reason to have it open.
ufw default deny incoming
ufw default allow outgoing
ufw allow proto tcp from any to any port 22
ufw allow proto tcp from any to any port 80
ufw allow proto tcp from any to any port 443
Nginx rate limiting was configured in the http block of nginx.conf:
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s;
And applied in the server block:
limit_req zone=req_limit burst=20 nodelay;
Ten requests per second per IP, with a burst allowance of 20. Enough headroom for normal browsing, enough restriction to throttle automated scanners.
fail2ban was installed and configured with four active jails:
nginx-botsearchโ bans IPs scanning for common vulnerabilities, 24-hour bannginx-http-authโ bans repeated authentication failuresnginx-limit-reqโ bans IPs that repeatedly trip the rate limitersshdโ bans SSH brute force attempts
Security headers were added to every Nginx response:
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
Going public
With the site complete and the security stack in place, the DNS A record was added in Fasthosts:
blog.omnitrix.uk โ 185.245.239.75
DNS propagated globally within minutes. Port forwarding rules were added in UniFi to forward ports 80 and 443 to 172.20.30.10. A quick curl from a Windows PC confirmed the site was publicly accessible.
Certbot handled the SSL certificate:
certbot --nginx -d blog.omnitrix.uk
Let’s Encrypt issued the certificate, Certbot configured Nginx to serve HTTPS and redirect HTTP, and the certificate auto-renews in the background. The baseURL in hugo.toml was updated to https://blog.omnitrix.uk and Hugo rebuilt the site.
Layer 8 was live.
Under attack within the hour
Less than an hour after going public, the first real attack appeared in the UniFi IDS/IPS logs:
CEF:0|Ubiquiti|UniFi Network|...
src=148.116.84.28
dst=172.20.30.10
UNIFIpolicyName=CINS Army Reputation List
UNIFIipsSignature=ET CINS Active Threat Intelligence Poor Reputation IP group 203
act=blocked
An IP from Canada, flagged by the CINS Army Reputation List โ a threat intelligence feed of known malicious actors โ attempted to connect to the blog server on port 443. UniFi’s IDS/IPS matched the signature and dropped the packet before it reached Nginx.
The Nginx error log also showed a credential harvesting bot from 45.88.138.44 attempting to access:
/web/.env
/id_ed25519
/server.pem
/privkey.pem
/id_rsa
/.ssh/id_ed25519
The rate limiter fired on it โ limiting requests, excess appearing repeatedly in the error log โ throttling its scan before it could complete. None of these files exist on the server, and even if they did, they’d never be reached at that request rate.
The full security stack in order:
Malicious IP
โ
โผ
UniFi IDS/IPS (Suricata) โ known bad actors blocked at the firewall
โ
โผ
UniFi Firewall rules โ only 80/443 reach the DMZ
โ
โผ
UFW โ secondary firewall on the container itself
โ
โผ
Nginx rate limiting โ throttles scanners and bots
โ
โผ
fail2ban โ bans IPs that trip thresholds
โ
โผ
DMZ isolation โ even a full compromise can't reach the LAN
What’s running now
The final stack serving this site:
- Hugo โ static site generator, builds in under 30ms
- Nginx 1.24 โ serves static files, handles SSL termination and rate limiting
- Certbot โ manages the Let’s Encrypt certificate with auto-renewal
- fail2ban โ 4 active jails watching Nginx and SSH logs
- UFW โ default deny, ports 22/80/443 only
- Python cron job โ fetches news, status, and Proxmox stats every 15 minutes
- Proxmox LXC โ Ubuntu 24.04, 2 cores, 2GB RAM, 20GB disk
- UniFi CG Fibre โ firewall, VLAN segmentation, IDS/IPS via Suricata
The entire thing runs on hardware I already owned, costs nothing beyond the domain registration, and is more capable than most managed hosting setups I’ve seen.
That’s the point of a home lab.