Repository navigation
OpenTofu Stacks and Modules
Never used Terraform or OpenTofu? What is infrastructure as code explains providers, resources, plan, apply and state from zero, and shows how to run this repo's tests with no cloud account.
Every resource in this project is declared in OpenTofu and nothing is clicked together in a console (the account hardening checklist in stage 0 is the one deliberate exception, because it has to happen before any automation has credentials).
OpenTofu is the open-source fork of Terraform, maintained under the Linux Foundation and licensed MPL 2.0. HashiCorp moved Terraform to the Business Source License in 2023. For a public teaching repo that people are invited to copy, the tool should stay under an open license that will not change under them. The language is the same, the providers are the same, and everything you learn here transfers to Terraform.
CI pins OpenTofu 1.12.6; anything 1.8 or newer runs the tests locally.
For a line-by-line explanation of every variable, resource and output, read the Terraform reference.

Diagram source (Mermaid)
flowchart LR
subgraph modules["modules/ (reusable building blocks, never applied alone)"]
mvpc["vpc"]:::ctrl
mfw["firewall"]:::ctrl
mtc["twingate-connector<br/>the droplet"]:::ctrl
mtb["twingate-bootstrap<br/>connector + tokens + cloud-init"]:::ctrl
end
subgraph stacks["stacks/ (what Spacelift actually applies)"]
s00["00-foundation<br/>project, baseline tag,<br/>VPC, baseline firewall"]:::cloud
s10["10-access<br/>site, SSH key lookup, 2 connectors,<br/>connector firewall, group, resources"]:::cloud
end
mvpc --> s00
mfw --> s00
mfw --> s10
mtc --> s10
s00 -- "outputs: vpc_id, project_id,<br/>baseline_tag, region" --> s10
classDef ctrl fill:#1e1b3a,stroke:#a78bfa,color:#ede9fe
classDef cloud fill:#0f2a3d,stroke:#38bdf8,color:#e0f2fe
-
Modules describe one thing well and take inputs. The same
firewallmodule builds both the baseline firewall and the connector firewall with different arguments. - Stacks wire modules together for one layer of the environment, and each stack is one Spacelift stack with its own state.
| Reason | What it buys |
|---|---|
| Blast radius | A change to the access layer cannot plan a change to the VPC, because the VPC is not in that state. |
| Different change rates | The foundation changes almost never; access changes whenever a person or host is added. |
| Different credentials | Stack 00 only needs a DigitalOcean token. Only stack 10 receives the Twingate API token. Least privilege at the run level. |
| Readable plans | A human confirms every apply, and a short plan gets read. A 300-line plan gets skimmed. |
| Destroy order is explicit | Destroy 10 first, then 00. The dependency is a real dependency, not something you remember. |
The numbers leave gaps (00, 10, later 20, 30, 40) so a layer can be slotted in later without renaming everything.
Outputs flow from 00 to 10 through Spacelift stack dependencies, mapped to TF_VAR_* environment variables, rather than through terraform_remote_state. That way stack 10 never needs read access to stack 00's entire state, only the four values it consumes.
-
Region is a variable everywhere. Names follow
<layer>-<region>-<role>. A second region is the same modules called again. -
Validation at the edge. Variables reject bad input before a plan touches an API: the baseline tag must start with
cah-baseline-(so a droplet cannot accidentally miss the baseline firewall), the Twingate network must be a bare subdomain and not a URL, the admin user list cannot be empty, a connector instance is one lowercase letter or digit. -
Look up, do not re-create, what already exists. The admin SSH key is already on the DigitalOcean account, and the API refuses a second registration of the same key with a 422. So stack 10 uses a
datasource to look it up by name. - Sensitive values stay out of outputs. A test pins the exact list of declared outputs, so adding an output (and maybe leaking a token through it) has to be done on purpose.
-
Provider lock files are committed (
.terraform.lock.hcl), so every run uses the same provider builds. - Only local module sources. The cost gate refuses remote module sources it cannot read; see CI, tests and the cost gate.
Every module and stack has tests/*.tftest.hcl files that run with mocked providers, so tofu test needs no credentials and costs nothing. They assert the properties that matter for security, for example:
- the baseline firewall has zero inbound rules;
- the connector firewall has exactly one inbound rule, TCP 22, sourced from the connector tag and never an address;
- each SSH resource allows only TCP 22, denies UDP and has ICMP off;
- connector tokens never appear in module outputs;
- invalid inputs are rejected.
Run them:
tofu -chdir=modules/firewall init -backend=false
tofu -chdir=modules/firewall testRepeat for modules/vpc, modules/twingate-bootstrap, modules/twingate-connector, stacks/00-foundation and stacks/10-access. CI runs all of them on every pull request.
You may see "deprecated source" warnings from tofu test about access_group on twingate_resource. The block itself is not deprecated; the provider deprecates two attributes nested inside it, which this repository does not set, and the warning appears where a test reads the whole block. Nothing needs replacing. The provider is pinned to 3.x; moving to a newer major version is an optional upgrade tracked in issue #93.
Next: Spacelift, step 16 of the core path on the Reading order page. The Terraform reference is the line-by-line lookup page; it is optional on the core path.
Source for these pages lives in docs/wiki/ and is published by the Wiki workflow on every merge to main. Corrections are welcome as pull requests; edits made in the wiki editor are overwritten on the next publish. Apache-2.0 licensed, see LICENSE and NOTICE in the repository.
Support this project: support.chiefgyk3d.com, or see Home.
Core path: read in this order
- Start here if you are new
- What is the cloud
- Networking basics
- Identity, secrets, zero trust
- Infrastructure as code
- Git and pull requests
- Containers and Docker (optional)
- How money works
- Enterprise habits
- Architecture overview
- Why ZTNA and not a VPN
- Twingate
- DigitalOcean network and firewalls
- Droplet hardening
- OpenTofu, stacks and modules
- Spacelift
- Doppler and secrets
- CI, tests and the cost gate
- Threat model
- Lessons learned
- Roadmap
Track: build it yourself
Track: understand the design
The rest of this track is on the core path; see Reading order.
Track: security and threat modelling
- Threat model exercise
- Threat Dragon exercise
- Threat frameworks: STRIDE, ATT&CK, ATLAS
- Troubleshooting index
The Threat model is core step 19.
Track: detection and monitoring
- A small example: the enterprise landscape
- Detection, Wazuh and the landscape
- Cloud telemetry: what and why (unverified draft)
- Multiple sites and identity (unverified draft)
- Telemetry crosses the trust boundary (unverified draft)
- Reading a dashboard as an operator (unverified draft)
- Build your own mini SOC (unverified draft)
- Stage 2 home-side runbook (unverified draft)
- Grafana dashboards (unverified draft)