feat/replace-pfsense-with-opnsense #29

Merged
mauritz.uphoff merged 3 commits from feat/replace-pfsense-with-opnsense into main 2026-05-27 08:01:11 +00:00
32 changed files with 55 additions and 45 deletions

View file

@ -57,8 +57,8 @@ variable "mgmt_ip_range" {
default = ""
}
variable "pfsense_machine_type" {
description = "Machine type for the pfSense firewall (e.g. c2i.2, c2i.4)."
variable "opnsense_machine_type" {
description = "Machine type for the OPNsense firewall (e.g. c2i.2, c2i.4)."
type = string
default = "c2i.2"
}

View file

@ -12,41 +12,52 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Place pfsense.qcow2 at ./image/pfsense.qcow2 before first apply.
# Download pfSense 2.7.x AMD64 from netgate.com and convert to qcow2 if needed.
resource "null_resource" "opnsense_image_file" {
triggers = {
always_run = timestamp()
}
provisioner "local-exec" {
command = "curl -o opnsense.qcow2 https://opnsense.object.storage.eu01.onstackit.cloud/opnsense-26.1-amd64-21-05-2026.qcow2"
}
lifecycle {
ignore_changes = all
}
}
resource "stackit_image" "pfsense_image" {
# Upload VPN Appliance Image to STACKIT
resource "stackit_image" "opnsense_image" {
project_id = local.hub_project_id
name = "pfsense-2.7.x-amd64"
local_file_path = "./image/pfsense.qcow2"
name = "opnsense-26.1-amd64-image"
local_file_path = "opnsense.qcow2"
disk_format = "qcow2"
min_disk_size = 10
depends_on = [null_resource.opnsense_image_file]
min_disk_size = 16
min_ram = 2
config = {
uefi = false
}
}
resource "stackit_volume" "pfsense_volume" {
resource "stackit_volume" "opnsense_volume" {
project_id = local.hub_project_id
name = "pfsense-root"
name = "opnsense-root"
availability_zone = var.default_zone
size = 16
performance_class = "storage_premium_perf4"
source = {
id = stackit_image.pfsense_image.image_id
id = stackit_image.opnsense_image.image_id
type = "image"
}
}
resource "stackit_server" "pfsense" {
resource "stackit_server" "opnsense" {
project_id = local.hub_project_id
name = "pfsense"
name = "opnsense"
availability_zone = var.default_zone
machine_type = var.pfsense_machine_type
machine_type = var.opnsense_machine_type
boot_volume = {
source_type = "volume"
source_id = stackit_volume.pfsense_volume.volume_id
source_id = stackit_volume.opnsense_volume.volume_id
}
# WAN boots first (vtnet0); LAN and MGMT are attached in order below vtnet12.
network_interfaces = [stackit_network_interface.nic_wan.network_interface_id]
@ -54,14 +65,14 @@ resource "stackit_server" "pfsense" {
resource "stackit_server_network_interface_attach" "attach_lan" {
project_id = local.hub_project_id
server_id = stackit_server.pfsense.server_id
server_id = stackit_server.opnsense.server_id
network_interface_id = stackit_network_interface.nic_lan.network_interface_id
depends_on = [stackit_server.pfsense]
depends_on = [stackit_server.opnsense]
}
resource "stackit_server_network_interface_attach" "attach_mgmt" {
project_id = local.hub_project_id
server_id = stackit_server.pfsense.server_id
server_id = stackit_server.opnsense.server_id
network_interface_id = stackit_network_interface.nic_mgmt.network_interface_id
depends_on = [stackit_server_network_interface_attach.attach_lan]
}

View file

@ -23,16 +23,16 @@ output "hub_project_id" {
}
output "firewall_lan_ip" {
description = "pfSense LAN IP — set as hub_firewall_lan_ip in spoke terraform.tfvars."
description = "OPNsense LAN IP — set as hub_firewall_lan_ip in spoke terraform.tfvars."
value = stackit_network_interface.nic_lan.ipv4
}
output "wan_public_ip" {
description = "WAN public IP of the pfSense firewall."
description = "WAN public IP of the OPNsense firewall."
value = stackit_public_ip.wan_public_ip.ip
}
output "mgmt_public_ip" {
description = "Public IP of the pfSense MGMT interface. Access the web UI at https://<ip>/"
description = "Public IP of the OPNsense MGMT interface. Access the web UI at https://<ip>/"
value = stackit_public_ip.mgmt_public_ip.ip
}

View file

@ -63,7 +63,7 @@ variable "spoke_subnet" {
}
variable "hub_firewall_lan_ip" {
description = "LAN IP of the active pfSense node. Used as the default route next-hop for all spoke traffic. Run `terraform output firewall_lan_ip` in 001-hub-project."
description = "LAN IP of the active OPNsense node. Used as the default route next-hop for all spoke traffic. Run `terraform output firewall_lan_ip` in 001-hub-project."
type = string
default = "10.28.0.20"
}

View file

@ -63,7 +63,7 @@ variable "spoke_subnet" {
}
variable "hub_firewall_lan_ip" {
description = "LAN IP of the active pfSense node. Used as the default route next-hop for all spoke traffic. Run `terraform output firewall_lan_ip` in 001-hub-project."
description = "LAN IP of the active OPNsense node. Used as the default route next-hop for all spoke traffic. Run `terraform output firewall_lan_ip` in 001-hub-project."
type = string
default = "10.28.0.20"
}

View file

@ -1,8 +1,8 @@
# Hub-and-Spoke VPN on STACKIT — pfSense Reference Implementation
# Hub-and-Spoke VPN on STACKIT — OPNsense Reference Implementation
A reference implementation of a **hub-and-spoke network topology** on [STACKIT](https://www.stackit.de/), provisioned with Terraform.
The hub deploys a **pfSense firewall** as the central routing and security component. All spoke traffic is forwarded through pfSense for routing, NAT, and policy enforcement. Each project is a self-contained Terraform stack with independent state.
The hub deploys an **OPNsense firewall** as the central routing and security component. All spoke traffic is forwarded through OPNsense for routing, NAT, and policy enforcement. Each project is a self-contained Terraform stack with independent state.
---
@ -17,7 +17,7 @@ The hub deploys a **pfSense firewall** as the central routing and security compo
| +-------------------------------------------------------------+ |
| | 001-hub-project | |
| | | |
| | pfSense Firewall | |
| | OPNsense Firewall | |
| | +------------------+------------------+ | |
| | | Interface | IP | | |
| | +------------------+------------------+ | |
@ -38,7 +38,7 @@ The hub deploys a **pfSense firewall** as the central routing and security compo
+-------------------------------------------------------------------+
```
**Traffic flow:** All spoke traffic (including internet-bound) is forwarded to the pfSense LAN NIC (`10.28.0.20`) via a routing table route attached to each spoke network. pfSense handles routing, NAT, and firewall policy centrally.
**Traffic flow:** All spoke traffic (including internet-bound) is forwarded to the OPNsense LAN NIC (`10.28.0.20`) via a routing table route attached to each spoke network. OPNsense handles routing, NAT, and firewall policy centrally.
---
@ -46,22 +46,22 @@ The hub deploys a **pfSense firewall** as the central routing and security compo
```
hub-and-spoke-vpn/
├── 001-hub-project/ # Hub: pfSense firewall, network area, routing tables
│ ├── 000-backend.tf # S3 remote state backend
│ ├── 000-variables.tf # Input variables (pfsense_machine_type, mgmt_ip_range)
│ ├── 010-provider.tf # STACKIT provider
│ ├── 020-projects.tf # STACKIT project + shared network area (SNA)
│ ├── 030-network.tf # Subnets, routing tables, NICs, security groups
│ ├── 040-hub-fw-pfsense.tf # pfSense image, volume, server, public IPs
│ ├── 050-outputs.tf # network_area_id, firewall_lan_ip, public IPs (needed by spokes)
│ └── backend.conf.example # Backend credential template
├── 001-hub-project/ # Hub: OPNsense firewall, network area, routing tables
│ ├── 000-backend.tf # S3 remote state backend
│ ├── 000-variables.tf # Input variables (opnsense_machine_type, mgmt_ip_range)
│ ├── 010-provider.tf # STACKIT provider
│ ├── 020-projects.tf # STACKIT project + shared network area (SNA)
│ ├── 030-network.tf # Subnets, routing tables, NICs, security groups
│ ├── 040-hub-fw-opnsense.tf # OPNsense image, volume, server, public IPs
│ ├── 050-outputs.tf # network_area_id, firewall_lan_ip, public IPs (needed by spokes)
│ └── backend.conf.example # Backend credential template
├── 002-spoke-project/ # Spoke A: example Linux servers (RHEL 9)
│ ├── 000-backend.tf
│ ├── 000-variables.tf
│ ├── 010-provider.tf
│ ├── 020-projects.tf
│ ├── 030-network.tf # Spoke subnet + routing table (default → pfSense LAN)
│ ├── 030-network.tf # Spoke subnet + routing table (default → OPNsense LAN)
│ ├── 040-servers.tf # Two Linux server examples (different machine types)
│ ├── 050-outputs.tf
│ └── backend.conf.example
@ -161,7 +161,7 @@ The minimum required values are documented in each `000-variables.tf`.
Deploy in numbered order. The hub creates the shared network area that spokes depend on.
```sh
# Step 1 — Deploy the hub (creates the network area and the pfSense firewall)
# Step 1 — Deploy the hub (creates the network area and the OPNsense firewall)
cd 001-hub-project
terraform init -backend-config=backend.conf
terraform apply
@ -182,9 +182,9 @@ terraform apply
---
## Hub Firewall (pfSense)
## Hub Firewall (OPNsense)
pfSense is provisioned from a qcow2 image with three network interfaces:
OPNsense is provisioned from a qcow2 image with three network interfaces:
| Interface | Subnet | IP | Purpose |
| --------- | --------------- | ------------ | -------------------------- |
@ -194,7 +194,7 @@ pfSense is provisioned from a qcow2 image with three network interfaces:
The WAN interface boots first (`vtnet0`); LAN and MGMT are attached sequentially and appear as `vtnet1` and `vtnet2`. The MGMT interface is protected by a security group that restricts SSH, HTTP, and HTTPS access to the CIDR set in `mgmt_ip_range`.
**pfSense image:** Place `pfsense.qcow2` at `001-hub-project/image/pfsense.qcow2` before the first apply. Download pfSense 2.7.x AMD64 from netgate.com and convert to qcow2 if needed.
**OPNsense image:** The image is downloaded automatically during `terraform apply` via a `null_resource` provisioner. The qcow2 image is fetched from the STACKIT Object Storage endpoint and uploaded to STACKIT as a custom image.
---
@ -244,7 +244,6 @@ A single generic module used by all spokes. Select the OS by passing the appropr
| Backend credentials | `backend.conf` per project | Object Storage bucket + S3 keys |
| Service account key | `keys/service-account.json` per project | Downloaded from STACKIT portal |
| Cloud-init password | `cloud-init/*.yml` | Replace placeholder hash with a real one |
| pfSense image | `001-hub-project/image/pfsense.qcow2` | Download separately |
---
@ -252,4 +251,4 @@ A single generic module used by all spokes. Select the OS by passing the appropr
- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs)
- [STACKIT Documentation](https://docs.stackit.cloud/)
- [pfSense Documentation](https://docs.netgate.com/pfsense/en/latest/)
- [OPNsense Documentation](https://docs.opnsense.org/)

View file

@ -39,7 +39,7 @@ default_zone = "eu01-1"
# --- Hub: network access control (001-hub-project only) ----------------------
# CIDR allowed to reach the pfSense management interface (SSH, HTTP, HTTPS).
# CIDR allowed to reach the OPNsense management interface (SSH, HTTP, HTTPS).
# Example: your office public IP or VPN exit in /32 or /24 notation.
# Leave empty (or remove) to create no management access rules.
# mgmt_ip_range = "<x.x.x.x/x>"
@ -51,6 +51,6 @@ default_zone = "eu01-1"
# 003-spoke-project default: 10.28.2.0/28
# spoke_subnet = "10.28.1.0/28"
# LAN IP of the pfSense firewall — used as the default route next-hop for spoke traffic.
# LAN IP of the OPNsense firewall — used as the default route next-hop for spoke traffic.
# Run `terraform output firewall_lan_ip` in 001-hub-project.
# hub_firewall_lan_ip = "10.28.0.20"