# Network Isolation, Allowed Domains & Permissions _Part of the [ado-aw documentation](../AGENTS.md)._ ## Network Isolation (AWF) Network isolation is provided by AWF (Agentic Workflow Firewall), which provides L7 (HTTP/HTTPS) egress control using Squid proxy and Docker containers. AWF restricts network access to an allowlist of approved domains. Generated pipelines run AWF v0.27.32+ in **strict topology mode**: both the Agent and Detection jobs invoke AWF rootlessly with an explicit `--network-isolation` flag — there is no `sudo`, `--enable-host-access`, or `--legacy-security` fallback, and no author-facing knob to opt back into the legacy topology. The Agent additionally passes `--topology-attach awmg-mcpg` so the trusted MCPG container is attached to AWF's internal `awf-net`, and appends that hostname to `NO_PROXY`/`no_proxy` so MCP traffic bypasses Squid; Detection has no MCPG attachment. See [`docs/mcpg.md`](mcpg.md) for the MCPG topology and [`docs/mcp.md`](mcp.md) for MCP server configuration. The `ado-aw` compiler binary is distributed via [GitHub Releases](https://github.com/githubnext/ado-aw/releases) with SHA256 checksum verification. The AWF binary is distributed via [GitHub Releases](https://github.com/github/gh-aw-firewall/releases) with SHA256 checksum verification. Docker is sourced via the `DockerInstaller@0` ADO task. ## Default Allowed Domains The following domains are always allowed via `CORE_ALLOWED_HOSTS` in `allowed_hosts.rs`. `host.docker.internal` is deliberately **not** on this list — the agent has no route to the host under AWF's strict network topology, and SafeOutputs no longer runs as a host-side process at all: MCPG spawns it as a hardened, network-isolated (`--network none`) sibling stdio container; see [`docs/mcpg.md`](mcpg.md): | Host Pattern | Purpose | |-------------|---------| | `dev.azure.com`, `*.dev.azure.com` | Azure DevOps | | `vstoken.dev.azure.com` | Azure DevOps tokens | | `vssps.dev.azure.com` | Azure DevOps identity | | `*.visualstudio.com` | Visual Studio services | | `*.vsassets.io` | Visual Studio assets | | `*.vsblob.visualstudio.com` | Visual Studio blob storage | | `*.vssps.visualstudio.com` | Visual Studio identity | | `pkgs.dev.azure.com`, `*.pkgs.dev.azure.com` | Azure DevOps Artifacts/NuGet | | `aex.dev.azure.com`, `aexus.dev.azure.com` | Azure DevOps CDN | | `vsrm.dev.azure.com`, `*.vsrm.dev.azure.com` | Visual Studio Release Management | | `github.com` | GitHub main site | | `api.github.com` | GitHub API | | `*.githubusercontent.com` | GitHub raw content | | `*.github.com` | GitHub services | | `*.copilot.github.com` | GitHub Copilot | | `*.githubcopilot.com` | GitHub Copilot | | `copilot-proxy.githubusercontent.com` | GitHub Copilot proxy | | `login.microsoftonline.com` | Microsoft identity (OAuth) | | `login.live.com` | Microsoft account authentication | | `login.windows.net` | Azure AD authentication | | `*.msauth.net`, `*.msftauth.net` | Microsoft authentication assets | | `*.msauthimages.net` | Microsoft authentication images | | `graph.microsoft.com` | Microsoft Graph API | | `management.azure.com` | Azure Resource Manager | | `*.blob.core.windows.net` | Azure Blob storage | | `*.table.core.windows.net` | Azure Table storage | | `*.queue.core.windows.net` | Azure Queue storage | | `*.applicationinsights.azure.com` | Application Insights telemetry | | `*.in.applicationinsights.azure.com` | Application Insights ingestion | | `dc.services.visualstudio.com` | Visual Studio telemetry | | `rt.services.visualstudio.com` | Visual Studio runtime telemetry | | `config.edge.skype.com` | Configuration | When `permissions.read` enables credential-isolated Azure DevOps reads, the Azure CLI extension additionally contributes `aka.ms` (Microsoft's link shortener, used by `az` subcommand metadata). See [Proxy-gated Azure CLI (`az`)](#proxy-gated-azure-cli-az) below. ## Proxy-gated Azure CLI (`az`) `permissions.read` is both the trusted `ado-proxy` token source and the activation gate for wrapped `az`. With no read permission, the compiler emits no Azure CLI detection, mount, PATH entry, shell permission, prompt, or proxy topology. The pinned AWF agent image contains no built-in `az`, so the command is absent rather than available unproxied. When `permissions.read` is present, the compiler emits a small *Detect Azure CLI on host* prepare step early in the Agent job. If the runner has Azure CLI, the real binary is mounted only behind the generated wrapper and running proxy. The extension also adds `aka.ms`; its other declared hosts already exist in `CORE_ALLOWED_HOSTS`. ### Runtime detection and graceful degradation Because `azure-cli` is not universally pre-installed on every ADO runner image (notably some 1ES self-hosted pools), the compiler does **not** declare static AWF bind-mounts for `/opt/az` and `/usr/bin/az`. Static mounts would cause `docker run` to fail with "bind source path does not exist" on runners without `az`, breaking the pipeline before the agent ever started. Instead, the prepare step does the detection itself at pipeline time: * If both `/usr/bin/az` (the launcher shim) and `/opt/az` (the Python venv that `az` actually runs in) exist on the host, the step sets the ADO pipeline variable `AW_AZ_MOUNTS=--mount /opt/az:/opt/az:ro --mount /usr/bin/az:/usr/bin/az:ro` via `##vso[task.setvariable]`. * If either is missing, the step emits a `##vso[task.logissue type=warning]` explaining `az` won't be available inside the agent sandbox and sets `AW_AZ_MOUNTS` to the *empty string* (also via `##vso[task.setvariable]` — leaving the variable undefined would make ADO render the literal `$(AW_AZ_MOUNTS)` in the AWF bash step, where bash would interpret it as a `$(...)` command substitution and kill the step under `set -e`). The AWF invocation in the compiled YAML then includes a literal `$(AW_AZ_MOUNTS) \` line on its own in the `--mount` chain. At step start, ADO interpolates that pipeline variable into the bash script: when az is present the two `--mount` args appear; when it's absent the line collapses to empty whitespace + the `\` continuation, which is a no-op. ### Agent prompt advisory (conditional) When (and only when) `AW_AZ_MOUNTS` is non-empty, a follow-up *Append Azure CLI prompt* step appends an Azure CLI advisory section to `/tmp/awf-tools/agent-prompt.md`. The agent reads the prompt on startup and learns that `az` is on PATH, what it's good for (catalogued Azure DevOps reads through `az devops`, `az repos`, `az pipelines`, `az boards`, and `az rest`), what is deliberately unavailable (writes, secrets, ARM, and Graph), and the fallback path (`missing-tool` safe output naming `azure-cli`). The advisory tells the agent not to sign in: the wrapper carries only a sentinel and the proxy owns the real credential. The step is gated by `condition: ne(variables['AW_AZ_MOUNTS'], '')`, which reuses the same pipeline variable the detection step writes. On runners where `az` is missing, the advisory step is skipped entirely — the agent never sees Azure CLI guidance and never tries to call `az`, avoiding the "told to use `az`, fails with command not found" failure mode. ### Operator implications - **Microsoft-hosted `ubuntu-latest`**: `az` is detected, mounted, and available inside the agent sandbox. Nothing to do. - **1ES self-hosted runners *with* azure-cli baked in**: same as above. - **1ES self-hosted runners *without* azure-cli**: the pipeline runs successfully, but agents that invoke `az` get the standard `command not found` inside the sandbox. The warning emitted by the prepare step is visible in the ADO log as a yellow-flagged issue on the build summary; treat it as a signal to either ignore (if no agent on that runner needs `az`) or to install `azure-cli` on the runner image. See [`docs/tools.md`](tools.md#built-in-clis) for the agent-facing contract (auth scope, available subcommands). ## Adding Additional Hosts Agents can specify additional allowed hosts in their front matter using either ecosystem identifiers or raw domain patterns: ```yaml network: allowed: - python # Ecosystem identifier — expands to Python/PyPI domains - rust # Ecosystem identifier — expands to Rust/crates.io domains - "*.mycompany.com" # Raw domain pattern - "api.external-service.com" # Raw domain ``` ### Ecosystem Identifiers Ecosystem identifiers are shorthand names that expand to curated domain lists for common language ecosystems and services. The domain lists are sourced from [gh-aw](https://github.com/github/gh-aw) and kept up to date via an automated workflow. Available ecosystem identifiers include: | Identifier | Includes | |------------|----------| | `defaults` | Certificate infrastructure, Ubuntu mirrors, common package registries | | `github` | GitHub domains (`github.com`, `*.githubusercontent.com`, etc.) | | `gh-aw` | GitHub domains needed for AWF/gh-aw binary downloads (`codeload.github.com`, `raw.githubusercontent.com`, etc.) | | `local` | Loopback addresses (`localhost`, `127.0.0.1`, `::1`) | | `containers` | Docker Hub, GHCR, Quay, Kubernetes | | `linux-distros` | Debian, Alpine, Fedora, CentOS, Arch Linux package repositories | | `dev-tools` | CI/CD and developer tool services (Codecov, Shields.io, Snyk, etc.) | | `python` | PyPI, pip, Conda, Anaconda | | `rust` | crates.io, rustup, static.rust-lang.org | | `node` | npm, Yarn, pnpm, Bun, Deno, Node.js | | `go` | proxy.golang.org, pkg.go.dev, Go module proxy | | `java` | Maven Central, Gradle, JDK downloads | | `dotnet` | NuGet, .NET SDK | | `ruby` | RubyGems, Bundler | | `swift` | Swift.org, CocoaPods | | `terraform` | HashiCorp releases, Terraform registry | | `threat-detection` | Copilot API and telemetry domains used by the Detection stage | **Compound identifier** (expands to a union of component identifiers): | Identifier | Expands to | |------------|------------| | `default-safe-outputs` | `defaults` + `dev-tools` + `github` + `local` — the standard set of domains needed for most safe-output execution scenarios | Additional ecosystems: `bazel`, `chrome`, `clojure`, `dart`, `deno`, `elixir`, `fonts`, `github-actions`, `haskell`, `julia`, `kotlin`, `latex`, `lean`, `lua`, `node-cdns`, `ocaml`, `perl`, `php`, `playwright`, `powershell`, `python-native`, `r`, `scala`, `zig`. The full domain lists for direct identifiers are defined in `src/data/ecosystem_domains.json`. Compound identifiers are defined in `src/ecosystem_domains.rs`. All hosts (core + MCP-specific + ecosystem expansions + user-specified) are combined into a comma-separated domain list passed to AWF's `--allow-domains` flag. ### Blocking Hosts The `network.blocked` field removes hosts from the combined allowlist. Both ecosystem identifiers and raw domain strings are supported. Blocking an ecosystem identifier removes all of its domains. Blocking a raw domain uses exact-string matching — blocking `"github.com"` does **not** also remove `"*.github.com"`. ```yaml network: allowed: - python - node blocked: - python # Remove all Python ecosystem domains - "github.com" # Remove exact domain - "*.github.com" # Remove wildcard variant too ``` ## Repository resource endpoints Azure DevOps repository resources backed by an external service connection must declare `endpoint:`. ado-aw validates this at compile time so the generated YAML does not fail later in Azure Pipelines. | Repository `type` | `endpoint:` required? | Notes | |-------------------|-----------------------|-------| | `git` | No | Same-organization Azure Repos checkout using the build's OAuth token. | | `github` | Yes | Azure DevOps GitHub service connection. | | `githubenterprise` | Yes | Azure DevOps GitHub Enterprise service connection. | | `bitbucket` | Yes | Azure DevOps Bitbucket service connection. | ```yaml repos: - name: octo/shared-components alias: shared-components type: github endpoint: github-shared-components ref: refs/heads/main checkout: false ``` ### Template targets (`target: job` / `target: stage`) `target: job` and `target: stage` emit Azure DevOps templates, and templates cannot declare top-level `resources.repositories`. Ordinary `repos:` entries therefore remain the parent pipeline's responsibility. Standalone and 1ES targets own their top-level resources, so ado-aw emits the repository resource directly. Reusable markdown imports are resolved entirely at compile time and do not create runtime repository resources or use ADO service-connection endpoints. See [`imports:`](imports.md) for the ADO-first compile-time `repository` and `source` syntax. ## Permissions (ADO Access Tokens) The ARM service-connection scope does not determine what its identity may do in Azure DevOps. `permissions.read` and `permissions.write` describe intended pipeline roles and token placement; operators must separately grant each underlying identity the minimum Azure DevOps permissions. The executor (Stage 3) always has a write-capable token; what changes is its *source* and *attribution*: | Source | When | Identity | | ----------------------------------- | --------------------------------------------- | ----------------------------------------------- | | `$(System.AccessToken)` *(default)* | No `permissions.write` configured | `Project Collection Build Service (org)` | | `$(SC_WRITE_TOKEN)` *(opt-in)* | `permissions.write: ` | The federated identity behind the ARM SC | The agent (Stage 1) never receives the executor's token. Stage separation — not token type — is the trust boundary. **`System.AccessToken` exceptions.** Two other steps also map `System.AccessToken`: 1. **Setup-job trigger filter gate** — self-cancels the build when filters don't match (`PATCH _apis/build/builds/{id}`) and fetches PR metadata for Tier 2 filters (labels, draft status, changed files). Runs before the agent, outside the AWF sandbox. 2. **Stage 3 executor** — when no ARM write SC is configured (the default), the executor's `SYSTEM_ACCESSTOKEN` env var is sourced from `$(System.AccessToken)`. Both require the pipeline setting "Allow scripts to access the OAuth token" to be enabled (the ADO default). `System.AccessToken` is scoped by the pipeline's **"Limit job authorization scope to current project"** toggle. With this on (strongly recommended), writes are limited to the pipeline's host project. Operators can scope further per-pipeline by editing the build definition's *Run-time settings*. ```yaml permissions: read: my-read-arm-connection # trusted ado-proxy token source # write: my-write-arm-connection # Optional — see below ``` ### When to set `permissions.write` The default (`$(System.AccessToken)`) is sufficient for the vast majority of agents. Set `permissions.write` only when you need: 1. **Cross-org or cross-project writes** — `System.AccessToken` is scoped to the host project. Targeting work items or repos in a different ADO project / organization requires an ARM SC with broader scope. 2. **Named-identity attribution** — `System.AccessToken` writes are attributed to the `Project Collection Build Service` identity. An ARM SC attributes writes to its underlying federated identity (e.g. `safe-output-bot@contoso.com`), useful when audit logs or work-item notifications need a specific actor. ### Security Model - **`permissions.read`**: Mints an ADO-audience token for the trusted `ado-proxy` process when `tools.azure-devops` is enabled. The raw token is not injected into the Agent, Azure CLI, MCPG, or Azure DevOps MCP container. The proxy attaches it only after a deny-by-default catalog and scope check. The scalar form enables all read capabilities for the implicit current organization/project/repository scope: ```yaml permissions: read: my-read-sc ``` The **object form** narrows capabilities and adds explicit scopes: ```yaml permissions: read: service-connection: my-read-sc capabilities: [core, repos] # discovery is always on allow: # beyond the current org/project/repo - organization: other-org projects: - project: Other Project project-id: 33333333-3333-3333-3333-333333333333 # optional repositories: [other-repo] # omit for project-scoped reads only ``` `allow:` is additive to the current scope. It is organization-relative: a project granted in one organization does not match the same project name in another. `project-id` is optional; without it, name-form calls work and a client using a cached GUID fails closed. Azure Repos `type: git` entries under `repos:` also grant API reads for that repository, including `checkout: false`. This is repository-only: declaring `Project/repo` does **not** grant work-item, build, or pipeline reads for `Project`. Non-ADO repository types grant nothing. Cross-organization reads use the same ADO-audience token and therefore work only where the service-connection identity has access in the same AAD tenant. Cross-tenant reads require another credential and are unsupported. An organization entry with no `projects` is rejected because omission must never grant an entire organization. - **`permissions.write` (optional)**: Mints a write-capable ADO-scoped token used **only** by the executor in Stage 3 (`SafeOutputs` job). Overrides the default `$(System.AccessToken)` for write operations. Never exposed to the agent. - **Both omitted**: The agent has no ADO API access. The executor still has a write-capable token via `$(System.AccessToken)`, scoped by the pipeline's job-authorization settings. ### Examples ```yaml # Scoped MCP and wrapped-az reads work through ado-proxy; executor writes via $(System.AccessToken). permissions: read: my-read-sc # Cross-org / named-identity attribution — executor writes via ARM SC. permissions: read: my-read-sc write: my-write-sc # Agent has no ADO read access; executor still writes via $(System.AccessToken). # (Empty front matter — no `permissions:` key at all.) ```