Summary
The hosted Azure environment sets admin_enabled = true on the container registry, reads admin_username / admin_password off the resource, and hands them to the container-apps module, which materialises the password as a secret block on azurerm_container_app.main and authenticates pulls with it. The same file grants the Container App's managed identity AcrPull, so the workload could pull with no credential at all; that role assignment governs no pull today and could be deleted with no observable change. The result is a long-lived push-capable registry credential sitting in Terraform state (on both the registry resource and the container app) and in the ARM resource definition, readable by anyone with Reader on the resource group. The reusable modules/registry/azure defaults enable_admin_user = false; the environment declares its own registry resource and overrides that. Blast radius is our own hosted infrastructure.
Location
terraform/environments/azure/registry.tf:10 at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (admin_enabled = true)
terraform/environments/azure/registry.tf:16-21 (the unused AcrPull assignment)
terraform/environments/azure/compute.tf:101-103 (registry_username / registry_password passed into the module)
terraform/modules/compute/azure/container-apps/main.tf:88-96 (registry block uses username + password_secret_name, never identity)
terraform/modules/compute/azure/container-apps/main.tf:224-230 (the password as a container-app secret)
Failure scenario
A principal with Reader on the resource group, or read access to the Terraform state backend, reads the ACR admin password. Because the admin account has push as well as pull, they can replace the image the Container App runs. Separately, revoking the managed identity's AcrPull changes nothing, so an operator auditing access sees a least-privilege grant that is not the one in use.
Evidence
resource "azurerm_container_registry" "main" {
name = local.acr_name
sku = "Basic"
admin_enabled = true # Enables username/password login for docker push
}
resource "azurerm_role_assignment" "acr_pull" {
scope = azurerm_container_registry.main.id
role_definition_name = "AcrPull"
principal_id = module.compute_container_apps[0].managed_identity_principal_id
}
registry_server = azurerm_container_registry.main.login_server
registry_username = azurerm_container_registry.main.admin_username
registry_password = azurerm_container_registry.main.admin_password
Suggested fix
Set admin_enabled = false, drop the three registry_* inputs at compute.tf:101-103, and give the Container App a registry { server = ..., identity = <UAMI id> } block so the pull runs on the existing AcrPull assignment. Add depends_on = [azurerm_role_assignment.acr_pull] on the container app so the first pull does not race RBAC propagation (LeanerCloud/cloud-commitments-cli#1627 already tracks the same ordering hazard for the sibling assignments, finding A13c-006). If CI still pushes with the admin account, move that push to the deploy SP's own AcrPush assignment first.
Related: #127 notes in passing that registry_login_command carries this same admin credential.
Found by the 2026-09-02 codebase audit, finding A13c-005, reported by one reviewer and independently confirmed by a second. Full report: docs/audits/codebase-audit-2026-09-02.md. Finding A13-010 (the dead AcrPull assignment) is the same defect seen from the other side and is folded in here.
Summary
The hosted Azure environment sets
admin_enabled = trueon the container registry, readsadmin_username/admin_passwordoff the resource, and hands them to the container-apps module, which materialises the password as asecretblock onazurerm_container_app.mainand authenticates pulls with it. The same file grants the Container App's managed identityAcrPull, so the workload could pull with no credential at all; that role assignment governs no pull today and could be deleted with no observable change. The result is a long-lived push-capable registry credential sitting in Terraform state (on both the registry resource and the container app) and in the ARM resource definition, readable by anyone with Reader on the resource group. The reusablemodules/registry/azuredefaultsenable_admin_user = false; the environment declares its own registry resource and overrides that. Blast radius is our own hosted infrastructure.Location
terraform/environments/azure/registry.tf:10at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (admin_enabled = true)terraform/environments/azure/registry.tf:16-21(the unusedAcrPullassignment)terraform/environments/azure/compute.tf:101-103(registry_username/registry_passwordpassed into the module)terraform/modules/compute/azure/container-apps/main.tf:88-96(registry block usesusername+password_secret_name, neveridentity)terraform/modules/compute/azure/container-apps/main.tf:224-230(the password as a container-appsecret)Failure scenario
A principal with Reader on the resource group, or read access to the Terraform state backend, reads the ACR admin password. Because the admin account has push as well as pull, they can replace the image the Container App runs. Separately, revoking the managed identity's
AcrPullchanges nothing, so an operator auditing access sees a least-privilege grant that is not the one in use.Evidence
Suggested fix
Set
admin_enabled = false, drop the threeregistry_*inputs at compute.tf:101-103, and give the Container App aregistry { server = ..., identity = <UAMI id> }block so the pull runs on the existingAcrPullassignment. Adddepends_on = [azurerm_role_assignment.acr_pull]on the container app so the first pull does not race RBAC propagation (LeanerCloud/cloud-commitments-cli#1627 already tracks the same ordering hazard for the sibling assignments, finding A13c-006). If CI still pushes with the admin account, move that push to the deploy SP's ownAcrPushassignment first.Related: #127 notes in passing that
registry_login_commandcarries this same admin credential.Found by the 2026-09-02 codebase audit, finding
A13c-005, reported by one reviewer and independently confirmed by a second. Full report:docs/audits/codebase-audit-2026-09-02.md. FindingA13-010(the deadAcrPullassignment) is the same defect seen from the other side and is folded in here.