Warmup Guide — Secrets, Key Vault & Managed Identity
Zero-to-principal primer for Phase 12: the one principle that organizes all of cloud secret management (the most secure secret is the one you never store), how a Managed Identity turns that principle into a mechanism via IMDS, what a Key Vault actually is (a versioned, RBAC-gated, soft-deletable store where control plane is a different system from data plane), and the envelope encryption trick that makes key rotation cost a few bytes instead of a petabyte. Every concept goes from what it is to why it exists to the mechanism under the hood (diagrams, tables, code) to production significance to the misconceptions that cause breaches and 2 a.m. pages.
Table of Contents
- Chapter 1: The One Principle — Don't Store the Secret
- Chapter 2: Managed Identity — An Azure-Managed Service Principal
- Chapter 3: IMDS and the Token Flow
- Chapter 4: Key Vault — The Versioned, RBAC-Gated Store
- Chapter 5: Envelope Encryption — DEK, KEK, and the Wrap
- Chapter 6: Key Rotation and Versioning
- Chapter 7: Soft-Delete and Purge Protection
- Chapter 8: Managed HSM, FIPS, and Certificates
- Chapter 9: Where Secrets Sit in the Platform
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: The One Principle — Don't Store the Secret
From zero. A secret is anything that, if an attacker has it, lets them act as you: a password, an API key, a connection string, a private key, a client secret. The entire discipline of secret management exists because secrets spread. You put a connection string in a config file; the config file goes into git; git gets cloned to ten laptops; one laptop gets a crash dump uploaded to a support portal; now your database password is in a vendor's ticketing system. Every link in that chain is a place the secret can leak, and you control none of them.
The principal-level reframing — the sentence that organizes this whole phase — is:
The most secure secret is the one you never store.
If there is no stored secret, there is nothing to leak, rotate, or expire by surprise. This is not a slogan; it is an engineering target, and Azure gives you the mechanisms to hit it:
- For one Azure resource calling another Azure service (a Function calling Storage, a VM calling Key Vault), you can have zero stored secrets — a Managed Identity (Ch. 2) gets a short-lived token from the platform on demand.
- For secrets you cannot eliminate (a third-party API key, a partner's database password, a TLS cert you must present), you store them in one hardened, audited, access- controlled place — Key Vault (Ch. 4) — and read them using the managed identity, so the only "credential" the app holds is, again, the managed identity (which isn't a stored secret).
- For encryption, you don't store the data's key with the data; you wrap it with a key that lives in the vault/HSM — envelope encryption (Ch. 5).
The reducible thing — the one credential you can't make disappear — is called secret-zero. The principal's job is to make secret-zero a managed identity (a platform-issued, auto- rotated, audience-scoped token) rather than a string in a file. Hold this principle; every mechanism below is in service of it.
Chapter 2: Managed Identity — An Azure-Managed Service Principal
What it is. A Managed Identity is a special kind of Entra (Azure AD) service principal whose credential is created, stored, and rotated by Azure itself. Your code never sees the credential. When the code needs to call an Azure service, it asks the platform for a short-lived access token (a JWT — you validated those in P03) and presents that. No password, no client secret, no certificate in your code or config.
Why it exists. Before managed identities, an app authenticated to Azure as an Entra app registration with a client secret or certificate — a real credential you had to generate, store somewhere (ironically, often in Key Vault, which you then needed another credential to reach), and rotate before it expired. Expiring client secrets are a perennial outage cause: the secret silently lapses at midnight and the app starts returning 401s with nobody knowing where the secret was even configured. Managed identities delete that entire class of problem by making Azure own the credential lifecycle.
The two kinds — this is an interview staple:
| System-assigned | User-assigned | |
|---|---|---|
| Lifecycle | created with the resource, deleted with it | a standalone Azure resource; independent lifecycle |
| Count | exactly one per resource | attach one identity to many resources |
| Identity = | the resource itself | a shareable identity object |
| Selected by | (implicit — the resource's own) | its client_id (or object id / resource id) |
| Use when | the identity's life is the resource's; nothing else needs it | several resources share one identity (grant RBAC once), or the identity must exist before the resource (break a deploy ordering chicken-and-egg) |
System-assigned: User-assigned:
┌───────────┐ ┌──────────────────┐
│ Function │── owns ──▶ Identity │ Identity (UAI) │
└───────────┘ (1:1, dies with it) └───────┬──────────┘
attached to ▼ ▼ ▼
Function VM AKS pod (1 identity → N resources)
The principal nuance. User-assigned wins more often than juniors expect, for two
reasons. (1) RBAC grant amplification: if 30 functions need Key Vault Secrets User,
30 system-assigned identities means 30 role assignments to manage; one user-assigned
identity means one. (2) Deploy ordering: a system-assigned identity doesn't exist
until the resource does, so you can't pre-assign its roles in the same template without a
two-phase deploy; a user-assigned identity you create first, grant roles to, then attach
— the deploy is clean and idempotent.
Chapter 3: IMDS and the Token Flow
What it is. The Instance Metadata Service (IMDS) is a read-only REST endpoint
available inside every Azure VM / App Service / Function / container at the link-local
address 169.254.169.254. Link-local means it is non-routable — packets to it never
leave the host; only code on that resource can reach it. Among other metadata, IMDS issues
OAuth2 access tokens for the resource's managed identity:
GET http://169.254.169.254/metadata/identity/oauth2/token
?api-version=2018-02-01
&resource=https://vault.azure.net ◀── the AUDIENCE you want a token for
Header: Metadata: true ◀── anti-SSRF guard (a browser can't set it trivially)
Why the IP is non-routable — and why that's the security model. Because
169.254.169.254 is reachable only from the host, possession of "I can call IMDS" is the
proof of identity. There is no secret to present because being able to ask is the
credential. (This is also why SSRF — tricking a server into making a request on your
behalf — is the canonical attack on IMDS: if you can make the app fetch a URL you control,
you make it fetch IMDS and steal its token. IMDS v2 mitigations and the Metadata: true
header requirement exist for exactly this.)
The flow, end to end:
your code IMDS (169.254.169.254) Entra Key Vault
│ get_token("vault") │ │ │
├───────────────────────────────▶│ "give me a token, │ │
│ │ aud=vault.azure.net" │ │
│ ├─────────────────────────▶│ mint JWT (aud, │
│ │◀─────────────────────────┤ exp, oid, tid) │
│◀───────────────────────────────┤ {access_token, expires_on, resource} │
│ GET /secrets/db-pwd (Bearer <token>) ───────────────────────────────────▶│
│◀──────────────────────────────────────────────────── 200 {value} (or 403) │
The audience (resource/scope) is load-bearing. The token's aud claim names the
one service it's valid for. A token minted for https://vault.azure.net is rejected by
https://storage.azure.com. The most common "it returns a token but the call 401s" bug is
asking IMDS for the wrong audience. In the lab, token_is_valid(token, resource, now) checks
both the audience match and the expiry — because those are the two independent ways a
token silently fails.
Caching — the production-critical part. A function that reads Key Vault in a hot loop
must not call IMDS on every iteration; that would hammer the metadata endpoint and add
latency to every call. The credential SDK (DefaultAzureCredential /
ManagedIdentityCredential) caches the token per (identity, audience) and only
refetches when it's near expiry. The lab models exactly this:
key = (identity.principal_key, resource)
cached = self._cache.get(key)
if cached is not None and token_is_valid(cached, resource, now, skew_s=self.refresh_skew_s):
return cached # ◀── cache hit: SAME token, no IMDS call
# else mint, cache, count an imds hit
The refresh skew (refresh_skew_s) makes the SDK refetch before the hard expiry — so a
request never races a token expiring mid-flight. In the lab, imds_calls counts real
fetches; three acquire_token calls (before expiry, before expiry, after expiry) produce
exactly two IMDS hits. That assertion is the caching contract.
Misconception to kill: "Managed identity = no token." No — there is very much a token (a
full JWT with aud, exp, oid, tid, xms_mirid). What's missing is a stored
credential. The token is short-lived, audience-scoped, and fetched on demand; it just isn't
a secret you saved anywhere.
Beyond IMDS — workload identity federation (no secret at all). For AKS pods and CI
runners (GitHub Actions, Azure DevOps), there's an even cleaner pattern: the workload
presents an external OIDC token (issued by Kubernetes or GitHub), and Entra exchanges it
for an Azure access token via a configured federated credential trust — no client
secret, no IMDS. The trust is the OIDC issuer + subject (e.g.
repo:contoso/app:ref:refs/heads/main). It's the same "don't store the secret" principle
projected across a trust boundary; you built the OIDC validation half in P03/P08.
Chapter 4: Key Vault — The Versioned, RBAC-Gated Store
What it is. Azure Key Vault is a managed service for storing and controlling access to three kinds of object, all versioned:
| Object | What it is | Key data-plane ops |
|---|---|---|
| Secret | an opaque value (string, ≤ 25 KB) — API keys, connection strings | get, set, list, delete |
| Key | an asymmetric/symmetric key you use without exporting | encrypt/decrypt, wrapKey/unwrapKey, sign/verify |
| Certificate | an X.509 cert + its key, with a lifecycle | create, import, auto-rotate, CA policy |
The critical word is versioned. Every write creates a new immutable version with its own id and URL:
https://myvault.vault.azure.net/secrets/db-password/9f3c... ◀── a specific version
https://myvault.vault.azure.net/secrets/db-password ◀── "current" (latest)
Old versions stay readable. This is what makes rotation non-breaking: a client pinned to
version 9f3c… keeps working while you set a new current version, and you flip consumers
over deliberately.
Why it exists. Centralizing secrets into one hardened, audited, access-controlled service is the only way to make the chaos of Chapter 1 tractable: one place to rotate, one audit log of who read what, one firewall/private-endpoint boundary, one backup. The alternative — secrets scattered across config files, pipelines, and env vars — is unauditable and unrotatable.
The two authorization models. This is a real design decision, not trivia:
- Azure RBAC (recommended). The same role-assignment engine as the rest of Azure (you
built it in P04). Roles like
Key Vault Secrets User(read secrets),Key Vault Secrets Officer(read+write),Key Vault Administrator,Key Vault Crypto User. Assignments inherit down the MG → Sub → RG → vault → individual secret hierarchy, are deny-aware, and are auditable like everything else. Default-deny: no matching assignment → no access. - Access policies (legacy). A per-vault list of
(principal → allowed operations). Simpler to reason about for one vault, but not inheritable, not deny-aware, capped in count, and harder to audit at scale. New vaults should use RBAC.
Control plane vs data plane — Key Vault is the sharpest example in all of Azure. Burn this in:
CONTROL PLANE (ARM + RBAC) DATA PLANE (vault endpoint + data-plane RBAC)
──────────────────────────── ─────────────────────────────────────────────
create / delete the vault read a secret's VALUE
configure firewall, networking wrap / unwrap / sign with a key
SET the authorization model list secret names
roles: Owner, Contributor roles: Key Vault Secrets User, Crypto User
An Owner of the vault can delete the entire thing but gets a 403 reading a single
secret, because reading a value is a data-plane action and Owner is a control-plane
role. This trips up nearly everyone once. In the lab:
owner_only = [RoleAssignment(principal=app, role="Owner")]
kv.get_secret("api-key", principal=app, role_assignments=owner_only) # ▶ PermissionError
reader = [RoleAssignment(principal=app, role="Key Vault Secrets User")]
kv.get_secret("api-key", principal=app, role_assignments=reader) # ▶ ok
(There is a deliberate exception in real Azure: with RBAC, a control-plane Owner can
grant themselves a data-plane role — but holding Owner alone does not read secrets.)
Chapter 5: Envelope Encryption — DEK, KEK, and the Wrap
The problem. You want "encryption at rest with a key you control" for a 50 TB storage account. The naive approach — encrypt all 50 TB directly with a Key Vault key — fails for two reasons: (1) Key Vault keys (especially RSA) are slow and rate-limited; you can't push 50 TB through them, and (2) when you rotate the key you'd have to re-encrypt all 50 TB, which is absurd.
The solution — envelope encryption. Use two keys:
- A DEK (data encryption key) — a fast symmetric key (AES) that actually encrypts the data. It can be per-object, generated fresh from a CSPRNG.
- A KEK (key encryption key) — a key that lives in Key Vault / the HSM and never leaves it. You use it only to wrap (encrypt) the tiny DEK.
┌─────────────── stored together at rest ───────────────┐
data ──▶ [ enc with DEK ] ──▶ ciphertext │
DEK ──▶ [ wrap with KEK in the vault/HSM ] ──▶ wrapped_DEK │
└────────────────────────────────────────────────────────┘
KEK stays in the vault — only its wrap/unwrap operations are called over the wire.
To decrypt: send wrapped_DEK to the vault, get the DEK back (the vault unwrapKeys it
with the KEK that never left), then decrypt the ciphertext locally with the DEK. The lab's
envelope_encrypt/envelope_decrypt are exactly this:
def envelope_encrypt(kek, plaintext, dek):
ciphertext = enc(dek, plaintext) # fast symmetric over the data
wrapped = wrap_dek(kek, dek) # encrypt the small DEK under the KEK
return ciphertext, wrapped # persist BOTH
def envelope_decrypt(kek, ciphertext, wrapped_dek):
dek = unwrap_dek(kek, wrapped_dek) # KEK unwraps the DEK
return dec(dek, ciphertext) # DEK decrypts the data
⚠️ The lab's cipher is a TEACHING cipher — SHA-256 counter-mode keystream XORed into the plaintext, with no authentication, no nonce, and keystream reuse. It is structurally identical to real envelope encryption (DEK/KEK, wrap/unwrap, rewrap-on- rotate), which is the entire lesson, but it must never encrypt anything real. Azure uses AES-GCM for the data and RSA-OAEP / AES-KW for the wrap, inside FIPS-validated HSMs.
Chapter 6: Key Rotation and Versioning
Why envelope encryption is worth it: rotation becomes cheap. Here is the payoff that makes the whole construction worthwhile. To rotate the KEK (because it's old, or suspected compromised, or policy says rotate every 90 days), you do not re-encrypt the data. You:
def rotate_kek(old_kek, new_kek, wrapped_dek):
dek = unwrap_dek(old_kek, wrapped_dek) # 1. unwrap the DEK with the old KEK
return wrap_dek(new_kek, dek) # 2. rewrap it with the new KEK
# the DATA ciphertext is NEVER touched.
The cost of rotating the KEK is O(number of DEKs), not O(bytes of data). Rotating the
key for a 50 TB account rewraps a handful of DEKs — milliseconds — and the 50 TB of ciphertext
is byte-for-byte unchanged. The lab proves this with an assertion that the data ciphertext
object is identical before and after rotation:
ciphertext_before = bytes(ciphertext)
new_wrapped = rotate_kek(old_kek, new_kek, wrapped)
assert ciphertext == ciphertext_before # ◀── the whole point
assert envelope_decrypt(new_kek, ciphertext, new_wrapped) == plaintext
Key versioning makes rotation non-breaking. When you rotate, the new KEK is a new version of the same key object. Old wrapped DEKs were wrapped against the old version, which still exists, so they still unwrap — there's no "rotate and everything that hasn't been rewrapped breaks" cliff. You rewrap lazily or on a schedule. The same versioning logic that keeps a pinned secret version readable (Ch. 4) keeps old key versions usable for decryption.
Production significance. This is the literal engine behind customer-managed keys (CMK) for Azure Storage, SQL, Disk, and Cosmos: the service stores a wrapped DEK, calls your Key Vault to unwrap it via its managed identity, and when you rotate your KEK it rewraps — your data is never re-encrypted, and if you disable the KEK, the wrapped DEKs can no longer be unwrapped and the data is cryptographically unreadable (the revocation / "crypto-shredding" story). Rotation is a few bytes; revocation is instant; neither touches the data. That is the elegance.
Chapter 7: Soft-Delete and Purge Protection
The problem. Secrets and keys get deleted — by accident (a bad script, a fat finger), by malice (a disgruntled admin, ransomware), or as collateral in a wrong cleanup. If a delete is immediate and permanent, a single mistake or a single compromised credential can destroy the keys protecting your data — which, with envelope encryption, means destroying the data. That is an unacceptable single point of catastrophe.
Soft-delete (always on now). A delete does not remove the bytes; it moves the object to
a deleted-but-recoverable state for a retention window (configurable 7–90 days, default
90). Within that window you can recover it. The state machine the lab implements:
set_secret delete_secret(now) recover_secret(now < purge_on)
(none) ──────────▶ [ active ] ───────────────────▶ [ soft-deleted ] ──────────────────────▶ [ active ]
▲ │ recoverable until
│ │ scheduled_purge_on = deleted_on+retention
└──────────────────────────────┘
│ purge_secret(now)
▼
[ gone ] (permanent)
def delete_secret(self, name, now):
entry.deleted = True
entry.scheduled_purge_on = now + self.retention_s # recoverable until here
While soft-deleted, the secret is not readable (get_secret raises) but its bytes are
not gone. After retention elapses it is auto-purged and recover fails.
Purge protection — the security guarantee. Soft-delete alone isn't enough against a
determined attacker: if they can delete and purge, they make the deletion permanent
immediately. Purge protection, when enabled (and it's irreversible once on), means a
soft-deleted object cannot be purged before the retention window elapses — by anyone,
including a Global Admin, including the attacker. The lab:
def purge_secret(self, name, now):
if self.purge_protection and now < entry.scheduled_purge_on:
raise PermissionError("purge protection: cannot purge until retention elapses")
del self._secrets[name]
So even a fully compromised admin credential cannot make the deletion permanent — you have a guaranteed window to detect the deletion (it fires an event/alert) and recover. Enable soft-delete and purge protection on any vault that protects something you can't afford to lose. The tradeoff: you cannot reuse a vault/secret name until retention elapses, which occasionally annoys CI that creates-and-destroys vaults — a small price.
Chapter 8: Managed HSM, FIPS, and Certificates
Standard Key Vault vs Managed HSM. Standard Key Vault is multi-tenant and validated to FIPS 140-2 Level 2 (software-protected keys; HSM-backed keys in the Premium SKU). Azure Key Vault Managed HSM is a single-tenant, fully customer-controlled pool of FIPS 140-2 Level 3 hardware HSMs — the key material is generated and used only inside the HSM, never extractable, with cryptographic attestation. You reach for Managed HSM for regulated workloads (PCI, FIPS L3 mandates), the highest key-isolation requirements, or when you need a security-domain you alone control. It costs meaningfully more and has its own local RBAC roles — the cost/isolation tradeoff is a principal decision, not a default.
| Key Vault (Standard/Premium) | Managed HSM | |
|---|---|---|
| Tenancy | multi-tenant | single-tenant |
| FIPS 140-2 | Level 2 (Premium: HSM-backed keys) | Level 3 |
| Key extractable | no (Premium HSM keys) | never |
| Use when | most apps, secrets, certs | regulated / highest isolation |
Certificates. A Key Vault certificate bundles a private key + an X.509 cert and adds a lifecycle: issuance (self-signed, or via an integrated CA like DigiCert), an issuance policy, and auto-rotation (Key Vault renews the cert a configured number of days before expiry, generating a new version — the same versioning model). The principal value is the same as everywhere in this phase: the renewal is automated and the private key never leaves the vault, so a cert expiring at midnight stops being an outage.
Chapter 9: Where Secrets Sit in the Platform
Secrets and identity are not a corner of the platform — they are the connective tissue that makes every other phase secure:
- P03 (Entra/JWT) — a managed-identity token is a JWT; IMDS is a token endpoint; the
target service validates
aud/exp/signature with the validator you built. - P04 (RBAC) — Key Vault's recommended authorization is the same role + inheritance +
default-deny engine; "read a secret" is a
DataAction. - P11 (Functions/Durable) — a function authenticates outbound (to Storage, to Key Vault, to another API) with its managed identity; Key Vault references inject secrets into app settings, resolved via that identity, so the code holds nothing.
- P10 (Event Grid/Service Bus) — Key Vault emits
SecretNearExpiry/SecretExpiredevents; an event-driven rotation function reacts with no human in the loop. - P08 (CI/CD) — pipelines authenticate via workload identity federation (OIDC → Entra, no stored secret) and read deploy-time secrets from a scoped vault.
- P00 (control vs data plane) — Key Vault is the canonical example, and the lesson
("
Owner≠ secret reader") is one you'll apply to Storage, Service Bus, and everything else.
The mental model: identity (a managed identity) replaces the credential; Key Vault holds the irreducible secrets; envelope encryption protects the data; RBAC gates the data plane; and soft-delete/purge-protection make the whole thing recoverable. Those five sentences are the phase.
Lab Walkthrough Guidance
Lab 01 — Key Vault & Managed Identity, suggested order (matches lab.py top-to-bottom):
Identity+system_assigned/user_assigned(Ch. 2) — the validation is the lesson: system-assigned has noclient_id; user-assigned requires one.principal_keyis the stable cache key.InstanceMetadataService.acquire_token+token_is_valid(Ch. 3) — the centerpiece of Part 1. Build the cache: look up(identity, resource); return the cached token if still valid with the refresh skew; else mint (expires_on = now + ttl), cache, bumpimds_calls.token_is_validchecks audience and expiry. Maketest_token_cached_before_expiry_same_tokenand..._refreshed_after_expiry_...pass — that pair is the caching contract.keystream/enc/dec(Ch. 5) — SHA-256 counter mode;decis justenc. Get the round-trip green first.generate_dek/wrap_dek/unwrap_dek— thin wrappers overenc/dec; the naming is the teaching (a DEK wrapped under a KEK).envelope_encrypt/envelope_decrypt/rotate_kek(Ch. 5–6) — the payoff. Maketest_kek_rotation_data_ciphertext_object_unchangedpass and understand why it's the whole point: rotation rewraps the DEK only.KeyVaultversioning + RBAC (Ch. 4) —set_secretappends a version;get_secretreturns latest or a pinned version and denies by default unless the principal holds a data-plane role (Owneris not one).- Soft-delete state machine (Ch. 7) —
delete→recover(within retention) →purge(blocked by purge protection inside retention). Make the purge-protection test pass and you own the security guarantee.
Run it red, make it green: pytest test_lab.py -v, then
LAB_MODULE=solution pytest test_lab.py -v, then python solution.py.
Success Criteria
You are ready for the next phase when you can, from memory:
- State the one principle ("don't store the secret") and name secret-zero.
- Draw the managed-identity token flow
code → IMDS (169.254.169.254) → Entra → token (aud=vault) → vault, and explain why the IP is non-routable and why the token is cached. - Pick system- vs user-assigned with a concrete reason for each (RBAC grant amplification; deploy ordering).
- Explain why an
Ownerof a Key Vault gets a 403 reading a secret (control vs data plane). - Choose RBAC over access policies and say why (inheritable, deny-aware, auditable).
- Explain envelope encryption (DEK/KEK/wrap) and why KEK rotation is
O(#DEKs), notO(bytes)— and never re-encrypts the data. - Explain soft-delete vs purge, and exactly what purge protection guarantees against a compromised admin.
Interview Q&A
Q: "My app needs to read a database password and call a storage account. Walk me through a
design with no stored secrets."
The storage hop has zero stored secrets: give the app a managed identity and a
data-plane role (Storage Blob Data Reader); it gets an audience-scoped token from IMDS on
demand. The database password I can't eliminate (the DB isn't Entra-native, say), so it goes
in Key Vault — but the app reads it using the same managed identity (granted Key Vault Secrets User), so the only "credential" anywhere is the managed identity, which isn't a stored
secret. If the app is a Function, I'd use a Key Vault reference so the secret is injected as
an app setting resolved via the identity, and the code never even touches the vault SDK. Net:
secret-zero is a managed identity, not a string in a file.
Q: "I gave my service principal Owner on the Key Vault and it still can't read the secret.
Why?"
Because reading a secret's value is a data-plane action and Owner is a control-plane
role. Owner/Contributor let you manage the vault resource — create it, delete it, set its
firewall, even change its authorization model — but not read what's inside. You need a
data-plane role like Key Vault Secrets User. This control-plane-vs-data-plane split is the
single most common Key Vault 403 and shows up identically across Azure (an account Owner
isn't a Blob Data Reader either).
Q: "Explain envelope encryption and why you'd use it for encryption at rest."
You encrypt the data with a fast symmetric DEK, then wrap (encrypt) the small DEK with
a KEK that lives in Key Vault / an HSM and never leaves it; you store the ciphertext and
the wrapped DEK together. Two payoffs. Performance: the bulk crypto is local AES, not slow
rate-limited vault calls. Rotation: to rotate the KEK you only unwrap the DEK with the old
key and rewrap with the new — O(#DEKs), never re-encrypting the data — and key versioning
keeps old wrapped DEKs decryptable so rotation is non-breaking. It's the engine behind Azure's
customer-managed-key encryption, and it also gives you instant revocation: disable the KEK
and every wrapped DEK becomes unusable, so the data is cryptographically unreadable without
ever touching it.
Q: "What does soft-delete with purge protection actually protect you from?"
Soft-delete makes a delete recoverable for a retention window (default 90 days) — it defends
against accidents. Purge protection defends against malice: when it's on (irreversibly), a
soft-deleted object cannot be purged before retention elapses by anyone, including a Global
Admin or a fully compromised credential. So an attacker who deletes your keys can't make it
permanent on demand; you get a guaranteed window — during which the deletion fires an alert —
to detect and recover. Without purge protection, delete + purge is an instant, irreversible
loss; with it, deletion can't be weaponized. I enable both on anything I can't afford to lose.
Q: "System-assigned or user-assigned managed identity — how do you choose?" System-assigned when the identity's lifecycle is the resource's and nothing else needs that identity — it's created and destroyed with the resource, one-to-one, nothing to clean up. User-assigned when (a) several resources should share one identity so I grant the RBAC role once instead of N times, or (b) the identity must exist before the resource so I can pre-assign its roles and avoid a two-phase deploy (a system-assigned identity doesn't exist until its resource does, which creates a chicken-and-egg in IaC). At scale I lean user-assigned for the RBAC-grant economy and clean deploy ordering.
Q: "How does a CI pipeline authenticate to Azure without a stored secret?"
Workload identity federation. The pipeline (GitHub Actions / Azure DevOps) presents the
OIDC token its platform issues; I configure a federated credential on an Entra app /
user-assigned identity that trusts that issuer + subject (e.g.
repo:org/app:ref:refs/heads/main); Entra exchanges the external OIDC token for an Azure access
token. No client secret to store, rotate, or leak. It's the same "don't store the secret"
principle as a managed identity, extended across the cloud boundary — and it's why
secretless OIDC deploys are now the standard over the old service-principal-with-a-secret
pattern.
References
- Microsoft Learn — What are managed identities for Azure resources? (https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview)
- Microsoft Learn — Azure Instance Metadata Service (IMDS) and the identity token endpoint (https://learn.microsoft.com/azure/virtual-machines/instance-metadata-service)
- Microsoft Learn — Azure Key Vault basic concepts and Authentication / RBAC vs access policies (https://learn.microsoft.com/azure/key-vault/general/basic-concepts)
- Microsoft Learn — Key Vault soft-delete overview and purge protection (https://learn.microsoft.com/azure/key-vault/general/soft-delete-overview)
- Microsoft Learn — Azure Storage encryption / envelope encryption with customer-managed keys (https://learn.microsoft.com/azure/storage/common/storage-service-encryption)
- Microsoft Learn — Azure Key Vault Managed HSM and FIPS 140-2 Level 3 (https://learn.microsoft.com/azure/key-vault/managed-hsm/overview)
- Microsoft Learn — Workload identity federation (https://learn.microsoft.com/azure/active-directory/workload-identities/workload-identity-federation)
- RFC 6749 (OAuth 2.0) and RFC 7519 (JSON Web Token) — the token the lab models is a JWT (you validate it in P03)
- The track's own CHEATSHEET.md and GLOSSARY.md