Terraform Modules¶
Reusable Terraform modules published by b'nerd for common infrastructure patterns on b'nerd Cloud (OpenStack-based).
Source: app/terraform/common
Available Modules¶
| Module | Purpose |
|---|---|
ipsec-gateway | Site-to-site IPsec VPN gateway VM (Ubuntu + strongSwan) |
ipsec-gateway¶
Deploys a Linux IPsec VPN gateway VM on OpenStack — a lightweight site-to-site tunnel endpoint with one public/management NIC and one dedicated NIC per tunnel.
What it does¶
- Creates one OpenStack network + subnet + router interface per tunnel.
- Creates a public/management port on an existing network you provide.
- Associates a pre-allocated floating IP with the first tunnel port.
- Creates two security groups: public (IKE/NAT-T/ESP/SSH) and internal ("allow all" on tunnel-side ports).
- Bootstraps the VM via cloud-init: enables IP forwarding and installs
strongswan+strongswan-swanctl.
What it does NOT do¶
- Configure swanctl, PSKs, iptables forwarding rules, or any application running on top (HAProxy, BGP, etc.). Software configuration is done separately — typically with Ansible against the floating IP after apply.
- Create the router, management network/subnet, or floating IP — provide these as inputs.
- Generate or manage SSH keys; you supply a public key.
Architecture¶
Site A ── IPsec Tunnel 1 ──►┐
│ Gateway VM (<floating_ip>)
│ Ubuntu 24.04 + strongSwan
Site B ── IPsec Tunnel 2 ──►│
│ eth0: public/mgmt NIC
│ eth1: tunnel1 NIC (10.0.0.0/24)
│ eth2: tunnel2 NIC (10.0.1.0/24)
└──► Existing OpenStack router
(each tunnel subnet attached as
router interface)
The floating IP binds to the first tunnel port by default, which is where incoming IKE/NAT-T packets should terminate. Use floating_ip_target = "mgmt" only when external SSH must reach the VM without depending on L3-router conntrack.
Prerequisites¶
- Terraform >= 1.5
- OpenStack provider
~> 3.0(credentials viaOS_*env vars orclouds.yaml) - An existing OpenStack router
- An existing network + subnet for the management port, attached to that router
- A pre-allocated floating IP
- A VM image (Ubuntu 24.04 recommended)
Usage¶
From the Git source (recommended — pin a tag):
module "ipsec_gateway" {
source = "git::https://git.bnerd.net/cloud/terraform/common.git//modules/ipsec-gateway?ref=v1.0.0"
prefix = "acme-gw"
router_id = data.openstack_networking_router_v2.router.id
network_id = "<management-network-uuid>"
subnet_id = "<management-subnet-uuid>"
floating_ip = "203.0.113.10"
image_id = data.openstack_images_image_v2.ubuntu.id
ssh_public_key = tls_private_key.gateway.public_key_openssh
tunnels = [
{
name = "tunnel1"
local_cidr = "10.0.0.0/24"
remote_cidrs = ["192.168.10.0/24"]
},
{
name = "tunnel2"
local_cidr = "10.0.1.0/24"
remote_cidrs = ["192.168.20.0/24", "192.168.30.0/24"]
},
]
}
Vendored locally:
module "ipsec_gateway" {
source = "../../path/to/terraform/common/modules/ipsec-gateway"
# same inputs as above
}
Inputs¶
Required¶
| Name | Type | Description |
|---|---|---|
prefix | string | Prefix applied to every resource name this module creates |
router_id | string | ID of the existing router to attach tunnel networks to |
network_id | string | ID of the existing network for the management port |
subnet_id | string | ID of the existing subnet on the management network |
floating_ip | string | Pre-allocated floating IP address |
image_id | string | OpenStack image ID for the VM |
ssh_public_key | string | SSH public key to inject into the VM |
tunnels | list(object({ name=string, local_cidr=string, remote_cidrs=list(string) })) | One entry per tunnel. At least one required. |
Optional¶
| Name | Type | Default | Description |
|---|---|---|---|
flavor_name | string | "m1.small" | OpenStack flavor for the VM |
availability_zone | string | "az1" | OpenStack availability zone |
dns_nameservers | list(string) | ["8.8.8.8", "8.8.4.4"] | DNS servers injected into tunnel subnets |
ssh_allowed_cidrs | list(string) | ["0.0.0.0/0"] | Source CIDRs allowed to SSH. Restrict this in production. |
sshd_port | number | 2222 | SSH port (moved off 22 when HAProxy handles that port) |
floating_ip_target | string | "tunnel" | Bind floating IP to "tunnel" (first tunnel port) or "mgmt" |
Outputs¶
| Name | Description |
|---|---|
gateway_floating_ip | Public floating IP of the gateway |
gateway_internal_ip | Internal IP on the management network |
gateway_instance_id | OpenStack instance ID |
tunnel_network_ids | Map of tunnel name → network ID |
tunnel_subnet_ids | Map of tunnel name → subnet ID |
tunnel_gateway_ips | Map of tunnel name → gateway IP on that tunnel net |
Security model¶
The public security group (<prefix>-ipsec-gw) allows:
| Port / proto | Purpose | Source |
|---|---|---|
500/udp | IKE | 0.0.0.0/0 |
4500/udp | NAT-T (IPsec NAT traversal) | 0.0.0.0/0 |
IP proto 50 | ESP (encapsulated payload) | 0.0.0.0/0 |
var.sshd_port | SSH management | var.ssh_allowed_cidrs |
The internal security group (<prefix>-ipsec-internal) allows all IPv4 ingress on the tunnel-side ports — required because the gateway forwards arbitrary traffic from remote sites that OpenStack port security would otherwise drop. Each tunnel port also has allowed_address_pairs entries for the local subnet and every declared remote subnet.
NAT traversal
The floating IP is a 1:1 NAT in OpenStack. IKEv2 handles this automatically via NAT-T (UDP 4500). Use local_addrs = %any in your swanctl config so strongSwan detects its real address behind NAT.
Notes¶
- First tunnel is special:
var.tunnels[0]is the entry whose port carries the floating IP. Reordering the list will recreate the association. - Existing router only: the module attaches to a router you already own — it never creates one, avoiding collisions with shared networks.
- Next steps: after
terraform apply, configure swanctl/PSKs/iptables on the VM (typically via Ansible against the floating IP). - A complete, runnable example is in the source under
modules/ipsec-gateway/examples/basic/.
See Also¶
- Terraform Provider — manage b'nerd Cloud resources as code
- Infrastructure as Code guide