first commit
This commit is contained in:
173
content/posts/pf-vlans.md
Normal file
173
content/posts/pf-vlans.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
title: "Setting Up pf with VLANs"
|
||||
date: 2025-02-10
|
||||
tags: [pf, networking, openbsd]
|
||||
slug: pf-vlans
|
||||
description: "Configuring OpenBSD pf.conf with VLAN segmentation — separating servers, desktop, IoT, and game traffic with sensible firewall rules."
|
||||
draft: false
|
||||
---
|
||||
|
||||
Network segmentation is one of the first things to get right. Once it's working, everything
|
||||
else builds on top of it. Once it's broken, debugging why `ssh` works but `rsync` doesn't
|
||||
becomes a special kind of misery.
|
||||
|
||||
This post covers the VLAN setup and the pf rules that go with it.
|
||||
|
||||
## The VLAN Layout
|
||||
|
||||
Five VLANs, each on a different subnet:
|
||||
|
||||
| VLAN | ID | Subnet | Purpose |
|
||||
|------|-----|---------------|--------------------------------|
|
||||
| mgmt | 1 | 10.0.1.0/24 | Switches, OOB, firewall mgmt |
|
||||
| srv | 10 | 10.0.10.0/24 | Servers (srv01, srv02) |
|
||||
| desk | 20 | 10.0.20.0/24 | Desktop and personal devices |
|
||||
| game | 30 | 10.0.30.0/24 | Game clients and VMs |
|
||||
| iot | 40 | 10.0.40.0/24 | Untrusted / IoT / Guest |
|
||||
|
||||
The physical layout: one NIC on fw01 is trunked to the main switch. OpenBSD VLAN interfaces
|
||||
(`vlan10`, `vlan20`, etc.) are configured on top of it. Each VLAN interface gets an IP address
|
||||
in its respective subnet and acts as the default gateway for devices in that VLAN.
|
||||
|
||||
## Configuring VLAN Interfaces
|
||||
|
||||
In `/etc/hostname.em1` (the trunked NIC):
|
||||
```
|
||||
up
|
||||
```
|
||||
|
||||
Then individual VLAN interface files, e.g. `/etc/hostname.vlan10`:
|
||||
```
|
||||
vlandev em1 vlanid 10
|
||||
inet 10.0.10.1 255.255.255.0
|
||||
up
|
||||
```
|
||||
|
||||
Repeat for each VLAN. After a reboot (or `sh /etc/netstart`), `ifconfig` should show
|
||||
all the VLAN interfaces up with their addresses.
|
||||
|
||||
## The pf Configuration
|
||||
|
||||
This is a simplified version of the actual `pf.conf`. The real one has more rules, but
|
||||
this captures the structure.
|
||||
|
||||
```
|
||||
# /etc/pf.conf
|
||||
|
||||
# --- Interfaces ---
|
||||
ext_if = "em0" # WAN
|
||||
vlan_mgmt = "vlan1"
|
||||
vlan_srv = "vlan10"
|
||||
vlan_desk = "vlan20"
|
||||
vlan_game = "vlan30"
|
||||
vlan_iot = "vlan40"
|
||||
|
||||
# --- Tables ---
|
||||
table <martians> const { 0.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, \
|
||||
172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 }
|
||||
|
||||
# --- Options ---
|
||||
set block-policy drop
|
||||
set loginterface $ext_if
|
||||
set skip on lo
|
||||
|
||||
# --- Normalization ---
|
||||
match in all scrub (no-df random-id max-mss 1440)
|
||||
|
||||
# --- Default deny ---
|
||||
block all
|
||||
|
||||
# --- Antispoofing ---
|
||||
antispoof for $ext_if inet
|
||||
|
||||
# --- Block martians on WAN ---
|
||||
block in quick on $ext_if from <martians> to any
|
||||
block out quick on $ext_if from any to <martians>
|
||||
|
||||
# --- Inbound: allow public traffic to services ---
|
||||
pass in on $ext_if proto tcp to port 80 keep state
|
||||
pass in on $ext_if proto tcp to port 443 keep state
|
||||
pass in on $ext_if proto tcp to port 22 keep state
|
||||
pass in on $ext_if proto udp to port 51820 keep state # WireGuard
|
||||
|
||||
# --- Allow all outbound from firewall ---
|
||||
pass out on $ext_if all keep state
|
||||
|
||||
# --- Management VLAN: full access ---
|
||||
pass in on $vlan_mgmt all keep state
|
||||
pass out on $vlan_mgmt all keep state
|
||||
|
||||
# --- Server VLAN: allow inter-VLAN from desk to srv ---
|
||||
pass in on $vlan_srv all keep state
|
||||
pass in on $vlan_desk to $vlan_srv keep state
|
||||
# Block srv -> desk (servers shouldn't initiate to desktop)
|
||||
block in on $vlan_srv to 10.0.20.0/24
|
||||
|
||||
# --- Desktop VLAN: full internet, access to servers ---
|
||||
pass in on $vlan_desk all keep state
|
||||
pass out on $vlan_desk all keep state
|
||||
|
||||
# --- Game VLAN: internet only, no access to other VLANs ---
|
||||
pass in on $vlan_game proto { tcp udp } to port { 80 443 } keep state
|
||||
pass in on $vlan_game proto udp keep state # game protocols
|
||||
block in on $vlan_game to 10.0.0.0/8 # no access to RFC1918
|
||||
|
||||
# --- IoT VLAN: internet only, fully isolated ---
|
||||
pass in on $vlan_iot proto tcp to port { 80 443 } keep state
|
||||
pass in on $vlan_iot proto udp to port 53 keep state # DNS
|
||||
block in on $vlan_iot to 10.0.0.0/8
|
||||
```
|
||||
|
||||
## The Key Design Decisions
|
||||
|
||||
**Default deny with explicit allows** is the only sane approach. Start with `block all` and
|
||||
add passes for what you actually need. Never the other way around.
|
||||
|
||||
**IoT is fully isolated.** Smart home devices get internet access and nothing else. They
|
||||
cannot reach any other VLAN. If one of them is compromised, the blast radius is just
|
||||
"attacker can make API calls from your IP." Annoying, not catastrophic.
|
||||
|
||||
**Game VLAN blocks RFC1918.** Game clients and VMs get internet but cannot reach any
|
||||
private address space. This isolates them from everything internal.
|
||||
|
||||
**Servers can't initiate to desktop.** A compromised service on srv01 shouldn't be able to
|
||||
reach my desktop. The server VMs serve; they don't call home.
|
||||
|
||||
## relayd for Reverse Proxying
|
||||
|
||||
External traffic hits fw01 on port 80/443. `relayd(8)` forwards it to srv01. The pf rules
|
||||
above allow the initial connection in; relayd handles the rest.
|
||||
|
||||
Minimal `/etc/relayd.conf`:
|
||||
|
||||
```
|
||||
# TLS termination and forwarding
|
||||
http protocol "https" {
|
||||
tls { keypair ridgwaysystems.org }
|
||||
pass request header "X-Forwarded-For" value "$REMOTE_ADDR"
|
||||
pass
|
||||
}
|
||||
|
||||
relay "web" {
|
||||
listen on egress port 443 tls
|
||||
protocol "https"
|
||||
forward to 10.0.10.10 port 8080 check http "/" code 200
|
||||
}
|
||||
```
|
||||
|
||||
Let's Encrypt certificates via `acme-client(1)` handle the TLS side. A daily cron job
|
||||
runs `acme-client` and sends SIGHUP to relayd when certs are renewed.
|
||||
|
||||
## Debugging
|
||||
|
||||
When rules aren't working as expected, `pfctl -ss` shows current state table entries.
|
||||
`tcpdump -i pflog0` shows what pf is logging. `pfctl -sr` shows the active ruleset.
|
||||
|
||||
The most common mistake: forgetting that `block` rules need `quick` to stop rule evaluation
|
||||
immediately, while without `quick` pf continues evaluating and the last matching rule wins.
|
||||
Learn this early.
|
||||
|
||||
## What's Next
|
||||
|
||||
With the network segmented, the next step is getting services deployed on srv01 — starting
|
||||
with httpd and this website.
|
||||
98
content/posts/the-hardware.md
Normal file
98
content/posts/the-hardware.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: "The Hardware: What's in the Rack"
|
||||
date: 2025-01-28
|
||||
tags: [hardware, homelab]
|
||||
slug: the-hardware
|
||||
description: "A tour of the physical hardware — SuperMicro 1U firewall, Dell R720 primary server, Dell R710 secondary, and the desktop control node."
|
||||
draft: false
|
||||
---
|
||||
|
||||
Before getting into software configuration, it's worth documenting the physical layer. What's
|
||||
actually in the rack, why those machines, and what each one does.
|
||||
|
||||
## The Firewall: SuperMicro 1U
|
||||
|
||||
The firewall is a SuperMicro 1U server with a Xeon E3-1230v2 and 16GB ECC RAM. This runs
|
||||
OpenBSD and handles everything at the network edge:
|
||||
|
||||
- **pf** — stateful packet filtering, VLAN routing
|
||||
- **relayd** — reverse proxy, TLS termination for external services
|
||||
- **WireGuard** — VPN for remote access
|
||||
- **unbound** — recursive DNS resolver for internal clients
|
||||
- **dhcpd** — DHCP for all VLANs
|
||||
|
||||
The E3-1230v2 is modest by current standards but easily handles firewall workloads. More
|
||||
importantly, it supports AES-NI (important for VPN throughput) and runs cool and quiet.
|
||||
|
||||
Multiple NICs: one for WAN, one trunked to the main switch carrying tagged VLANs for each
|
||||
network segment.
|
||||
|
||||
## The Primary Server: Dell R720
|
||||
|
||||
The R720 is the workhorse. Dual Xeon E5-2600 series processors, 64GB ECC RAM, a few SSDs
|
||||
for the OS and data volumes.
|
||||
|
||||
This runs OpenBSD and hosts:
|
||||
|
||||
- **httpd** — web server for this site
|
||||
- **Gitea** — self-hosted git
|
||||
- **OpenSMTPD** — email (outbound, plus some inbound)
|
||||
- **Prometheus + Grafana** — metrics collection and dashboards
|
||||
- **Matrix** (Conduit) — self-hosted chat
|
||||
- **Various smaller services** — RSS aggregator, bookmarks, etc.
|
||||
|
||||
The R720 is loud. Server-grade loud. It's in a separate room with the rack, so this is
|
||||
tolerable. The fans throttle down after a few minutes under light load, but under any real
|
||||
IO they spin up fast.
|
||||
|
||||
Power draw is significant — plan for 150-300W depending on load. Not a machine you run
|
||||
on a home circuit without thinking about it.
|
||||
|
||||
## The Secondary Server: Dell R710
|
||||
|
||||
The R710 is older — Xeon 5500/5600 series — but has more RAM slots, currently at 48GB.
|
||||
It runs a mix of OpenBSD base with some Linux VMs managed by `vmm(4)`.
|
||||
|
||||
Primary roles:
|
||||
|
||||
- **nsd** — authoritative DNS for ridgwaysystems.org zones
|
||||
- **Linux VMs** — game servers (Minecraft, Valheim, etc.), running in `vmm(4)`
|
||||
- **Jellyfin** — media server
|
||||
- **Backup target** — receiving rsync backups from srv01
|
||||
|
||||
The R710 is even louder than the R720 under load. Old server hardware wasn't designed with
|
||||
home environments in mind. ILO (Dell iDRAC) makes remote management workable — I rarely
|
||||
need to touch it physically.
|
||||
|
||||
## The Desktop: Daily Driver and Ansible Control Node
|
||||
|
||||
Standard desktop setup: Ryzen, 32GB RAM, NVMe storage, running Linux.
|
||||
|
||||
This is the Ansible control node. All infrastructure changes go through playbooks on this
|
||||
machine and push to the servers. The goal is to get to a point where I could wipe any server
|
||||
and bring it back up cleanly with `ansible-playbook`.
|
||||
|
||||
Not there yet — the playbooks are more "documentation that happens to be executable" than
|
||||
a fully idempotent rebuild-from-scratch system. That's the project.
|
||||
|
||||
## Why Old Server Hardware?
|
||||
|
||||
Two reasons: price and ECC RAM.
|
||||
|
||||
A Dell R720 can be had for $200-400 on eBay depending on configuration. For that price you
|
||||
get two server-grade CPUs, ECC RAM, hot-swap storage bays, iDRAC out-of-band management,
|
||||
and hardware RAID if you want it. Nothing in the consumer space touches this value per
|
||||
dollar for a home server.
|
||||
|
||||
The tradeoffs are power consumption and noise. For a rack in a utility room or basement,
|
||||
those are manageable.
|
||||
|
||||
ECC RAM matters for a storage server. Silent corruption from a bit flip in a RAID controller
|
||||
or filesystem is the worst kind of failure — the kind you don't notice until you need the
|
||||
data. ECC doesn't eliminate all failure modes, but it eliminates the commonest one.
|
||||
|
||||
## What's Next
|
||||
|
||||
Next up: the pf configuration and VLAN setup. This is where most of the interesting work
|
||||
happens — separating untrusted IoT devices from servers, routing WireGuard traffic, and
|
||||
setting up relayd to proxy external services.
|
||||
85
content/posts/why-openbsd.md
Normal file
85
content/posts/why-openbsd.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "Why OpenBSD for a Homelab"
|
||||
date: 2025-01-15
|
||||
tags: [openbsd, homelab]
|
||||
slug: why-openbsd
|
||||
description: "The case for running OpenBSD as the foundation of a homelab — security model, pf, clean base system, and the value of good documentation."
|
||||
draft: false
|
||||
---
|
||||
|
||||
A few people have asked why I chose OpenBSD for this homelab instead of the usual suspects —
|
||||
Proxmox, TrueNAS, some flavor of Linux. The short answer: I wanted to actually understand what
|
||||
my infrastructure is doing, and OpenBSD forces that in a way nothing else does.
|
||||
|
||||
## The Security Model Is Different
|
||||
|
||||
Most operating systems bolt security on. OpenBSD builds it in.
|
||||
|
||||
`pledge(2)` and `unveil(2)` are the clearest expression of this. Programs declare up front exactly
|
||||
what syscalls they'll use and which filesystem paths they'll touch. The kernel enforces it. If a
|
||||
daemon gets compromised, the blast radius is bounded by what it pledged. This isn't theoretical —
|
||||
the base system uses these everywhere.
|
||||
|
||||
Compare that to a typical Linux daemon running as root (or even a non-root user) with access to
|
||||
the full filesystem. The attack surface is enormous by default.
|
||||
|
||||
## pf Is the Best Firewall I've Used
|
||||
|
||||
I've spent time with iptables, nftables, and a few others. `pf(4)` is in a different category.
|
||||
|
||||
The rule syntax reads like English. State tracking is intelligent by default. `relayd(8)` handles
|
||||
reverse proxying and TLS termination in a way that integrates naturally with pf. The whole
|
||||
networking stack hangs together coherently.
|
||||
|
||||
Here's a minimal pf.conf to give a sense of the syntax:
|
||||
|
||||
```
|
||||
set skip on lo
|
||||
|
||||
block all
|
||||
|
||||
pass in on egress proto tcp to port { 80 443 } keep state
|
||||
pass in on egress proto tcp to port 22 keep state
|
||||
pass out all keep state
|
||||
```
|
||||
|
||||
That's readable. You can audit that in two minutes.
|
||||
|
||||
## The Base System Is Clean
|
||||
|
||||
OpenBSD ships a complete, minimal base system. No package manager drama, no systemd, no
|
||||
six-hundred-dependency init system. The base is coherent. Everything in `/usr/bin` and
|
||||
`/sbin` has been reviewed and fits together.
|
||||
|
||||
When I install a service, I know exactly what I'm adding on top of a well-understood foundation.
|
||||
On a typical Linux distro, I'm never quite sure what's lurking in the base.
|
||||
|
||||
## The Documentation Is Accurate
|
||||
|
||||
This is underrated. OpenBSD man pages are written by the people who wrote the code. They are
|
||||
current. They are correct. When the man page says `pledge(2)` takes these promises in this
|
||||
order, that is exactly what happens.
|
||||
|
||||
How many times have you followed a Linux man page only to find it describes behavior from four
|
||||
kernel versions ago? Or read documentation that's accurate for one distro but not another?
|
||||
|
||||
With OpenBSD, the man page is the specification. This matters enormously when you're learning
|
||||
a new tool or debugging an obscure issue at 2am.
|
||||
|
||||
## The Tradeoffs
|
||||
|
||||
OpenBSD isn't the right choice for everything. ZFS isn't in the base (use FreeBSD for that).
|
||||
The package ecosystem is smaller than Linux. Some software just doesn't run on OpenBSD, or
|
||||
runs with limitations.
|
||||
|
||||
For a homelab where the goal is to *understand* the infrastructure rather than just consume
|
||||
services, these tradeoffs are worth it. The constraint of a smaller, more deliberate ecosystem
|
||||
means you end up with a leaner, more auditable system.
|
||||
|
||||
If you want to run Kubernetes on your homelab, OpenBSD is the wrong choice. If you want to
|
||||
actually know what your firewall is doing and why, it's worth the investment.
|
||||
|
||||
## What's Next
|
||||
|
||||
The next post covers the hardware — what's actually in the rack and why those particular
|
||||
machines. After that, I'll get into the pf configuration and VLAN setup.
|
||||
Reference in New Issue
Block a user