From 0810451e2da8488d5fc3bc1dbdc7591be5a0ef19 Mon Sep 17 00:00:00 2001 From: Pawel Winogrodzki Date: Thu, 23 Apr 2026 23:21:27 +0000 Subject: [PATCH] docs: add policy for ADO PR checks --- .../instructions/ado-pipeline.instructions.md | 4 + .../ado-pr-check-fork-policy.instructions.md | 152 +++++++ docs/ado-pipelines-fork-pr-security.md | 384 ++++++++++++++++++ 3 files changed, 540 insertions(+) create mode 100644 .github/instructions/ado-pr-check-fork-policy.instructions.md create mode 100644 docs/ado-pipelines-fork-pr-security.md diff --git a/.github/instructions/ado-pipeline.instructions.md b/.github/instructions/ado-pipeline.instructions.md index 32b7448db54..63065a91ffd 100644 --- a/.github/instructions/ado-pipeline.instructions.md +++ b/.github/instructions/ado-pipeline.instructions.md @@ -177,6 +177,10 @@ Python scripts are easier to test locally, easier to review, and avoid the foot- ## Security hardening +> Fork-PR builds for ADO pipelines with secrets access are governed by a +> separate policy: [`ado-pr-check-fork-policy.instructions.md`](ado-pr-check-fork-policy.instructions.md). +> Long-form rationale lives in [`docs/ado-pipelines-fork-pr-security.md`](../../docs/ado-pipelines-fork-pr-security.md). + Apply all of these unless there is a documented reason not to: - **`set -euo pipefail`** at the top of every non-trivial bash block. diff --git a/.github/instructions/ado-pr-check-fork-policy.instructions.md b/.github/instructions/ado-pr-check-fork-policy.instructions.md new file mode 100644 index 00000000000..fd4029c5b00 --- /dev/null +++ b/.github/instructions/ado-pr-check-fork-policy.instructions.md @@ -0,0 +1,152 @@ +--- +applyTo: ".github/workflows/ado/*.yml,.github/workflows/ado/templates/*.yml,.github/workflows/scripts/**" +description: "Policy: GitHub PR fork builds for ADO pipelines under .github/workflows/ado/. Defines what is allowed, what is forbidden, and how to enforce it. Long-form rationale lives in docs/ado-pipelines-fork-pr-security.md." +--- + +# Policy: GitHub PR fork builds for ADO pipelines + +This policy governs how ADO pipelines under `.github/workflows/ado/` may +be configured with respect to GitHub pull requests originating from +**forks** of this repository. It is the operative rule. Long-form +rationale, threat analysis, and design alternatives live in +[`docs/ado-pipelines-fork-pr-security.md`](../../docs/ado-pipelines-fork-pr-security.md). + +> If anything in this policy conflicts with what the user is asking for, +> **stop and ask** rather than guessing. + +## Scope + +- **In scope:** ADO YAML pipelines under `.github/workflows/ado/` + (wrappers and raw stages templates) and their helper scripts under + `.github/workflows/scripts/`. +- **Out of scope (today):** + - GitHub Actions workflows under `.github/workflows/*.yml`. They have + a different security model and are not addressed here. + - ADO pipelines **without secrets access** (see definition below). + Authors of such pipelines should still read the residual-risk + section in + [`docs/ado-pipelines-fork-pr-security.md`](../../docs/ado-pipelines-fork-pr-security.md#residual-risks-of-non-secret-ado-pipelines-on-fork-prs) + before opting in to fork PR builds. + +## Definitions + +- **Fork PR** -- a GitHub pull request whose source branch lives in a + fork of this repository (i.e. not pushed to a branch of the upstream + repository). +- **Upstream PR** -- a pull request whose source branch lives in the + upstream repository itself. +- **Merge queue commit** -- a commit produced by the GitHub merge queue + on a `gh-readonly-queue//pr--` branch. +- **Secrets access** -- an ADO pipeline has secrets access if **any** + of the following are true: + - It uses a service connection (e.g. `azureSubscription:`, + `serviceConnection:` inputs on tasks such as `AzureCLI@2`). + - It binds a variable group via `variables: - group: `. + - It downloads a secure file via `DownloadSecureFile@1` (or similar). + - It runs in an ADO Environment that has approvals or secret-bearing + resources attached. + - It otherwise has any pipeline-level mechanism through which a + credential, token, or sensitive value reaches the running job. + +## Normative rule + +ADO pipelines with secrets access **MUST NOT** be configured to build +fork PRs. + +**Why (brief):** a fork PR can rewrite any file in the PR HEAD, +including pipeline YAML, shell snippets, and helper scripts. If that +code runs in a job bound to a service connection or a secret-bearing +variable group, the attacker gets arbitrary code execution under the +pipeline's identity and can call any API that identity is permitted to +call. The full threat model and the design options that *could* +mitigate it (internal-template pattern in shared vs. isolated ADO +projects) are documented in +[`docs/ado-pipelines-fork-pr-security.md`](../../docs/ado-pipelines-fork-pr-security.md). + +## Hosted agents only + +All ADO pipelines under `.github/workflows/ado/` **MUST** run on +hosted agent pools -- either Microsoft-hosted agents or **1ES Hosted +Pools** (the internal Microsoft hosted-pool offering defined in Azure). +Self-hosted agent pools are forbidden. + +**Why:** hosted pools guarantee a clean worker for each build. This +eliminates the risk of one workload leaking state to another via +caches, modified dotfiles, planted binaries on `PATH`, or modified +language-toolchain caches. The self-hosted-pool poisoning class of +attack documented in +[`docs/ado-pipelines-fork-pr-security.md`](../../docs/ado-pipelines-fork-pr-security.md) does not +apply when this rule is followed. + +## Network class (R0 vs R1) + +When a pipeline executes any PR-derived content (which includes +upstream PRs and merge-queue commits), prefer the **lowest-trust +network class** the workload can run on (e.g. `R0` over `R1`). The +chosen `LinuxHostVersion.Network` value in the wrapper's OneBranch +`featureFlags` is a security-relevant decision and should be +explicitly justified by the wrapper author. Higher-trust network +classes increase the surface available to attacker-controlled code if +any of the other rules are ever bypassed. + +## CODEOWNERS coverage + +`.github/workflows/ado/**` and `.github/workflows/scripts/**` **MUST** +have CODEOWNERS entries that require maintainer review on any change. + +**Why:** the wrapper, the raw stages template, and the helper scripts +are all part of the trust boundary. A merged malicious change to any +of them can weaken every other control in this policy. + +## Enforcement + +To comply with the normative rule, configure the ADO pipeline as +follows: + +1. **GitHub PR trigger settings** (ADO pipeline UI -> Triggers -> Pull + request validation, **not** YAML): + - "Build pull requests from forks of this repository" -> **OFF**. + - "Make secrets available to builds of forks" -> **OFF** (defense + in depth: this must be off even if the previous toggle is also + off, in case the previous toggle is ever flipped by mistake). +2. **GitHub branch policy / merge queue** (GitHub repo settings): + - The pipeline's check is required only on PRs from upstream + branches and on merge-queue commits. +3. **YAML triggers** stay as `trigger: none` / `pr: none` per the + existing [ADO pipeline instructions](ado-pipeline.instructions.md); + PR firing is configured in ADO, not in YAML. + +## Reviewer checklist (apply on every change to an ADO pipeline) + +Tick these off before approving: + +- [ ] Pipeline still runs on a hosted pool (Microsoft-hosted or 1ES + Hosted Pool). No self-hosted pool introduced. +- [ ] The ADO pipeline's GitHub PR trigger does **not** enable fork PR + builds (verify in the ADO UI; YAML cannot guarantee this). +- [ ] If the change adds or modifies a service connection, variable + group, secure file, or secret-bearing environment, the policy + author confirms the pipeline is still upstream-PR-only. +- [ ] PR-derived strings (branch names, commit SHAs, PR numbers, + filenames produced by `git diff`, etc.) are sanitized / + regex-validated before being used in shell, file paths, or HTTP + calls. +- [ ] CODEOWNERS still covers the modified files. +- [ ] Network class (`LinuxHostVersion.Network`) is no higher-trust + than the workload requires. + +## Exceptions + +There are **no standing exceptions** to the normative rule today. A +future exception (i.e. allowing a specific secret-bearing ADO pipeline +to build fork PRs) requires: + +1. Implementations of at minimum the **internal-template + pattern in an isolated ADO project** (Variant B in + [`docs/ado-pipelines-fork-pr-security.md`](../../docs/ado-pipelines-fork-pr-security.md#variant-b--isolated-ado-project)). +2. Discussion and explicit approval by project maintainers. +3. Documentation of the approved exception in this file (or a + sibling). + +Until such an exception is approved and documented, the rule is +absolute. diff --git a/docs/ado-pipelines-fork-pr-security.md b/docs/ado-pipelines-fork-pr-security.md new file mode 100644 index 00000000000..0d119bfd86f --- /dev/null +++ b/docs/ado-pipelines-fork-pr-security.md @@ -0,0 +1,384 @@ +# Securing ADO PR-check pipelines against fork PRs + +This document is the long-form rationale for the project's fork-PR policy +(see [`ado-pr-check-fork-policy.instructions.md`](../.github/instructions/ado-pr-check-fork-policy.instructions.md)). +It explains how an ADO pipeline under [`.github/workflows/ado/`](../.github/workflows/ado/) that has +**secrets access** (any service connection, secret-bearing variable group, +secure file, or environment with secrets) could be made safe to run as a +GitHub PR check / merge queue workload **when the PR may originate from an +attacker-controlled fork** — and why the project today defaults to *not* +running fork PRs against such pipelines at all. + +The doc focuses on a single architectural option — the **internal-template +pattern** — and compares two deployment variants of it (shared vs. isolated +ADO project). It then describes the much simpler alternative of **not +running fork PRs at all** (the path the policy currently codifies) and the +residual risks that fork PRs still pose to ADO pipelines *without* secrets +access (which the policy does **not** restrict today). + +--- + +## Threat model (one paragraph) + +A fork PR can rewrite any file under the PR's HEAD, including pipeline YAML, +shell snippets, `requirements.txt`, and helper scripts. If any of that code +runs in a job that is bound to a real Service Connection (SC) or has a +secret-bearing variable group attached, the attacker gets **arbitrary code +execution under the WIF identity of the SC** and can call any API that +identity is allowed to call. The goal is to make it structurally impossible +for attacker-controlled YAML/scripts to ever execute in the same job that +holds the SC token. + +--- + +## The internal-template pattern (mandatory baseline) + +The trust boundary is moved out of the public GitHub repo and into ADO +configuration + a private ADO-hosted git repo that the attacker cannot +submit PRs against. + +### Required pieces + +1. **Private template repo in ADO** (in whichever ADO project hosts the + pipeline). Contains the *entire* logic of the pipeline's raw stages + template plus any helper scripts it invokes. + + - Branch-protected: no force-push, required reviewers from a small, + trusted owner group. + - Pinned ref (a protected branch or a tag) referenced by the SC's + "Required template" check. + +2. **Wrapper pipeline in the GitHub repo** (the `.yml` under + [`.github/workflows/ado/`](../.github/workflows/ado/)) is reduced to: + + - `trigger: none` / `pr: none` (PR firing is configured in ADO). + - `resources.repositories` for OneBranch governed templates **and** for + the private template repo. + - `extends:` the private template (no parameters, or only ADO-controlled + parameters such as predefined `Build.*` variables). + - No `stages:`, `jobs:`, `steps:`, no `script:`, no `parameters:` that + pass attacker-controlled data into the template. + +3. **Service Connection hardening** (Project Settings → Service Connections + → the relevant SC → "Approvals and checks"): + + - **Required template check** pinned to the private repo + path + ref. + This is evaluated against the *compiled* pipeline YAML at token-issue + time. A fork PR that rewrites the wrapper to extend a different + template will fail the check and never receive a WIF token. + - **Branch control** restricted to the protected base branches of the + GitHub repo (`3.0`, `main`, etc.). Fork PR runs use + `refs/pull/N/merge` — they are rejected before the SC issues a token. + - **Pipeline permissions**: granted only to this one pipeline. + "Open access" must be off. + +4. **Variable group(s)** consumed by the pipeline: same scoping — + pipeline-permissions limited to this one pipeline; "Open access" off. + +5. **Federated credential subject** on the Entra app set exactly to + `sc:////` (no wildcards), so a leaked SC config + cannot be reused elsewhere. + +6. **Template content rules** (these matter as much as the SC config): + + - The template MUST NOT execute any code from the PR HEAD checkout — + no `pip install -r` from a PR-controlled `requirements.txt`, no + `python3 `, no `bash `, no `go install ...@$(...)` + where the version comes from PR data. + - Helper scripts invoked by the template live in the private template + repo, not in the GitHub repo. + - PR-supplied data enters the job only via ADO predefined variables + (`Build.SourceBranch`, `Build.SourceVersion`, + `System.PullRequest.TargetBranch`) and is **regex-validated** before + being used in shell, paths, or HTTP calls: + - SHAs: `^[0-9a-f]{40}$` + - Branch names: `^[A-Za-z0-9._/-]{1,255}$` + - Component names derived from `git diff` filenames: + `^[a-z0-9._+-]{1,128}$` (reject the run on any failure). + - Any non-trivial logic is a Python script in the template repo (per the + [ADO pipeline instructions](../.github/instructions/ado-pipeline.instructions.md)), + not inline bash. + - All tool versions pinned (container image by digest, not floating tag; + package-manager installs pinned; Go / Python / etc. pinned by commit + or version). Only `GovernedTemplates@refs/heads/main` is exempt. + - The `OneBranch.Official.CrossPlat.yml` vs `NonOfficial` choice in the + wrapper must match the SC's environment (production SC → `Official`; + dev/staging SC → `NonOfficial`). + +7. **GitHub-side hygiene** (does not affect SC security but prevents the + wrapper itself from being silently weakened): + + - CODEOWNERS over `.github/workflows/ado/**` and + `.github/workflows/scripts/**` requiring maintainer review. + - Branch protection on the base branches with required reviews. + +### Why this is the strongest option + +The trust boundary is enforced by **ADO at token-issuance time**, against a +template that lives in a repo the attacker has no PR access to. No amount +of YAML rewriting in a fork PR can move that boundary. + +--- + +## Variant A — shared ADO project + +Put the private template repo, the pipeline, the SC, and the variable +group into an existing ADO project that also hosts other pipelines, repos, +SCs, and resources. + +### What is *not* automatically exposed + +With the SC scoped to this pipeline only and "Required template" + branch +control configured, the **direct path** to the Control Tower SC is +identical to the isolated-project case. Other SCs, variable groups, secure +files, and environments in the project are not automatically reachable — +they each have their own pipeline-permissions list. + +### Security flaws / residual exposure + +1. **Project Build Service identity is broadly scoped.** + The pipeline runs as ` Build Service ()`. In a shared + project this identity often has Read (sometimes Contribute) on many + repos, ADO Artifacts feeds, wikis, and work items. A fork PR that + modifies the wrapper to add `checkout:` of another project repo, or a + `DownloadPipelineArtifact@2` of another pipeline's output, can read + anything that identity can read. The "Required template" check stops + the SC token from being issued, but it does NOT stop other tasks in + the wrapper from running under the build identity. +2. **Self-hosted agent pool poisoning.** If the project uses a shared + self-hosted pool, a fork PR build can leave behind poisoned caches, + modified `~/.gitconfig`, planted binaries on `PATH`, or modified Go + module cache, which the next trusted pipeline on that pool will pick + up. This is the single biggest realistic risk in a shared project. +3. **Cross-pipeline artifact access.** Fork build can pull artifacts + produced by other pipelines in the same project (signed binaries, + internal reports, etc.) using the build identity. +4. **Configuration drift.** Pipeline-permissions on SCs and VGs in a busy + shared project drift toward "Open access" through one-click mistakes + over time. More admins = more chances of a misconfigured neighbor that + weakens the posture indirectly (e.g., a neighbor SC compromise that + then targets your pool). +5. **Larger administrative blast radius.** Anyone with Project + Administrator on the shared project can modify SC checks, grant + pipeline permissions, add federated credentials, or change branch + protections on the template repo. The set of people who can weaken + your security equals the (typically much larger) set of project admins. +6. **Audit/forensics noise.** Distinguishing fork-PR build activity from + normal project activity in audit logs is harder. + +### Acceptability conditions + +A shared project is acceptable **only if all** of these hold: + +- The pipeline runs on **Microsoft-hosted agents** (or a self-hosted pool + dedicated to this pipeline only). +- The project Build Service identity has been audited and does not have + access to anything you would mind a fork PR reading. +- Project Administrator membership is small, reviewed, and trusted. +- No SC/VG in the project is set to "Open access". + +If any of these are shaky, prefer Variant B. + +--- + +## Variant B — isolated ADO project + +Create a new ADO project whose **only** purpose is to host this pipeline, +its private template repo, the Control Tower SC, the variable group, and +nothing else. + +### Security flaws / residual exposure + +1. **Wrapper supply chain in the GitHub repo.** The wrapper still lives in + the public repo and can be modified through a merged malicious PR. + CODEOWNERS + branch protection + reviewer discipline are still + required. (This flaw is identical in both variants — it is a property + of the wrapper-in-public-repo design, not of project topology.) +2. **Template repo administration.** Whoever is admin on the isolated + project can disable branch protection on the template repo and push + malicious template content, then trigger a build. Keep the admin set + tiny and audit it. +3. **Federated credential drift.** If someone adds a second federated + credential subject to the Entra app pointing at a different SC, the + isolation is broken. Audit federated credentials on the Entra app + periodically. +4. **One-time cost.** Setup, ownership, and ongoing maintenance of a + dedicated project is more work than reusing an existing one. Not a + security flaw per se, but a real operational tradeoff. + +### What this variant eliminates that the shared one does not + +- Build Service identity has access to essentially nothing — lateral + reads of other repos / feeds / artifacts are structurally impossible. +- No shared self-hosted pool to poison (use hosted agents; if a + self-hosted pool is needed, it has a single consumer). +- Configuration drift on neighboring SCs/VGs cannot affect this + pipeline because there are no neighbors. +- Administrative blast radius is limited to the isolated project's + admin list. +- Audit trail is high-signal: every event in the project relates to + this pipeline. + +### Recommendation + +Variant B is the strongest posture available for an ADO PR-check +pipeline that holds production-adjacent secrets and runs against fork +PRs. Variant A is acceptable only under the conditions listed above. + +--- + +## Side-by-side comparison + +| Risk surface | Shared project (A) | Isolated project (B) | +|---|---|---| +| Direct SC token theft from fork PR | Blocked by SC "Required template" + branch control | Blocked by SC "Required template" + branch control | +| Build identity reads other repos / feeds / artifacts | **Possible** — depends on identity's project-wide permissions | Near-zero — nothing else exists in the project | +| Self-hosted agent pool poisoning | **Possible** if pool is shared with other pipelines | Eliminated (single consumer; use hosted agents) | +| Misconfigured neighbor SC/VG weakens posture | **Possible** — drift, mistakes by other admins | Not applicable — no neighbors | +| Admin blast radius | Project Admins of shared project (often many) | Project Admins of isolated project (small) | +| Wrapper in GitHub repo can be replaced via merged malicious PR | Same risk in both — mitigated by CODEOWNERS + branch protection | Same risk in both — mitigated by CODEOWNERS + branch protection | +| Federated credential subject drift | Same risk in both — mitigated by periodic Entra audit | Same risk in both — mitigated by periodic Entra audit | +| Operational cost | Low (reuse existing project) | Higher one-time setup; small ongoing overhead | + +--- + +## Alternative — do not run fork PRs at all + +**This is the option the project's policy currently codifies for all ADO +pipelines with secrets access** (see +[`ado-pr-check-fork-policy.instructions.md`](../.github/instructions/ado-pr-check-fork-policy.instructions.md)). +If fork PRs are not built against secret-bearing pipelines, the design +simplifies dramatically. + +### What changes + +- No isolated project required. +- No private template repo required. +- No "Required template" check on the SC required (branch control still + recommended as defense in depth). +- The standard wrapper + raw stages template structure under + [`.github/workflows/ado/`](../.github/workflows/ado/) can be used directly, with the standard + hardening from + [`ado-pipeline.instructions.md`](../.github/instructions/ado-pipeline.instructions.md) + (pinned versions, regex-validated PR inputs, secrets via `env:`, + Python over shell, explicit `timeoutInMinutes`, etc.). +- The pipeline runs only for PRs from branches in the upstream repo and + for the merge queue (which by definition operates on commits already + approved into a `gh-readonly-queue/...` branch by maintainers). + +### How to enforce "no fork PRs" + +Two layers, both required: + +1. **ADO pipeline trigger settings** (GitHub PR trigger configuration + on the ADO pipeline, not in YAML): + - Disable "Build pull requests from forks of this repository". + - If forks must be allowed for some other reason, at minimum: + "Make secrets available to builds of forks" = OFF and "Require a + team member's comment before building a fork pull request" = ON. + (Note: the comment gate is a human safeguard; treat it as weaker + than full disabling.) +2. **Branch policy / merge queue configuration** on the GitHub side: + - The check is required only on PRs from upstream branches and on + merge-queue commits. Fork PRs simply do not block merge. + +### Security implications + +- **Eliminated risks:** the entire fork-PR threat model goes away. + Pipeline-injection through PR HEAD is no longer possible because PR + HEAD is never executed against the SC. +- **Residual risks:** + - Insider risk — a maintainer with push access to an upstream branch + can still inject malicious YAML/scripts. Mitigated by CODEOWNERS, + branch protection, and required reviews on + `.github/workflows/ado/**` and `.github/workflows/scripts/**`. + - Merge-queue commits are derived from PR HEAD; they are trusted only + because a maintainer approved the merge. The same CODEOWNERS rule + above is what keeps that trust honest. + - Standard supply-chain hardening (pinned tool versions, internal + feeds, sanitized PR-derived strings used only as data) still + applies because merge-queue commits include the PR's contents. +- **Functional cost:** fork contributors cannot get pre-merge feedback + from this specific check. Maintainers must either (a) push the fork's + branch to the upstream repo to run the check, or (b) rely on the + merge-queue run as the gate. + +### When to choose this option + +Choose "no fork PRs" when: + +- Fork PRs to this repo are rare or not a primary contribution path, + **or** +- The engineering cost of the isolated-project design is not justified + by the value of pre-merge feedback on fork PRs, **or** +- The SC's permissions are sensitive enough that the residual risk of + the internal-template design is still considered too high. + +This is the simplest secure design. It trades contributor convenience +for a much smaller attack surface and lower operational burden, and is +the option the project policy mandates today. + +--- + +## Residual risks of non-secret ADO pipelines on fork PRs + +The project policy restricts only **ADO pipelines with secrets access**. +An ADO pipeline that has *no* service connection, *no* secret-bearing +variable group, *no* secure file, and *no* secret-bearing environment is +out of scope of the policy and may today be configured to build fork PRs. +Authors of such pipelines should still understand the residual risks a +fork PR build poses even without secrets, and weigh them before opting in: + +1. **Parallelism abuse / queue starvation.** A fork PR can trigger + arbitrary builds in the project's hosted-pool quota. A coordinated + attacker can sustain enough fork-PR traffic to delay legitimate + builds (effectively a low-grade denial-of-service against CI). +2. **Internal-network probing.** ADO hosted pools (including 1ES Hosted + Pools) often run on Microsoft-internal network classes (e.g. `R1`) + with reachability to internal services that are not exposed on the + public internet. A fork PR running arbitrary code on such an agent + can probe and fingerprint that internal network, even with no + credentials of its own. +3. **Internal package-feed and registry abuse.** Hosted agents are + typically pre-authenticated to internal Go module proxies, internal + pip indexes, MCR, and similar feeds. A fork PR can issue arbitrary + queries / downloads against these as the agent identity, which may + be logged as legitimate corporate traffic and is hard to attribute. +4. **Cross-pipeline reads via the project Build Service identity.** + The pipeline runs as the project's Build Service account. Anything + that identity can read in the ADO project (other repos in the + project, ADO Artifacts feeds, wikis, work items, downloadable + artifacts of other pipelines) is reachable by a fork PR build that + adds the appropriate tasks to the wrapper. +5. **Log disclosure of internal metadata.** Build logs frequently + include internal hostnames, IP ranges, agent image versions, MCR + tags, and other infrastructure details that an attacker would + otherwise have to guess. Fork PR build logs are visible to the PR + author. +6. **Shared self-hosted pool poisoning.** *Not applicable in this repo* + because the policy mandates hosted pools only — but documented for + completeness: a fork PR running on a shared self-hosted pool can + leave behind poisoned caches, modified `~/.gitconfig`, planted + binaries on `PATH`, or modified Go module cache, which the next + trusted pipeline on that pool will pick up. + +These risks apply *regardless* of whether the pipeline holds secrets. +The project does not policy-restrict non-secret pipelines today, but the +default recommendation for any new non-secret ADO pipeline is still to +run fork PRs only after the author has consciously assessed the items +above and decided the value of pre-merge fork feedback is worth them. + +--- + +## Summary recommendation + +1. **For ADO pipelines with secrets access:** the project policy + mandates the "do not run fork PRs at all" option. See + [`ado-pr-check-fork-policy.instructions.md`](../.github/instructions/ado-pr-check-fork-policy.instructions.md). + Future exceptions, if approved by maintainers, must use the + internal-template pattern in an **isolated ADO project** (Variant B); + Variant A (shared project) is acceptable only under all of the + conditions listed in its section above. +2. **For ADO pipelines without secrets access:** out of scope of the + policy today. Authors should weigh the residual risks above before + enabling fork PR builds; if in doubt, default to upstream-only.