Secure an account in Python
In this exercise, you host a product reader in Azure Container Instances (ACI), authenticate with a system-assigned managed identity, and test read-only access and network restrictions. Your local Azure CLI identity manages resources; the hosted identity accesses data.
This exercise takes approximately 45 minutes to complete.
Before you start
To complete this exercise, you need an Azure subscription with permission to create resources and assign roles.
If your lab environment isn’t set up yet, follow Set up your lab environment to install Visual Studio Code, Git, the Azure CLI, and PowerShell 7.
The application runs inside the container, so you don’t need to install Python locally.
This exercise also requires:
- Azure Contributor access to the resources in the resource group you use.
- Registered
Microsoft.DocumentDBandMicrosoft.ContainerInstanceresource providers, or permission to register them at subscription scope. See Azure resource providers and types. - A region, such as
eastus, that supports Azure Cosmos DB serverless and Linux Azure Container Instances, with quota for 2 CPUs and 4 GB of memory. The container needs outbound access to Azure Cosmos DB, the managed identity endpoint, GitHub, and package repositories.
Charges include ACI uptime and Azure Cosmos DB serverless request units (RUs) and storage. Complete the cleanup task even after a failure.
Set up your Azure Cosmos DB resources
This exercise creates a separate Azure Cosmos DB account. Delete this account at the end of the exercise.
⚠ Use only the account this script creates for this exercise. The network test deliberately blocks all public data access to that account. Never point this exercise at a shared training or production account.
-
Start Visual Studio Code.
-
If you don’t have the lab code yet, clone the repository for DP-420: open the command palette with Ctrl+Shift+P, run Git: Clone, and enter the following URL. Choose a local folder when prompted. Otherwise, open the folder from your previous clone.
https://github.com/microsoftlearning/dp-420-cosmos-db-dev -
Once the repository is cloned, open that local folder in Visual Studio Code.
-
In the Explorer pane, browse to the Allfiles/Labs/Shared folder.
-
Open the context menu for the folder and select Open in Integrated Terminal. If the terminal isn’t PowerShell, select the dropdown beside the + in the terminal toolbar and choose PowerShell. Keep this local PowerShell session open for the whole exercise: its variables stay available after you exit the container shell.
-
Sign in to the Azure CLI and confirm the subscription you’re working in. Follow the sign-in prompts for your environment. Select the intended subscription if prompted.
az login az account show --query '{Subscription:name,Id:id}' --output table -
Set variables for the resource group and region. Use
ResourceGroup1, or the group supplied by your lab if its name differs.$resourceGroup = "ResourceGroup1" $location = "eastus" -
Run the setup script.
./setup.ps1 -ResourceGroup $resourceGroup -Location $location -NamePrefix dp420lab09 -LabProfile securityAzure Cosmos DB account names have to be globally unique, so the script builds one for you by adding six random characters to the prefix, giving a name like
dp420lab09a7f3k9. -
Wait for the script to finish. The whole script takes 5-10 minutes to run.
-
Record the Account name and Account endpoint values the script prints. You need both throughout this exercise, and the endpoint looks like
https://<your-account-name>.documents.azure.com:443/. -
Set variables for the account and the container group so the commands in this exercise can use them.
$account = "<your-account-name>" $containerGroupName = "security-client" $endpoint = az cosmosdb show --resource-group $resourceGroup --name $account ` --query documentEndpoint --output tsv -
Confirm the security baseline. Expect LocalAuthDisabled to be True and PublicAccess to be Enabled.
az cosmosdb show --resource-group $resourceGroup --name $account ` --query '{LocalAuthDisabled:disableLocalAuth,PublicAccess:publicNetworkAccess}' --output table
The script creates the following resources:
| Resource | Configuration |
|---|---|
| Azure Cosmos DB account | API for NoSQL, serverless, with key-based authentication disabled and public network access enabled |
cosmicworks database |
Holds the container this exercise uses |
product container |
Partitioned on /categoryId, serverless, and initially empty |
The setup script grants no data-plane role to your local Azure CLI identity. You assign data-plane permissions to the hosted managed identity in Task 2.
Task 1: Write the managed identity client
The client downloads and caches 295 CosmicWorks products inside the container. Each test uses the first product’s item ID and partition key.
The client has three modes: seed loads the catalog, read performs a point read, and check verifies that reading succeeds while replacement is denied with HTTP 403.
-
In local PowerShell, create a
dp420-securitysubfolder under the current Allfiles/Labs/Shared folder and change to it.$workFolder = Join-Path $PWD.Path 'dp420-security' New-Item -ItemType Directory -Path $workFolder -Force | Out-Null Set-Location $workFolder -
In the dp420-security folder, create
app.pyin Visual Studio Code with the following code.import json import os import sys from pathlib import Path from urllib.request import urlopen from azure.cosmos import CosmosClient from azure.cosmos.exceptions import CosmosHttpResponseError from azure.identity import ManagedIdentityCredential def main(): # Exit codes: 0 = expected result, 1 = unexpected result, # 2 = invalid arguments or configuration, 3 = seed or read denied. if len(sys.argv) != 2 or sys.argv[1] not in {"seed", "read", "check"}: print("Usage: seed | read | check") return 2 mode = sys.argv[1] credential = None try: endpoint = os.environ.get("COSMOS_ENDPOINT") if not endpoint or not endpoint.strip(): print("COSMOS_ENDPOINT is required.") return 2 # Cache the catalog for reuse across test runs. source_url = "https://raw.githubusercontent.com/AzureCosmosDB/CosmicWorks/main/data/database-v4/product" cache_file = Path("products.json") if not cache_file.exists(): with urlopen(source_url, timeout=60) as response: cache_file.write_bytes(response.read()) # Validate the catalog before accessing Cosmos DB. products = json.loads(cache_file.read_text(encoding="utf-8")) if not isinstance(products, list) or len(products) != 295 or any( not isinstance(product, dict) or not isinstance(product.get("id"), str) or not isinstance(product.get("categoryId"), str) for product in products ): raise ValueError("Invalid dataset") # Use the same product and partition key for every test. first_product = products[0] product_id = first_product["id"] category_id = first_product["categoryId"] # Authenticate as the container's system-assigned managed identity. credential = ManagedIdentityCredential() with CosmosClient(endpoint, credential=credential) as client: database = client.get_database_client("cosmicworks") container = database.get_container_client("product") if mode == "seed": for product in products: container.upsert_item(body=product) print(f"Seed succeeded: {len(products)} items.") return 0 # Confirm read access before testing write denial. container.read_item(item=product_id, partition_key=category_id) print("Read succeeded.") if mode == "read": return 0 try: # Test write permission using the original product values. container.replace_item(item=product_id, body=first_product) print("UNEXPECTED: Write succeeded.") return 1 except CosmosHttpResponseError as exception: # Only a 403 from replacement is an expected write denial. if exception.status_code != 403: raise print("Write denied (403). Expected.") return 0 except CosmosHttpResponseError as exception: # Return a distinct result when seeding or reading is denied. if exception.status_code == 403: print("Seed denied (403)." if mode == "seed" else "Read denied (403).") return 3 print(f"Unexpected Cosmos status ({exception.status_code}).") return 1 except Exception as exception: # Report the error type without exposing request details. print(f"Unexpected failure ({type(exception).__name__}).") return 1 finally: if credential is not None: credential.close() if __name__ == "__main__": sys.exit(main()) -
Set the image and source filename in local PowerShell. Don’t run the program locally: only the container exposes its managed identity.
$image = 'python:3.12-slim-bookworm' $sourceFile = 'app.py'
Task 2: Host the client and load products
Temporarily grant Data Contributor to load the catalog, then replace it with Data Reader for the application. Use a separate loader identity in production.
Create the host identity
-
Encode the source file in local PowerShell for transfer to the container. Base64 encoding doesn’t encrypt the source.
$sourceBase64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path $sourceFile))) -
Create an ACI container group with a system-assigned identity. Keep the container running for the following tests.
az container create --resource-group $resourceGroup --name $containerGroupName ` --location $location --image $image --os-type Linux --cpu 2 --memory 4 ` --assign-identity --restart-policy Never --command-line 'tail -f /dev/null' ` --environment-variables "COSMOS_ENDPOINT=$endpoint" "SOURCE_BASE64=$sourceBase64" -
Confirm that the container is
Runningand capture its managed identity’s principal object ID. Continue only when the ID is nonempty.az container show --resource-group $resourceGroup --name $containerGroupName ` --query 'containers[0].instanceView.currentState.state' --output tsv $principalId = az container show --resource-group $resourceGroup --name $containerGroupName ` --query identity.principalId --output tsv $principalId -
Grant temporary native Data Contributor. Retain the assignment ID for exact deletion later. Verify the returned principal, role ending in
0002, and product-container scope.$scope = '/dbs/cosmicworks/colls/product' $seedAssignment = [Guid]::NewGuid().ToString() az cosmosdb sql role assignment create --resource-group $resourceGroup ` --account-name $account --role-assignment-id $seedAssignment ` --principal-id $principalId --scope $scope ` --role-definition-id '00000000-0000-0000-0000-000000000002'
Initialize the container workspace
-
Enter container Bash from local PowerShell. ACI exec starts one shell command; enter subsequent commands at the Bash prompt.
az container exec --resource-group $resourceGroup --name $containerGroupName --exec-command /bin/bash -
Initialize the project in container Bash. Decode the source and install the required packages.
mkdir -p /work/security cd /work/security printenv SOURCE_BASE64 | base64 --decode > app.py python -m venv .venv .venv/bin/python -m pip install azure-cosmos azure-identity -
Load the products in container Bash. Expect
Seed succeeded: 295 items.and exit code0. Role assignments can take several minutes to propagate; wait and retry if needed..venv/bin/python app.py seed echo $? -
Return to local PowerShell. Exiting Bash leaves the idle container running and preserves your PowerShell variables.
exit
Keep the container running between tasks so its workspace and cached catalog remain available.
Task 3: Enforce and test read-only access
Role permissions are additive. Remove the temporary Contributor assignment before testing Data Reader, and keep the identity and network settings unchanged.
-
Add native Data Reader in local PowerShell, at the same scope. Data Reader is the application’s intended steady-state permission.
$readerAssignment = [Guid]::NewGuid().ToString() az cosmosdb sql role assignment create --resource-group $resourceGroup ` --account-name $account --role-assignment-id $readerAssignment ` --principal-id $principalId --scope $scope ` --role-definition-id '00000000-0000-0000-0000-000000000001' -
Delete the exact temporary seed assignment in local PowerShell.
az cosmosdb sql role assignment delete --resource-group $resourceGroup ` --account-name $account --role-assignment-id $seedAssignment --yes -
Inspect all native assignments for the principal in local PowerShell. Confirm that only Data Reader, ending in
0001, remains at product-container scope.az cosmosdb sql role assignment list --resource-group $resourceGroup --account-name $account ` --query "[?principalId=='$principalId'].{Role:roleDefinitionId,Scope:scope,Assignment:id}" --output table -
Reenter container Bash from local PowerShell.
az container exec --resource-group $resourceGroup --name $containerGroupName --exec-command /bin/bash -
Run the check in container Bash.
cd /work/security .venv/bin/python app.py check echo $?Expect a successful read followed by a denied replacement, with exit code
0. Allow time for role changes to propagate and rerun if needed before continuing.Read succeeded. Write denied (403). Expected. 0 -
Return to local PowerShell before changing account settings.
exit
Task 4: Block and restore the public network path
Disable and restore public network access while keeping the managed identity and Data Reader assignment unchanged.
⚠ This account has no private endpoint. Disabling public network access blocks all data access until you enable it again. Use only the lab account.
Verify the blocked read
-
Disable public access in local PowerShell. Don’t change any permissions or reload data.
az cosmosdb update --resource-group $resourceGroup --name $account --public-network-access DISABLEDNote: This command can take 5 minutes or more to complete and might not display progress while Azure applies the change. Wait for the PowerShell prompt to return before continuing.
-
Inspect the setting in local PowerShell. Expect
Disabled.az cosmosdb show --resource-group $resourceGroup --name $account --query publicNetworkAccess --output tsv -
Enter container Bash from local PowerShell.
az container exec --resource-group $resourceGroup --name $containerGroupName --exec-command /bin/bash -
Run the point read in container Bash. Expect
Read denied (403).and exit code3. Network changes can take up to 15 minutes to propagate; wait and repeat the read if needed.cd /work/security .venv/bin/python app.py read echo $? -
Return to local PowerShell to restore public access, even if the test failed.
exit
Verify restored access and retained restrictions
-
Restore public access in local PowerShell. The final command needs to show
Enabled. Keep Reader and key-based authentication settings unchanged.az cosmosdb update --resource-group $resourceGroup --name $account --public-network-access ENABLED az cosmosdb show --resource-group $resourceGroup --name $account --query publicNetworkAccess --output tsvNote: The update command can take 5 minutes or more to complete and might not display progress while Azure applies the change. Wait for the PowerShell prompt to return before continuing.
-
Reenter container Bash from local PowerShell for the final comparison.
az container exec --resource-group $resourceGroup --name $containerGroupName --exec-command /bin/bash -
Repeat the read and authorization check in container Bash. Expect read success, then the read-success and write-denial pair, each with exit code
0. Allow time for the network change to propagate and retry if needed.cd /work/security .venv/bin/python app.py read echo $? .venv/bin/python app.py check echo $? -
Return to local PowerShell for cleanup.
exit
Compare your results with the expected outcomes below. Record each result, timestamp, managed identity principal ID, and role scope without credentials or product values.
| Stage | Public access | Native role | Expected read | Expected replacement |
|---|---|---|---|---|
| Before blocking | Enabled | Data Reader | Succeeds | Denied, 403 |
| Blocked | Disabled | Same Data Reader | Denied, 403 | Not attempted |
| Restored | Enabled | Same Data Reader | Succeeds | Denied, 403 |
Learn more about managed identities in ACI, Cosmos DB data-plane roles, and account network restrictions.
Clean up resources
Remove this exercise’s disposable resources when you finish, even after a partial or failed run. Run the group-deletion steps only if you created the group and it contains only this exercise’s resources. If your lab provided ResourceGroup1, keep the group and use the individual account and container-group deletion commands instead.
-
Inspect the subscription, group, and resources in local PowerShell. Resolve any unexpected resource before deletion.
az account show --query '{Subscription:name,Id:id}' --output table az group show --name $resourceGroup --query '{Name:name,Id:id}' --output json az resource list --resource-group $resourceGroup --query '[].{Name:name,Type:type}' --output table -
Delete the inspected group in local PowerShell. Deleting the group permanently removes the account, its data, and the ACI host. Review the interactive confirmation before approving.
az group delete --name $resourceGroup -
Verify deletion in local PowerShell. Confirm that the result is
false.az group exists --name $resourceGroup
If your lab provided the resource group, or the group holds other resources, delete only this exercise’s account and container group:
az cosmosdb delete --name $account --resource-group $resourceGroup --yes
az container delete --name $containerGroupName --resource-group $resourceGroup --yes