```hcl
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}
```
Auth options, in order of preference for automation:
1. **OIDC / federated credentials.** `ARM_USE_OIDC=true`, `ARM_CLIENT_ID`, `ARM_TENANT_ID`, plus the CI system's identity token. No secret to rotate. This is the current best practice for GitHub Actions.
2. **Service principal env vars.** ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID. Works everywhere; the secret expires, calendar it.
3. **Azure CLI.** `az login` then the provider uses the CLI token. Fine for local dev, banned in CI (non-reproducible, breaks when the token dies).
4. **Managed identity.** On an Azure VM / pipeline agent with identity: the provider picks it up.
Traps:
- **features {} is mandatory.** An empty features block is required even if you set nothing. Missing it is a confusing provider error.
- **Provider version pin.** `required_providers { azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" } }`. Major versions rename resources; unpinned upgrades break plans.
- **Backend auth.** The azurerm backend (state in Blob Storage) needs its own auth: same ARM_ vars or a SAS. A plan that authenticates fine but cannot read state is a backend-auth problem, not a provider problem.
- **Subscription in two places.** Provider subscription_id and backend storage account subscription can differ; set both explicitly.
Verify: `terraform plan` shows the expected subscription in the backend init output and no auth warnings, before apply.