Skip to content

Identity and secrets

  • Lesson 11
  • 45 min
  • Module 4: Cloud building blocks
Lesson 11 of 15 · Docker and Cloud Essentials for AI DevelopersIdentity and secrets

What you will be able to do

In the cloud, your app must prove who it is before it can pull its image or read a secret. Give it an identity with no password, and allow it only the one thing that it needs. Keep its secret where few people and programs can read it.

Before you startWhat to know and to have readyAbout 45 minutes

What you need to know

What you need on your computer

  • Your notes from the guided practice of Regions and service types. This lesson adds an access plan to them.
  • The project folder ~/projects/ticket-api at the end of Module 3. If it is missing, copy the snapshot end-of-m03, as in Use a snapshot.
  • No Azure account. Every Azure output in this lesson is a live output from the real run. You run these commands in Module 5.

The Azure steps were run with Azure CLI 2.88.0 and the containerapp extension 1.3.0b5, in region canadacentral.

On her computer, Amira pulls an image from a private registry after az acr login. The registry asks two questions, and her sign-in answers both:

  1. Who are you? Amira, a user in her organisation’s tenant.
  2. What may you do here? Push and pull images in this registry.

In the cloud, the container app pulls the image itself, at 3 a.m., with nobody signed in. It needs its own answer to both questions. There are two ways to give it one:

  • A password. The registry has an admin user with a password. You copy the password into the app’s settings. Now a password exists that never expires by itself, that anyone who sees it can use, and that you must remember to change.
  • An identity. Azure gives the app an identity of its own and proves it for the app. You allow that identity one thing: to pull images from this one registry. No password exists, so no password can leak.

The course uses the second way.

An identity is anything that can sign in and ask for access: a user, such as Amira, or a program. A managed identity is an identity that Azure creates and manages for a resource. With it, the resource can sign in to other Azure services without a password. Azure keeps and renews its credentials. Nobody sees them, not even you.

There are two kinds:

System-assigned User-assigned
What it is Part of one resource, such as one container app A resource of its own, in a resource group
Lifecycle Deleted when that resource is deleted Exists until you delete it
How many resources can use it Only that one One or more
When it exists Only after the resource is created Before the resource, if you create it first

The course uses a user-assigned identity, id-ticket-api, for one reason: it can exist before the app. Amira creates the identity, gives it its permission, and only then creates the app with it. So the app’s very first start can already pull the image. A system-assigned identity exists only after Azure creates the app. Without extra steps, it cannot have its role in time for that first pull.

Other clouds have the same idea: an IAM role for a service on AWS, a service account on Google Cloud.

An identity alone may do nothing. Access comes from a role assignment, which has three parts:

  • Who: the identity, for example id-ticket-api.
  • The role: a named list of allowed actions, for example AcrPull.
  • The scope: where the role applies. In Azure, the scope is one of four levels, from wide to narrow: a management group, a subscription, a resource group, or one resource. A role at a wide scope applies to everything inside it.

A role is easier to understand when you read its actions. This command only reads:

Terminal (optional, needs an Azure account)
az role definition list --name AcrPull --query "[0].{name:roleName, actions:permissions[0].actions, dataActions:permissions[0].dataActions}" -o json
{
"actions": [
"Microsoft.ContainerRegistry/registries/pull/read"
],
"dataActions": [],
"name": "AcrPull"
}

One action: pull from a registry. Compare the role Owner:

{
"actions": [
"*"
],
"name": "Owner"
}

* means every action. An identity with Owner on the subscription can create, change and delete every resource in it, and can give access to others.

Least privilege is the rule to give each identity the smallest role, at the narrowest scope, that still lets it do its job. For the ticket API, that is AcrPull, on the registry ticketapi4821 only. If someone steals the app’s access, they can pull one image, and nothing more.

Nested boxes: a tenant, the organisation's directory, contains a subscription, which contains the resource group rg-ticket-api-practice. In the group are the registry ticketapi4821, the container app ticket-api and the optional Key Vault kv-ticket-api-4821. Amira, a user, has the role Owner on the subscription. The managed identity id-ticket-api has AcrPull on the registry only, and, optionally, Key Vault Secrets User on the vault only. The container app pulls its image as id-ticket-api.
Each role assignment is an identity, a role and a scope. The app's identity gets the smallest roles, on single resources. Amira's Owner role on the subscription is for a person who creates and deletes resources, not for an app.
Text description of the diagram

The boxes show the scopes, one inside the other: the tenant, which is the organisation’s directory, then the subscription, then the resource group rg-ticket-api-practice. In the group are three resources: the registry ticketapi4821, the container app ticket-api, and the optional Key Vault kv-ticket-api-4821. On the left are two identities. Amira, a user, has the role Owner on the whole subscription, because she creates and deletes the resources. The managed identity id-ticket-api has two narrow roles: AcrPull on the registry only, and Key Vault Secrets User on the optional vault only. The container app has no role of its own. It pulls its image as id-ticket-api.

In Module 5, Amira creates the identity in the practice group (live output; the IDs are shortened):

Terminal (Module 5)
az identity create --resource-group rg-ticket-api-practice --name id-ticket-api -o table
ClientId Location Name PrincipalId ResourceGroup
------------------------------------ ------------- ------------- ------------------------------------ ----------------------
faf6b35b-... canadacentral id-ticket-api d2066809-... rg-ticket-api-practice

The PrincipalId is the identity’s ID in the tenant: role assignments use it. Then she gives the identity AcrPull, at the scope of the registry:

Terminal (Module 5)
PRINCIPAL_ID=$(az identity show -g rg-ticket-api-practice -n id-ticket-api --query principalId -o tsv)
REGISTRY_ID=$(az acr show -n ticketapi4821 --query id -o tsv)
az role assignment create --assignee-object-id $PRINCIPAL_ID --assignee-principal-type ServicePrincipal --role AcrPull --scope $REGISTRY_ID -o table

The output (columns shortened):

CreatedOn PrincipalType ResourceGroup Scope
-------------------------------- ---------------- ---------------------- ---------------------------------------------------------------------------------------------------------------
2026-10-07T16:12:09.352851+00:00 ServicePrincipal rg-ticket-api-practice /subscriptions/.../resourceGroups/rg-ticket-api-practice/providers/Microsoft.ContainerRegistry/registries/ticketapi4821

Read the Scope from right to left: the registry ticketapi4821, in the group rg-ticket-api-practice, in a subscription. That is the narrowest scope for a registry. --assignee-principal-type ServicePrincipal tells Azure what kind of identity it is. Without it, the command can fail for a new identity that the role service does not know yet.

When she creates the app, two flags connect it to the identity. --user-assigned gives the identity to the app. --registry-identity says “pull from the registry as this identity”. Module 5 explains every flag.

The registry also has an admin user. The real run showed that it is off when the registry is created:

NAME RESOURCE GROUP LOCATION SKU LOGIN SERVER CREATION DATE ADMIN ENABLED
------------- ---------------------- ------------- ----- ------------------------ -------------------- ---------------
ticketapi4821 rg-ticket-api-practice canadacentral Basic ticketapi4821.azurecr.io 2026-10-07T16:10:57Z False

The admin user is one shared account for the whole registry, with push and pull access. Everyone who uses its password looks like the same user, so you cannot tell who did what. If you change the password, every program that uses it stops. Microsoft’s documentation calls it a feature mainly for tests by one person. The course never turns it on.

The app needs one secret: the API key. You already know two places where it must never be: in the image (Module 2) and in Git (the API course). In the cloud there are two good places, and one poor one:

Where How the app gets it Who can read the value Use it for
A plain environment variable: --env-vars API_KEY=the-key As an environment variable Anyone who can read the app’s configuration, as with docker inspect in Module 3 Settings that are not secret, such as SHOW_DOCS
A platform secret: --secrets api-key=the-key and --env-vars API_KEY=secretref:api-key As an environment variable, filled from the secret Only identities with a role that can list the app’s secrets The practice: simple, and nothing extra to delete
A secret store: a Key Vault secret, and a reference to it on the app The platform reads the vault as the app’s identity and fills the secret Only identities with a Key Vault role on the vault A real service: one place for secrets, with versions and an access log

secretref: means “take the value from the secret with this name”. The app’s configuration then holds a reference, not the key. The real run shows it. az containerapp show lists the names of the secrets, not their values (live output, shortened):

{
"ingress": {"external": true, "port": 8000},
"registries": [{"identity": "/subscriptions/.../userAssignedIdentities/id-ticket-api", "server": "ticketapi4821.azurecr.io"}],
"secrets": ["api-key"]
}

A platform secret is not perfect. Microsoft’s documentation warns that some built-in roles, such as Container Apps Contributor, can list secret values in plain text. It advises a Key Vault reference in production. That is why the Key Vault route is the stretch below.

A Key Vault is Azure’s secret store. In the real run, Amira also moved the API key there. These are the steps and what they showed. Module 5 marks this route as optional. It costs a few operations, and it makes one extra thing to clean up.

  1. Create the vault with role-based access (RBAC). The vault had soft delete on, with 90 days of retention:

    Name Rbac SoftDelete Retention
    ------------------ ------ ------------ -----------
    kv-ticket-api-4821 True True 90
  2. Give yourself the role Key Vault Secrets Officer on the vault, so that you can write secrets. Being Owner of the subscription is not enough: Owner manages the vault, but reading and writing its secrets needs a Key Vault data role.

  3. Give the app’s identity the role Key Vault Secrets User on the vault. Its actions, from az role definition list:

    "dataActions": [
    "Microsoft.KeyVault/vaults/secrets/getSecret/action",
    "Microsoft.KeyVault/vaults/secrets/readMetadata/action"
    ]

    It can read secrets. It cannot write or delete them.

  4. Write the key into the vault with az keyvault secret set. The first try failed (see the common mistake below). The second try, 20 seconds later, worked.

  5. Change the app’s secret to a reference: api-key=keyvaultref:https://kv-ticket-api-4821.vault.azure.net/secrets/api-key,identityref:<the identity's ID>. keyvaultref: names the secret in the vault, and identityref: names the identity that reads it.

  6. Restart the revision. Then a classify request with the key returned 200, and a wrong key returned 401.

After step 5, the app’s configuration held only the vault’s address and the identity, not the value (live output):

[
{
"identity": "/subscriptions/.../userAssignedIdentities/id-ticket-api",
"keyVaultUrl": "https://kv-ticket-api-4821.vault.azure.net/secrets/api-key",
"name": "api-key"
}
]

Remember for Lesson 3: a deleted Key Vault is kept in a soft-deleted state for 90 days. It does not charge, but its name stays taken until you purge it.

Write the access plan for the practice design. Add it to your notes from Lesson 1.

  1. List every identity in the design. Include yourself.
  2. For each identity, write what it must do, in one sentence.
  3. Choose the smallest built-in role for that job. Use AcrPull, Key Vault Secrets User, Key Vault Secrets Officer or Owner.
  4. Choose the narrowest scope: the subscription, the resource group, or one resource.
  5. List every secret, where it lives, and who can read its value.
  6. Write which settings are not secret.
Show an example answer
Identity Must do Role Scope
You (Amira) Create, change and delete the practice resources Owner (or Contributor) Your subscription, or only the practice group if someone else owns the subscription
id-ticket-api Pull the image ticket-api:1.1.0 AcrPull The registry ticketapi4821
id-ticket-api (stretch) Read the API key from the vault Key Vault Secrets User The vault kv-ticket-api-4821
You (stretch) Write the API key into the vault Key Vault Secrets Officer The vault kv-ticket-api-4821
Secret Where it lives Who can read the value
The API key The platform secret api-key, used as API_KEY=secretref:api-key (or a Key Vault secret) You, the app, and identities with a role that can list the app’s secrets (or Key Vault roles on the vault)

Not secret: REQUIRE_API_KEY=true, SHOW_DOCS=false, the region, the registry name, the app’s address. There is no registry password in the plan, because the admin user stays off.

A later version of the API loads a model file, model.joblib, from a container called models in a storage account (Blob Storage). It only reads the file. Change your access plan for this one condition. Which role, at which scope? What do you not give?

Show one answer

Give id-ticket-api the role Storage Blob Data Reader at the scope of the models container in the storage account (a blob container can be a scope too). Its data action is Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read: it can read blobs, and nothing more. Do not give Storage Blob Data Contributor, which can also write and delete blobs. Do not give the role on the whole storage account if other containers hold other data. Do not give the app the storage account’s access key, which is a password for everything in the account.

Common mistake

Forbidden, right after the role was given

Symptom: In the real run, Amira’s own account got Key Vault Secrets Officer on the vault. The next command wrote the secret at once, and it failed:

ERROR: (Forbidden) Caller is not authorized to perform action on resource.
If role assignments, deny assignments or role definitions were changed recently, please observe propagation time.

Cause: A new role assignment takes a short time to reach every part of Azure. This is called propagation. The role was correct, but the vault did not know about it yet.

Fix:

  1. Read the message to the end. Here it names the cause: “observe propagation time”.
  2. Wait, then run the same command again. In the real run, it worked about 20 seconds later.
  3. If it still fails after a few minutes, check the role assignment: the right identity, the right role, the right scope.
  4. Do not “fix” it with a bigger role, such as Owner on the subscription. That turns a short wait into a lasting risk.

In automated deployments, for example in CI, add a short wait or a retry after each role assignment. Then a correct deployment does not fail because it was too fast.

Check your understanding

What is a managed identity, and why does the course use a user-assigned one instead of a system-assigned one?

Show the answer

A managed identity is an identity that Azure creates and manages for a resource. With it, the resource can sign in to other Azure services with no password. Azure keeps its credentials, and nobody sees them. The course uses a user-assigned identity because it is a resource of its own that can exist before the app. Amira gives it AcrPull on the registry first. Then the app’s very first start can pull the image. A system-assigned identity exists only after Azure creates the app, so it cannot have the role before the first pull.

Check your understanding

A teammate gives id-ticket-api the role Owner on the subscription, because it is quicker. Predict what can go wrong, and say what to give instead.

Show the answer

Everything that can use the identity can then create, change and delete every resource in the subscription, and give access to others. An attacker who runs code in the container, or a bug, could then damage the whole subscription, not one image. Give AcrPull on the registry ticketapi4821 only: the smallest role at the narrowest scope (least privilege). The app then works the same, and a mistake can cost much less.

Check your understanding

Name three places where the API key can be put in the cloud design, and say which one the practice uses and why.

Show the answer

A plain environment variable: poor, because anyone who can read the app’s configuration sees the value. A platform secret (--secrets api-key=... and API_KEY=secretref:api-key): the practice uses it, because it keeps the value out of the configuration and adds nothing to delete. A Key Vault reference (keyvaultref: with identityref:): better for a real service. The value lives only in the vault, and only identities with a Key Vault role can read it. Never in the image or in Git.

Recap

  • An identity answers “who are you?”. A role assignment (identity, role, scope) answers “what may you do, and where?”.
  • A managed identity lets a resource sign in without a password. A user-assigned identity can exist before the app, so it can get its role first.
  • Least privilege: the smallest role at the narrowest scope. For the ticket API: AcrPull on one registry.
  • Keep the registry’s admin user off. It is one shared password with push and pull access.
  • The key goes in a platform secret (secretref:) or in Key Vault (keyvaultref: + identityref:), never in a plain setting, the image or Git. A changed secret needs a restart or a new revision.
  • A new role assignment can take a few seconds to work. Wait and retry. Do not widen the role.

Next, you find every part of the design that can cost money, and how to stop each one.

Next: Cost awareness