OpenTofu vs. Terraform: The 2026 Fork Decision Matrix
Sources
- OpenTofu v1.13.0-rc1 Release Notes (Sep 17, 2026)
- OpenTofu v1.12.0 Release Notes — WinRM deprecation
- Terraform v1.16.3 Changelog (Sep 16, 2026)
- Terraform v1.17.0-beta1 — Policy GA, -minimal-refresh
- OpenTofu State and Plan Encryption Documentation
- Symbol Libraries RFC — OpenTofu PR #4052
- OpenTofu Language and Compatibility Settings
- OpenTofu Dependency Lock File Documentation
- OpenTofu CLI Environment Variables
- Terraform Policy Documentation (HashiCorp Developer)
- Terraform LICENSE — Business Source License 1.1 (IBM)
- OpenTofu LICENSE — MPL 2.0
- Terraform v1.14.0 — Actions, List Resources, terraform query
- Terraform v1.16.0 — store blocks, module import, Mermaid graphs
- OpenTofu v1.9.0 — provider for_each, -exclude
- OpenTofu v1.10.0 — OCI registry support
Three years ago the OpenTofu fork was a licensing protest with a compatibility promise. This week it became two different products. On September 17, 2026, OpenTofu shipped v1.13.0-rc1 — the release that removes WinRM provisioning entirely, ships an experimental Symbol Libraries language extension that Terraform cannot even parse, and adds a built-in linter. Twenty-four hours earlier, on September 16, HashiCorp shipped Terraform 1.16.3, and the week before that the 1.17.0-beta1 preview took Terraform Policy to general availability and added a -minimal-refresh planning mode. Two releases, two directions: one project is rebuilding the language, the other is building an enterprise control plane around it.
This is the decision matrix for platform teams in September 2026 — where the forks have genuinely diverged, where they still share DNA, what a switch actually costs, and who should not bother. Every version number, flag, and configuration block below is checked against the release notes and documentation of the project that ships it, not against the marketing of either side.
The 30-Second Version
- Both forks shipped this week: OpenTofu v1.13.0-rc1 (Sep 17) and Terraform v1.16.3 (Sep 16) + v1.17.0-beta1 (Sep 9). The cadence gap is real but smaller than partisans claim: OpenTofu shipped 1.12.0 on May 14, 2026 — the same day Terraform shipped 1.12.0 in 2025, but a year apart in series.
- Pick OpenTofu if: state/plan encryption is a compliance requirement, you want provider-level
for_eachfor multi-region estates, you need-excludefor surgical applies, or your supply-chain policy requires OSI-approved licenses (MPL 2.0 vs Business Source License 1.1, now licensed by IBM). - Pick Terraform if: you run HCP Terraform or Terraform Enterprise, you want Stacks, Actions, the GA policy framework, or List Resources (
terraform query) — all of which have no OpenTofu equivalent as of 1.13. - The switching cost is near-zero today and rising: the dependency lock file is still literally named
.terraform.lock.hclin OpenTofu, and allTF_*environment variables still work. But the moment you adopt the OpenTofulanguageblock or Symbol Libraries, your configuration stops being readable by Terraform. The compatibility promise is now opt-out, not default. - Neither is a safe harbor for WinRM shops: OpenTofu 1.13 removes the
winrmconnection type outright; Terraform still supports it but the ecosystem around it is decaying. Move your Windows provisioning to OpenSSH either way.
How We Got Here: The Fork in One Diagram
The fork's origin is well-documented: HashiCorp relicensed Terraform from MPL 2.0 to the Business Source License 1.1 in August 2023, the community forked the last MPL-licensed commit, and the Linux Foundation took stewardship. What the diagram below adds is where each side actually spent the three years since — and it is not symmetric.
MPL 2.0 Terraform (last common ancestor, Aug 2023)
|
+----------------+----------------+
| |
OPENTOFU (MPL 2.0) TERRAFORM (BUSL 1.1)
| |
1.6 Jan 2024 GA (tofu test) 1.7 Jan 2024 removed blocks
1.7 Apr 2024 state encryption 1.8 Apr 2024 (stable polish)
1.8 Jul 2024 registry v2 1.9 Jun 2024 (stable polish)
1.9 Jan 2025 provider for_each 1.10 Nov 2024 ephemeral resources
-exclude 1.11 Feb 2025 write-only attrs
1.10 Jun 2025 OCI registries 1.12 May 2025 (stable polish)
OpenTelemetry 1.13 Aug 2025 stacks CLI
1.11 Dec 2025 ephemeral+writeonly 1.14 Nov 2025 Actions, List Res
1.12 May 2026 destroy=false 1.15 Apr 2026 convert(), deprecate
-json-into 1.16 Aug 2026 store blocks
1.13 Sep 2026 WinRM REMOVED 1.16.3 Sep 16 bugfix line
Symbol Libraries* 1.17b1 Sep 9 Policy GA
-lint (experimental) -minimal-refresh
| |
language rebuilding control-plane building
+ encryption + supply chain + HCP gravity + policy
| |
TERRAFORM CANNOT PARSE: OPENTOFU CANNOT RUN:
language { experiments = [...] } terraform stacks
*.sym.hcl symbol libraries Actions blocks
symbols "name" { source = ... } tfquery.hcl list resources
-policies flag (Policy GA)
* experimental in 1.13.0-rc1, subject to change even in patches
Read the two right-hand columns as strategy documents. OpenTofu's releases cluster around language surface and supply chain: encryption (1.7), OCI registries (1.10), ephemeral values (1.11), provider checksums and machine-readable output (1.12), Symbol Libraries and linting (1.13). Terraform's cluster around the platform around the language: ephemeral resources (1.10), write-only attributes (1.11), Stacks and Actions (1.13–1.14), Policy (1.17). Neither side is copying the other anymore, with one exception we'll get to — the converged features both shipped independently.
| Dimension | OpenTofu (v1.13.0-rc1) | Terraform (v1.16.3 / 1.17-beta1) | Who it favors |
|---|---|---|---|
| License | MPL 2.0 — OSI-approved, no usage restrictions | Business Source License 1.1; licensor is now IBM Corp.; converts to MPL 2.0 four years after each version's publication. Production use is granted, but hosting/embedding it to compete with IBM's paid offerings is not. | OpenTofu, for regulated or reseller-sensitive orgs |
| Minor-release cadence since the fork | 8 minors in ~32 months (1.6 → 1.13), slower but meatier | 11 minors in the same window (1.6 → 1.17beta), several were polish releases | Terraform on paper; roughly a wash in practice |
| State & plan encryption | Native since 1.7 (Apr 2024): key_provider/method blocks, pbkdf2/aes_gcm, AWS KMS, GCP KMS, Azure Vault, OpenBao |
None native as of 1.16.3/1.17-beta1 — relies on backend-side at-rest encryption, which does not cover plan files or state pulled to an operator's disk | OpenTofu, decisively |
| Provider config | for_each on provider blocks since 1.9 — one aws.by_region block, N regions |
1.17-beta1 adds variables/locals in provider requirements; no provider for_each |
OpenTofu for multi-region/multi-account estates |
| Surgical operations | -exclude (1.9) plus -target |
-target, plus -minimal-refresh (1.17-beta1) to skip refreshing unchanged resources |
Split — OpenTofu excludes, Terraform refreshes less |
| Module/provider distribution | registry.opentofu.org + OCI registry support for providers and modules (1.10), providers lock -oci-mirror (1.13) |
Public registry + HCP private registry; OCI module/provider support in the HCP ecosystem | Even — Terraform's registry is bigger; OpenTofu's is API-compatible and mirrors it |
| Policy as code | None built in — run OPA/Conftest in CI | Terraform Policy GA in 1.17 (beta1 now); -policies on init/plan/apply, no experimental flag needed anymore |
Terraform, decisively |
| Orchestration | None — bring your own (Atlantis, Spacelift, CI) | Stacks, Actions (terraform query, -invoke), HCP run environment |
Terraform, if you accept the gravity |
| Windows provisioning | winrm connection type REMOVED in 1.13; SSH only |
winrm still works; Windows ARM64 builds added in 1.15 | Terraform for legacy WinRM; both prefer OpenSSH now |
| Env vars & lock file | All TF_* vars accepted (plus TOFU_* extras); lock file still .terraform.lock.hcl |
TF_*; lock file .terraform.lock.hcl |
Identical — this is why switching is cheap |
| Language extensions | Symbol Libraries (experimental, 1.13): shared types/functions/values in *.sym.hcl; built-in -lint |
Cannot parse the OpenTofu language block or symbol libraries — hard incompatibility by design |
Divergence — the point of no return for adopters |
What OpenTofu 1.13 Actually Changes (and Why It Matters)
The rc1 notes are unusually honest about why features leave: the Go libraries backing WinRM are unmaintained, so the choice was carry dead code or cut the feature. They cut. That discipline is the through-line of the release.
The five changes with operational consequences
- WinRM connection type removed. Deprecated with warnings in 1.12.0 (May 2026), gone in 1.13. If any
connectionblock in your estate still saystype = "winrm", plan/apply will fail on 1.13. The migration path is OpenSSH on Windows Server — which Microsoft itself now ships as an installable feature. - Symbol Libraries (experimental). Reusable types, functions, and values in standalone
*.sym.hclfiles, imported viasymbolsblocks. This is a real language extension — modules on module registries, but for logic. The docs carry a warning that the feature may change significantly even in patch releases, which is the loudest "do not production-ize this yet" you will read this year. - Built-in linting, behind
-lint. First-iteration linting on supported commands. Terraform's answer isterraform validateplus third-party tooling (tflint); OpenTofu is moving the baseline into the binary. base64gzipoutput changed. A new DEFLATE implementation produces equivalent-but-not-identical output. Ifbase64gzipfeeds a managed resource attribute, expect a plan diff — potentially replacements — on upgrade. Find every usage before you upgrade, not after.- 32-bit builds end with the 1.13 series. Official
*_386/*_armpackages stop with 1.14; 1.13 warns ontofu init. Mostly relevant to edge/IoT and some CI images still built on 386 base layers.
Two quieter 1.13 items deserve operator attention: errored.tfstate is now written on a Go runtime panic, giving you a partial-state recovery artifact that previous versions simply didn't produce, and the SSH client used for remote provisioners gained stricter file-format parsing — the release notes flag that some previously-accepted invalid input is now rejected. If you have a provisioner that "worked by accident," 1.13 is where it stops.
The WinRM migration you may owe
The fix is mechanical but must be staged. Before upgrading, audit for WinRM blocks:
# Every connection block still on winrm — run this before touching the binary:
grep -rn 'type.*=.*"winrm"' --include='*.tf' .
# On the Windows hosts (Server 2019+), OpenSSH is an installable capability:
# Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
# Set-Service sshd -StartupType Automatic; Start-Service sshd
# BEFORE — fails on OpenTofu 1.13, deprecated since 1.12:
provisioner "remote-exec" {
connection {
type = "winrm"
host = aws_instance.windows.public_ip
user = "Administrator"
password = var.admin_password
https = true
insecure = true # self-signed certs, everyone's favorite
}
inline = ["powershell -Command \"Install-WindowsFeature Web-Server\""]
}
# AFTER — OpenSSH, works on both forks, no deprecation clock ticking:
provisioner "remote-exec" {
connection {
type = "ssh"
host = aws_instance.windows.public_ip
user = "Administrator"
private_key = file(var.key_path)
}
inline = ["powershell -Command \"Install-WindowsFeature Web-Server\""]
}
Note what this migration actually removes: the WinRM path was the last common excuse for insecure = true and password-based provisioner auth in HCL. Moving to SSH with key auth closes a real audit finding on either fork.
Symbol Libraries: The Feature That Ends Drop-In Compatibility
This is the most consequential thing in 1.13, and it is not the functions. It is that adopting it makes your configuration unreadable by Terraform — permanently, by design. The experiment gate is the OpenTofu-only language block, which the docs state plainly is "not recognized by any version of Terraform."
# ./symbols/common/networking.sym.hcl
# The library: types, values, functions. No resources, no state access.
language {
edition = experimental2026
}
typedef "cidr_config" {
type = object({
vpc_cidr = string
subnet_bits = number
})
}
function "non_overlapping" {
type = bool
parameter "a" {
type = string
}
parameter "b" {
type = string
}
validation {
condition = cidrnetmask(a) == cidrnetmask(b)
error_message = "Masks differ; comparison is meaningless"
}
locals {
a_net = cidrhost(a, 0)
b_net = cidrhost(b, 0)
}
return = a_net != b_net
}
# main.tofu in the consuming module
language {
experiments = [symbol_libraries] # <- the point of no return
}
symbols "common" {
source = "./symbols/common"
}
variable "network" {
type = symbols::common::cidr_config()
}
resource "aws_subnet" "edge" {
# ... address calculations referencing symbols::common:: helpers
}
The experiments = [symbol_libraries] line is doing the governance work here: it is an explicit, greppable marker of which modules have left Terraform's parse tree. Treat it exactly like you treat a #!/usr/bin/env python3 shebang in a shell script repository — a declaration that this file is no longer portable. If your platform serves multiple teams on a shared module registry, the correct policy today is: symbol libraries in leaf repositories owned by the team that consumes them, never in shared library modules, until the feature stabilizes. The OpenTofu project itself is asking for exactly that restraint in the RFC discussion.
Terraform's Counter-Bet: The Control Plane
While OpenTofu rebuilt the language, Terraform spent the same period building things that only matter if Terraform is your platform, not just your provisioner. Three of them have no OpenTofu equivalent as of 1.13:
- Terraform Policy, GA in 1.17. The
-policiesflag oninit,plan, andapplyno longer requires the experimental-features opt-in. Policy evaluation runs against the plan inside the Terraform run itself — the HCP dashboard shows outcome summaries. OpenTofu users get the same outcome only by bolting OPA/Conftest into CI, which works, but the verdicts land outside the apply gate unless you build that gate yourself. - Actions and List Resources (1.14). Provider-defined imperative actions (
-invoke, lifecycle-triggered) andtfquery.hclfiles withterraform queryfor importing what already exists. This is Terraform conceding that not everything is CRUD — the same concession that drove OpenTofu's Symbol Libraries, answered differently. - Stacks + the 1.16 plumbing. Planned-private-data preservation,
storeblocks onterraform_data, import blocks inside modules, Mermaid graph output. The last one is a small gift to platform teams:terraform graph -format=mermaidin CI produces dependency diagrams for docs for free.
# Refresh only what has proposed changes — the fix for the
# "40-minute plan because one IAM role drifted" problem:
terraform plan -minimal-refresh -out=tfplan
# Policy evaluation inside the run, GA path (no -allow-experimental-features):
terraform init -policies=./policies
terraform plan -policies=./policies -out=tfplan
# Free dependency diagram for your docs pipeline:
terraform graph -format=mermaid > docs/infra-graph.mmd
The honest read: if -minimal-refresh survives to GA, it neutralizes one of OpenTofu's most-quoted operational advantages (plan time on large estates). OpenTofu's answer to plan slowness was -exclude in 1.9; Terraform's is to refresh less. Different mechanisms, same pain point — expect this specific row of the matrix to keep moving.
State Encryption: OpenTofu's One Sided Advantage
Nothing in the Terraform 1.16.3 or 1.17-beta1 notes matches this, and nothing has since the fork: OpenTofu encrypts state and plan files in the client, before they touch any backend or disk. For teams where the state file is the crown jewel — and it is, it contains every secret every resource ever ingested — this is the single feature that justifies a fork migration on its own.
The design got real scrutiny and it holds up: key providers (pbkdf2, AWS KMS, GCP KMS, Azure Key Vault, OpenBao) feed methods (aes_gcm, aes_gcm_v2-style versions reserved), with a documented compatibility guarantee of one minor version and console warnings on deprecation. The 1.13 series even extended it — encryption_context for AWS KMS, additional_authenticated_data for GCP KMS, and associated_data for OpenBao.
# Step 1-2: enable encryption with a fallback that can read the OLD
# unencrypted state, so `tofu apply` re-writes state encrypted:
terraform {
encryption {
method "unencrypted" "migrate" {}
key_provider "pbkdf2" "state_key" {
passphrase = var.passphrase # min 16 chars, from a vault/secret store
key_length = 32
iterations = 600000 # default; min 200000
salt_length = 32
hash_function = "sha512"
}
method "aes_gcm" "state_method" {
keys = key_provider.pbkdf2.state_key
}
state {
method = method.aes_gcm.state_method
fallback {
method = method.unencrypted.migrate
}
}
plan {
method = method.aes_gcm.state_method
fallback {
method = method.unencrypted.migrate
}
}
}
}
# Step 3 (next apply): remove BOTH fallback blocks and flip enforcement on,
# so unencrypted state/plan files are refused outright:
# state { method = ... enforced = true }
# plan { method = ... enforced = true }
What the docs warn about, and your runbook should too
- Encryption does not protect against replay — an attacker replaying an older state or plan file is not stopped by encrypting the current one. Version-pinned state and plan signing remain your problem.
- Never rename key providers or methods after data is encrypted. The ciphertext embeds metadata keyed to their names; renames break decryption. Use the
encrypted_metadata_aliasfield if you must restructure, or afallbackblock for rotation. - AES-GCM has key-saturation limits. A long-lived static key approaching its safe usage ceiling is a real failure mode; the docs are explicit that KMS users should rotate keys, and pbkdf2-passphrase users should treat the passphrase as a rotating credential.
- One key per state file. Sharing a single KMS key across many states concentrates blast radius for no operational benefit.
On Terraform's side of the matrix the mitigation story is thinner: encrypt the backend bucket (S3 SSE-KMS), restrict who can read it, and accept that terraform plan -out files and any state pulled to a laptop are plaintext. Compliance teams notice the gap; so do auditors.
The Converged Core: Why Switching Is Still Cheap
Amid the divergence, the load-bearing compat facts have not moved. Verify them once and your migration plan writes itself:
- The lock file is still
.terraform.lock.hcl. OpenTofu's own docs name it that, deliberately. Provider selections and checksums carry across both directions — and since 1.12, the OpenTofu Registry writes every checksum format the lock file needs, so mirrors and plugin caches work without a separateproviders lockpass. - All
TF_*environment variables work in OpenTofu —TF_VAR_*,TF_LOG,TF_CLI_ARGS— plusTOFU_*extras for OpenTofu-specific behavior. Your CI templates barely change: swap the binary, keep the env. - Private registries speak the Terraform registry API, and OpenTofu's docs list the compatible implementations;
registry.opentofu.orgmirrors the provider/module ecosystem. - The features that converged anyway:
removedblocks (both 1.7, Jan 2024), theconvert()function (OpenTofu 1.13 / Terraform 1.15),lifecycle { destroy = false }(OpenTofu 1.12 / Terraform 1.16),contains()accepting null (OpenTofu 1.13 / Terraform 1.16), ephemeral resources and write-only attributes (Terraform 1.10/1.11, OpenTofu 1.11 about ten months later). Both roadmaps still meet in the middle when a feature is simply correct.
# What does NOT change: .terraform.lock.hcl, *.tf, TF_VAR_* env,
# backend blocks, private registry URLs, module sources.
# What changes:
brew install opentofu # or: apt/zip download per your repo policy
cd my-stack
tofu version # 1.13.0-rc1
tofu init # reads .terraform.lock.hcl as-is
tofu plan # byte-identical plan is the acceptance bar
# Pin the version change in CI:
# - uses: opentofu/setup-opentofu@v1 (replaces hashicorp/setup-terraform)
# Rollback plan: terraform binary, same directory, same lock file.
# The one-way door is ONLY the `language` block + symbol libraries.
The Decision Matrix, By Team Shape
| If your team is… | Run | Because |
|---|---|---|
| Regulated / public-sector, MPL-only procurement | OpenTofu | Terraform's BUSL 1.1 grant is production use, not ownership; IBM is now the licensor, and the "compete with the paid version" carve-out is a legal review you can skip entirely on MPL 2.0. |
| HCP Terraform / Enterprise customers | Terraform | Stacks, Policy GA, and the run environment are the product you are paying for; OpenTofu can act as a generic cloud-backend client but the integration surface is not supported parity. |
| Multi-region / multi-account cloud estates | OpenTofu | Provider for_each collapses N aliased provider blocks into one; nothing on the Terraform side matches it as of 1.17-beta1. |
| Security/compliance-driven (state = crown jewels) | OpenTofu | Client-side state and plan encryption with enforced mode; Terraform leaves plan files and pulled state in plaintext. |
| Large estates with slow plans | Watch Terraform 1.17 | -minimal-refresh targets the exact pain; OpenTofu's -exclude is a blunter tool. Re-evaluate at GA. |
| Air-gapped / supply-chain-hardened | OpenTofu | OCI distribution for providers and modules plus providers lock -oci-mirror — mirror everything into your own registry with no HCP dependency. |
| Module-library maintainers serving mixed teams | Terraform syntax, for now | Anything with a language block or symbol library is unreadable to Terraform users. Stay in the common parse tree until Symbol Libraries stabilize. |
| Small shop, single cloud, no compliance pressure | Either — stop reading comparisons | The converged core means the switching cost is a CI image swap. Decide on one weekend, not a quarter of committee time. |
Who Should Skip This Entire Debate
- Teams mid-migration to a Kubernetes-native control plane. If your trajectory is Crossplane or controller-based provisioning, investing fork-migration effort in Terraform-tooling policy is polishing the thing you are leaving. Land the control-plane migration; both forks become a legacy import problem afterward.
- Anyone waiting for Symbol Libraries to "be ready." The project's own warning — significant changes possible even in patch releases — is a promise to break your configs. It is a feedback channel, not a feature. Watch the RFC discussion, ship nothing.
- Teams that will upgrade to 1.13 for the wrong reason. The WinRM removal means everyone with Windows provisioning has a 1.13 chore, whatever they run. If your OpenTofu upgrade motivation is only "the release is out," you are doing release-driven ops. Upgrade when the WinRM audit is done and
base64gzipusages are known.
The Verdict
The "drop-in replacement" era of the fork is over, and that is healthy — it was always going to end the moment either side shipped something the other could not parse. OpenTofu used its three years to build the things individual operators feel daily: encryption, provider for_each, -exclude, OCI distribution, and now language-level reuse. Terraform used the same years to build the things only a platform owner feels: policy gates in the run loop, orchestration above the module, and an inventory/query layer. Neither roadmap is wrong; they are aimed at different buyers.
For a platform team, the September 2026 question is no longer "is OpenTofu safe?" — Fidelity's 50,000-state-file migration answered that a year ago, and the Linux Foundation stewardship answers the governance version. The question is which direction your next three years of friction come from: the language and its supply chain, or the control plane around it. Pick the side that owns your actual pain, write the switch plan in an afternoon — the lock file and env vars have not moved — and put a review date on your calendar for Terraform 1.17 GA. If -minimal-refresh lands as designed, the plan-time row of this matrix changes, and with it a lot of lazy arguments for staying put.