What OpenTofu is & why it forked
The Terraform license change and the fork.
A software license is the sign on the toolshed door. The hammer inside does not change when the sign changes. What changes is who may pick it up, what they may build with it, and whether they may hand a copy to the neighbor. On 10 August 2023 the sign on Terraform's door changed. The hammer did not. That gap is the entire story of OpenTofu.
Terraform is an infrastructure as code tool. You write text files describing the servers, networks, databases and permissions you want, and the tool makes a real cloud account match those files. You drive it from a CLI (command line interface, a program you run by typing into a terminal). Up to and including version 1.5.7 that CLI shipped under MPL 2.0 (the Mozilla Public License version 2, a genuine open source license, meaning anyone can use, modify and redistribute the code, including inside a product they sell). From version 1.6.0 onward it ships under BUSL 1.1 (the Business Source License version 1.1, which is source-available rather than open source: the code stays public to read, but the license names uses it forbids). One detail gets lost in the noise. The change hit the CLI, not the providers. The AWS provider is still MPL 2.0 today.
Read The License Yourself
Do not take anyone's summary of a license, this one included. The file is one command away and the top of it is written in plain English. The command below prints the parameter block and skips the middle, which spends thirty lines defining what counts as a competing product. Read that middle part too, before you ship anything.
curl -sS https://raw.githubusercontent.com/hashicorp/terraform/main/LICENSE \| sed -n '4,13p;43,44p'
ParametersLicensor: International Business Machines Corporation (IBM)Licensed Work: Terraform Version 1.6.0 or later. The Licensed Work is (c) 2024IBM Corp.Additional Use Grant: You may make production use of the Licensed Work, providedYour use does not include offering the Licensed Work to thirdparties on a hosted or embedded basis in order to compete withIBM Corp.'s paid version(s) of the Licensed Work. For purposesof this license:Change Date: Four years from the date the Licensed Work is published.Change License: MPL 2.0
Four fields carry the meaning. Licensor names the copyright holder, and it reads IBM because IBM finished acquiring HashiCorp in 2025. Licensed Work draws the line at version 1.6.0 or later, so everything before that line stays MPL 2.0 for good: an open source grant already handed out cannot be taken back. Change Date is a timer, so each BUSL release turns into MPL 2.0 four years after it ships, and Change License names what it turns into. Additional Use Grant is the carve-out: run it in production as much as you like, as long as you are not offering it to third parties on a hosted or embedded basis to compete with IBM's paid versions.
For most teams that carve-out already covers what they do, and the friction is rarely an actual violation. The friction is that somebody now has to answer "are we a competing offering?" before every upgrade, and the license hands that question to a lawyer instead of a checkbox. Teams selling a platform, a managed service, or a CI product (continuous integration, the automated pipelines that build and test code every time someone pushes) get the fuzziest answer of all, because their product overlaps with the paid version by design.
How The Fork Happened
HashiCorp announced the change on 10 August 2023. Two weeks later a group published the OpenTF manifesto, which gave the company until 25 August to reverse it. Nothing was reversed, so they forked, and the code went public in September 2023 as OpenTF. The cutoff picked itself, because a fork can only carry code the forkers are allowed to copy. Terraform 1.5.7, published on 7 September 2023, was the last release under MPL 2.0, and there was never a 1.5.8. Terraform 1.6.0 arrived on 4 October 2023 under BUSL. On 20 September 2023 the Linux Foundation adopted the fork and renamed it OpenTofu, and the first stable release, OpenTofu 1.6.0, shipped on 10 January 2024.
Handing the code to the Linux Foundation was the point rather than a formality. A single company can relicense its own project the day its business model shifts, which is exactly what happened here. A foundation cannot do that on a whim. The copyright sits with a neutral non-profit, changes run through published governance with a steering committee and a core team, and no board meeting can retroactively make your existing deployment non-compliant. You trade one company's roadmap for a committee's roadmap. For a binary that holds cloud administrator credentials, that is usually a trade worth making.
A License Change Is A Patching Problem
Here is the part that belongs in a security course rather than a legal one. Your infrastructure tool holds cloud administrator credentials, reads a file listing every resource you own, and can create or destroy all of it with one command. Very few binaries on your build machines are worth more to an attacker. How fast you can patch it matters.
Watch what a license does to that number. A team selling a hosted platform reads the Additional Use Grant, cannot get a clean yes from counsel, and pins Terraform 1.5.7 because it is the last MPL release. That version stopped receiving fixes in 2023. From that day on, every advisory affecting the CLI, its Go runtime, or the libraries compiled into it is an advisory the team cannot act on. A legal worry became a permanently unpatched binary sitting next to the credentials. Whichever way you land on the license question, notice that standing still is a decision too, and it carries a security cost.
Two Registries, Two Trust Anchors
A provider is a plugin: the piece that knows how to talk to AWS or Azure, downloaded on demand rather than shipped inside the binary. Both tools fetch providers from a registry, which behaves like a parts depot that hands you a sealed box plus a signature you can check. The depots are different. Terraform asks registry.terraform.io. OpenTofu asks registry.opentofu.org. The namespaces look identical (hashicorp/aws in both), the hostnames do not. So ask each depot where the same provider version actually comes from, and who signed it. The command below uses jq, a small tool for pulling fields out of JSON.
for host in registry.terraform.io registry.opentofu.org; doecho "== $host"curl -sS "https://$host/v1/providers/hashicorp/aws/6.55.0/download/linux/amd64" \| jq -r '" \(.download_url)\n key \(.signing_keys.gpg_public_keys[0].key_id)"'done
== registry.terraform.iohttps://releases.hashicorp.com/terraform-provider-aws/6.55.0/terraform-provider-aws_6.55.0_linux_amd64.zipkey 34365D9472D7468F== registry.opentofu.orghttps://github.com/opentofu/terraform-provider-aws/releases/download/v6.55.0/terraform-provider-aws_6.55.0_linux_amd64.zipkey 0C0AF313E5FD9F80
Same provider name, same version number, two different artifacts, two different keys. Key 34365D9472D7468F is HashiCorp's. Key 0C0AF313E5FD9F80 belongs to the OpenTofu project, and the download comes from opentofu/terraform-provider-aws, the project's own fork of the provider source. That is not how OpenTofu's registry works for everyone. Ask it for integrations/github and it points straight at the publisher's own GitHub release, signed with the publisher's own key, because the registry is mostly metadata rather than a warehouse. For the hashicorp namespace it points at a rebuild instead. Neither arrangement is wrong, and both are checked with real cryptography (GPG, GNU Privacy Guard, the tool that proves a file came from whoever it claims to). They are different trust anchors, and your team should be able to name out loud which one it is relying on.
That second hostname is also a supply chain door your network controls may not know about. If your egress proxy (the gateway all outbound traffic has to pass through), network policy or artifact allowlist names only registry.terraform.io, a migration either fails loudly at init or falls back to a path nobody reviewed. Pick a registry, put the hostname in the allowlist, and pin it in the configuration where a reviewer can see it.
Proving The Drop-In Claim
OpenTofu is deliberately the same tool. Same configuration language (HCL, the HashiCorp Configuration Language, the block-and-braces syntax your .tf files are written in). Same state model. Same init, plan and apply workflow. Same provider plugin protocol, which is why the AWS, Azure and Google providers and the whole public module ecosystem work untouched. The binary is called tofu instead of terraform, and on day one that is most of what you notice.
tofu version
OpenTofu v1.12.4on linux_amd64
Two lines, and one thing conspicuously absent. Run terraform version and the binary quietly contacts checkpoint.hashicorp.com to ask whether a newer release exists, unless you set CHECKPOINT_DISABLE in the environment or disable_checkpoint in the CLI configuration file. OpenTofu removed all checkpoint telemetry in 1.6.0. On an egress-filtered or air-gapped build host (air-gapped meaning no route to the internet at all), that is one fewer outbound connection to explain to whoever reads the firewall logs, and one fewer place your version inventory leaks to.
cd /srv/infra/prodtofu init -input=false
Initializing the backend...Initializing provider plugins...- Finding hashicorp/aws versions matching "~> 6.0"...- Installing hashicorp/aws v6.55.0...- Installed hashicorp/aws v6.55.0 (signed, key ID 0C0AF313E5FD9F80)Providers are signed by their developers.If you'd like to know more about provider signing, you can read about it here:https://opentofu.org/docs/cli/plugins/signing/OpenTofu has created a lock file .terraform.lock.hcl to record the providerselections it made above. Include this file in your version control repositoryso that OpenTofu can guarantee to make the same selections by default whenyou run "tofu init" in the future.OpenTofu has been successfully initialized!You may now begin working with OpenTofu. Try running "tofu plan" to seeany changes that are required for your infrastructure. All OpenTofu commandsshould now work.If you ever set or change modules or backend configuration for OpenTofu,rerun this command to reinitialize your working directory. If you forget, othercommands will detect it and remind you to do so if necessary.
Read what that output is telling you. Same lock file name, same provider namespace, and a key ID that matches exactly what the registry advertised a minute ago. OpenTofu verified the downloaded archive against that key before unpacking it, then wrote the resulting checksums into .terraform.lock.hcl. Commit that lock file. It is what turns a swapped provider from a silent event into a failed init on the next machine that runs one.
The State File Is The Real Compatibility Test
State is the ledger. Your configuration says what you want. The state file records what the tool believes it already built, down to resource IDs and attribute values. Every plan is a three-way comparison between configuration, state and the live cloud account. Get the ledger wrong and the tool will cheerfully delete production to make reality agree with a stale page.
jq '{version, terraform_version, serial, lineage}' terraform.tfstate
{"version": 4,"terraform_version": "1.12.4","serial": 42,"lineage": "8b0d2c5e-6f31-4a7d-b9c8-1e5f0a3d7c42"}
version is the state format version, and it has been 4 since Terraform 0.12. Both tools read and write format 4, and that single fact is the technical basis for the whole drop-in claim. terraform_version still spells out terraform because OpenTofu kept the key name on purpose: renaming it would break every backend, parser and pipeline that reads state. What changed is the value, which is now an OpenTofu version number. lineage is a random ID tying one chain of snapshots together, and serial increments on every write, which is how a backend notices two people writing at once.
Where The Two Have Diverged
Early on OpenTofu was close to a mirror of Terraform. It is not one now. It has added encryption of state and plan files, early evaluation of variables and locals (so a variable can be used inside a backend or module source block), for_each on provider blocks, an -exclude flag for planning around a resource, and provider and module distribution over OCI registries (Open Container Initiative, the same container image registries you already run). There is also a .tofu file extension, which OpenTofu reads and Terraform ignores completely, and where a .tofu file and a .tf file share a base name the .tofu one wins. For security work the encryption is the headline, because the Terraform CLI has no equivalent.
State files hold secrets in plaintext. That is not carelessness. The tool has to record whatever the provider handed back: a generated database password, the private key from a TLS resource (TLS, Transport Layer Security, the encryption behind HTTPS), an IAM access key (IAM, identity and access management, the cloud's permission system). Server-side encryption on an S3 bucket (Amazon's object storage) is a locked room. It protects those bytes on Amazon's disks and does nothing about the copy in /tmp on a CI runner, the plan file attached to a pull request, or the operator who legitimately has read access to the object. OpenTofu seals the envelope instead, encrypting the payload inside its own process before anything reaches a backend.
terraform {encryption {# PBKDF2 (password-based key derivation function 2) stretches a# passphrase into a real key. Passphrase minimum is 16 characters.key_provider "pbkdf2" "primary" {passphrase = var.state_passphraseiterations = 600000 # the default; the floor is 200000}# AES-GCM encrypts and authenticates, so tampering is detected too.method "aes_gcm" "primary" {keys = key_provider.pbkdf2.primary}state {method = method.aes_gcm.primaryenforced = true}plan {method = method.aes_gcm.primaryenforced = true}}}
enforced = true is the line that turns a feature into a control. Without it, OpenTofu falls back to plaintext state whenever the encryption configuration is missing, so a pipeline that loses its key variable writes an unencrypted ledger and nothing fails. With it, the run stops. Two other things to notice. The outer block is still called terraform rather than tofu, kept that way for compatibility. And the file is named encryption.tofu, so a Terraform binary in the same directory never reads it at all. Name it encryption.tf and Terraform would load it and die on an unknown encryption block. The variable has to resolve during init, before state exists, so supply it from the environment and say so in the repo README.
Never commit the passphrase. Pass the whole encryption block in through the TF_ENCRYPTION environment variable in your pipeline, or replace the pbkdf2 key provider with one backed by a key management service (aws_kms, gcp_kms, Azure Key Vault, or OpenBao, the open source fork of Vault). Every OpenTofu environment variable still uses the TF_ prefix, so TF_VAR_, TF_LOG, TF_DATA_DIR and TF_IN_AUTOMATION carry over with no edits. The scratch example below hardcodes a passphrase so you can watch what happens to the file. Do not do that on a shared host: anything you export lands in /proc/<pid>/environ, readable by that user and by root.
export TF_ENCRYPTION=$(cat <<'EOF'key_provider "pbkdf2" "primary" {passphrase = "correct-horse-battery-staple-9317"}method "aes_gcm" "primary" {keys = key_provider.pbkdf2.primary}state {method = method.aes_gcm.primaryenforced = true}EOF)tofu apply -auto-approve >/dev/nulljq -r 'keys[]' terraform.tfstate
encrypted_dataencryption_versionmeta
Three keys, and resources is not one of them. The ledger is now a base64 blob under encrypted_data, meta carries the salt the key provider needs to rebuild the key, and encryption_version is the marker telling OpenTofu this payload is encrypted at all. Somebody who exfiltrates that object from your bucket holds ciphertext. Somebody who loses the passphrase holds the same thing forever, so back the key up with the same care as the state, and use a fallback block when you rotate it instead of swapping the passphrase in place.
What To Check Before You Switch
Four things break more often than the tool does. CI actions are named after their tool, so hashicorp/setup-terraform becomes opentofu/setup-opentofu and any step calling terraform by name needs renaming. The cloud block and HCP Terraform (HashiCorp Cloud Platform, the hosted service that runs plans for you) are HashiCorp products, and remote execution there is the one genuinely non-portable piece. Wrapper tools such as Terragrunt, Atlantis, tflint and Checkov mostly support both, but check the version you actually pinned rather than the version on the project's website. And every alias, Makefile target and runbook in your repo still says terraform, so rename them or add a shim in one commit rather than discovering a half-migrated mix during an incident.
tofu plan -detailed-exitcode -lock-timeout=120s -out=migrate.tfplanecho "exit=$?"
aws_iam_role.deploy: Refreshing state... [id=acme-prod-deploy]aws_s3_bucket.logs: Refreshing state... [id=acme-prod-logs]aws_instance.bastion: Refreshing state... [id=i-0f3ac91b7d2e5c840]No changes. Your infrastructure matches the configuration.OpenTofu has compared your real infrastructure against your configuration andfound no differences, so no changes are needed.exit=0
-detailed-exitcode turns a plan into something a pipeline can gate on: 0 means no changes, 1 means the command failed, 2 means changes are waiting. Notice there is no "run tofu apply next" line at the bottom. OpenTofu skips that message when the plan is empty, because there is nothing to apply, even though migrate.tfplan was still written to disk. A correct migration is a zero-diff migration. If that first plan under the new binary wants to create, replace or destroy anything, the two tools disagree about your state, and applying it would apply a change nobody designed. Stop there, keep the state copy you took, and find the cause before you touch the account.
tofu init installs hashicorp/aws from registry.opentofu.org, the output reads "signed, key ID 0C0AF313E5FD9F80". Per this lesson, where does that provider archive come from and whose key signs it?state {} block does not set enforced = true. One run the secret injection breaks and the passphrase resolves empty. What happens on tofu apply?enforced = true would produce; without it there is no such failure.enforced = true, missing encryption config silently degrades to plaintext; that one line is what turns the feature into a control that stops the run.terraform {required_version = ">= 1.12.0, < 2.0.0"required_providers {aws = {# Fully qualified: pins the registry host, not only the provider name.source = "registry.opentofu.org/hashicorp/aws"version = "~> 6.0"}}}
Version constraints will not tell the two tools apart. OpenTofu and Terraform share one numbering scheme, and >= 1.12.0 is satisfied by both, since Terraform is well past that number already. The fully qualified source address is the line that actually pins your supply chain. It names the host, so a changed default registry or a mistyped mirror fails at init instead of quietly handing you a different artifact signed by a different key. Write it out in every root module, and a reviewer can confirm where your providers come from in about five seconds.
Try this
Run for host in registry.terraform.io registry.opentofu.org; do on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: snapshot state before the first apply. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.