Feature Request: Environment Variable Overrides for Terraform Provider Source Hostname and Namespace
This proposal introduces support for two environment variables, TF_PROVIDER_SOURCE_HOSTNAME and TF_PROVIDER_SOURCE_NAMESPACE, that override the hostname and namespace components of provider source addresses at runtime, without requiring any changes to module source code. Together, these are targeted, low-risk enhancements that eliminate repetitive boilerplate from large-scale enterprise Terraform deployments and improve operational flexibility.
A Terraform provider source address is composed of three parts:
<hostname>/<namespace>/<name>
| Component | Default | Required |
|---|---|---|
hostname |
registry.terraform.io |
No |
namespace |
hashicorp |
No |
name |
(none) | Yes |
The current defaults are surfaced clearly in the three canonical forms a source address can take:
# Implicit hostname + implicit namespace
terraform {
required_providers {
random = {
source = "random"
# hostname -> registry.terraform.io
# namespace -> hashicorp
# name = "random"
}
}
}# Implicit hostname + explicit namespace
terraform {
required_providers {
random = {
source = "my-org/random"
# hostname -> registry.terraform.io
# namespace -> my-org
# name = "random"
}
}
}# Explicit hostname + explicit namespace
terraform {
required_providers {
random = {
source = "tfe.company.com/my-org/random"
# hostname -> tfe.company.com
# namespace -> my-org
# name = "random"
}
}
}The hostname and namespace components each already have a well-established default. This proposal extends that model with two environment variables that allow organizations to replace those defaults for their environment. Both follow the same rule: the override applies only when the respective component is implicit in the source address. If a source address includes an explicit hostname, the hostname variable is ignored for that address. If it includes an explicit namespace, the namespace variable is ignored for that address. Explicit values in source addresses are always respected, preserving full backward compatibility and allowing gradual, targeted opt-out on a per-module or per-provider basis.
Before addressing the technical problem, it is worth establishing why large organizations operate private provider registries in the first place. HCP Terraform is one such option for hosting a private registry; others include self-managed Terraform Enterprise instances and third-party artifact repositories.
Enterprises with hundreds of Terraform modules and a sprawling cloud footprint, spanning multiple cloud providers, accounts, regions, and environments, cannot afford the operational and compliance risk of allowing Terraform to pull providers directly from the public internet during plan and apply operations.
Private registries solve this by acting as a controlled, internally governed distribution point for provider binaries. The motivations are concrete:
- Security scanning. Provider binaries are scanned for known vulnerabilities (CVEs) before being made available internally. Only vetted versions are promoted.
- Air-gapped and restricted environments. Many regulated environments (financial services, government, healthcare, defense) have outbound internet restrictions. A private registry is the only viable path.
- Provider signing. Providers can be re-signed with internal keys before being published to the private registry, enforcing organizational trust chains.
- Availability guarantees. Public registry outages have caused real production incidents. An internal registry with SLAs removes that external dependency.
This is not a niche edge case. This is standard practice at the scale these organizations operate.
An organization running a private registry at terraform-registry.company.com under the namespace company must encode both of those values into every required_providers block in every module. A typical module looks like this:
terraform {
required_providers {
aws = {
source = "terraform-registry.company.com/company/aws"
}
random = {
source = "terraform-registry.company.com/company/random"
}
tls = {
source = "terraform-registry.company.com/company/tls"
}
null = {
source = "terraform-registry.company.com/company/null"
}
kubernetes = {
source = "terraform-registry.company.com/company/kubernetes"
}
}
}This block, or one nearly identical to it, lives in every single module across the organization's module library. Looking at each source string, terraform-registry.company.com/company/aws, only one token out of three is actually meaningful at the module level: the provider name. The hostname is a deployment detail. The namespace is an organizational convention. Neither varies from module to module, and neither communicates anything about what the provider does or how the module uses it.
Both are pure boilerplate.
At scale, this means the same hostname and namespace are embedded across potentially hundreds of repositories. Every developer reading a module must visually parse past two repeated tokens to reach the one token that matters. Every code review, every diff, every audit includes this noise.
The hostname and namespace are infrastructure configuration masquerading as application code. Neither belongs hardcoded into module source.
With TF_PROVIDER_SOURCE_HOSTNAME=terraform-registry.company.com set in the execution environment, the module above simplifies to:
terraform {
required_providers {
aws = {
source = "company/aws"
}
random = {
source = "company/random"
}
tls = {
source = "company/tls"
}
null = {
source = "company/null"
}
kubernetes = {
source = "company/kubernetes"
}
}
}The hostname is gone. Source addresses now show namespace and name. Already a meaningful reduction, but the namespace is still repeated across every entry.
With TF_PROVIDER_SOURCE_NAMESPACE=company also set, the module simplifies further to:
terraform {
required_providers {
aws = {
source = "aws"
}
random = {
source = "random"
}
tls = {
source = "tls"
}
null = {
source = "null"
}
kubernetes = {
source = "kubernetes"
}
}
}The required_providers block now contains only what was always the meaningful part: the provider names. The hostname and namespace live where infrastructure configuration belongs, in the execution environment, set once and applied everywhere.
This is not a new source address form. The source = "aws" shorthand already exists and is valid today, resolving to registry.terraform.io/hashicorp/aws. These environment variables extend that same ergonomic pattern to work for any registry and any namespace, not just HashiCorp's defaults.
| Source address in code | TF_PROVIDER_SOURCE_HOSTNAME |
TF_PROVIDER_SOURCE_NAMESPACE |
Resolved address |
|---|---|---|---|
aws |
terraform-registry.company.com |
company |
terraform-registry.company.com/company/aws |
company/aws |
terraform-registry.company.com |
other |
terraform-registry.company.com/company/aws (namespace explicit, not overridden) |
terraform-registry.company.com/company/aws |
new-registry.company.com |
other |
terraform-registry.company.com/company/aws (both explicit, neither overridden) |
aws |
(not set) | (not set) | registry.terraform.io/hashicorp/aws (unchanged defaults) |
While the primary case for this feature is eliminating boilerplate, the environment variable approach also provides meaningful flexibility when registry hostnames or namespaces need to change. This is a secondary but valid benefit.
Private registries are not always permanent fixtures. Registries can change for legitimate and sometimes unavoidable reasons: migrating from a self-hosted instance to HCP Terraform, a domain change from corporate rebranding, or consolidating infrastructure after a merger.
In the current model, a hostname or namespace change requires updating source strings across every affected module. With these environment variables in place, both values already live outside of module code. A registry migration becomes a change to one or two environment variables in CI/CD rather than a coordinated, multi-repository code change effort.
This is not the driving argument for this feature, but it is a meaningful compounding benefit of keeping infrastructure configuration in the environment where it belongs.
A critical requirement for any change that affects provider resolution is that the .terraform.lock.hcl file remains consistent and trustworthy. The lock file records the resolved provider source addresses, checksums, and version constraints that were used during terraform init.
Both environment variables handle the lock file correctly, and the behavior is consistent between them:
- When either variable is set, the fully resolved source address, including any overridden components, is written to the lock file. The lock file always reflects the actual registry and namespace that were used.
- On subsequent
terraform initruns with the same environment variable values, the resolved address matches the lock file entry. No conflict occurs. - If either variable's value changes,
terraform initdetects the mismatch between the previously locked source and the newly resolved source and surfaces a clear error. The operator must acknowledge the change by runningterraform init -upgradeor reinitializing. - If either variable is unset in an environment where it was previously set, Terraform falls back to the original default (
registry.terraform.iofor hostname,hashicorpfor namespace). The resulting address mismatch with the lock file surfaces immediately as an error, preventing silent misconfiguration.
The lock file acts as a correctness check for both environment variables. Accidental or inconsistent configuration is caught at init time, not silently at runtime. This is consistent with how Terraform already uses the lock file to enforce provider identity across environments.
These changes are strictly additive and backward compatible:
- No existing configuration changes. If neither environment variable is set, behavior is identical to today.
- Explicit source address components are unaffected. A module with a full three-part source address continues to work exactly as before. A module with an explicit namespace but no hostname respects the explicit namespace and still benefits from the hostname variable.
- Incremental cleanup is supported. Organizations can set one or both variables and progressively remove hardcoded components from modules at their own pace. All source address forms work simultaneously during any transition.
- Each variable is independent. Setting only
TF_PROVIDER_SOURCE_HOSTNAMEwithoutTF_PROVIDER_SOURCE_NAMESPACEis fully valid and useful on its own, and vice versa.
The provider_installation stanza in the CLI configuration can redirect provider lookups using network_mirror or filesystem_mirror. However:
- It operates at the binary/artifact level, not the source address level. The source address in the lock file still reflects the original address.
- It requires changes to the CLI configuration file, which is per-machine and harder to control in ephemeral CI/CD environments than environment variables.
- It does not remove the hostname or namespace from module source code. The boilerplate problem remains entirely unaddressed.
Scripted mass replacement is fragile across version-pinned, tagged, release-managed module repositories. It does not address the ongoing boilerplate problem for new modules, and it moves no configuration out of source code.
This is the status quo. It is what this proposal is designed to improve.
TF_PROVIDER_SOURCE_HOSTNAME and TF_PROVIDER_SOURCE_NAMESPACE are focused, low-risk additions that directly address a well-documented source of boilerplate in large-scale enterprise Terraform deployments. They align with Terraform's existing philosophy of allowing environment-specific configuration to live in the environment rather than in source code, and they extend the defaulting model that already exists for both components in a natural and consistent way.
The behavior is predictable, the lock file implications are safe and correct, and the changes are fully backward compatible. For organizations managing hundreds of modules against private provider registries, these variables remove the two tokens in every source address that have never belonged in module code, and replace them with a pair of centrally managed environment variables.