Explanation¶
Scope
How OpenTofu works and why it is built this way: its origins and governance, the core engine (config loading, graph, evaluator, plan/apply), the provider plugin protocol, state and state encryption, ephemeral values, how providers and modules are installed, the threat model, and the new engine architecture now being designed. Recipes are in How-to Guides. Tables of flags, versions and options are in Reference.
OpenTofu is a community-driven fork of Terraform. It started after HashiCorp moved Terraform to the Business Source License (BSL 1.1) in August 2023. It keeps Terraform's HCL language, state format, provider protocol and core execution model, and it adds features Terraform does not have, such as client-side state and plan encryption, early evaluation, provider for_each and OCI distribution.
Origins and Governance¶
HashiCorp's license change applied to Terraform 1.6 and later. The last MPL-2.0 code line was 1.5.x. A group of vendors and users published the OpenTF manifesto, forked the 1.5 code line, and moved the project to the Linux Foundation as OpenTofu in September 2023. The first GA release, 1.6.0, shipped in January 2024.
| Milestone | Date | Notes |
|---|---|---|
| Terraform relicensed to BSL 1.1 | 2023-08 | Applies from Terraform 1.6 |
| OpenTofu becomes a Linux Foundation project | 2023-09 | Founding backers include Spacelift, Harness, Gruntwork, env0 and Scalr |
| OpenTofu 1.6.0 GA | 2024-01 | Drop-in replacement for Terraform 1.5.x |
| 1.7.0: state encryption | 2024-04-30 | The first major feature Terraform does not have |
| Accepted into the CNCF Sandbox | 2025-04-23 | The CNCF Governing Board granted an exception for MPL-2.0, which is not a default CNCF license |
| 1.12.0 | 2026-05-14 | Current stable series (1.12.6, 2026-08-19) |
| 1.13.0-rc1 | 2026-09 | Experimental Symbol Libraries |
The Technical Steering Committee (TSC) is responsible for technical direction under a charter administered by LF Projects. It meets every two weeks, and anyone can attend. Adding a TSC member takes a two-thirds vote. Rules on voting power limit how many members may come from the same organization. Decisions are recorded publicly within three days (GOVERNANCE.md). Security issues go to a Product Security Team made up of Steering Committee and core team members (SECURITY.md).
Why the governance matters
Terraform's roadmap and license are controlled by one vendor, now HashiCorp as part of IBM. OpenTofu's MPL-2.0 license and foundation ownership mean no single company can relicense it. This is the main non-technical reason teams adopt it. Commercial TACOS (Spacelift, env0, Scalr, Harness) support it, but the project runs no first-party SaaS.
Component Overview¶
The diagram shows the main subsystems of a tofu process and the external systems it talks to.
graph TB
subgraph CLI["tofu CLI (internal/command)"]
CMD["init / plan / apply / test"]
CFGF["CLI config (.tofurc)<br/>provider_installation"]
end
subgraph CORE["OpenTofu Core"]
HCL["configs loader<br/>(.tf / .tofu, HCL)"]
STATIC["Static evaluator<br/>(early eval: backend, module source, encryption)"]
GRAPH["Graph builders<br/>(plan / apply / destroy DAG)"]
EVAL["Evaluator<br/>(expressions, count, for_each, enabled)"]
PLAN["Planning engine"]
APPLY["Apply engine"]
end
subgraph INSTALL["Provider and module installation"]
GETP["Provider installer<br/>(lock file h1:/zh: checks)"]
GETM["Module fetcher<br/>(git, http, s3, oci://)"]
end
subgraph PLUGINS["Plugin processes"]
PV["Provider plugins<br/>(gRPC tfplugin5 / tfplugin6)"]
end
subgraph STATE["State layer"]
ENC["Encryption layer<br/>(key providers + aes_gcm)"]
BE["Backend<br/>(local, s3, gcs, azurerm, pg, ...)"]
end
subgraph EXT["External"]
OREG["registry.opentofu.org"]
OCI["OCI registry<br/>(oci_mirror / oci:// modules)"]
KMS["KMS / OpenBao / Azure Key Vault"]
CLOUD["Cloud APIs"]
end
CMD --> HCL
HCL --> STATIC
STATIC --> GETM
STATIC --> BE
CFGF --> GETP
GETP --> OREG
GETP --> OCI
GETM --> OCI
HCL --> GRAPH
GRAPH --> EVAL
EVAL --> PLAN
PLAN --> APPLY
PLAN --> PV
APPLY --> PV
PV --> CLOUD
PLAN --> ENC
APPLY --> ENC
ENC --> KMS
ENC --> BE
Core Engine¶
The core engine is written in Go. It keeps the architecture Terraform had up to version 1.5.x, with OpenTofu-specific additions.
HCL Parser and Config Loader¶
- Parses
.tf,.tf.json,.tofuand.tofu.jsonfiles into a configuration structure. Iffoo.tofuandfoo.tfboth exist, onlyfoo.tofuis loaded (1.8+). Module authors use this to ship OpenTofu-only features without breaking Terraform users. - Resolves module sources (local paths, git, HTTP, S3, the OpenTofu Registry and, since 1.10,
oci://) and merges them into a single configuration tree. - Parses variable definitions, outputs, provider configurations, the
encryptionblock and, since 1.12, thelanguageblock.
Static (Early) Evaluation¶
Terraform evaluates everything during the graph walk. Settings needed before the graph exists, such as the backend, module sources and encryption keys, therefore have to be literals. Since 1.8, OpenTofu has a static evaluator. It resolves variables and locals that do not depend on resources, data sources or module outputs during tofu init, before any state is read. That is what allows these:
backend "s3" { bucket = "state-${var.env}" }module "x" { source = "...?ref=${var.version}" }- Encryption passphrases and key IDs from variables
- Provider
for_each(1.9). The set of provider instances must be known before the graph is built, so the iterated collection must be statically known.
Since 1.12, const = true on a variable declares that it must be statically evaluable, so misuse fails early.
Graph Builder¶
- Builds a directed acyclic graph (DAG) of dependencies. Nodes are resources, data sources, ephemeral resources, providers, variables, locals and outputs.
- Edges come from explicit
depends_onand from references found in HCL expressions. - There are separate builders for each operation:
PlanGraphBuilderworks out which changes are needed.ApplyGraphBuildercarries out the planned changes, including create-before-destroy ordering.- For destroy, the plan graph is built in destroy mode, which reverses the dependency order.
Evaluator¶
- Walks the graph and evaluates HCL expressions, expanding
count,for_eachand, since 1.11,lifecycle { enabled = ... }. - Resolves variables, locals, data source results and provider-defined function calls (1.7+).
- Tracks value marks such as
sensitive,ephemeraland deprecated. This is how OpenTofu refuses to put an ephemeral value in a non-ephemeral context, and how the==null comparison stays non-sensitive in 1.12.
Planning Engine¶
The planning engine compares three representations:
- Configuration: what the HCL files declare.
- Prior state: what was recorded after the last successful apply.
- Refreshed state: what the provider says exists in the real world now (
ReadResource).
From these it produces a plan of create, update, replace, delete and forget actions. A plan can be saved with -out and, when encryption is configured, it is encrypted. Since 1.13 saved plans embed the provider schemas needed to render them.
Apply Engine¶
- Loads the saved or freshly computed plan and walks the apply graph.
- Runs independent nodes concurrently, up to
-parallelism(default 10). - For each node, calls the provider's
ApplyResourceChangeover gRPC. - Writes state incrementally as each operation finishes. If an error occurs mid-apply, the partial state is kept, so you can re-run without creating duplicates. Since 1.13, a Go panic also produces
errored.tfstateto help recovery.
Plugin Protocol¶
Providers are separate executables. OpenTofu launches each one as a child process, reads a handshake line from the plugin's stdout, and connects as a gRPC client over the loopback interface. Protocol major versions 5 and 6 are both supported, so provider binaries built for Terraform generally work unchanged. The RPC list is in Reference: provider protocol RPCs.
This sequence shows one provider's lifecycle during a plan and apply (v6 RPC names).
sequenceDiagram
participant Core as OpenTofu Core
participant Plugin as Provider plugin process
Core->>Plugin: exec binary (child process)
Plugin-->>Core: stdout handshake with protocol version and address
Core->>Plugin: GetProviderSchema
Plugin-->>Core: Resource, data source, ephemeral and function schemas
Core->>Plugin: ValidateProviderConfig
Core->>Plugin: ConfigureProvider
Core->>Plugin: ReadResource (refresh)
Core->>Plugin: PlanResourceChange
Plugin-->>Core: Planned new state
Core->>Plugin: ApplyResourceChange
Plugin-->>Core: New state
Core->>Plugin: StopProvider or process kill at end of phase
Protocol compatibility
The handshake settles the protocol version. The provider ecosystem is shared with Terraform. The OpenTofu Registry lists the same hashicorp/aws, hashicorp/google and other providers, built from the same upstream sources. Features that need newer protocol messages, such as resource identity, ephemeral resources and write-only attributes, only work when both the provider and OpenTofu support them.
Plan / Apply Lifecycle¶
This is the end-to-end flow of tofu plan -out followed by tofu apply, including state locking and encryption.
sequenceDiagram
participant User
participant CLI as tofu CLI
participant Core as OpenTofu Core
participant Enc as Encryption layer
participant BE as Backend
participant Prov as Provider plugins
User->>CLI: tofu plan -out=plan.tfplan
CLI->>Core: Load config and run static evaluation
Core->>BE: Acquire state lock
BE-->>Enc: Encrypted state bytes
Enc-->>Core: Decrypted prior state
Core->>Prov: ReadResource per instance (refresh)
Prov-->>Core: Current real-world state
Core->>Prov: PlanResourceChange per instance
Prov-->>Core: Planned changes
Core->>Enc: Encrypt plan file
Core->>BE: Release lock
CLI-->>User: Plan summary
User->>CLI: tofu apply plan.tfplan
CLI->>Core: Decrypt and load saved plan
Core->>BE: Acquire state lock
loop Each change in apply-graph order
Core->>Prov: ApplyResourceChange
Prov-->>Core: New resource state
Core->>Enc: Encrypt state snapshot
Enc->>BE: Persist state
end
Core->>BE: Release lock
CLI-->>User: Apply summary
Key insight
The plan phase produces a saveable plan file. The apply phase can run separately, even on another machine that shares the remote backend. Because the plan file itself can be encrypted, CI systems can pass it between jobs without exposing the secrets inside.
Plan phase¶
- Refresh: for every resource in state, call
ReadResourceto get its current real-world state. - Diff: compare the refreshed state with the configuration.
- Graph walk: go through the DAG in dependency order and work out the change for each instance.
- Output: present the plan: which resources will be created, updated (with attribute diffs), replaced, destroyed or forgotten.
Apply phase¶
- Graph reconstruction: rebuild the apply graph. It can differ from the plan graph because of destroy dependencies.
- Parallel execution: walk the graph and run independent nodes concurrently.
- Provider calls: call
ApplyResourceChangefor each node. - State write: write the new state to the backend after each successful operation.
- Output: report the result for each resource.
State Management¶
State file format¶
- JSON that holds every managed resource instance, its dependencies, outputs, a
serial(incremented on every write) and alineage(a UUID that identifies one state's history). The same format as Terraform 1.5. - Sensitive values are stored in plaintext unless encryption is enabled or the value is ephemeral (1.11+).
sensitive = trueonly hides values in CLI output. serialandlineagelet OpenTofu refuse to overwrite a newer state or one from a different history. They are consistency checks, not tamper-proofing. Authenticated encryption provides integrity.
Backends and locking¶
A backend decides where state lives and how it is locked. Locking stops two runs from writing the same state at once. Where the backend supports it, OpenTofu takes the lock before any operation that may write state. The locking mechanism of each backend is listed in Reference: backends. Two recent design changes matter:
- S3 without DynamoDB (1.10): S3 now supports conditional writes (
If-None-Match). OpenTofu can therefore create a lock object in the state bucket itself withuse_lockfile = true, so no separate DynamoDB table is needed. Both mechanisms can run at the same time during migration. - pg locking change (1.10): the PostgreSQL backend's locking implementation changed. Mixing versions against one database can cause conflicting writes.
State Encryption¶
State encryption is OpenTofu's flagship feature, and Terraform has no equivalent. It encrypts state and plan files on the client before they reach any backend, including the local disk. Anyone with read access to the bucket sees only ciphertext.
Design¶
The design (docs/state_encryption.md) separates three concerns:
| Concept | Role | Examples |
|---|---|---|
| Key provider | Supplies key material as bytes, plus optional non-secret metadata stored next to the ciphertext | pbkdf2, aws_kms, gcp_kms, azure_vault, openbao, external |
| Method | Encrypts and decrypts using the provided key | aes_gcm (AEAD), external, unencrypted (migration only) |
| Target | Which data a method applies to | state, plan, remote_state_data_sources |
Key providers can be chained. For example, an external provider can fetch a passphrase that feeds pbkdf2. The library resolves the order, so each key provider only ever sees resolved inputs. The metadata is stored under the key provider's name, or under encrypted_metadata_alias since 1.9. That is why renaming a key provider breaks decryption unless you use an alias or a fallback.
This sequence shows how a KMS-backed key provider encrypts a state write (envelope encryption).
sequenceDiagram
participant Core as OpenTofu Core
participant KP as Key provider (aws_kms)
participant KMS as AWS KMS
participant M as Method (aes_gcm)
participant BE as Backend (S3)
Core->>KP: Request encryption key
KP->>KMS: GenerateDataKey (key_spec AES_256)
KMS-->>KP: Plaintext data key and encrypted data key
KP-->>Core: Key bytes and metadata (encrypted data key)
Core->>M: Encrypt serialized state JSON
M-->>Core: Ciphertext with nonce and auth tag
Core->>BE: Write ciphertext plus key-provider metadata
Note over BE: Backend readers see only ciphertext
Note over Core,KMS: On read, the encrypted data key from metadata goes to KMS Decrypt
What encryption does and does not protect¶
| Protects against | Does not protect against |
|---|---|
| Someone reading state or plan files at rest (bucket leak, stolen laptop, CI artifact exposure) | Data loss or corruption, so keep backups and bucket versioning |
| Undetected modification of ciphertext, because AES-GCM is authenticated | Replay: an attacker substituting an older validly encrypted state or plan |
Tampered plaintext state. OpenTofu refuses plaintext once encryption is configured, unless an unencrypted fallback exists. |
Whoever runs tofu with the key. They can read every value. |
| Losing the key. Encrypted state cannot be recovered without it. |
Key saturation
AES-GCM becomes unsafe if one key encrypts too many messages. Use a key-derivation provider (PBKDF2 with a long passphrase) or a KMS that rotates keys regularly. Do not use short static keys.
Key rotation¶
The flow shows how a fallback block rotates keys without downtime.
flowchart LR
Old["Old key/method<br/>(fallback)"] --> Read["Read: try primary,<br/>then fallback"]
Read --> Reencrypt["Write: always<br/>primary method"]
Reencrypt --> New["New key/method<br/>(primary)"]
style Old fill:#c62828,color:#fff
style New fill:#2e7d32,color:#fff
On read, OpenTofu tries the primary method first and then the fallback. On write, it always uses the primary. One tofu apply therefore moves each state to the new key. Since 1.9 this happens even when there are no resource changes. The same mechanism, with the unencrypted method, migrates plaintext state to encrypted state and back.
Ephemeral Values¶
Before 1.11, every value a provider returned ended up in state. That included generated passwords and secrets read from a secret store. Ephemeral values (1.11) exist only in memory for a single phase:
ephemeral "type" "name" {}blocks are opened when needed, possibly renewed, and closed once the phase no longer needs them. They are never written to state or plan.- Input variables and outputs can be declared
ephemeral = true. - Write-only attributes (for example
secret_string_wo) accept ephemeral values on managed resources. The provider uses the value but never returns it. A companion*_wo_versionattribute tells the provider when to push a new value.
This diagram shows the lifecycle of an ephemeral resource instance within one phase.
stateDiagram-v2
[*] --> Validated: ValidateEphemeralResourceConfig
Validated --> Deferred: config not fully known during plan
Deferred --> Opened: apply phase, dependencies satisfied
Validated --> Opened: OpenEphemeralResource
Opened --> Opened: RenewEphemeralResource (if lease expires)
Opened --> Closed: CloseEphemeralResource
Closed --> [*]
The evaluator attaches an ephemeral mark to these values. Using one in a non-ephemeral context, such as a normal resource argument, count, for_each or lifecycle.enabled, is an error.
Provider and Module Installation¶
The OpenTofu Registry¶
registry.opentofu.orgimplements the same provider and module registry protocol as Terraform's registry. Provider addresses such ashashicorp/awsresolve toregistry.opentofu.org/hashicorp/awsby default.- The registry is generated from metadata in the public opentofu/registry repository and served through Cloudflare. Providers, modules and provider signing keys are added by submitting a GitHub issue form. search.opentofu.org is the browsable UI with docs.
- Providers are GPG-signed by their publishers. OpenTofu checks the signature on the
SHA256SUMSfile and records hashes in.terraform.lock.hcl. Since 1.12 the registry serves bothzh:andh1:hashes, so onetofu initlocks every platform. - Modules are not signed. Integrity comes from pinning versions, git commit SHAs or OCI digests.
OCI distribution (1.10+)¶
Many organizations already run a container registry (ECR, ACR, GAR, Harbor, and others) with access control, replication and scanning. OpenTofu 1.10 can therefore pull providers through an oci_mirror and modules from oci:// sources. This works well air-gapped. Since 1.13, OpenTofu keeps separate credentials per repository for registries that issue repository-scoped tokens.
This decision flow shows how the provider installer chooses a source for a provider address.
flowchart TD
A["required_providers source<br/>e.g. hashicorp/aws"] --> B{"provider_installation<br/>block in CLI config?"}
B -->|"No"| R["Direct: registry.opentofu.org<br/>(verify GPG sig + SHA256SUMS)"]
B -->|"Yes"| C{"Matching method"}
C -->|"filesystem_mirror"| F["Local directory"]
C -->|"network_mirror"| N["HTTPS mirror<br/>(optionally trust its hashes, 1.12+)"]
C -->|"oci_mirror"| O["OCI registry via<br/>repository_template (1.10+)"]
C -->|"direct"| R
F --> L["Check / record hashes in<br/>.terraform.lock.hcl"]
N --> L
O --> L
R --> L
L --> P["Unpack to .terraform/providers<br/>(or shared TF_PLUGIN_CACHE_DIR)"]
Threat Model Overview¶
| Threat surface | Mitigation in OpenTofu |
|---|---|
| State or plan exposure at rest | Client-side encryption (AES-GCM with a KMS or PBKDF2 key), plus backend encryption |
| Secrets persisted in state | Ephemeral resources, variables and outputs, and write-only attributes (1.11+) |
| State tampering or cross-history overwrite | AEAD integrity when encrypted. lineage and serial checks. Plaintext rejected once encryption is configured. |
| Provider credential leakage | Credentials are never written to state by OpenTofu itself. Use env vars, OIDC or IAM roles, or ephemeral secret fetches. |
| Malicious or compromised providers | GPG-signed provider releases and lock-file hashes |
| Malicious modules | Not signed. Pin versions or digests and review the source. Git URL handling was hardened in 1.12.3. |
| Registry or mirror abuse | TLS. Redirect credential leak and crafted-URL resource exhaustion fixed in 1.12.6. |
| Concurrent state corruption | Backend state locking |
| Attacks through provisioner SSH | SSH library fixes across 1.12.x. WinRM removed in 1.13. |
The full advisory list and a hardening checklist are in Reference.
Next-Generation Engine Architecture¶
In 2025 the core team started redesigning the evaluator and the plan/apply engines, which are inherited largely unchanged from Terraform. The RFC A new approach to configuration evaluation, planning, and applying builds on an earlier problem statement from 2025-07. It proposes:
- Splitting compilation of configuration from evaluation, with concurrent dynamic analysis instead of one big graph walk.
- Changing how provider instances are handled during planning, and how "deposed" objects are treated.
- Saving execution graphs to disk so that apply runs exactly the graph that planning computed.
Status
The RFC is a design direction with an initial implementation sketch, not a shipped feature. The current stable releases (1.12, and 1.13 once released) still use the classic graph builders described above. When this changes, check the release notes.
Comparison with Terraform¶
| Aspect | OpenTofu | Terraform |
|---|---|---|
| License | MPL-2.0 | BSL 1.1 |
| State encryption | Native, client-side | Not available. Relies on backend or HCP Terraform at-rest encryption. |
Early evaluation, provider for_each, -exclude, OCI |
Yes | No |
| Provider protocol | gRPC v5/v6, the same binaries | gRPC v5/v6 |
| Registry | registry.opentofu.org | registry.terraform.io |
| Language server | tofu-ls (work in progress) plus the OpenTofu VS Code extension | terraform-ls |
| Key differentiator | Community-governed, encryption-first | HashiCorp/IBM ecosystem, HCP Terraform |
Migration path
Migration from Terraform 1.5.x or earlier normally means installing tofu, running tofu init and checking that tofu plan shows no changes. The steps are in How-to Guides. Divergence grows with every release, so configurations that use Terraform-only features added after 1.5 need review.