Deployment guide

boring-registry

Overview

boring-registry is a simple, open source registry for Terraform and OpenTofu modules and providers. It is a private, self-hosted alternative to the public Terraform Registry. Point your developers and CI pipelines at it, and they consume your organisation's own modules and providers with the standard source syntax. You keep full control over what is published and who can reach it. boring-registry implements the module registry protocol, the provider registry protocol and the provider network mirror protocol.

This SecureAnyCloud image installs boring-registry 0.18.0 from the official upstream release, a single self-contained Go binary, on Ubuntu 24.04 on Azure. boring-registry has no web interface. It is a headless API server that you drive with the terraform/tofu command and the boring-registry command line.

The registry server is bound to the loopback interface only (127.0.0.1:5601), and nginx is the only network-facing service, on port 80. nginx serves a public health check and the registry service discovery document. It reverse proxies the /v1 module and provider protocol to the registry, which enforces a per-instance API token.

boring-registry stores modules and providers in object storage and has no local filesystem backend. The image therefore bundles a lightweight S3-compatible object store (SeaweedFS) on the same virtual machine, bound to the loopback interface. The registry works out of the box, with no external cloud account or bucket to configure. Module and provider archives are streamed back through the registry itself (its --download-proxy mode), so your clients never need to reach the object store.

Nothing secret is baked into the image. On the first boot of every instance, the image generates a unique API token that protects the registry protocol and unique credentials for the on-box object store. It writes them to a root-only file. The registry rejects unauthenticated requests to the /v1 protocol, and publishing new modules and providers requires the per-instance storage credentials.

What you get:

  • boring-registry 0.18.0 from the official upstream release binary, run under systemd (boring-registry.service) as an unprivileged boring user with the registry bound to 127.0.0.1:5601
  • A bundled S3-compatible object store (SeaweedFS 4.39, seaweedfs.service) bound to loopback, providing the storage backend with no external cloud dependency
  • nginx (nginx.service) on port 80 as the only network-facing service. It serves a public /healthz probe and the public /.well-known/terraform.json service discovery document, and reverse proxies the /v1 registry protocol to the loopback registry
  • A per-instance registry API token and object store credentials, generated fresh on first boot, with the per-instance values written to /root/boring-registry-credentials.txt
  • A small example module (sac/hello/local), published on first boot so that you can confirm the registry works end to end immediately

Prerequisites

  • An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region
  • A subscription to the boring-registry listing on Azure Marketplace
  • Network Security Group rules that allow TCP 22 (administration) and TCP 80 (the registry protocol) from the developers and build agents that use the registry

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample for a team registry. Increase the size and enlarge the OS disk when you publish a large module and provider catalogue.

1

Deploy from the Azure Portal

Search for boring-registry in Marketplace, select the SecureAnyCloud publisher, and choose Create. Configure the Network Security Group to allow TCP 80 from the developer workstations and build agents that will use the registry, and TCP 22 for administration.

Keep port 80 restricted to the networks you serve. A private registry rarely needs to be reachable from the whole internet.

2

Deploy from the Azure CLI

RG="boring-registry-prod"; LOCATION="eastus"; VM_NAME="boring-registry1" IMAGE_URN="sac1647283583153:boring-registry-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:boring-registry-ubuntu-24-04:default:latest
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$IMAGE_URN" \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values "$SSH_KEY" \
  --public-ip-sku Standard
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1002
3

First boot and per-instance credentials

On first boot, the image generates a per-instance registry API token and per-instance object store credentials. It creates the storage bucket, publishes the example module, and writes everything to /root/boring-registry-credentials.txt. This completes within seconds. SSH in as azureuser and read the credentials file:

sudo cat /root/boring-registry-credentials.txt

The file is 0600 root:root. It records the registry URL and API token, the on-box object store endpoint, bucket and credentials, and examples that you can paste directly. These values are unique to this instance.

4

Confirm the services are running

Check that seaweedfs.service, boring-registry.service and nginx.service are all active, and that boring-registry version reports 0.18.0. Then use ss to confirm that nginx owns port 80 and that the registry and the object store are bound to loopback only, on ports 5601 and 8334.

systemctl is-active seaweedfs.service boring-registry.service nginx.service /opt/boring-registry/boring-registry version
sudo ss -tlnp | grep -E ':80 |:5601 |:8334 '
5

The registry is secure by default

The registry is reachable only through nginx on port 80. The health probe and the service discovery document are public. Every /v1 module and provider request requires the per-instance API token. An unauthenticated request returns 401, and a request with the token returns 200.

The following block reads the token from the instance credentials file, so it works unchanged on any instance.

TOKEN=$(sudo grep -E '^REGISTRY_TOKEN=' /root/boring-registry-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'healthz (no auth) -> HTTP %{http_code}\n' http://127.0.0.1/healthz
curl -s -o /dev/null -w 'well-known/terraform.json (no auth) -> HTTP %{http_code}\n' http://127.0.0.1/.well-known/terraform.json
curl -s -o /dev/null -w 'v1/modules/sac/hello/local/versions (no auth) -> HTTP %{http_code}\n' http://127.0.0.1/v1/modules/sac/hello/local/versions
curl -s -o /dev/null -w 'v1/modules/sac/hello/local/versions (token) -> HTTP %{http_code}\n' -H "Authorization: Bearer $TOKEN" http://127.0.0.1/v1/modules/sac/hello/local/versions
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1/v1/modules/sac/hello/local/versions

The public health probe and discovery document return 200. The unauthenticated module request returns 401. The authenticated request returns 200 with the versions of the bundled sac/hello/local example module: {"modules":[{"versions":[{"version":"0.1.0"}]}]}.

6

Publish your own module

Publishing writes directly to the bundled object store using the per-instance storage credentials, so run it on this VM (or over SSH). Create a small module with a boring-registry.hcl metadata file, then upload it with the boring-registry command line. The following block reads the object store credentials from the instance credentials file.

AK=$(sudo grep -E '^S3_ACCESS_KEY=' /root/boring-registry-credentials.txt | cut -d= -f2-)
SK=$(sudo grep -E '^S3_SECRET_KEY=' /root/boring-registry-credentials.txt | cut -d= -f2-)
M=$(mktemp -d)
cat > "$M/main.tf" <<'TF' variable "name" { type = string default = "world" } output "greeting" { value = "Hello, ${var.name}" } TF cat > "$M/boring-registry.hcl" <<'HCL' metadata { namespace = "sac" name = "example" provider = "local" version = "1.0.0" } HCL
sudo AWS_ACCESS_KEY_ID="$AK" AWS_SECRET_ACCESS_KEY="$SK" AWS_REGION=us-east-1 \
  /opt/boring-registry/boring-registry upload module \
  --storage-s3-bucket=boring-registry --storage-s3-endpoint=http://127.0.0.1:8334 \
  --storage-s3-pathstyle=true --storage-s3-region=us-east-1 --ignore-existing=true "$M"
TOKEN=$(sudo grep -E '^REGISTRY_TOKEN=' /root/boring-registry-credentials.txt | cut -d= -f2-)
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1/v1/modules/sac/example/local/versions

The upload publishes sac/example/local version 1.0.0, and the versions endpoint serves it immediately. Publish providers the same way with boring-registry upload provider. See the boring-registry documentation for the provider signing key requirements.

7

Consume a module over the download proxy

A registry client fetches a module in two steps. It asks the registry for the download location, then downloads the archive. This image runs boring-registry in --download-proxy mode, so the registry streams the archive back itself and the client never needs to reach the object store.

The following block performs both steps with curl and the per-instance token.

TOKEN=$(sudo grep -E '^REGISTRY_TOKEN=' /root/boring-registry-credentials.txt | cut -d= -f2-)
GET=$(curl -s -D - -o /dev/null -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1/v1/modules/sac/hello/local/0.1.0/download \
  | grep -i '^X-Terraform-Get:' | tr -d '\r' | awk '{print $2}')
echo "download location: $GET" curl -s -o /tmp/hello.tar.gz -w 'archive fetch -> HTTP %{http_code}, %{size_download} bytes\n' \
  -H "Authorization: Bearer $TOKEN" "http://127.0.0.1$GET"

The download endpoint returns an X-Terraform-Get location that points back at the registry's own /v1/proxy/... path. Fetching that location returns the module archive with HTTP 200. This is the same exchange that terraform init and tofu init perform.

8

Point Terraform and OpenTofu at the registry

To consume modules with the native source syntax, give your client the per-instance token and reference the module by its registry address. Replace <vm-host> with this instance's address or a DNS name you point at it.

# Give the terraform/tofu CLI the per instance token as a host credential.
# Dots in the host become underscores, e.g. host 10.0.0.4 -> TF_TOKEN_10_0_0_4:
# export TF_TOKEN_10_0_0_4="<the REGISTRY_TOKEN from /root/boring-registry-credentials.txt>"
#
# Then reference a module in your Terraform/OpenTofu configuration:
# module "hello" {
# source = "10.0.0.4/sac/hello/local"
# }

Note on HTTPS. Terraform requires a registry host to be served over HTTPS. This image serves the registry over plain HTTP on port 80, so that you can front it with your own TLS termination and DNS name. Use an Azure Application Gateway or Load Balancer, or nginx with your own certificate. This is the standard way to run a private registry with a real domain.

Until then, use curl with the token as shown above, or OpenTofu with a host that terminates TLS. The per-instance token continues to protect the /v1 protocol behind your TLS front.

9

Provider network mirror

boring-registry also implements the provider network mirror protocol, so you can mirror upstream providers for reproducible, network-isolated builds. Add a provider installation network_mirror block to your CLI configuration that points at this registry. Upload the providers you need with boring-registry upload provider. Your terraform init / tofu init then pulls providers from your own mirror instead of the public internet.

10

Security notes

  • Restrict the NSG. Allow TCP 80 only from the developer subnets and build agents that use the registry, and TCP 22 only from your administration network. A private registry rarely needs to be public.
  • Protect the token and storage keys. They live in /root/boring-registry-credentials.txt (0600 root:root). The registry protocol is reachable only with the token, and publishing requires the object store keys. Rotate the token by editing BORING_REGISTRY_AUTH_STATIC_TOKEN in /etc/boring-registry/boring-registry.env and restarting boring-registry.service.
  • Terminate TLS in front of the registry. Put a DNS name and a CA-signed certificate in front of the registry with an Azure Application Gateway, a Load Balancer, or an nginx TLS front. Then terraform init and tofu init can use the registry with the native source syntax.
  • Back up the object store. Your published modules and providers live under /var/lib/seaweedfs. Snapshot the disk or export the bucket as part of your backup routine.
  • Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
11

Support and Licensing

boring-registry is open source software distributed under the MIT License. This SecureAnyCloud image bundles the unmodified official boring-registry release binary and the SeaweedFS object store (Apache License 2.0). SecureAnyCloud provides the packaging, the systemd hardening, the nginx front, the loopback-only registry with a per-instance API token, the self-contained on-box object store, the per-instance credential automation, the paired deploy guide, and 24/7 support with a guaranteed 24-hour response SLA.

SecureAnyCloud is not affiliated with or endorsed by the boring-registry project. boring-registry is a mark of its respective owner and is used here only to identify the software.

Deploy on Azure

Find boring-registry on Ubuntu 24.04 LTS on the Azure Marketplace, published by SecureAnyCloud. Deploy from the Portal or the Azure CLI as shown above.

Need Help?

Email sales@secureanycloud.com for deployment help, configuration questions or licensing enquiries.

Deploy on Azure

Launch boring-registry 0.18.0 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure