10 KiB
CI Threat Model and Baseline
Date: 2026-07-14 Scope: GitHub Actions, repository security settings, and required checks Status: Baseline only — no security fix is implemented by this document
Security Objective
Pull requests must be able to test contributor code without giving that code repository write access, secrets, identity tokens, or a path into later privileged jobs. Trusted automation must use narrowly scoped permissions and must not silently bypass review.
Threat Actors
- A malicious external contributor opening a fork pull request.
- A compromised contributor, maintainer, bot, action publisher, or dependency.
- A well-intentioned contributor whose workflow or script has unsafe side effects.
- An attacker posting crafted issue or PR comments to trigger automation.
- A compromised upstream action referenced by a mutable tag or branch.
Protected Assets
GITHUB_TOKENwrite permissions and repository contents.- Pull requests, issues, releases, tags, branches, and rulesets.
ANTHROPIC_API_KEY,OPENCODE_API_KEY, and any repository secrets.- OIDC identity available through
id-token: write. - Published npm packages, release artifacts, registry data, and generated documentation.
- Maintainer trust and the integrity of
main.
Trust Boundaries
- Fork pull-request code is untrusted. Repository scripts, package manifests, lockfiles, actions, generated files, and artifacts from the fork are attacker-controlled.
- Default-branch code is trusted only after review. A merge can still introduce compromised automation.
- Third-party actions are external code. Mutable tags and branches can change after review.
- Artifacts and caches cross job boundaries. Data from an untrusted job must never become executable input to a privileged job.
- Comments and workflow inputs are untrusted strings. Authorization checks do not make their content safe for shell interpolation.
- GitHub settings are part of the security boundary. Safe workflow files are insufficient if default token permissions and rulesets remain weak.
Critical Untrusted Execution Path
.github/workflows/validate-registry.yml currently creates a direct privileged path:
- It runs on
pull_request_target(lines 21–29). - It grants
contents: writeandpull-requests: write(lines 42–44). - It checks out the pull request head repository and branch (
lines 78–87). - It runs
bun installusing the contributor-controlled lockfile/package graph (lines 120–124). - It executes contributor-controlled shell and TypeScript files (
lines 126–248). - The same job retains its write-capable token while running this code.
Impact: A malicious fork can attempt arbitrary code execution with a repository write token. Fork-specific logic later in the job does not remove the token or undo earlier execution.
Required remediation direction: Move untrusted validation to a pull_request workflow with contents: read and no secrets. If a pull_request_target workflow remains, it must be metadata-only and must never check out or execute the contributor head.
Workflow Inventory
| Workflow | Trigger | Current privilege or secret | Baseline concern |
|---|---|---|---|
validate-registry.yml |
pull_request_target, manual |
Contents and PR write | Critical: executes fork code with write token |
pr-checks.yml |
Pull request | Implicit defaults | Output names do not match values written; relevant builds are skipped |
installer-checks.yml |
Pull request, push, manual | Implicit defaults | Mutable action-shellcheck@master; final status ignores compatibility/profile failures |
validate-test-suites.yml |
Pull request, push, manual | Implicit defaults; PR comment attempt | No explicit permissions; artifact contents originate from PR code |
update-registry.yml |
Push to main, manual |
Contents write | Direct push to main; validation failure only warns; generated change bypasses PR review |
sync-docs.yml |
Push to main, manual |
Contents, PR, and issues write | Broad workflow-level permissions; creates branches/issues and deletes branches on failure |
post-merge-pr.yml |
Push to main, manual |
Contents and PR write | Broad workflow-level permissions; creates version branches and PRs |
create-release.yml |
Push to main, manual |
Contents write | Tag/release authority; actions are tag-pinned rather than SHA-pinned |
opencode.yml |
Issue comment | OIDC, contents, PR, issues write; Anthropic key | sst/opencode/github@latest is mutable and receives powerful credentials |
evals/run-evaluations.yml |
Schedule, push to main, manual |
OpenCode API key | User-supplied workflow inputs reach a shell command without a strict allowlist |
Confirmed Configuration Baseline
Read-only GitHub API inspection on 2026-07-14 reported:
- Default workflow token permission: write.
- Actions may approve pull-request reviews: enabled.
- Allowed actions: all.
- Full-SHA pinning requirement: disabled.
- Fork workflow approval policy: first-time contributors only.
- Private vulnerability reporting: disabled.
- CodeQL default setup: not configured.
- Dependabot security updates: disabled.
- Secret scanning: enabled.
- Secret scanning push protection: enabled.
- Automatic branch deletion after merge: disabled.
- No classic branch protection rule; an active repository ruleset protects
main.
Active main ruleset
- Pull requests are required.
- Required approving reviews: 0.
- Code-owner review: not required.
- Latest-push approval: not required.
- Conversation resolution: not required.
- Strict up-to-date status checks: disabled.
- Only required check:
validate-and-update. - No bypass actors are configured.
The only required check is the workflow containing the critical pull_request_target vulnerability.
PR Check Correctness
.github/workflows/pr-checks.yml declares:
has-evals: ${{ steps.filter.outputs.evals }}
has-docs: ${{ steps.filter.outputs.docs }}
has-workflows: ${{ steps.filter.outputs.workflows }}
The filter step actually writes has-evals, has-docs, and has-workflows. Therefore dependent jobs can see empty outputs and skip required validation. The workflow builds and validates eval suites but does not run Vitest.
Action Pinning Baseline
Current workflows use mutable references including:
sst/opencode/github@latestludeeus/action-shellcheck@master- Major-version tags such as
actions/checkout@v4,actions/setup-node@v4,actions/github-script@v7,actions/upload-artifact@v4, andoven-sh/setup-bun@v2
Current repository settings neither restrict allowed actions nor require full commit SHA pinning.
Additional High-Risk Findings
Registry writes after validation failure
update-registry.yml validates generated registry state but intentionally does not fail on an invalid registry. It can then commit and push directly to main using [skip ci].
Installer summary can report false success
installer-checks.yml displays compatibility and profile results, but its final FAILED calculation checks only shellcheck, syntax, non-interactive, and end-to-end jobs.
Comment-triggered privileged AI action
opencode.yml limits triggers to repository owners/members, but gives a mutable third-party action an API key, OIDC, and write access to contents, PRs, and issues. A compromised action version would inherit all of them.
Missing governance files
The repository currently has no:
- Root
SECURITY.md. .github/CODEOWNERS..github/dependabot.yml.
Target Security Invariants
The implementation tasks following this baseline must establish:
- Untrusted PR code runs only with read permissions and no secrets.
- Privileged workflows never execute or source untrusted code or artifacts.
- Workflow and job permissions are explicitly declared and minimized.
- External actions are pinned to reviewed full commit SHAs.
- Registry and documentation generators create reviewed PRs instead of pushing unvalidated changes directly.
- Required checks exercise the behavior affected by a PR and cannot report success after a required job fails.
- Security-sensitive workflow changes require code-owner review.
- Vulnerability reporters have a private channel and documented response expectations.
- Dependency and code-scanning results participate in merge protection where available.
Reproducible Static Inspection
These commands are read-only:
# Locate privileged triggers, permissions, secrets, and mutable actions
rg -n 'pull_request_target|workflow_run|issue_comment|permissions:|contents: write|pull-requests: write|id-token: write|secrets\.|uses:.*@(main|master|latest|v[0-9]+)' .github/workflows
# Inspect PR output wiring
rg -n 'has-evals|outputs\.evals|has-docs|outputs\.docs|has-workflows|outputs\.workflows' .github/workflows/pr-checks.yml
# Validate YAML syntax with Ruby's safe parser
ruby -e 'require "yaml"; Dir[".github/workflows/*.{yml,yaml}"].each { |f| YAML.safe_load(File.read(f), aliases: true); puts f }'
# Inspect repository-side security settings
gh api repos/darrenhinde/OpenAgentsControl/actions/permissions/workflow
gh api repos/darrenhinde/OpenAgentsControl/actions/permissions
gh api repos/darrenhinde/OpenAgentsControl/actions/permissions/fork-pr-contributor-approval
gh api repos/darrenhinde/OpenAgentsControl/rulesets
gh api repos/darrenhinde/OpenAgentsControl/private-vulnerability-reporting
gh api repos/darrenhinde/OpenAgentsControl/code-scanning/default-setup
Validation for This Baseline
- Confirm every active workflow is represented in the inventory.
- Confirm cited line ranges against current files.
- Run Markdown internal-link validation.
- Run
git diff --check. - Do not alter workflows or GitHub settings in this task.