Skip to content

Latest commit

 

History

History
440 lines (358 loc) · 21.8 KB

network.md

File metadata and controls

440 lines (358 loc) · 21.8 KB

Network Isolation, Allowed Domains & Permissions

Part of the ado-aw documentation.

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; when the credential-isolated ado-proxy sidecar is enabled (permissions.read is configured), the Agent passes a second --topology-attach awmg-ado-proxy and adds that hostname to NO_PROXY/no_proxy too. Detection has no MCPG/ado-proxy attachment. See docs/mcpg.md for the MCPG topology, docs/mcp.md for MCP server configuration, and docs/ado-proxy-design.md for the ado-proxy sidecar.

The ado-aw compiler binary is distributed via GitHub Releases with SHA256 checksum verification. The AWF binary is distributed via GitHub 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:

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) 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 for the agent-facing contract (auth scope, available subcommands).

Renewable Azure authentication for MCP servers

mcp-servers.<name>.azure-auth is a trusted-infrastructure credential path for containerized stdio MCP servers. It is separate from the agent-facing Azure CLI wrapper described above:

  • AzureCLI@3 receives an ARM workload-identity service connection and exposes the initial federated assertion only to its trusted setup script.
  • System.AccessToken and the initial assertion are streamed to a dedicated refresh sidecar over a one-shot FIFO; neither is stored in Docker environment, command arguments, generated YAML, or a host credential file.
  • The sidecar keeps the ADO request credential in memory and writes only the renewable federated assertion beneath $(Agent.TempDirectory).
  • MCPG mounts the assertion's token-only directory read-only into the configured MCP container, so atomic file replacement is visible without exposing sidecar status or material channels.
  • The AWF agent receives no credential mount, no identity environment variables from azure-auth, and no route to the refresher container. The compiler excludes its internal client/tenant ID variables from both Agent and Detection environment passthrough.

The host auth root and per-server directory use mode 0700. Only the token-only directory is 0755, with assertion files 0644, so an MCP container running under a different UID can read its read-only mount. Unrelated unprivileged host users cannot traverse the private parent directories. These permissions do not isolate processes sharing the runner UID or host root; agent isolation depends on keeping the auth directory out of AWF's filesystem mounts, including its workspace and /host aliases.

The credential directory must not move to runner /tmp: AWF mounts runner /tmp into the agent chroot, making files there agent-readable. See docs/mcp.md for configuration.

Adding Additional Hosts

Agents can specify additional allowed hosts in their front matter using either ecosystem identifiers or raw domain patterns:

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 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
copilot-vendor Copilot vendor API/telemetry domains (api.business.githubcopilot.com, api.enterprise.githubcopilot.com, api.individual.githubcopilot.com, telemetry.enterprise.githubcopilot.com)

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, copilot-vendor, 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".

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.
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: for the ADO-first compile-time repository and source syntax.

Permissions (ADO Access Tokens)

The service-connection resource 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) Any permissions.write service connection The configured connection's Entra identity

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 write service connection 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.

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 may use an Azure Resource Manager connection. Cross-organization repository writes require an Azure DevOps connection and explicit allow scope.
  2. Named-identity attribution — System.AccessToken writes are attributed to the Project Collection Build Service identity. A service connection 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:

    permissions:
      read: my-read-sc

    The object form narrows capabilities and adds explicit scopes:

    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.

    Scalar form preserves the Azure Resource Manager connection contract:

    permissions:
      write: my-arm-connection

    Expanded form selects AzureCLI@3's connection type and adds exact cross-organization repository scopes:

    permissions:
      write:
        service-connection: ado-repository-writer
        connection-type: azureDevOps
        allow:
          - organization: other-org
            projects:
              - project: Other Project
                repositories: [target-repo]

    connection-type is either azureRM or azureDevOps. Only azureDevOps permits cross-organization repository writes. The Azure DevOps service connection must be backed by Entra workload identity federation; its identity must belong to the same tenant, be added to every target organization, and receive repository ACLs there. allow is additive to the implicit current organization and is deny-by-default for cross-org targets.

  • 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

# Scoped MCP and wrapped-az reads work through ado-proxy; executor writes via $(System.AccessToken).
permissions:
  read: my-read-sc

# Named-identity attribution through an Azure Resource Manager connection.
permissions:
  read: my-read-sc
  write: my-write-sc

# Cross-org repository writes through an Azure DevOps WIF connection.
permissions:
  write:
    service-connection: ado-repository-writer
    connection-type: azureDevOps
    allow:
      - organization: other-org
        projects:
          - project: Other Project
            repositories: [target-repo]

# Agent has no ADO read access; executor still writes via $(System.AccessToken).
# (Empty front matter — no `permissions:` key at all.)