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 unprivilegedboringuser with the registry bound to127.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/healthzprobe and the public/.well-known/terraform.jsonservice discovery document, and reverse proxies the/v1registry 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.
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.
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 1002First 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.txtThe 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.
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 '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/versionsThe 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"}]}]}.
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/versionsThe 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.
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.
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.
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.
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 editingBORING_REGISTRY_AUTH_STATIC_TOKENin/etc/boring-registry/boring-registry.envand restartingboring-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 initandtofu initcan use the registry with the nativesourcesyntax. - 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.
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
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 AWS. 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 instance, 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 unprivilegedboringuser with the registry bound to127.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/healthzprobe and the public/.well-known/terraform.jsonservice discovery document, and reverse proxies the/v1registry 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
Connecting to your instance
Connect over SSH on port 22 with the default login user for the operating system variant you launched. The image ships no application login. First boot generates the registry API token and object store keys for each instance.
OS variant SSH login user
Ubuntu 24.04 LTS ubuntu
ssh ubuntu@<public-ip>Prerequisites
- An AWS account and a subscription to the boring-registry listing on AWS Marketplace
- A key pair for SSH administration and a VPC subnet in your target region
- A security group that allows TCP 22 (administration) and TCP 80 (the registry protocol) from the developer workstations and build agents that use the registry
Recommended instance type: m5.large (2 vCPU, 8 GB RAM) is ample for a team registry. Increase the instance size and enlarge the root EBS volume when you publish a large module and provider catalogue.
Subscribe and launch from AWS Marketplace
Find boring-registry on AWS Marketplace, published by SecureAnyCloud, and subscribe. Launch an instance from the listing into your VPC. Choose the m5.large instance type and a security group that allows 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.
Launch from the AWS CLI
aws ec2 run-instances \
--image-id <ami-id> \
--instance-type m5.large \
--key-name <key-name> \
--security-group-ids <sg-id> \
--metadata-options 'HttpTokens=required' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=boring-registry}]'Replace <ami-id> with the AMI id from your Marketplace subscription, <key-name> with your EC2 key pair, and <sg-id> with a security group that opens TCP 22 and TCP 80 to your networks.
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 ubuntu and read the credentials file:
sudo cat /root/boring-registry-credentials.txtThe 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.
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 '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/versionsThe 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"}]}]}.
Publish your own module
Publishing writes directly to the bundled object store using the per-instance storage credentials, so run it on this instance (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/versionsThe 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.
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.
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 <instance-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 = "<instance-host>/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 AWS Application 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.
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.
Security notes
- Restrict the security group. 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 editingBORING_REGISTRY_AUTH_STATIC_TOKENin/etc/boring-registry/boring-registry.envand restartingboring-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 AWS Application Load Balancer or an nginx TLS front. Then
terraform initandtofu initcan use the registry with the nativesourcesyntax. - Back up the object store. Your published modules and providers live under
/var/lib/seaweedfs. Snapshot the EBS volume or export the bucket as part of your backup routine. - Keep the OS patched. Unattended security upgrades remain enabled on the running instance.
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.
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 AWS
Find boring-registry on AWS Marketplace, published by SecureAnyCloud. Subscribe and launch from the AWS console or the AWS CLI, as shown above.
Need Help?
Email sales@secureanycloud.com for deployment help, configuration questions or licensing enquiries.
Deploy on AWS
Launch boring-registry on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com