You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
|`sandboxes`|`src/modules/sandboxes/`| Lightweight sandbox projects for experimentation |
36
+
|`firewall-config`|`src/modules/firewall-config/`| Optional: rules, NAT and internet hardening pushed to the OPNsense appliance through its API |
36
37
37
38
### Governance
38
39
@@ -103,9 +104,12 @@ When `connectivity.firewall` is set, a VM running OPNsense (provided as a `.qcow
103
104
|`vtnet0` (WAN) |`wan_network`: attached to the WAN routing table | Outbound internet egress, assigned a static public IP |
104
105
|`vtnet1` (LAN) |`lan_network`: a dedicated private subnet | Internal next-hop for all corporate landing zone traffic |
105
106
106
-
The firewall's LAN IP is exported as `firewall_next_hop_ip` and passed to every corporate landing zone so they can point their default route at it.
107
+
The firewall's LAN IP is exported as `firewall_next_hop_ip` and passed to every corporate landing zone so they can point their default route at it. Everything the platform sends outwards therefore crosses the appliance.
107
108
108
-
Source: `src/modules/connectivity/`
109
+
Both interfaces are DHCP clients and pick up the fixed addresses STACKIT assigns them. The image boots with almost no policy: only the two default `allow LAN to any` rules (IPv4 and IPv6), no gateways, no static routes, no IPsec, and outbound NAT on `automatic`. It filters nothing until a policy is pushed.
110
+
111
+
> [!WARNING]
112
+
> After the first apply the web GUI answers on the public IP to the whole internet, and it still carries the `root` password shipped in the image, which is identical on every copy. Port 443 is open, 80 and 22 are not, and there is no security group in front of the WAN interface. Pushing the policy closes it, so do not leave a deployment sitting between the two applies — see [Configure OPNsense firewall](getting-started.md#configure-opnsense-firewall). Binding the GUI to the LAN interface only (**System → Settings → Administration → Listen Interfaces**) is the alternative that does not depend on the ruleset.
109
113
110
114
### Landing Zone
111
115
@@ -219,6 +223,39 @@ Traffic flow for a corporate landing zone (firewall flavor):
219
223
220
224
East-west traffic between corporate LZs stays within the Network Area and can be permitted or denied by firewall policies.
221
225
226
+
## Site-to-Site VPN (optional)
227
+
228
+
Any hub-spoke flavor can terminate an IPsec VPN in the connectivity project, connecting the Network Area to on-premises or another cloud. Enable it with the `connectivity.vpn` block — see the commented example in `src/config/hub-and-spoke.tfvars`.
229
+
230
+
The gateway is highly available: it runs two tunnels in separate availability zones, each with its own public IP. Because both sides need the other's address, roll it out in two applies — provision the gateway with `connections = {}`, read `tofu output connectivity_vpn_public_ips`, configure the remote peer, then add the connection.
231
+
232
+
Pre-shared keys are kept out of the config object in the separate `vpn_pre_shared_keys` variable, so the tunnel topology stays committable:
Routes to the remote prefixes are distributed through the Network Area automatically, so every corporate landing zone reaches the remote site without per-spoke configuration.
239
+
240
+
### What goes through the firewall
241
+
242
+
In the firewall flavor, two of the four traffic directions run through the appliance cleanly. Neither VPN direction can be filtered with the managed gateway: one breaks if you try, the other is not steerable at all.
243
+
244
+
| Direction | Through the firewall? | Mechanism |
245
+
|---|---|---|
246
+
| LZ → Internet | Yes, by default | Default route of the landing zone routing table points at the firewall LAN address. Needs an `outbound_nat` entry too, otherwise traffic reaches the appliance and stops there. |
247
+
| LZ → LZ | Yes, by default |`system_routes = false` suppresses project-to-project routes, so spoke-to-spoke falls to the default route. Needs a static route back into the Network Area, otherwise the traffic leaves through WAN and the egress NAT rewrites the source address. |
248
+
| LZ → on-premises (VPN) |**No**| The VPN prefix beats `0.0.0.0/0`, so it bypasses the appliance. |
249
+
| on-premises → LZ (VPN) |**No**| Delivered straight to the spoke through the STACKIT-managed routing table. Not steerable. |
250
+
251
+
Inbound VPN traffic bypasses the appliance because `static_routes` of a VPN connection are distributed as *dynamic* routes across the Network Area, and a remote prefix is always more specific than `0.0.0.0/0`. The VPN gateway itself forwards through a STACKIT-managed routing table that cannot be modified or reassigned.
252
+
253
+
> [!WARNING]
254
+
> Setting `dynamic_routes = false` does not fix the outbound direction, it breaks it. Outbound then goes through the firewall while inbound still bypasses it, and the stateful filter drops the half-flow it sees.
255
+
256
+
If VPN traffic has to be inspected, terminate the tunnel on the OPNsense appliance itself instead of using the managed gateway — it is already the default route for every corporate landing zone, so both directions stay symmetric. The trade-off is losing the managed gateway's active-active HA.
257
+
258
+
222
259
## Resource Naming
223
260
224
261
All resources follow a consistent convention driven by `company_code`:
Copy file name to clipboardExpand all lines: docs/getting-started.md
+91-7Lines changed: 91 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -27,6 +27,10 @@ Three ready-to-use configurations are provided in `src/config/`:
27
27
28
28
Choose the flavour that matches your requirements and adjust the corresponding `.tfvars` file before deployment (step 7). At a minimum, update `owner_email`, `organization_id`, `company_name`, and `company_code`.
29
29
30
+
The firewall flavour takes one extra step: the appliance boots unconfigured and its policy is pushed in a second apply, from the `firewall_config` block that ships commented out in the same `.tfvars` file. Until then it filters nothing and its web GUI is reachable from the internet — see [Configure OPNsense firewall](#configure-opnsense-firewall).
31
+
32
+
Both hub-spoke flavours can additionally terminate a site-to-site IPsec VPN in the hub. It is disabled by default — see the commented `connectivity.vpn` block in the `.tfvars` file and [Site-to-Site VPN](architecture.md#site-to-site-vpn-optional). If you deploy the firewall flavour, read [what traffic the firewall actually sees](architecture.md#what-goes-through-the-firewall) before relying on it for VPN inspection.
33
+
30
34
> [!NOTE]
31
35
> This single-root-module approach works well for smaller environments. At larger scale — typically beyond 10 landing zones — you may encounter STACKIT API rate limits during applies and slower plan/refresh cycles due to a growing state file. Tools like [Terragrunt](https://terragrunt.gruntwork.io/), [Terramate](https://terramate.io/), or [Spacelift](https://spacelift.io/) can help by splitting landing zones into isolated state files and orchestrating root module calls with proper concurrency controls. If you are planning a larger enterprise deployment, reach out to [STACKIT](https://stackit.de) or a partner offering a verified landing zone solution via the [STACKIT Marketplace](https://marketplace.stackit.cloud/de/catalog?marketplaceFilters=industries:Service%20%26%20IT%20Provider,deliveryMethod:PROFESSIONAL_SERVICE,categories:DevOps).
32
36
@@ -124,6 +128,17 @@ tofu init
124
128
125
129
### 9. Deploy the landing zone
126
130
131
+
If you enabled the `connectivity.vpn` block, export the pre-shared keys as an environment variable first. They are kept out of the `tfvars` on purpose:
> If you deploy the provided OPNsense firewall make sure to follow [Configure OPNsense firewall](#configure-opnsense-firewall) afterwards. Until then the appliance filters nothing and its web GUI answers on the public IP.
139
+
140
+
In any case run opentofu to deploy the infrastructure:
Step 9 boots the appliance but leaves it unconfigured: it filters nothing and its web GUI answers on the public IP. Pushing a policy is a **second apply**, because the API key is derived from the appliance login and a provider configuration has to resolve before any resource is planned, so a key created during an apply cannot configure the provider in that same run.
242
+
243
+
What the policy does, and which traffic directions it can actually see, is in [What goes through the firewall](architecture.md#what-goes-through-the-firewall).
244
+
245
+
> [!NOTE]
246
+
> The policy is pushed with the [`browningluke/opnsense`](https://registry.terraform.io/providers/browningluke/opnsense/latest/docs) provider. It maps OPNsense's expanded read format back to what was written, so in-place updates and drift detection work — the generic `Mastercard/restapi` provider cannot do this and makes objects effectively write-once. It is community maintained and its author advises against production use, so weigh that before relying on it.
247
+
248
+
**1. Enable the policy**
249
+
250
+
Uncomment the `firewall_config` block in `config/hub-and-spoke-firewall.tfvars`. It comes with default rules: internet egress with NAT, spoke-to-spoke limited to HTTPS and ICMP, and the two floating rules that take the web GUI off the internet.
251
+
252
+
Two entries need your values:
253
+
254
+
```hcl
255
+
# Public: OpenTofu runs outside the Network Area* — a workstation, or a CI runner on the public internet. This is the normal case for a first deployment, because nothing is inside the area yet. Traffic goes over the appliance's public address:
256
+
257
+
endpoint = "https://<connectivity_firewall_public_ip>" # get it from running "tofu output connectivity_firewall_public_ip"
258
+
fw_management = {
259
+
content = ["10.0.0.0/16", "<your public address>/32"] # get it eg by running "curl -s https://ifconfig.me" or better use a fixed ip range
260
+
}
261
+
262
+
263
+
# Private: OpenTofu runs inside the Network Area* — a CI runner in a landing zone, a jumphost, or an established site-to-site VPN. Nothing to look up: the LAN address is the default, and your source address is already covered by `10.0.0.0/16`:
264
+
265
+
# omit endpoint
266
+
fw_management = {
267
+
content = ["10.0.0.0/16"]
268
+
}
269
+
```
270
+
271
+
> [!WARNING]
272
+
> Whatever runs OpenTofu must be covered by the `fw_management` alias before this apply. Otherwise the same run that closes the GUI cuts off its own path to the API, and recovery goes through the serial console (`stackit server console <server-id> --project-id <connectivity-project-id>`).
273
+
>
274
+
> A dynamic home or office address will eventually change and lock out a later run.
275
+
276
+
**2. Bootstrap the API key, then push the policy**
The first `tofu apply` derives the API key (waiting for the appliance to finish booting, which takes a few minutes) and caches it in `src/.firewall-api-credentials.json`, gitignored. The second `tofu apply` stores it in the management Secrets Manager and pushes aliases, static routes, rules and NAT.
283
+
284
+
Two runs are unavoidable: the provider needs the key at plan time, and a value created during an apply cannot configure a provider in that same run.
285
+
286
+
The appliance login it uses comes from `firewall_admin_password`, which defaults to the `root` password baked into the STACKIT OPNsense image, so nothing has to be exported for a fresh deployment.
287
+
288
+
> [!IMPORTANT]
289
+
> Change the password on the appliance (**System → Access → Users**). The password is only used to derive the API key. Once the key exists, it is no longer read at all.
290
+
291
+
**3. Drop the bootstrap variable and delete the cache file**
292
+
293
+
From here on every apply is a plain `tofu apply` with no variables:
294
+
295
+
```bash
296
+
rm src/.firewall-api-credentials.json
297
+
tofu apply
298
+
```
299
+
300
+
**Rotating the key**
301
+
302
+
Re-running the bootstrap revokes every existing API key on the appliance and mints a fresh one, which immediately invalidates the copy in the Secrets Manager. The follow-up is therefore not optional: bump `firewall_api_secret_version` by one, so the new key is written through. Forgetting the bump fails with a 401 instead of silently keeping a dead key.
303
+
304
+
```bash
305
+
rm -f src/.firewall-api-credentials.json
306
+
tofu apply -var firewall_bootstrap=true # mints the new key
307
+
# bump firewall_api_secret_version in your tfvars, then
308
+
tofu apply -var firewall_bootstrap=true # writes it to the Secrets Manager
309
+
rm src/.firewall-api-credentials.json
310
+
```
311
+
312
+
### Kubernetes: DNS automation for Gateway API resources
225
313
226
314
For Gateway API resources (for example Envoy Gateway with `Gateway` + `HTTPRoute`), use DNS records directly via `stackit_dns_record_set` until native provider support for `extensions.dns.gatewayApi` is available.
227
315
228
-
For the existing sample content in this repository (`landing_zone_sample_gateway` + `landing_zone_sample_http_route` in `src/namespace-service.tf`), the DNS record is created automatically based on the Envoy Gateway LoadBalancer endpoint discovered via `kubernetes_resources`.
316
+
For the existing sample content in this repository (`landing_zone_sample_gateway` + `landing_zone_sample_http_route` in `src/_landing-zone-kubernetes.tf`), the DNS record is created automatically based on the Envoy Gateway LoadBalancer endpoint discovered via `kubernetes_resources`.
0 commit comments