Skip to content

sec(iac/azure): ACR admin credentials are used for pulls, leaving the AcrPull grant dead #254

Description

@cristim

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions