Adding Custom Certificates with Custom Assembly
How to add custom certificates to customized images with Custom Assembly.
For the complete documentation index, see llms.txt.
Note: If you’re authenticating from a workload running in Azure Kubernetes Service (AKS), refer to the Kubernetes identity guide instead.
Chainguard’s assumable identities are identities that can be assumed by external applications or workflows in order to perform certain tasks that would otherwise have to be done by a human.
This procedural tutorial outlines how to create an identity that can be assumed by an Azure workload — such as a VM, Container App, or Function — using an Azure managed identity and then used to interact with the Chainguard API.
To complete this guide, you will need the following.
chainctl — the Chainguard command line interface tool — installed on your
local machine. Follow our guide on
How to Install chainctl
to set this up.az),
authenticated with az login.terraform
installed locally.Each Entra ID tenant has its own OIDC issuer URL, which has the following format:
https://login.microsoftonline.com/<tenant-id>/v2.0Retrieve the tenant ID of your current Azure session with:
az account show --query tenantId --output tsvRegister an Entra ID application to serve as the audience for tokens issued
to managed identities in your tenant. We’ll grant it no API permissions and
expose no scopes — its only purpose is to provide a unique, tenant-specific
aud claim that Chainguard’s STS can match on.
az ad app create \
--display-name chainguard-audience \
--sign-in-audience AzureADMyOrgNote down the appId from the output — this is the client ID we’ll use as the
token audience. Substitute it for <client-id> in the following commands.
Configure the application to issue v2.0 access tokens and to register
api://<client-id> as an identifier URI, so that Entra ID will resolve it as
a valid scope when requesting tokens.
az ad app update \
--id <client-id> \
--requested-access-token-version 2 \
--identifier-uris "api://<client-id>"Then create a service principal for the application. Entra ID requires this in order to recognise the application as a valid token resource when a managed identity requests a token for it.
az ad sp create --id <client-id>If you don’t already have a
user-assigned managed identity
to bind the Chainguard identity to, create one now. Substitute
<resource-group> and <location> (e.g. eastus) for your own values.
az identity create \
--name chainguard-identity \
--resource-group <resource-group> \
--location <location>Note down the principalId from the output — we’ll use this as the subject of
the Chainguard identity in the next step. Note the clientId too, as the
workload will need it later to request a token.
This guide outlines two methods for creating the Chainguard identity: one
using chainctl over a command-line interface, and another using Terraform.
Run this chainctl command to create the identity and bind it to the
registry.pull role. Substitute <tenant-id> with the tenant ID you
retrieved earlier, <principal-id> with the managed identity’s principal ID,
and <client-id> with the application’s client ID.
chainctl iam id create azure-identity \
--identity-issuer="https://login.microsoftonline.com/<tenant-id>/v2.0" \
--subject="<principal-id>" \
--audience="<client-id>" \
--role=registry.pullThis will return the UIDP (unique identity path) of the identity, which we’ll use when assuming it in the next section.
If you need to retrieve the UIDP later, list the identity with this command:
chainctl iam identities list --name=azure-identityYou can also create the application registration, managed identity, and Chainguard identity together with the Chainguard Terraform provider and the AzureRM and AzureAD providers.
Substitute <org-name> with your Chainguard organization name,
<resource-group> with your Azure resource group, and <location> with the
Azure region you want to deploy into.
terraform {
required_providers {
azurerm = { source = "hashicorp/azurerm" }
azuread = { source = "hashicorp/azuread" }
chainguard = { source = "chainguard-dev/chainguard" }
}
}
provider "azurerm" {
features {}
}
data "azurerm_client_config" "current" {}
data "azuread_client_config" "current" {}
# A permission-free Entra ID application registration. Its sole purpose is
# to provide a unique audience for tokens exchanged with Chainguard's STS.
resource "azuread_application" "chainguard_audience" {
display_name = "chainguard-audience"
owners = [data.azuread_client_config.current.object_id]
api {
requested_access_token_version = 2
}
lifecycle {
ignore_changes = [identifier_uris]
}
}
# A service principal — required for Entra ID to recognise the application
# as a valid token audience when a managed identity requests a token.
resource "azuread_service_principal" "chainguard_audience" {
client_id = azuread_application.chainguard_audience.client_id
owners = [data.azuread_client_config.current.object_id]
}
# Register api://<client-id> as the identifier URI so Entra ID resolves
# it as a valid resource for token requests.
resource "azuread_application_identifier_uri" "chainguard_audience" {
application_id = azuread_application.chainguard_audience.id
identifier_uri = "api://${azuread_application.chainguard_audience.client_id}"
}
# A user-assigned managed identity that will assume the Chainguard identity.
# Attach this to any Azure workload that supports managed identities
# (VMs, Container Apps, Functions, etc).
resource "azurerm_user_assigned_identity" "chainguard" {
name = "chainguard-identity"
resource_group_name = "<resource-group>"
location = "<location>"
}
data "chainguard_group" "org" {
name = "<org-name>"
}
resource "chainguard_identity" "azure" {
parent_id = data.chainguard_group.org.id
name = "azure-identity"
description = "Assumed by Azure workloads via the chainguard-identity managed identity."
claim_match {
issuer = "https://login.microsoftonline.com/${data.azurerm_client_config.current.tenant_id}/v2.0"
subject = azurerm_user_assigned_identity.chainguard.principal_id
audience = azuread_application.chainguard_audience.client_id
}
}
data "chainguard_role" "registry_pull" {
name = "registry.pull"
}
resource "chainguard_rolebinding" "registry_pull" {
identity = chainguard_identity.azure.id
role = data.chainguard_role.registry_pull.items[0].id
group = data.chainguard_group.org.id
}
output "chainguard_identity_id" {
value = chainguard_identity.azure.id
}
output "token_audience" {
value = "api://${azuread_application.chainguard_audience.client_id}"
}
output "managed_identity_client_id" {
value = azurerm_user_assigned_identity.chainguard.client_id
}The chainguard_identity_id output provides the identity’s UIDP. The
token_audience and managed_identity_client_id outputs provide the values
your workload will need to request a managed identity token and exchange it
with Chainguard.
For a full end-to-end example of using this pattern from an Azure Container
App, see the
image-copy-acr example
in Chainguard’s public platform-examples repository.
Attach the managed identity to the Azure workload you want to authenticate from. For example, to attach it to an existing VM:
az vm identity assign \
--name <vm-name> \
--resource-group <resource-group> \
--identities <managed-identity-resource-id>From inside the workload, request a token from the
Azure Instance Metadata Service (IMDS)
for the audience you configured on the application. Substitute <client-id>
with the application’s client ID, and <managed-identity-client-id> with the
clientId of the managed identity.
AZURE_TOKEN=$(curl -sSf \
-H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=api://<client-id>&client_id=<managed-identity-client-id>" \
| jq -r .access_token)The following sections outline two methods for using the Azure token to
interact with Chainguard: one using chainctl, and another by exchanging
the token directly with the Chainguard STS over the API.
Use the Azure token to log in with chainctl. Substitute <identity-id>
with the UIDP of the Chainguard identity created earlier.
chainctl auth login \
--identity <identity-id> \
--identity-token "${AZURE_TOKEN}"Now you can issue chainctl commands under this assumed identity. For
instance, you could list the container image repositories available to your
organization:
chainctl image repo listTo pull images from the Chainguard registry with Docker, configure a
credential helper with chainctl. Provide the same identity and token:
chainctl auth configure-docker \
--identity <identity-id> \
--identity-token "${AZURE_TOKEN}"You can now pull images with docker. Substitute <org-name> with your
Chainguard organization name and <image> with an image available to your
organization.
docker pull cgr.dev/<org-name>/<image>:latestIf chainctl isn’t installed in the workload, you can exchange the Azure
token for a Chainguard API token directly. Substitute <identity-id> with
the UIDP of the Chainguard identity created earlier.
API_TOKEN=$(curl -sSf \
-H "Authorization: Bearer ${AZURE_TOKEN}" \
"https://issuer.enforce.dev/sts/exchange?aud=https://console-api.enforce.dev&identity=<identity-id>" \
| jq -r .token)Use the API token to interact with the Chainguard API. For instance, to list the container image repositories available to your organization:
curl -sSf -H "Authorization: Bearer ${API_TOKEN}" \
https://console-api.enforce.dev/registry/v1/repos | jq -r .items[].nameIf you’re authenticating from Go, the
Chainguard SDK and the
Azure SDK for Go can
do the token exchange for you. The
image-copy-acr example
demonstrates this pattern.
Delete the Chainguard identity.
chainctl iam id delete <identity-id>Delete the managed identity.
az identity delete \
--name chainguard-identity \
--resource-group <resource-group>Delete the Entra ID application registration. This will also remove the associated service principal.
az ad app delete --id <client-id>For more information about how assumable identities work in Chainguard, check out our conceptual overview of assumable identities.
Last updated: 2026-05-15 00:00