|
| 1 | +# Pull Request and Pull Request Target Workflow Templates |
| 2 | + |
| 3 | +Templates for safely handling internal and external fork pull requests in GitHub Actions workflows that require secrets, following Bitwarden security patterns. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +Different strategies are needed for internal vs. fork PRs when workflows require secrets: |
| 8 | + |
| 9 | +- **Internal PRs**: Use `pull_request` trigger (runs with limited permissions, no secrets needed) |
| 10 | +- **Fork PRs**: Use `pull_request_target` (runs in target repo context with approval gate for secret access) |
| 11 | + |
| 12 | +## The Two-Workflow Pattern |
| 13 | + |
| 14 | +### `example-workflow.yml` - Main Workflow |
| 15 | + |
| 16 | +Contains actual job logic (build, test, deploy, etc.). Triggered by `pull_request` for internal PRs and `workflow_call` for reuse. Skips fork PRs via condition: `if: github.event.pull_request.head.repo.full_name == github.repository` |
| 17 | + |
| 18 | +### `example-workflow-target.yml` - Fork Handler |
| 19 | + |
| 20 | +Safely handles fork PRs needing secrets. Triggered by `pull_request_target` (targets default branch only). Runs `check-run` security gate first, then calls main workflow with `secrets: inherit`. Only executes for forks via condition: `if: github.event.pull_request.head.repo.full_name != github.repository` |
| 21 | + |
| 22 | +## Security Model |
| 23 | + |
| 24 | +**The Problem**: Fork PRs can't access secrets with `pull_request`, but `pull_request_target` runs untrusted code in your repo's context with secret access. |
| 25 | + |
| 26 | +**The Solution**: Two-phase approval process: |
| 27 | +1. `pull_request_target` workflow runs immediately for fork PRs |
| 28 | +2. `check-run` job fails (fork user lacks write permissions) |
| 29 | +3. Maintainer reviews code for malicious behavior |
| 30 | +4. Maintainer re-runs workflow (passes check-run with maintainer identity, grants secret access) |
| 31 | + |
| 32 | +**Protection Layers**: |
| 33 | +- Permission check validates write access |
| 34 | +- Conditional execution separates internal/fork paths |
| 35 | +- Mandatory code review before secret access |
| 36 | +- Workflow linter enforces `pull_request_target` targets default branch only and workflows include `check-run` |
| 37 | + |
| 38 | +## ⚠️ Critical Security Warning |
| 39 | + |
| 40 | +**ALWAYS review fork PR code BEFORE re-running failed workflows.** Once re-run, workflows access repository secrets and can exfiltrate data. |
| 41 | + |
| 42 | +**Check for**: |
| 43 | +- Uploads to external services |
| 44 | +- Unexpected network requests |
| 45 | +- Base64 encoding/obfuscation |
| 46 | +- Secrets exposed in artifacts or logs |
| 47 | + |
| 48 | +## Approval Process |
| 49 | + |
| 50 | +**For `pull_request` (no secrets)**: Click "Approve and run" to unblock workflow execution. |
| 51 | + |
| 52 | +**For `pull_request_target` (with secrets)**: |
| 53 | +1. Workflow runs immediately, `check-run` fails (expected) |
| 54 | +2. Review PR code thoroughly |
| 55 | +3. Click "Re-run failed jobs" (grants secret access via maintainer identity) |
| 56 | + |
| 57 | +## Usage Instructions |
| 58 | + |
| 59 | +1. **Copy templates** to `.github/workflows/` and rename appropriately |
| 60 | +2. **Customize main workflow**: Update name, paths, jobs, secrets, and inputs |
| 61 | +3. **Customize target workflow**: Update name, workflow reference in `uses:`, paths, permissions, and inputs |
| 62 | +4. **Test both scenarios**: Create internal PR (should trigger main workflow) and fork PR (should trigger target workflow with failed check-run) |
| 63 | + |
| 64 | +## Best Practices |
| 65 | + |
| 66 | +**DO**: |
| 67 | +- Use `check-run` job first in `pull_request_target` workflows |
| 68 | +- Review fork code before approving |
| 69 | +- Set minimal permissions per job |
| 70 | +- Pin actions to commit hashes |
| 71 | +- Use conditions to separate internal/fork execution |
| 72 | + |
| 73 | +**DON'T**: |
| 74 | +- Skip `check-run` security gate |
| 75 | +- Use `pull_request_target` unless secrets are required |
| 76 | +- Grant unnecessary permissions |
| 77 | +- Expose secrets in logs/outputs |
| 78 | +- Run both workflows for same PR |
| 79 | + |
| 80 | +## Troubleshooting |
| 81 | + |
| 82 | +**Both workflows running for internal PR**: Check conditional expressions match template patterns. |
| 83 | + |
| 84 | +**"Check PR run" failed (EXPECTED)**: This is the security gate working. Review code, then re-run to grant secret access. |
| 85 | + |
| 86 | +**Secrets unavailable in fork PR**: Ensure using `pull_request_target` pattern with `secrets: inherit`. |
| 87 | + |
| 88 | +**Workflow linter failing**: Verify `pull_request_target` targets default branch only. |
| 89 | + |
| 90 | +**Compliance**: Templates comply with all Bitwarden workflow-linter rules (pinned runners/actions, explicit permissions, proper naming, `pull_request_target` restrictions). |
| 91 | + |
| 92 | +## References |
| 93 | + |
| 94 | +- [GitHub Actions: pull_request vs pull_request_target](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) |
| 95 | +- [Bitwarden Workflow Linter Rules](https://github.com/bitwarden/workflow-linter/tree/main/src/bitwarden_workflow_linter/rules) |
| 96 | +- [Bitwarden Clients Example](https://github.com/bitwarden/clients/blob/main/.github/workflows/build-web-target.yml) |
| 97 | +- [Keeping your GitHub Actions secure](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) |
0 commit comments