[package] Terraform HTTP Backend for OpenWRT

Hello everyone!

Do you also have this chicken-and-egg problem where you want to set up your homelab with IaC but keep the state local? While looking at the Terraform documentation, I noticed it supports an HTTP backend, so I had the idea to implement this in OpenWrt. The router already keeps the state of our networks; why can't it keep track of the metal infra too?

Here's the package that I developed :

How it Works

  • There's no extra daemon. It's a ucode handler loaded by the uhttpd that's already serving LuCI, mounted under /terraform.
  • States are stored in /etc/terraform/<workspace>/<state>.tfstate (kept across sysupgrade). Each write goes to a temp file and is renamed into place, so readers never see a half-written state.
  • Locking is supported (POST/DELETE on .../lock). Locks live on tmpfs (/var/run/terraform) and are guarded by flock(), because uhttpd forks a process per request.
  • Uploads are checked against Terraform's Content-MD5 header, and large states are streamed, so memory use doesn't grow with state size (default limit: 16 MiB).
  • The package is architecture-independent (PKGARCH:=all), so one build runs on every target.
  • Dependencies: uhttpd, uhttpd-mod-ucode, ucode-mod-digest, ucode-mod-fs, ucode-mod-log, ucode-mod-uci.

Install

I publish signed package repositories on GitHub Pages, so updates arrive as normal package upgrades.

OpenWrt 25.12 and newer (apk)
wget -O /etc/apk/keys/terraform-backend.pem https://enzoalbornoz.github.io/openwrt-terraform-backend-http/keys/openwrt-terraform-backend-http.pub
echo 'https://enzoalbornoz.github.io/openwrt-terraform-backend-http/apk/packages.adb' >> /etc/apk/repositories.d/customfeeds.list
apk update
apk add terraform-backend-http
OpenWrt 24.10 and older (opkg)
wget -O /tmp/terraform-backend.pub https://enzoalbornoz.github.io/openwrt-terraform-backend-http/keys/be6b587fbd94bc4f
opkg-key add /tmp/terraform-backend.pub
echo 'src/gz terraform https://enzoalbornoz.github.io/openwrt-terraform-backend-http/opkg' >> /etc/opkg/customfeeds.conf
opkg update
opkg install terraform-backend-http

Installing registers the handler with uhttpd.main and restarts uhttpd. Removing the package undoes that.

Usage

Create a user on the router. The password is a random token and is shown only once:

terraform-backend-user add ci
# Created user 'ci'. Its password is shown only once:
# 9f2c...

Point Terraform/OpenTofu at it:

terraform {
  backend "http" {
    address        = "https://192.168.1.1/terraform/homelab/network"
    lock_address   = "https://192.168.1.1/terraform/homelab/network/lock"
    unlock_address = "https://192.168.1.1/terraform/homelab/network/lock"
    lock_method    = "POST"
    unlock_method  = "DELETE"

    # uhttpd usually serves a self-signed certificate
    skip_cert_verification = true
  }
}
export TF_HTTP_USERNAME=ci
export TF_HTTP_PASSWORD=9f2c...
tofu init   # or terraform init

Security

State files often contain secrets, so the defaults are strict:

  • HTTPS only by default. Plain HTTP must be enabled explicitly, and is meant for use behind a TLS-terminating proxy.
  • Authentication on by default. Passwords are stored only as salted SHA-256 hashes in /etc/config/terraform-backend.
  • Users can be restricted to specific workspaces:
    terraform-backend-user add ci home lab   # may only access workspaces "home" and "lab"
    terraform-backend-user add admin         # may access all workspaces
    
  • Failed logins and denied workspaces are logged to syslog (logread -e terraform-backend).

Configuration

Everything is in UCI (/etc/config/terraform-backend) and takes effect on the next request, with no restart:

Option Default Description
storage.state_dir /etc/terraform Where states are stored (e.g., move it to a USB drive to spare the flash)
storage.lock_dir /var/run/terraform Where locks are kept (keep it on tmpfs)
storage.max_state_size 16M Largest accepted state
auth.enabled 1 Require user credentials
auth.allow_http 0 Also accept plain HTTP

If you move state_dir off /etc/terraform, add the new path to /etc/sysupgrade.conf to keep it across upgrades.

Under the hood

For anyone curious about ucode: the handler is written in TypeScript and compiled to ucode, with a small h3-style routing layer on top of the uhttpd ucode plugin and type definitions for the ucode modules it uses (fs, uci, digest, log, ...). Tests run against a mocked ucode runtime. The .ipk and .apk are built with the official 24.10 and 25.12 SDKs in GitHub Actions and signed there.

AI Disclosure

Most of this code was created with Claude's help, but I made the main architecture and critical decisions. This was not a one-shot thing. I went through several iterations, and I fought Claude a lot to get it to follow what I wanted.

Feedback welcome

This is still early (0.x). I'd love to hear:

  • whether it works on your device and release,
  • rough edges in install or upgrade,
  • whether there's interest in getting it into the official packages feed.

Issues and PRs are welcome on GitHub. Licensed MIT.

Thanks!

1 Like

Great work, I like TF alot. I dont care if AI wrote it, we all soon will be just a proxy sitting infront of a keyboard.

2 Likes