From 9cf06e2717b737e37aa9d21014b71adaf94e3556 Mon Sep 17 00:00:00 2001 From: Sven Schmidt Date: Wed, 15 Apr 2026 16:41:06 +0200 Subject: [PATCH 1/6] add example --- .../001-hub-project/000-backend.tf | 31 +++ .../001-hub-project/000-variables.tf | 64 +++++ .../001-hub-project/010-provider.tf | 29 ++ .../001-hub-project/020-projects.tf | 49 ++++ .../001-hub-project/030-network.tf | 153 +++++++++++ .../001-hub-project/040-hub-fw-pfsense.tf | 77 ++++++ .../001-hub-project/050-outputs.tf | 38 +++ .../001-hub-project/backend.conf.example | 11 + .../002-spoke-project/000-backend.tf | 31 +++ .../002-spoke-project/000-variables.tf | 69 +++++ .../002-spoke-project/010-provider.tf | 29 ++ .../002-spoke-project/020-projects.tf | 22 ++ .../002-spoke-project/030-network.tf | 42 +++ .../002-spoke-project/040-servers.tf | 39 +++ .../002-spoke-project/050-outputs.tf | 28 ++ .../002-spoke-project/backend.conf.example | 11 + .../003-spoke-project/000-backend.tf | 31 +++ .../003-spoke-project/000-variables.tf | 69 +++++ .../003-spoke-project/010-provider.tf | 29 ++ .../003-spoke-project/020-projects.tf | 22 ++ .../003-spoke-project/030-network.tf | 42 +++ .../003-spoke-project/040-servers.tf | 45 +++ .../003-spoke-project/050-outputs.tf | 28 ++ .../003-spoke-project/backend.conf.example | 11 + examples/pfsense-hub-and-spoke/README.md | 257 ++++++++++++++++++ .../cloud-init/user-init-linux.yml | 24 ++ .../cloud-init/user-init-windows.yml | 12 + .../modules/server/main.tf | 55 ++++ .../modules/server/outputs.tf | 26 ++ .../modules/server/variables.tf | 74 +++++ .../terraform.tfvars.example | 63 +++++ 31 files changed, 1511 insertions(+) create mode 100644 examples/pfsense-hub-and-spoke/001-hub-project/000-backend.tf create mode 100644 examples/pfsense-hub-and-spoke/001-hub-project/000-variables.tf create mode 100644 examples/pfsense-hub-and-spoke/001-hub-project/010-provider.tf create mode 100644 examples/pfsense-hub-and-spoke/001-hub-project/020-projects.tf create mode 100644 examples/pfsense-hub-and-spoke/001-hub-project/030-network.tf create mode 100644 examples/pfsense-hub-and-spoke/001-hub-project/040-hub-fw-pfsense.tf create mode 100644 examples/pfsense-hub-and-spoke/001-hub-project/050-outputs.tf create mode 100644 examples/pfsense-hub-and-spoke/001-hub-project/backend.conf.example create mode 100644 examples/pfsense-hub-and-spoke/002-spoke-project/000-backend.tf create mode 100644 examples/pfsense-hub-and-spoke/002-spoke-project/000-variables.tf create mode 100644 examples/pfsense-hub-and-spoke/002-spoke-project/010-provider.tf create mode 100644 examples/pfsense-hub-and-spoke/002-spoke-project/020-projects.tf create mode 100644 examples/pfsense-hub-and-spoke/002-spoke-project/030-network.tf create mode 100644 examples/pfsense-hub-and-spoke/002-spoke-project/040-servers.tf create mode 100644 examples/pfsense-hub-and-spoke/002-spoke-project/050-outputs.tf create mode 100644 examples/pfsense-hub-and-spoke/002-spoke-project/backend.conf.example create mode 100644 examples/pfsense-hub-and-spoke/003-spoke-project/000-backend.tf create mode 100644 examples/pfsense-hub-and-spoke/003-spoke-project/000-variables.tf create mode 100644 examples/pfsense-hub-and-spoke/003-spoke-project/010-provider.tf create mode 100644 examples/pfsense-hub-and-spoke/003-spoke-project/020-projects.tf create mode 100644 examples/pfsense-hub-and-spoke/003-spoke-project/030-network.tf create mode 100644 examples/pfsense-hub-and-spoke/003-spoke-project/040-servers.tf create mode 100644 examples/pfsense-hub-and-spoke/003-spoke-project/050-outputs.tf create mode 100644 examples/pfsense-hub-and-spoke/003-spoke-project/backend.conf.example create mode 100644 examples/pfsense-hub-and-spoke/README.md create mode 100644 examples/pfsense-hub-and-spoke/cloud-init/user-init-linux.yml create mode 100644 examples/pfsense-hub-and-spoke/cloud-init/user-init-windows.yml create mode 100644 examples/pfsense-hub-and-spoke/modules/server/main.tf create mode 100644 examples/pfsense-hub-and-spoke/modules/server/outputs.tf create mode 100644 examples/pfsense-hub-and-spoke/modules/server/variables.tf create mode 100644 examples/pfsense-hub-and-spoke/terraform.tfvars.example diff --git a/examples/pfsense-hub-and-spoke/001-hub-project/000-backend.tf b/examples/pfsense-hub-and-spoke/001-hub-project/000-backend.tf new file mode 100644 index 0000000..58fdb74 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/001-hub-project/000-backend.tf @@ -0,0 +1,31 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + backend "s3" { + bucket = "" + key = "001-hub-project/terraform.tfstate" + endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" + } + region = "eu01" + skip_credentials_validation = true + skip_region_validation = true + skip_s3_checksum = true + skip_requesting_account_id = true + # Credentials: set via backend.conf or environment variables (see above) + # access_key = "" + # secret_key = "" + } +} diff --git a/examples/pfsense-hub-and-spoke/001-hub-project/000-variables.tf b/examples/pfsense-hub-and-spoke/001-hub-project/000-variables.tf new file mode 100644 index 0000000..b2ef2c1 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/001-hub-project/000-variables.tf @@ -0,0 +1,64 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "stackit_service_account_key_path" { + description = "Path to the STACKIT service account key file (JSON). Keep this file out of version control." + type = string + default = "./keys/service-account.json" +} + +variable "stackit_organization_id" { + description = "STACKIT Organization ID (UUID). Found in the portal under Organization > Settings." + type = string +} + +variable "stackit_folder_id" { + description = "STACKIT Folder ID (UUID) that will contain the hub project." + type = string +} + +variable "stackit_region" { + description = "STACKIT region (e.g. eu01)." + type = string + default = "eu01" +} + +variable "default_zone" { + description = "Availability zone within the region (e.g. eu01-1)." + type = string + default = "eu01-1" +} + +variable "project_name" { + description = "Display name of the hub project in STACKIT." + type = string + default = "hub-project" +} + +variable "org_admin" { + description = "Email address of the STACKIT user who will be set as project owner." + type = string +} + +variable "mgmt_ip_range" { + description = "CIDR range allowed to access the firewall management interface (SSH, HTTP, HTTPS). Example: your office or VPN exit IP in /32 or /24 notation." + type = string + default = "" +} + +variable "pfsense_machine_type" { + description = "Machine type used for both pfSense nodes (e.g. c2i.2, c2i.4). Both nodes use the same type." + type = string + default = "c2i.2" +} diff --git a/examples/pfsense-hub-and-spoke/001-hub-project/010-provider.tf b/examples/pfsense-hub-and-spoke/001-hub-project/010-provider.tf new file mode 100644 index 0000000..a26b44e --- /dev/null +++ b/examples/pfsense-hub-and-spoke/001-hub-project/010-provider.tf @@ -0,0 +1,29 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.80.0" + } + } +} + +provider "stackit" { + default_region = var.stackit_region + service_account_key_path = var.stackit_service_account_key_path + experiments = ["routing-tables", "network", "iam"] + enable_beta_resources = true +} diff --git a/examples/pfsense-hub-and-spoke/001-hub-project/020-projects.tf b/examples/pfsense-hub-and-spoke/001-hub-project/020-projects.tf new file mode 100644 index 0000000..5f90d71 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/001-hub-project/020-projects.tf @@ -0,0 +1,49 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +locals { + sna_id = stackit_network_area.sna.network_area_id + hub_project_id = stackit_resourcemanager_project.hub.project_id +} + +resource "stackit_network_area" "sna" { + name = "hub-and-spoke-sna" + organization_id = var.stackit_organization_id + labels = { + "preview/routingtables" = "true" + } +} + +resource "stackit_network_area_region" "sna" { + organization_id = var.stackit_organization_id + network_area_id = stackit_network_area.sna.network_area_id + ipv4 = { + transfer_network = "172.3.0.0/16" + network_ranges = [ + { + prefix = "10.28.0.0/16" + } + ] + default_nameservers = ["1.1.1.1"] + } +} + +resource "stackit_resourcemanager_project" "hub" { + parent_container_id = var.stackit_folder_id + name = var.project_name + owner_email = var.org_admin + labels = { + "networkArea" = stackit_network_area.sna.network_area_id + } +} diff --git a/examples/pfsense-hub-and-spoke/001-hub-project/030-network.tf b/examples/pfsense-hub-and-spoke/001-hub-project/030-network.tf new file mode 100644 index 0000000..bf295f4 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/001-hub-project/030-network.tf @@ -0,0 +1,153 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_routing_table" "rt_firewall_lan" { + network_area_id = stackit_network_area.sna.network_area_id + organization_id = var.stackit_organization_id + name = "rt_firewall_lan" + system_routes = true +} + +resource "stackit_routing_table" "rt_firewall_wan" { + network_area_id = stackit_network_area.sna.network_area_id + organization_id = var.stackit_organization_id + name = "rt_firewall_wan" + system_routes = true +} + +resource "stackit_routing_table_route" "fw_network" { + network_area_id = stackit_network_area.sna.network_area_id + organization_id = var.stackit_organization_id + routing_table_id = stackit_routing_table.rt_firewall_lan.routing_table_id + destination = { + type = "cidrv4" + value = "0.0.0.0/0" + } + next_hop = { + type = "ipv4" + value = stackit_network_interface.nic_lan.ipv4 + } +} + +resource "stackit_routing_table_route" "fw_network_wan" { + network_area_id = stackit_network_area.sna.network_area_id + organization_id = var.stackit_organization_id + routing_table_id = stackit_routing_table.rt_firewall_wan.routing_table_id + destination = { + type = "cidrv4" + value = "0.0.0.0/0" + } + next_hop = { + type = "internet" + } +} + +resource "stackit_network" "wan_network" { + project_id = local.hub_project_id + name = "wan-network" + ipv4_nameservers = ["1.1.1.1", "9.9.9.9"] + ipv4_prefix = "10.28.0.0/28" + routing_table_id = stackit_routing_table.rt_firewall_wan.routing_table_id +} + +resource "stackit_network" "lan_network" { + project_id = local.hub_project_id + name = "lan-network" + ipv4_nameservers = ["1.1.1.1", "9.9.9.9"] + ipv4_prefix = "10.28.0.16/28" + routing_table_id = stackit_routing_table.rt_firewall_lan.routing_table_id +} + +resource "stackit_network" "mgmt_network" { + project_id = local.hub_project_id + name = "mgmt-network" + ipv4_nameservers = ["1.1.1.1", "9.9.9.9"] + ipv4_prefix = "10.28.0.32/28" +} + +resource "stackit_network_interface" "nic_wan" { + project_id = local.hub_project_id + network_id = stackit_network.wan_network.network_id + security = false + ipv4 = "10.28.0.4" +} + +resource "stackit_network_interface" "nic_lan" { + project_id = local.hub_project_id + network_id = stackit_network.lan_network.network_id + security = false + ipv4 = "10.28.0.20" +} + +resource "stackit_security_group" "mgmt_sg" { + project_id = local.hub_project_id + name = "firewall-mgmt-sg" + description = "Allow SSH and HTTPS to the hub firewall management interface" +} + +resource "stackit_security_group_rule" "allow_ssh" { + count = var.mgmt_ip_range != "" ? 1 : 0 + + project_id = local.hub_project_id + security_group_id = stackit_security_group.mgmt_sg.security_group_id + direction = "ingress" + protocol = { + name = "tcp" + } + port_range = { + min = 22 + max = 22 + } + ip_range = var.mgmt_ip_range +} + +resource "stackit_security_group_rule" "allow_http" { + count = var.mgmt_ip_range != "" ? 1 : 0 + + project_id = local.hub_project_id + security_group_id = stackit_security_group.mgmt_sg.security_group_id + direction = "ingress" + protocol = { + name = "tcp" + } + port_range = { + min = 80 + max = 80 + } + ip_range = var.mgmt_ip_range +} + +resource "stackit_security_group_rule" "allow_https" { + count = var.mgmt_ip_range != "" ? 1 : 0 + + project_id = local.hub_project_id + security_group_id = stackit_security_group.mgmt_sg.security_group_id + direction = "ingress" + protocol = { + name = "tcp" + } + port_range = { + min = 443 + max = 443 + } + ip_range = var.mgmt_ip_range +} + +resource "stackit_network_interface" "nic_mgmt" { + project_id = local.hub_project_id + network_id = stackit_network.mgmt_network.network_id + security = true + ipv4 = "10.28.0.36" + security_group_ids = [stackit_security_group.mgmt_sg.security_group_id] +} diff --git a/examples/pfsense-hub-and-spoke/001-hub-project/040-hub-fw-pfsense.tf b/examples/pfsense-hub-and-spoke/001-hub-project/040-hub-fw-pfsense.tf new file mode 100644 index 0000000..dee5ae7 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/001-hub-project/040-hub-fw-pfsense.tf @@ -0,0 +1,77 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# 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 "stackit_image" "pfsense_image" { + project_id = local.hub_project_id + name = "pfsense-2.7.x-amd64" + local_file_path = "./image/pfsense.qcow2" + disk_format = "qcow2" + min_disk_size = 10 + min_ram = 2 + config = { + uefi = false + } +} + +resource "stackit_volume" "pfsense_volume" { + project_id = local.hub_project_id + name = "pfsense-root" + availability_zone = var.default_zone + size = 16 + performance_class = "storage_premium_perf4" + source = { + id = stackit_image.pfsense_image.image_id + type = "image" + } +} + +resource "stackit_server" "pfsense" { + project_id = local.hub_project_id + name = "pfsense" + availability_zone = var.default_zone + machine_type = var.pfsense_machine_type + boot_volume = { + source_type = "volume" + source_id = stackit_volume.pfsense_volume.volume_id + } + # WAN boots first (vtnet0); LAN and MGMT are attached in order below → vtnet1–2. + network_interfaces = [stackit_network_interface.nic_wan.network_interface_id] +} + +resource "stackit_server_network_interface_attach" "attach_lan" { + project_id = local.hub_project_id + server_id = stackit_server.pfsense.server_id + network_interface_id = stackit_network_interface.nic_lan.network_interface_id + depends_on = [stackit_server.pfsense] +} + +resource "stackit_server_network_interface_attach" "attach_mgmt" { + project_id = local.hub_project_id + server_id = stackit_server.pfsense.server_id + network_interface_id = stackit_network_interface.nic_mgmt.network_interface_id + depends_on = [stackit_server_network_interface_attach.attach_lan] +} + +resource "stackit_public_ip" "wan_public_ip" { + project_id = local.hub_project_id + network_interface_id = stackit_network_interface.nic_wan.network_interface_id +} + +resource "stackit_public_ip" "mgmt_public_ip" { + project_id = local.hub_project_id + network_interface_id = stackit_network_interface.nic_mgmt.network_interface_id +} diff --git a/examples/pfsense-hub-and-spoke/001-hub-project/050-outputs.tf b/examples/pfsense-hub-and-spoke/001-hub-project/050-outputs.tf new file mode 100644 index 0000000..29c09aa --- /dev/null +++ b/examples/pfsense-hub-and-spoke/001-hub-project/050-outputs.tf @@ -0,0 +1,38 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "network_area_id" { + description = "Shared Network Area ID — required by all spoke projects." + value = local.sna_id +} + +output "hub_project_id" { + description = "STACKIT project ID of the hub." + value = local.hub_project_id +} + +output "firewall_lan_ip" { + description = "pfSense 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." + 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:///" + value = stackit_public_ip.mgmt_public_ip.ip +} diff --git a/examples/pfsense-hub-and-spoke/001-hub-project/backend.conf.example b/examples/pfsense-hub-and-spoke/001-hub-project/backend.conf.example new file mode 100644 index 0000000..673dfa8 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/001-hub-project/backend.conf.example @@ -0,0 +1,11 @@ +# Copy this file to backend.conf and fill in your STACKIT Object Storage credentials. +# backend.conf is gitignored and must never be committed to version control. +# +# Usage: terraform init -backend-config=backend.conf +# +# Create an Object Storage bucket in the STACKIT portal and generate +# S3-compatible access credentials under "Object Storage > Credentials". + +bucket = "" +access_key = "" +secret_key = "" diff --git a/examples/pfsense-hub-and-spoke/002-spoke-project/000-backend.tf b/examples/pfsense-hub-and-spoke/002-spoke-project/000-backend.tf new file mode 100644 index 0000000..b8a3207 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/002-spoke-project/000-backend.tf @@ -0,0 +1,31 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + backend "s3" { + bucket = "" + key = "002-spoke-project/terraform.tfstate" + endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" + } + region = "eu01" + skip_credentials_validation = true + skip_region_validation = true + skip_s3_checksum = true + skip_requesting_account_id = true + # Credentials: set via backend.conf or environment variables (see above) + # access_key = "" + # secret_key = "" + } +} diff --git a/examples/pfsense-hub-and-spoke/002-spoke-project/000-variables.tf b/examples/pfsense-hub-and-spoke/002-spoke-project/000-variables.tf new file mode 100644 index 0000000..f359f80 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/002-spoke-project/000-variables.tf @@ -0,0 +1,69 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "stackit_service_account_key_path" { + description = "Path to the STACKIT service account key file (JSON). Keep this file out of version control." + type = string + default = "./keys/service-account.json" +} + +variable "stackit_organization_id" { + description = "STACKIT Organization ID (UUID)." + type = string +} + +variable "stackit_network_area_id" { + description = "Shared Network Area ID from the hub project. Run `terraform output network_area_id` in 001-hub-project." + type = string +} + +variable "stackit_folder_id" { + description = "STACKIT Folder ID (UUID) that will contain this spoke project." + type = string +} + +variable "stackit_region" { + description = "STACKIT region (e.g. eu01)." + type = string + default = "eu01" +} + +variable "default_zone" { + description = "Availability zone within the region (e.g. eu01-1)." + type = string + default = "eu01-1" +} + +variable "project_name" { + description = "Display name of this spoke project in STACKIT." + type = string + default = "spoke-project-02" +} + +variable "org_admin" { + description = "Email address of the STACKIT user who will be set as project owner." + type = string +} + +variable "spoke_subnet" { + description = "IPv4 prefix for this spoke's network. Must be within the network area range (10.28.0.0/16)." + type = string + default = "10.28.1.0/28" +} + +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." + type = string + default = "10.28.0.20" +} diff --git a/examples/pfsense-hub-and-spoke/002-spoke-project/010-provider.tf b/examples/pfsense-hub-and-spoke/002-spoke-project/010-provider.tf new file mode 100644 index 0000000..a26b44e --- /dev/null +++ b/examples/pfsense-hub-and-spoke/002-spoke-project/010-provider.tf @@ -0,0 +1,29 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.80.0" + } + } +} + +provider "stackit" { + default_region = var.stackit_region + service_account_key_path = var.stackit_service_account_key_path + experiments = ["routing-tables", "network", "iam"] + enable_beta_resources = true +} diff --git a/examples/pfsense-hub-and-spoke/002-spoke-project/020-projects.tf b/examples/pfsense-hub-and-spoke/002-spoke-project/020-projects.tf new file mode 100644 index 0000000..ed8fa64 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/002-spoke-project/020-projects.tf @@ -0,0 +1,22 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_resourcemanager_project" "spoke" { + parent_container_id = var.stackit_folder_id + name = var.project_name + owner_email = var.org_admin + labels = { + "networkArea" = var.stackit_network_area_id + } +} diff --git a/examples/pfsense-hub-and-spoke/002-spoke-project/030-network.tf b/examples/pfsense-hub-and-spoke/002-spoke-project/030-network.tf new file mode 100644 index 0000000..68ec9b1 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/002-spoke-project/030-network.tf @@ -0,0 +1,42 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_routing_table" "rt_default" { + name = "rt_spoke_002" + network_area_id = var.stackit_network_area_id + organization_id = var.stackit_organization_id + system_routes = false +} + +resource "stackit_routing_table_route" "default" { + destination = { + type = "cidrv4" + value = "0.0.0.0/0" + } + next_hop = { + type = "ipv4" + value = var.hub_firewall_lan_ip + } + network_area_id = var.stackit_network_area_id + organization_id = var.stackit_organization_id + routing_table_id = stackit_routing_table.rt_default.routing_table_id +} + +resource "stackit_network" "spoke_network" { + project_id = stackit_resourcemanager_project.spoke.project_id + name = "spoke-network" + ipv4_nameservers = ["1.1.1.1", "9.9.9.9"] + ipv4_prefix = var.spoke_subnet + routing_table_id = stackit_routing_table.rt_default.routing_table_id +} diff --git a/examples/pfsense-hub-and-spoke/002-spoke-project/040-servers.tf b/examples/pfsense-hub-and-spoke/002-spoke-project/040-servers.tf new file mode 100644 index 0000000..e093380 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/002-spoke-project/040-servers.tf @@ -0,0 +1,39 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module "server_a" { + source = "../modules/server" + + project_id = stackit_resourcemanager_project.spoke.project_id + network_id = stackit_network.spoke_network.network_id + availability_zone = var.default_zone + name = "server-a" + machine_type = "c2i.2" + disk_size = 50 + disk_performance_class = "storage_premium_perf1" + user_data = templatefile("${path.module}/../cloud-init/user-init-linux.yml", {}) +} + +module "server_b" { + source = "../modules/server" + + project_id = stackit_resourcemanager_project.spoke.project_id + network_id = stackit_network.spoke_network.network_id + availability_zone = var.default_zone + name = "server-b" + machine_type = "m1a.8d" + disk_size = 100 + disk_performance_class = "storage_premium_perf1" + user_data = templatefile("${path.module}/../cloud-init/user-init-linux.yml", {}) +} diff --git a/examples/pfsense-hub-and-spoke/002-spoke-project/050-outputs.tf b/examples/pfsense-hub-and-spoke/002-spoke-project/050-outputs.tf new file mode 100644 index 0000000..bb25a36 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/002-spoke-project/050-outputs.tf @@ -0,0 +1,28 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "spoke_project_id" { + description = "STACKIT project ID of this spoke." + value = stackit_resourcemanager_project.spoke.project_id +} + +output "server_a_ip" { + description = "Primary IP address of server-a." + value = module.server_a.primary_ip +} + +output "server_b_ip" { + description = "Primary IP address of server-b." + value = module.server_b.primary_ip +} diff --git a/examples/pfsense-hub-and-spoke/002-spoke-project/backend.conf.example b/examples/pfsense-hub-and-spoke/002-spoke-project/backend.conf.example new file mode 100644 index 0000000..673dfa8 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/002-spoke-project/backend.conf.example @@ -0,0 +1,11 @@ +# Copy this file to backend.conf and fill in your STACKIT Object Storage credentials. +# backend.conf is gitignored and must never be committed to version control. +# +# Usage: terraform init -backend-config=backend.conf +# +# Create an Object Storage bucket in the STACKIT portal and generate +# S3-compatible access credentials under "Object Storage > Credentials". + +bucket = "" +access_key = "" +secret_key = "" diff --git a/examples/pfsense-hub-and-spoke/003-spoke-project/000-backend.tf b/examples/pfsense-hub-and-spoke/003-spoke-project/000-backend.tf new file mode 100644 index 0000000..3264b5b --- /dev/null +++ b/examples/pfsense-hub-and-spoke/003-spoke-project/000-backend.tf @@ -0,0 +1,31 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + backend "s3" { + bucket = "" + key = "003-spoke-project/terraform.tfstate" + endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" + } + region = "eu01" + skip_credentials_validation = true + skip_region_validation = true + skip_s3_checksum = true + skip_requesting_account_id = true + # Credentials: set via backend.conf or environment variables (see above) + # access_key = "" + # secret_key = "" + } +} diff --git a/examples/pfsense-hub-and-spoke/003-spoke-project/000-variables.tf b/examples/pfsense-hub-and-spoke/003-spoke-project/000-variables.tf new file mode 100644 index 0000000..ba627ad --- /dev/null +++ b/examples/pfsense-hub-and-spoke/003-spoke-project/000-variables.tf @@ -0,0 +1,69 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "stackit_service_account_key_path" { + description = "Path to the STACKIT service account key file (JSON). Keep this file out of version control." + type = string + default = "./keys/service-account.json" +} + +variable "stackit_organization_id" { + description = "STACKIT Organization ID (UUID)." + type = string +} + +variable "stackit_network_area_id" { + description = "Shared Network Area ID from the hub project. Run `terraform output network_area_id` in 001-hub-project." + type = string +} + +variable "stackit_folder_id" { + description = "STACKIT Folder ID (UUID) that will contain this spoke project." + type = string +} + +variable "stackit_region" { + description = "STACKIT region (e.g. eu01)." + type = string + default = "eu01" +} + +variable "default_zone" { + description = "Availability zone within the region (e.g. eu01-1)." + type = string + default = "eu01-1" +} + +variable "project_name" { + description = "Display name of this spoke project in STACKIT." + type = string + default = "spoke-project-03" +} + +variable "org_admin" { + description = "Email address of the STACKIT user who will be set as project owner." + type = string +} + +variable "spoke_subnet" { + description = "IPv4 prefix for this spoke's network. Must be within the network area range (10.28.0.0/16)." + type = string + default = "10.28.2.0/28" +} + +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." + type = string + default = "10.28.0.20" +} diff --git a/examples/pfsense-hub-and-spoke/003-spoke-project/010-provider.tf b/examples/pfsense-hub-and-spoke/003-spoke-project/010-provider.tf new file mode 100644 index 0000000..a26b44e --- /dev/null +++ b/examples/pfsense-hub-and-spoke/003-spoke-project/010-provider.tf @@ -0,0 +1,29 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.80.0" + } + } +} + +provider "stackit" { + default_region = var.stackit_region + service_account_key_path = var.stackit_service_account_key_path + experiments = ["routing-tables", "network", "iam"] + enable_beta_resources = true +} diff --git a/examples/pfsense-hub-and-spoke/003-spoke-project/020-projects.tf b/examples/pfsense-hub-and-spoke/003-spoke-project/020-projects.tf new file mode 100644 index 0000000..ed8fa64 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/003-spoke-project/020-projects.tf @@ -0,0 +1,22 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_resourcemanager_project" "spoke" { + parent_container_id = var.stackit_folder_id + name = var.project_name + owner_email = var.org_admin + labels = { + "networkArea" = var.stackit_network_area_id + } +} diff --git a/examples/pfsense-hub-and-spoke/003-spoke-project/030-network.tf b/examples/pfsense-hub-and-spoke/003-spoke-project/030-network.tf new file mode 100644 index 0000000..8f786ad --- /dev/null +++ b/examples/pfsense-hub-and-spoke/003-spoke-project/030-network.tf @@ -0,0 +1,42 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_routing_table" "rt_default" { + name = "rt_spoke_003" + network_area_id = var.stackit_network_area_id + organization_id = var.stackit_organization_id + system_routes = false +} + +resource "stackit_routing_table_route" "default" { + destination = { + type = "cidrv4" + value = "0.0.0.0/0" + } + next_hop = { + type = "ipv4" + value = var.hub_firewall_lan_ip + } + network_area_id = var.stackit_network_area_id + organization_id = var.stackit_organization_id + routing_table_id = stackit_routing_table.rt_default.routing_table_id +} + +resource "stackit_network" "spoke_network" { + project_id = stackit_resourcemanager_project.spoke.project_id + name = "spoke-network" + ipv4_nameservers = ["1.1.1.1", "9.9.9.9"] + ipv4_prefix = var.spoke_subnet + routing_table_id = stackit_routing_table.rt_default.routing_table_id +} diff --git a/examples/pfsense-hub-and-spoke/003-spoke-project/040-servers.tf b/examples/pfsense-hub-and-spoke/003-spoke-project/040-servers.tf new file mode 100644 index 0000000..37701cb --- /dev/null +++ b/examples/pfsense-hub-and-spoke/003-spoke-project/040-servers.tf @@ -0,0 +1,45 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +locals { + windows_image_id = "c3304694-a03f-47c7-8d4c-348eecc7d212" +} + +module "windows_server_a" { + source = "../modules/server" + + project_id = stackit_resourcemanager_project.spoke.project_id + network_id = stackit_network.spoke_network.network_id + availability_zone = var.default_zone + name = "windows-server-a" + image_id = local.windows_image_id + machine_type = "m2i.8" + disk_size = 100 + disk_performance_class = "storage_premium_perf2" + user_data = templatefile("${path.module}/../cloud-init/user-init-windows.yml", {}) +} + +module "windows_server_b" { + source = "../modules/server" + + project_id = stackit_resourcemanager_project.spoke.project_id + network_id = stackit_network.spoke_network.network_id + availability_zone = var.default_zone + name = "windows-server-b" + image_id = local.windows_image_id + machine_type = "g2i.16" + disk_size = 100 + disk_performance_class = "storage_premium_perf1" + user_data = templatefile("${path.module}/../cloud-init/user-init-windows.yml", {}) +} diff --git a/examples/pfsense-hub-and-spoke/003-spoke-project/050-outputs.tf b/examples/pfsense-hub-and-spoke/003-spoke-project/050-outputs.tf new file mode 100644 index 0000000..7a0980c --- /dev/null +++ b/examples/pfsense-hub-and-spoke/003-spoke-project/050-outputs.tf @@ -0,0 +1,28 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "spoke_project_id" { + description = "STACKIT project ID of this spoke." + value = stackit_resourcemanager_project.spoke.project_id +} + +output "windows_server_a_ip" { + description = "Primary IP address of windows-server-a." + value = module.windows_server_a.primary_ip +} + +output "windows_server_b_ip" { + description = "Primary IP address of windows-server-b." + value = module.windows_server_b.primary_ip +} diff --git a/examples/pfsense-hub-and-spoke/003-spoke-project/backend.conf.example b/examples/pfsense-hub-and-spoke/003-spoke-project/backend.conf.example new file mode 100644 index 0000000..673dfa8 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/003-spoke-project/backend.conf.example @@ -0,0 +1,11 @@ +# Copy this file to backend.conf and fill in your STACKIT Object Storage credentials. +# backend.conf is gitignored and must never be committed to version control. +# +# Usage: terraform init -backend-config=backend.conf +# +# Create an Object Storage bucket in the STACKIT portal and generate +# S3-compatible access credentials under "Object Storage > Credentials". + +bucket = "" +access_key = "" +secret_key = "" diff --git a/examples/pfsense-hub-and-spoke/README.md b/examples/pfsense-hub-and-spoke/README.md new file mode 100644 index 0000000..0e55270 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/README.md @@ -0,0 +1,257 @@ +# Hub-and-Spoke VPN on STACKIT — pfSense 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. + +--- + +## Architecture + +``` + Internet + | + +-------------------------------------------------------------------+ + | STACKIT Network Area | + | | + | +-------------------------------------------------------------+ | + | | 001-hub-project | | + | | | | + | | pfSense Firewall | | + | | +------------------+------------------+ | | + | | | Interface | IP | | | + | | +------------------+------------------+ | | + | | | WAN | 10.28.0.4 | | | + | | | LAN | 10.28.0.20 <-- | | | + | | | MGMT | 10.28.0.36 | | | + | | +------------------+------------------+ | | + | | | | + | | default route next-hop: 10.28.0.20 | | + | +---------------------------+---------------------------------+ | + | | | + | +----------------+----------------+ | + | | | | + | +----------+----------+ +------------+---------+ | + | | 002-spoke-project | | 003-spoke-project | | + | | 10.28.1.0/28 | | 10.28.2.0/28 | | + | +---------------------+ +----------------------+ | + +-------------------------------------------------------------------+ +``` + +**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. + +--- + +## Repository Structure + +``` +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 +│ +├── 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) +│ ├── 040-servers.tf # Two Linux server examples (different machine types) +│ ├── 050-outputs.tf +│ └── backend.conf.example +│ +├── 003-spoke-project/ # Spoke B: example Windows Server instances +│ ├── 000-backend.tf +│ ├── 000-variables.tf +│ ├── 010-provider.tf +│ ├── 020-projects.tf +│ ├── 030-network.tf +│ ├── 040-servers.tf # Two Windows server examples (standard + GPU-enabled) +│ ├── 050-outputs.tf +│ └── backend.conf.example +│ +├── modules/ +│ └── server/ # Generic compute module (volume + NIC + server) +│ ├── main.tf +│ ├── variables.tf # Pass image_id to choose OS; see variable description +│ └── outputs.tf +│ +├── cloud-init/ +│ ├── user-init-linux.yml # Cloud-init for Linux instances +│ └── user-init-windows.yml # Cloud-init for Windows instances +│ +├── terraform.tfvars.example # Variable template for all projects +└── Architecture/ + └── hub-and-spoke.drawio # Architecture diagram (draw.io) +``` + +**File numbering convention:** Files within each project are numbered to make the dependency and deployment order explicit: + +- `000` — backend and variables (no dependencies) +- `010` — provider configuration +- `020` — STACKIT project and network area +- `030` — networking (routing tables, subnets, interfaces) +- `040` — compute (VMs) +- `050` — outputs + +--- + +## Prerequisites + +- [Terraform](https://developer.hashicorp.com/terraform/install) >= 1.5 +- A STACKIT account with an organization, a folder, and sufficient IAM permissions +- A STACKIT service account with a JSON key file +- A STACKIT Object Storage bucket for Terraform remote state +- S3-compatible access credentials for that bucket + +--- + +## Setup + +### 1. Service account key + +Download your STACKIT service account key from the portal and place it in the `keys/` directory of each project you want to deploy: + +``` +001-hub-project/keys/service-account.json +002-spoke-project/keys/service-account.json +003-spoke-project/keys/service-account.json +``` + +The `keys/` directory is gitignored. Never commit key files. + +### 2. Backend credentials + +Copy `backend.conf.example` to `backend.conf` in each project directory: + +```sh +cp 001-hub-project/backend.conf.example 001-hub-project/backend.conf +# edit the file and fill in your bucket name and credentials +``` + +`backend.conf` is gitignored. Initialize Terraform with: + +```sh +terraform init -backend-config=backend.conf +``` + +### 3. Variable values + +Copy `terraform.tfvars.example` into each project directory and fill in your values: + +```sh +cp terraform.tfvars.example 001-hub-project/terraform.tfvars +cp terraform.tfvars.example 002-spoke-project/terraform.tfvars +cp terraform.tfvars.example 003-spoke-project/terraform.tfvars +# edit each file +``` + +The minimum required values are documented in each `000-variables.tf`. + +--- + +## Deployment Order + +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) +cd 001-hub-project +terraform init -backend-config=backend.conf +terraform apply + +# Step 2 — Copy outputs into spoke terraform.tfvars +terraform output network_area_id # → set as stackit_network_area_id in spokes +terraform output firewall_lan_ip # → set as hub_firewall_lan_ip in spokes (default: 10.28.0.20) + +# Step 3 — Deploy spokes (independently, in any order) +cd ../002-spoke-project +terraform init -backend-config=backend.conf +terraform apply + +cd ../003-spoke-project +terraform init -backend-config=backend.conf +terraform apply +``` + +--- + +## Hub Firewall (pfSense) + +pfSense is provisioned from a qcow2 image with three network interfaces: + +| Interface | Subnet | IP | Purpose | +| --------- | --------------- | ------------ | -------------------------- | +| WAN | `10.28.0.0/28` | `10.28.0.4` | Internet uplink | +| LAN | `10.28.0.16/28` | `10.28.0.20` | Default gateway for spokes | +| MGMT | `10.28.0.32/28` | `10.28.0.36` | Web UI / SSH access | + +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](https://www.netgate.com/pfsense-plus-software/try-pfsense) and convert to qcow2 if needed. + +--- + +## Spoke Projects + +### 002-spoke-project — Linux servers (RHEL 9) + +Two servers showing different machine type profiles: + +| Server | Machine Type | Purpose | +| ---------- | ------------ | ------------------------ | +| `server-a` | `c2i.2` | General-purpose compute | +| `server-b` | `m1a.8d` | Memory-optimized compute | + +### 003-spoke-project — Windows Server instances + +Two servers showing Windows with different compute profiles: + +| Server | Machine Type | Purpose | +| ------------------ | ------------ | ---------------------------- | +| `windows-server-a` | `m2i.8` | Standard Windows workload | +| `windows-server-b` | `g2i.16` | GPU-enabled Windows workload | + +--- + +## Server Module (`modules/server`) + +A single generic module used by all spokes. Select the OS by passing the appropriate `image_id`: + +| OS | UUID (eu01) | +| ------------------- | -------------------------------------- | +| RHEL 9 _(default)_ | `857bf127-1a68-4f34-bda6-4772e8d04a08` | +| Windows Server 2022 | `c3304694-a03f-47c7-8d4c-348eecc7d212` | +| Debian 12 | `b80c8bf2-3f0b-4049-9473-1487141a8e2a` | + +Verify image UUIDs in the STACKIT portal before deploying — they can change between releases. + +--- + +## What You Must Adapt Before Use + +| Value | Where | Description | +| ------------------------- | --------------------------------------- | ------------------------------------------------------- | +| `stackit_organization_id` | all `000-variables.tf` / tfvars | Your STACKIT org UUID | +| `stackit_folder_id` | all `000-variables.tf` / tfvars | Folder that contains the projects | +| `stackit_network_area_id` | spoke `000-variables.tf` / tfvars | Output of `001-hub-project` | +| `org_admin` | all `000-variables.tf` / tfvars | Project owner email | +| `mgmt_ip_range` | `001-hub-project` tfvars | CIDR allowed to reach the firewall management interface | +| 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 | + +--- + +## References + +- [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/) diff --git a/examples/pfsense-hub-and-spoke/cloud-init/user-init-linux.yml b/examples/pfsense-hub-and-spoke/cloud-init/user-init-linux.yml new file mode 100644 index 0000000..6850de0 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/cloud-init/user-init-linux.yml @@ -0,0 +1,24 @@ +#cloud-config +# --------------------------------------------------------------------------- +# Example cloud-init for Linux instances (RHEL / Debian). +# Creates a local admin user for initial access. +# +# IMPORTANT: Replace the password hash below with a hash of your own +# secure password before deploying. Never use a shared or well-known +# default password in production. +# +# Generate a SHA-512 hash on Linux/macOS: +# python3 -c "import crypt; print(crypt.crypt('YourPassword', crypt.mksalt(crypt.METHOD_SHA512)))" +# --------------------------------------------------------------------------- +users: + - name: admin-user + groups: sudo + shell: /bin/bash + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + lock_passwd: false + passwd: "" + +chpasswd: + expire: false + +ssh_pwauth: true diff --git a/examples/pfsense-hub-and-spoke/cloud-init/user-init-windows.yml b/examples/pfsense-hub-and-spoke/cloud-init/user-init-windows.yml new file mode 100644 index 0000000..89103d4 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/cloud-init/user-init-windows.yml @@ -0,0 +1,12 @@ +#cloud-config +# --------------------------------------------------------------------------- +# Example cloud-init for Windows Server instances. +# Creates a local administrator account. +# +# IMPORTANT: Replace the username and password below with your own values +# before deploying. Never use shared or well-known default credentials. +# --------------------------------------------------------------------------- +users: + - name: admin-user + groups: administrators + passwd: "" diff --git a/examples/pfsense-hub-and-spoke/modules/server/main.tf b/examples/pfsense-hub-and-spoke/modules/server/main.tf new file mode 100644 index 0000000..f5d34b9 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/modules/server/main.tf @@ -0,0 +1,55 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + } + } +} + +resource "stackit_volume" "boot_volume" { + project_id = var.project_id + name = "${var.name}-volume" + availability_zone = var.availability_zone + size = var.disk_size + performance_class = var.disk_performance_class + source = { + type = "image" + id = var.image_id + } +} + +resource "stackit_network_interface" "nic" { + project_id = var.project_id + network_id = var.network_id + security = var.security_enabled +} + +resource "stackit_server" "server" { + project_id = var.project_id + name = var.name + availability_zone = var.availability_zone + machine_type = var.machine_type + + boot_volume = { + source_type = "volume" + source_id = stackit_volume.boot_volume.volume_id + } + + network_interfaces = [stackit_network_interface.nic.network_interface_id] + + user_data = var.user_data +} diff --git a/examples/pfsense-hub-and-spoke/modules/server/outputs.tf b/examples/pfsense-hub-and-spoke/modules/server/outputs.tf new file mode 100644 index 0000000..0d25ff3 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/modules/server/outputs.tf @@ -0,0 +1,26 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "server_id" { + value = stackit_server.server.server_id +} + +output "server_name" { + value = stackit_server.server.name +} + +output "primary_ip" { + description = "Primary IP address of the server's network interface." + value = stackit_network_interface.nic.ipv4 +} diff --git a/examples/pfsense-hub-and-spoke/modules/server/variables.tf b/examples/pfsense-hub-and-spoke/modules/server/variables.tf new file mode 100644 index 0000000..e8ef1aa --- /dev/null +++ b/examples/pfsense-hub-and-spoke/modules/server/variables.tf @@ -0,0 +1,74 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "project_id" { + description = "STACKIT project ID." + type = string +} + +variable "network_id" { + description = "Network ID (UUID) to attach the server to." + type = string +} + +variable "name" { + description = "Server hostname." + type = string +} + +variable "availability_zone" { + description = "Availability zone (e.g. eu01-1)." + type = string +} + +variable "machine_type" { + description = "Machine type / flavor (e.g. c2i.2, m1a.8d, g2i.16)." + type = string + default = "c2i.2" +} + +variable "image_id" { + description = <<-EOT + Boot image UUID. + Defaults to RHEL 9 (eu01) — verify the current ID in the STACKIT portal under Compute > Images. + For Windows Server 2022 (eu01) use: c3304694-a03f-47c7-8d4c-348eecc7d212 + For Debian 12 (eu01) use: b80c8bf2-3f0b-4049-9473-1487141a8e2a + EOT + type = string + default = "857bf127-1a68-4f34-bda6-4772e8d04a08" +} + +variable "disk_size" { + description = "Boot volume size in GB." + type = number + default = 50 +} + +variable "disk_performance_class" { + description = "Storage performance class (e.g. storage_premium_perf1)." + type = string + default = "storage_premium_perf1" +} + +variable "user_data" { + description = "Cloud-init user data string." + type = string + default = "" +} + +variable "security_enabled" { + description = "Enable port security on the network interface." + type = bool + default = false +} diff --git a/examples/pfsense-hub-and-spoke/terraform.tfvars.example b/examples/pfsense-hub-and-spoke/terraform.tfvars.example new file mode 100644 index 0000000..f9269a5 --- /dev/null +++ b/examples/pfsense-hub-and-spoke/terraform.tfvars.example @@ -0,0 +1,63 @@ +# --------------------------------------------------------------------------- +# terraform.tfvars.example +# +# Copy this file into a project directory (e.g. 001-hub-project/terraform.tfvars) +# and fill in your values. Do NOT commit terraform.tfvars to version control. +# +# Alternatively, export variables as environment variables: +# export TF_VAR_stackit_organization_id="" +# --------------------------------------------------------------------------- + +# --- STACKIT Identity -------------------------------------------------------- + +# Your STACKIT Organization ID. +# Portal: Organization > Settings > Organization ID +stackit_organization_id = "" + +# Folder that will contain the STACKIT project(s). +# Portal: Resource Manager > Folders +stackit_folder_id = "" + +# Network Area ID — created by 001-hub-project on first apply. +# For spoke projects: run `terraform output network_area_id` in 001-hub-project. +# Not needed for 001-hub-project itself. +# stackit_network_area_id = "" + +# Email address of the STACKIT user set as project owner. +org_admin = "" + +# --- Service Account --------------------------------------------------------- + +# Path to the STACKIT service account key file. +# Download: STACKIT portal > Service Accounts > Keys > Create key +stackit_service_account_key_path = "./keys/service-account.json" + +# --- Region ------------------------------------------------------------------ + +stackit_region = "eu01" +default_zone = "eu01-1" + +# --- Hub: network access control (001-hub-project only) ---------------------- + +# CIDR allowed to reach the pfSense 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 = "" + +# --- Hub: pfSense HA (001-hub-project only) ----------------------------------- + +# Machine type for both pfSense nodes. Default: c2i.2 +# pfsense_machine_type = "c2i.2" + +# --- Spoke: networking (002 and 003 spoke projects) -------------------------- + +# Subnet for this spoke. Must be within the network area range (10.28.0.0/16). +# 002-spoke-project default: 10.28.1.0/28 +# 003-spoke-project default: 10.28.2.0/28 +# spoke_subnet = "10.28.1.0/28" + +# LAN IP of the active pfSense node — used as the default route next-hop for spoke traffic. +# Run `terraform output firewall_lan_ip` in 001-hub-project. +# On failover: update this to the secondary node IP (10.28.0.21) and re-apply spokes, +# or update the routing table route directly via the STACKIT API. +# hub_firewall_lan_ip = "10.28.0.20" -- 2.49.1 From 4170ba87431224791a19da81748803a19e63d89c Mon Sep 17 00:00:00 2001 From: Sven Schmidt Date: Wed, 15 Apr 2026 17:02:56 +0200 Subject: [PATCH 2/6] removed static image UUID --- examples/pfsense-hub-and-spoke/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/pfsense-hub-and-spoke/README.md b/examples/pfsense-hub-and-spoke/README.md index 0e55270..336d105 100644 --- a/examples/pfsense-hub-and-spoke/README.md +++ b/examples/pfsense-hub-and-spoke/README.md @@ -222,15 +222,13 @@ Two servers showing Windows with different compute profiles: ## Server Module (`modules/server`) -A single generic module used by all spokes. Select the OS by passing the appropriate `image_id`: +A single generic module used by all spokes. Select the OS by passing the appropriate `image_id`. Supported operating systems include: -| OS | UUID (eu01) | -| ------------------- | -------------------------------------- | -| RHEL 9 _(default)_ | `857bf127-1a68-4f34-bda6-4772e8d04a08` | -| Windows Server 2022 | `c3304694-a03f-47c7-8d4c-348eecc7d212` | -| Debian 12 | `b80c8bf2-3f0b-4049-9473-1487141a8e2a` | +- RHEL 9 _(default)_ +- Windows Server 2022 +- Debian 12 -Verify image UUIDs in the STACKIT portal before deploying — they can change between releases. +**Image UUIDs:** Image IDs change between releases and vary by region. Retrieve the current image UUIDs from the STACKIT portal before deploying. Set the appropriate `image_id` value in your Terraform configuration. --- -- 2.49.1 From 4b523a9fe8629617598abd73741c561d8dec066d Mon Sep 17 00:00:00 2001 From: Sven Schmidt Date: Wed, 15 Apr 2026 17:03:45 +0200 Subject: [PATCH 3/6] fix: update reference to STACKIT portal in README --- examples/pfsense-hub-and-spoke/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pfsense-hub-and-spoke/README.md b/examples/pfsense-hub-and-spoke/README.md index 336d105..e81b16d 100644 --- a/examples/pfsense-hub-and-spoke/README.md +++ b/examples/pfsense-hub-and-spoke/README.md @@ -228,7 +228,7 @@ A single generic module used by all spokes. Select the OS by passing the appropr - Windows Server 2022 - Debian 12 -**Image UUIDs:** Image IDs change between releases and vary by region. Retrieve the current image UUIDs from the STACKIT portal before deploying. Set the appropriate `image_id` value in your Terraform configuration. +**Image UUIDs:** Image IDs change between releases and vary by region. Retrieve the current image UUIDs from STACKIT before deploying. Set the appropriate `image_id` value in your Terraform configuration. --- -- 2.49.1 From 9a1f91eaa0169ba2c3ecae956ca22fb41abf11b4 Mon Sep 17 00:00:00 2001 From: Sven Schmidt Date: Wed, 15 Apr 2026 17:09:22 +0200 Subject: [PATCH 4/6] fix: update machine types for server configurations in examples --- examples/pfsense-hub-and-spoke/002-spoke-project/040-servers.tf | 2 +- examples/pfsense-hub-and-spoke/003-spoke-project/040-servers.tf | 2 +- examples/pfsense-hub-and-spoke/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/pfsense-hub-and-spoke/002-spoke-project/040-servers.tf b/examples/pfsense-hub-and-spoke/002-spoke-project/040-servers.tf index e093380..2c330aa 100644 --- a/examples/pfsense-hub-and-spoke/002-spoke-project/040-servers.tf +++ b/examples/pfsense-hub-and-spoke/002-spoke-project/040-servers.tf @@ -32,7 +32,7 @@ module "server_b" { network_id = stackit_network.spoke_network.network_id availability_zone = var.default_zone name = "server-b" - machine_type = "m1a.8d" + machine_type = "m2a.8d" disk_size = 100 disk_performance_class = "storage_premium_perf1" user_data = templatefile("${path.module}/../cloud-init/user-init-linux.yml", {}) diff --git a/examples/pfsense-hub-and-spoke/003-spoke-project/040-servers.tf b/examples/pfsense-hub-and-spoke/003-spoke-project/040-servers.tf index 37701cb..458bd64 100644 --- a/examples/pfsense-hub-and-spoke/003-spoke-project/040-servers.tf +++ b/examples/pfsense-hub-and-spoke/003-spoke-project/040-servers.tf @@ -38,7 +38,7 @@ module "windows_server_b" { availability_zone = var.default_zone name = "windows-server-b" image_id = local.windows_image_id - machine_type = "g2i.16" + machine_type = "n2.14d.g1" disk_size = 100 disk_performance_class = "storage_premium_perf1" user_data = templatefile("${path.module}/../cloud-init/user-init-windows.yml", {}) diff --git a/examples/pfsense-hub-and-spoke/README.md b/examples/pfsense-hub-and-spoke/README.md index e81b16d..fd53011 100644 --- a/examples/pfsense-hub-and-spoke/README.md +++ b/examples/pfsense-hub-and-spoke/README.md @@ -216,7 +216,7 @@ Two servers showing Windows with different compute profiles: | Server | Machine Type | Purpose | | ------------------ | ------------ | ---------------------------- | | `windows-server-a` | `m2i.8` | Standard Windows workload | -| `windows-server-b` | `g2i.16` | GPU-enabled Windows workload | +| `windows-server-b` | `n2.14d.g1` | GPU-enabled Windows workload | --- -- 2.49.1 From 7f78fc6cb68bba336aaa88b22e969756cd02d536 Mon Sep 17 00:00:00 2001 From: Sven Schmidt Date: Wed, 15 Apr 2026 17:10:24 +0200 Subject: [PATCH 5/6] fix: remove hyperlink from pfSense download instructions in README --- examples/pfsense-hub-and-spoke/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pfsense-hub-and-spoke/README.md b/examples/pfsense-hub-and-spoke/README.md index fd53011..46de94b 100644 --- a/examples/pfsense-hub-and-spoke/README.md +++ b/examples/pfsense-hub-and-spoke/README.md @@ -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](https://www.netgate.com/pfsense-plus-software/try-pfsense) and convert to qcow2 if needed. +**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. --- -- 2.49.1 From c7198110b89efbc8351d5f34c376e1c7cecb773c Mon Sep 17 00:00:00 2001 From: Sven Schmidt Date: Wed, 15 Apr 2026 17:15:00 +0200 Subject: [PATCH 6/6] fix: refine description for pfSense machine type and update comments in example variables --- .../001-hub-project/000-variables.tf | 2 +- examples/pfsense-hub-and-spoke/terraform.tfvars.example | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/examples/pfsense-hub-and-spoke/001-hub-project/000-variables.tf b/examples/pfsense-hub-and-spoke/001-hub-project/000-variables.tf index b2ef2c1..4ce0077 100644 --- a/examples/pfsense-hub-and-spoke/001-hub-project/000-variables.tf +++ b/examples/pfsense-hub-and-spoke/001-hub-project/000-variables.tf @@ -58,7 +58,7 @@ variable "mgmt_ip_range" { } variable "pfsense_machine_type" { - description = "Machine type used for both pfSense nodes (e.g. c2i.2, c2i.4). Both nodes use the same type." + description = "Machine type for the pfSense firewall (e.g. c2i.2, c2i.4)." type = string default = "c2i.2" } diff --git a/examples/pfsense-hub-and-spoke/terraform.tfvars.example b/examples/pfsense-hub-and-spoke/terraform.tfvars.example index f9269a5..9720e4a 100644 --- a/examples/pfsense-hub-and-spoke/terraform.tfvars.example +++ b/examples/pfsense-hub-and-spoke/terraform.tfvars.example @@ -44,11 +44,6 @@ default_zone = "eu01-1" # Leave empty (or remove) to create no management access rules. # mgmt_ip_range = "" -# --- Hub: pfSense HA (001-hub-project only) ----------------------------------- - -# Machine type for both pfSense nodes. Default: c2i.2 -# pfsense_machine_type = "c2i.2" - # --- Spoke: networking (002 and 003 spoke projects) -------------------------- # Subnet for this spoke. Must be within the network area range (10.28.0.0/16). @@ -56,8 +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 active pfSense node — used as the default route next-hop for spoke traffic. +# LAN IP of the pfSense firewall — used as the default route next-hop for spoke traffic. # Run `terraform output firewall_lan_ip` in 001-hub-project. -# On failover: update this to the secondary node IP (10.28.0.21) and re-apply spokes, -# or update the routing table route directly via the STACKIT API. # hub_firewall_lan_ip = "10.28.0.20" -- 2.49.1