OpenTofu vs. Terraform: The 2026 Fork Decision Matrix

Sources

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

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 connection block in your estate still says type = "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.hcl files, imported via symbols blocks. 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 is terraform validate plus third-party tooling (tflint); OpenTofu is moving the baseline into the binary.
  • base64gzip output changed. A new DEFLATE implementation produces equivalent-but-not-identical output. If base64gzip feeds 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/*_arm packages stop with 1.14; 1.13 warns on tofu 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:

# 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_alias field if you must restructure, or a fallback block 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:

# 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

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.