-
Notifications
You must be signed in to change notification settings - Fork 321
feat(agents): secure-by-default private networking via azure.yaml #8708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
m5i-work
merged 26 commits into
huimiu/foundry-azure-yaml
from
m5i/foundry-private-network
Jun 24, 2026
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
8410b07
feat(agents): private networking for host: microsoft.foundry
m5i-work bffcab3
fix(agents): preserve network ${VAR} refs through eject
m5i-work ec4fcec
test(agents): add Foundry private-networking E2E harness
m5i-work f296fbf
test(agents): run network E2E phases 0-4 without --image
m5i-work 72c52b5
fix(agents): correct network bicep preflight + network-mode output ca…
m5i-work 65196bf
test(agents): make network E2E harness run green end-to-end
m5i-work da8f5b7
test(agents): build phase-5 image in ABAC-enabled ACR
m5i-work 7dbe27a
test(agents): grant private ACR pull to Foundry project identity
m5i-work 0813c19
docs(agents): add BYO image VNet cheatsheet
m5i-work 75939f5
docs(agents): move private networking guide to docs
m5i-work 9e99a4a
fix(agents): surface managed-network isolation output
m5i-work 6caace1
fix(agents): keep managed-network data plane reachable
m5i-work 6111b2e
feat(agents): secure-by-default private networking for Foundry services
m5i-work 36e16d5
docs(agents): use 'azd ai agent init --image' and assert real state i…
m5i-work 050986c
docs(agents): order init before azure.yaml in managed-egress cheatsheet
m5i-work 6a82c36
docs(agents): simplify private-networking cheatsheets
m5i-work f1f0d06
test(agents): fix jumpbox fallback for DNS-reference deploy
m5i-work 1d3f748
test(agents): exclude private-networking e2e harness from PR
m5i-work 115e1e1
Merge remote-tracking branch 'origin/huimiu/foundry-azure-yaml' into …
m5i-work ea84132
test(agents): ignore bicep generator metadata in ARM drift check
m5i-work 56e9532
feat(agents): validate distinct subnet names; drop debug log
m5i-work 16e3459
Merge remote-tracking branch 'origin/huimiu/foundry-azure-yaml' into …
m5i-work 52196bc
feat(agents): refuse Terraform eject when network: is declared
m5i-work 5dd48f9
docs+test(agents): document Bicep eject customization; verify full ne…
m5i-work d60e802
docs(agents): merge networking Requirements/Known limitations into on…
m5i-work 0f838e8
docs(agents): rename networking quick guides to parallel Scenario 1-3…
m5i-work File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
314 changes: 314 additions & 0 deletions
314
cli/azd/extensions/azure.ai.agents/docs/private-networking.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,314 @@ | ||
| # Private networking for `host: azure.ai.agent` | ||
|
|
||
| A Foundry service can be provisioned as a **network-secured (VNet-bound)** | ||
| account by adding a `network:` block to the service body in `azure.yaml`. When | ||
| `network:` is omitted the account uses public networking (unchanged behavior). | ||
|
|
||
| When `network:` is present, azd always provisions an **account private | ||
| endpoint** and disables public network access — the data plane is never left | ||
| public. Dependent stores (Cosmos DB, AI Search, Storage) stay platform-managed. | ||
|
|
||
| The block models two orthogonal axes: | ||
|
|
||
| - **Egress** (agent runtime network) — set `agentSubnet` to inject the agent | ||
| into your subnet (BYO VNet), or omit it to use the Microsoft-managed network. | ||
| `isolationMode` tunes the managed network's outbound posture and is valid only | ||
| when `agentSubnet` is omitted. | ||
| - **Ingress** (account data plane) — `peSubnet` is **required** and always | ||
| yields an account private endpoint, so callers (`azd deploy`, | ||
| `azd ai agent invoke`) must reach the account from inside the VNet, a peered | ||
| VNet, or VPN. | ||
|
|
||
| ```yaml | ||
| services: | ||
| my-project: | ||
| host: azure.ai.agent | ||
| network: | ||
| # ----- Egress: agent runtime network (pick ONE) ----- | ||
| # | ||
| # (a) Managed egress (shown live below): omit agentSubnet so the agent | ||
| # runs in the Microsoft-managed network. isolationMode is valid only | ||
| # in this mode. | ||
| isolationMode: AllowOnlyApprovedOutbound # or AllowInternetOutbound (default) | ||
| # | ||
| # (b) BYO egress: inject the agent into your subnet instead. Replace the | ||
| # isolationMode line above with an agentSubnet block (same VNet as | ||
| # peSubnet in v1): | ||
| # agentSubnet: | ||
| # vnet: ${AZURE_VNET_ID} | ||
| # name: agent-subnet | ||
| # prefix: 192.168.10.0/24 # omit prefix to reference an existing subnet | ||
|
|
||
| # ----- Ingress: account private endpoint (REQUIRED) ----- | ||
| peSubnet: | ||
| vnet: ${AZURE_VNET_ID} # ARM id of the VNet (must already exist) | ||
| name: pe-subnet | ||
| prefix: 192.168.11.0/24 # omit prefix to reference an existing subnet | ||
|
|
||
| # ----- Private DNS (optional) ----- | ||
| dns: | ||
| resourceGroup: rg-private-dns # omit to let azd create + link the zones | ||
| subscription: ${AZURE_DNS_SUBSCRIPTION_ID} # optional; defaults to the deployment subscription | ||
| agents: | ||
| - name: my-agent | ||
| kind: hosted | ||
| project: src/my-agent | ||
| image: myprivacr.azurecr.io/agents/my-agent:v1 # BYO image required | ||
| ``` | ||
|
|
||
| > You do not hand-author the `agents:` entry above. Run | ||
| > `azd ai agent init --no-prompt --agent-name my-agent --image <registry/image:tag>` | ||
| > to scaffold it (it writes `agent.yaml`); then add the `network:` block to the | ||
| > generated service. | ||
|
|
||
| > The example above uses **managed egress** so every field — including | ||
| > `isolationMode` — is shown as valid YAML. For **BYO egress**, swap the | ||
| > `isolationMode` line for an `agentSubnet` block (see comment `(b)` and | ||
| > Scenario 2 below); `isolationMode` is then invalid and must be removed. | ||
|
|
||
| ### Field reference | ||
|
|
||
| | Field | Rule | | ||
| | --- | --- | | ||
| | `agentSubnet` | Optional. Present: the agent is injected into this customer subnet (BYO egress). Absent: the agent uses the Microsoft-managed network (managed egress). | | ||
| | `peSubnet` | **Required.** Subnet for the account private endpoint. Establishes the private data plane (public access disabled). | | ||
| | `isolationMode` | Optional. `AllowInternetOutbound` or `AllowOnlyApprovedOutbound`. Valid **only** when `agentSubnet` is omitted (managed egress). | | ||
| | subnet `vnet` | Required. ARM id of the VNet that holds (or will hold) the subnet. Supports `${VAR}`. When `agentSubnet` is present, it must reference the same VNet as `peSubnet`. | | ||
| | subnet `name` | Required. Subnet name. | | ||
| | subnet `prefix` | Optional. Omit to reference an existing subnet; set to create the subnet with that CIDR. | | ||
| | `dns.resourceGroup` | Omitted: azd creates and links the AI private DNS zones. Set: azd references existing zones in that resource group. Requires `peSubnet`. | | ||
| | `dns.subscription` | Optional. Defaults to the deployment subscription. Accepts a bare GUID or `${VAR}`. | | ||
|
|
||
| ### Environment variables | ||
|
|
||
| Network fields support `${VAR}` references resolved client-side from the azd | ||
| environment (run `azd env set <KEY> <value>`). The variable names are | ||
| user-chosen; the example above uses: | ||
|
|
||
| | Variable | Format | Used by | | ||
| | --- | --- | --- | | ||
| | `AZURE_VNET_ID` | ARM resource id of an existing `Microsoft.Network/virtualNetworks` | subnet `vnet` | | ||
| | `AZURE_DNS_SUBSCRIPTION_ID` | bare GUID or `/subscriptions/<guid>` | `network.dns.subscription` | | ||
|
|
||
| ### Limitations | ||
|
|
||
| - **Single VNet (v1).** When `agentSubnet` is present it must live in the same | ||
| VNet as `peSubnet`; azd errors otherwise. Cross-VNet topologies (agent and | ||
| account private endpoint in different VNets) are deferred — they need | ||
| customer-managed peering plus DNS-zone links to both VNets, which azd does not | ||
| provision. Managed egress is unaffected (it needs only the `peSubnet` VNet). | ||
| - **BYO container image required.** Secured agents must reference a pre-built | ||
| image via `agents[].image`; local build into a private ACR is not supported in | ||
| v1. The developer owns the registry's SKU, private endpoint, DNS, and firewall. | ||
| - **Brownfield (`endpoint:`) ignores `network:`.** When `endpoint:` is set the | ||
| account's network posture is fixed by whoever created it; azd warns and does | ||
| not reconcile `network:`. | ||
| - **One default-DNS account per VNet.** Without a `dns:` block azd links the | ||
| three `privatelink.*` AI zones to your VNet, and a VNet may hold only one link | ||
| per namespace. A second account (or a brownfield hub that pre-links the zones) | ||
| must use `dns:` **reference** mode to bind the private endpoint without | ||
| re-linking. | ||
| - **Terraform IaC is not supported for private networking (v1).** Bicep-only | ||
| today; `azd ai agent init --infra=terraform` is refused when `network:` is | ||
| declared. Eject Bicep instead (see *Scenario 3 — Eject and customize the | ||
| Bicep*). | ||
|
|
||
| ### Scenario 1 — Managed egress: private account, agent on Microsoft's network | ||
|
|
||
| Omit `agentSubnet` so the hosted-agent runtime uses a Microsoft-managed network | ||
| instead of your VNet. `peSubnet` is still required: the account data plane stays | ||
| private behind an account private endpoint in your VNet, reachable from inside | ||
| the VNet / VPN. | ||
|
|
||
| Scaffold the agent with a pre-built (BYO) image (writes `azure.yaml` and | ||
| `agent.yaml`): | ||
|
|
||
| ```bash | ||
| azd ai agent init --no-prompt --agent-name my-agent \ | ||
| --image myprivacr.azurecr.io/agents/my-agent:v1 | ||
| ``` | ||
|
|
||
| Then add a `network:` block to the generated service in `azure.yaml` (omit | ||
| `agentSubnet` for managed egress; `isolationMode` is valid only in this mode): | ||
|
|
||
| ```yaml | ||
| name: my-agent | ||
| infra: | ||
| provider: microsoft.foundry | ||
|
|
||
| services: | ||
| my-agent: | ||
| host: azure.ai.agent | ||
| deployments: [] | ||
| network: | ||
| isolationMode: AllowInternetOutbound # managed-egress outbound posture | ||
| peSubnet: | ||
| vnet: ${AZURE_VNET_ID} | ||
| name: pe-subnet | ||
| prefix: 192.168.11.0/24 | ||
| ``` | ||
|
|
||
| `azd ai agent init --image` already created and selected an azd environment and | ||
| set `AZD_AGENT_SKIP_ACR=true` (BYO image → no ACR build). Set the deployment | ||
| inputs on that environment and provision: | ||
|
|
||
| ```bash | ||
| azd env set AZURE_SUBSCRIPTION_ID "<sub>" | ||
| azd env set AZURE_LOCATION westus | ||
| azd env set AZURE_RESOURCE_GROUP "<rg>" | ||
| azd env set AZURE_VNET_ID "<vnet-resource-id>" | ||
| azd provision --no-prompt | ||
| ``` | ||
|
|
||
| Grant the Foundry project MI ACR pull permission, then run deploy/invoke from a | ||
| host that can reach the account private endpoint: | ||
|
|
||
| ```bash | ||
| azd deploy --no-prompt | ||
| azd ai agent invoke --new-session "hello" | ||
| ``` | ||
|
|
||
| > **`isolationMode` note.** When set, azd provisions the account's V2 | ||
| > managed network (`managednetworks/default`) with the chosen isolation mode. | ||
| > `AllowOnlyApprovedOutbound` additionally requires approved outbound rules for | ||
| > the agent to reach dependent resources; for the platform-managed stores used | ||
| > here those are managed by the Foundry platform. | ||
|
|
||
| ### Scenario 2 — BYO egress: agent injected into your VNet subnet | ||
|
|
||
| ACR requirements: | ||
|
|
||
| - The BYO image must be pullable by the Foundry **project managed identity**. | ||
| - For ABAC-enabled ACR, grant the project MI `Container Registry Repository Reader`. | ||
| - For private-only ACR, use Premium SKU, an ACR private endpoint, and a | ||
| `privatelink.azurecr.io` DNS zone linked to the VNet. Disable public access | ||
| only after the image is pushed. | ||
|
|
||
| Scaffold the agent with a pre-built (BYO) image — this writes `azure.yaml` and | ||
| `agent.yaml` for you, so there is no hand-edited manifest to keep in sync: | ||
|
|
||
| ```bash | ||
| azd ai agent init --no-prompt --agent-name my-agent \ | ||
| --image myprivacr.azurecr.io/agents/my-agent:v1 | ||
| ``` | ||
|
|
||
| Then add a `network:` block to the generated service in `azure.yaml`: | ||
|
|
||
| ```yaml | ||
| services: | ||
| my-agent: | ||
| host: azure.ai.agent | ||
| network: | ||
| agentSubnet: # omit the whole block for managed egress | ||
| vnet: ${AZURE_VNET_ID} | ||
| name: agent-subnet | ||
| prefix: 192.168.10.0/24 # omit prefix to reference an existing subnet | ||
| peSubnet: # required: makes the data plane private | ||
| vnet: ${AZURE_VNET_ID} | ||
| name: pe-subnet | ||
| prefix: 192.168.11.0/24 | ||
| ``` | ||
|
|
||
| Configure and provision (`init --image` already created/selected the env and set | ||
| `AZD_AGENT_SKIP_ACR=true`): | ||
|
|
||
| ```bash | ||
| azd env set AZURE_SUBSCRIPTION_ID "<sub>" | ||
| azd env set AZURE_LOCATION westus | ||
| azd env set AZURE_RESOURCE_GROUP "<rg>" | ||
| azd env set AZURE_VNET_ID "<vnet-resource-id>" | ||
| azd provision --no-prompt | ||
| ``` | ||
|
|
||
| Deploy and invoke from a host that can reach the Foundry private endpoint: | ||
|
|
||
| ```bash | ||
| azd deploy --no-prompt | ||
| azd ai agent invoke --new-session "hello" | ||
| ``` | ||
|
|
||
| Common failures: | ||
|
|
||
| - `403 Public access is disabled`: the data plane is private in every | ||
| network-bound mode — run deploy/invoke from inside the VNet, a peered VNet, or | ||
| VPN. | ||
| - `ImageError: registry authentication failed`: grant ACR pull permission to the Foundry project MI. | ||
|
|
||
| ### Scenario 3 — Eject and customize the Bicep (advanced) | ||
|
|
||
| The synthesized template covers the common private-networking shapes. When you | ||
| need something it doesn't express — an extra subnet, a private endpoint for a BYO | ||
| dependent store, custom DNS wiring, a non-default account property, additional | ||
| `networkInjections` rules — eject the Bicep, edit it directly, and let azd | ||
| provision your edited tree. | ||
|
|
||
| ```bash | ||
| # 1. Scaffold + declare a network: block in azure.yaml (see Scenarios 1–2 | ||
| # above), then eject the infrastructure: | ||
| azd ai agent init --infra # writes ./infra/ from azure.yaml | ||
| ``` | ||
|
|
||
| Eject writes the **full** Bicep tree from your `network:` block — | ||
| `infra/main.bicep`, `infra/modules/{resources,network,subnet,private-endpoint-dns,acr}.bicep`, | ||
| and `infra/main.parameters.json` — and **preserves `${VAR}` placeholders** | ||
| (resolved from the azd environment at provision time). `azure.yaml` is left | ||
| unchanged: `infra.provider` stays `microsoft.foundry`. | ||
|
|
||
| ```bash | ||
| # 2. Edit the ejected Bicep to taste. Two worked examples: | ||
|
|
||
| # (a) infra/modules/network.bicep — add a subnet the network: schema can't | ||
| # express (e.g. for a future dependent-store private endpoint). Pick a CIDR | ||
| # free in your VNet space. Use the '<vnet>/<subnet>' name form (vnet is an | ||
| # existing resource in a possibly different RG): | ||
| # | ||
| # resource extraStoreSubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { | ||
| # name: '${vnetName}/byo-store-pe-subnet' | ||
| # properties: { | ||
| # addressPrefix: '192.168.30.0/24' | ||
| # privateEndpointNetworkPolicies: 'Disabled' | ||
| # } | ||
| # dependsOn: [ peSubnet ] | ||
| # } | ||
|
|
||
| # (b) infra/modules/resources.bicep — set an extra account property directly on | ||
| # the foundryAccount resource, e.g. merge a tag: | ||
| # | ||
| # tags: union(tags, { editedByPowerUser: 'true' }) | ||
| ``` | ||
|
|
||
| ```bash | ||
| # 3. Provision the edited tree. azd detects ./infra/main.bicep and compiles it | ||
| # instead of synthesizing from azure.yaml: | ||
| azd env set AZURE_SUBSCRIPTION_ID "<sub>" | ||
| azd env set AZURE_LOCATION westus | ||
| azd env set AZURE_RESOURCE_GROUP "<rg>" | ||
| azd env set AZURE_VNET_ID "<vnet-resource-id>" | ||
| azd provision --no-prompt | ||
|
|
||
| # 4. Deploy and invoke from a host with line-of-sight to the account private | ||
| # endpoint (inside the VNet, a peered VNet, or VPN): | ||
| azd deploy --no-prompt | ||
| azd ai agent invoke --new-session "hello" | ||
| ``` | ||
|
|
||
| **How it works.** Once `./infra/main.bicep` exists, azd provisions it directly | ||
| and **stops synthesizing from `azure.yaml`** — your edited Bicep is now the | ||
| source of truth. Your `main.parameters.json` values are layered over azd's | ||
| host-derived parameters (subscription, location, resource group, project name, | ||
| `principalId`); you win on keys you set, and azd fills in the rest. | ||
|
|
||
| **Notes.** | ||
|
|
||
| - Re-running `azd ai agent init --infra` is **refused** while `./infra/` exists, | ||
| so your edits are never overwritten — delete `./infra/` to regenerate from | ||
| `azure.yaml`. | ||
| - After ejecting, further `network:` edits in `azure.yaml` have **no effect**; | ||
| change the Bicep directly. | ||
| - `infra/main.arm.json` is intentionally not ejected (it would go stale the | ||
| moment you edit `main.bicep`); azd compiles `main.bicep` on each provision. | ||
| - **Terraform is not supported for private networking.** | ||
| `azd ai agent init --infra=terraform` is refused for a service that declares | ||
| `network:` (the Terraform module has no VNet / private-endpoint / DNS | ||
| resources, so ejecting it would silently provision a public account). Use | ||
| `--infra` (Bicep) and customize as above. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.