example/pfsense-hub-and-spoke #6

Merged
mauritz.uphoff merged 6 commits from example/pfsense-hub-and-spoke into main 2026-04-16 05:53:58 +00:00
31 changed files with 1502 additions and 0 deletions

View file

@ -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 = "<your-state-bucket-name>"
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 = "<your-access-key>"
# secret_key = "<your-secret-key>"
}
}

View file

@ -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 for the pfSense firewall (e.g. c2i.2, c2i.4)."
type = string
default = "c2i.2"
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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]
}

View file

@ -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 vtnet12.
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
}

View file

@ -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://<ip>/"
value = stackit_public_ip.mgmt_public_ip.ip
}

View file

@ -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 = "<your-state-bucket-name>"
access_key = "<your-access-key-id>"
secret_key = "<your-secret-access-key>"

View file

@ -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 = "<your-state-bucket-name>"
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 = "<your-access-key>"
# secret_key = "<your-secret-key>"
}
}

View file

@ -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"
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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 = "m2a.8d"
disk_size = 100
disk_performance_class = "storage_premium_perf1"
user_data = templatefile("${path.module}/../cloud-init/user-init-linux.yml", {})
}

View file

@ -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
}

View file

@ -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 = "<your-state-bucket-name>"
access_key = "<your-access-key-id>"
secret_key = "<your-secret-access-key>"

View file

@ -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 = "<your-state-bucket-name>"
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 = "<your-access-key>"
# secret_key = "<your-secret-key>"
}
}

View file

@ -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"
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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 = "n2.14d.g1"
disk_size = 100
disk_performance_class = "storage_premium_perf1"
user_data = templatefile("${path.module}/../cloud-init/user-init-windows.yml", {})
}

View file

@ -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
}

View file

@ -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 = "<your-state-bucket-name>"
access_key = "<your-access-key-id>"
secret_key = "<your-secret-access-key>"

View file

@ -0,0 +1,255 @@
# 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 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` | `n2.14d.g1` | 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`. Supported operating systems include:
- RHEL 9 _(default)_
- Windows Server 2022
- Debian 12
**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.
---
## 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/)

View file

@ -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: "<replace-with-your-sha512-password-hash>"
chpasswd:
expire: false
ssh_pwauth: true

View file

@ -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: "<replace-with-your-secure-password>"

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,56 @@
# ---------------------------------------------------------------------------
# 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="<uuid>"
# ---------------------------------------------------------------------------
# --- STACKIT Identity --------------------------------------------------------
# Your STACKIT Organization ID.
# Portal: Organization > Settings > Organization ID
stackit_organization_id = "<your-stackit-organization-id>"
# Folder that will contain the STACKIT project(s).
# Portal: Resource Manager > Folders
stackit_folder_id = "<your-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 = "<output-of-hub-project>"
# Email address of the STACKIT user set as project owner.
org_admin = "<admin@example.com>"
# --- 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 = "<x.x.x.x/x>"
# --- 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 pfSense 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"