Skip to content

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, .tofu and .tofu.json files into a configuration structure. If foo.tofu and foo.tf both exist, only foo.tofu is 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 encryption block and, since 1.12, the language block.

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_on and from references found in HCL expressions.
  • There are separate builders for each operation:
    • PlanGraphBuilder works out which changes are needed.
    • ApplyGraphBuilder carries 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_each and, since 1.11, lifecycle { enabled = ... }.
  • Resolves variables, locals, data source results and provider-defined function calls (1.7+).
  • Tracks value marks such as sensitive, ephemeral and 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:

  1. Configuration: what the HCL files declare.
  2. Prior state: what was recorded after the last successful apply.
  3. 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 ApplyResourceChange over 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.tfstate to 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

  1. Refresh: for every resource in state, call ReadResource to get its current real-world state.
  2. Diff: compare the refreshed state with the configuration.
  3. Graph walk: go through the DAG in dependency order and work out the change for each instance.
  4. Output: present the plan: which resources will be created, updated (with attribute diffs), replaced, destroyed or forgotten.

Apply phase

  1. Graph reconstruction: rebuild the apply graph. It can differ from the plan graph because of destroy dependencies.
  2. Parallel execution: walk the graph and run independent nodes concurrently.
  3. Provider calls: call ApplyResourceChange for each node.
  4. State write: write the new state to the backend after each successful operation.
  5. 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 a lineage (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 = true only hides values in CLI output.
  • serial and lineage let 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 with use_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_version attribute 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.org implements the same provider and module registry protocol as Terraform's registry. Provider addresses such as hashicorp/aws resolve to registry.opentofu.org/hashicorp/aws by 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 SHA256SUMS file and records hashes in .terraform.lock.hcl. Since 1.12 the registry serves both zh: and h1: hashes, so one tofu init locks 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.

Sources