Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/ADRs/0007-per-role-github-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ The installer checks for existing app installations before creating new ones. If
- PEMs are write-once secrets: lost keys require app deletion and recreation.
- The per-app model scales linearly with roles. Adding a new role means creating a new app — no shared credential rotation needed.
- GitLab and Forgejo will need equivalent per-role identity mechanisms when their forge implementations are built.
- **Permission escalation note:** The `fullsend` orchestrator app requires `workflows: write` (combined with `contents: write`) to create and update shim workflow files in enrolled repos during enrollment reconciliation. A compromise of this app's private key would allow an attacker to deploy arbitrary workflow files to all installed repos. Mitigations: (1) the app should be installed on specific repos, not org-wide; (2) the PEM is stored as a repo-level secret on `.fullsend`, limiting access to that repo's workflows; (3) per-role workflows validate `source_repo` against the config allowlist before generating tokens.
- **Permission escalation note:** The `fullsend` orchestrator app and the `coder` app both require `workflows: write` (combined with `contents: write`). The orchestrator needs it to create and update shim workflow files during enrollment reconciliation. The coder needs it because issues may legitimately require modifying `.github/workflows/` files — without this permission, pushes that touch workflow files are rejected by GitHub, wasting the entire agent run. CODEOWNERS rules on `.github/workflows/` paths provide an equivalent human-approval gate without blocking the push. A compromise of either app's private key would allow an attacker to deploy arbitrary workflow files to installed repos. Mitigations: (1) the app should be installed on specific repos, not org-wide; (2) PEMs are stored as repo-level secrets on `.fullsend`, limiting access to that repo's workflows; (3) per-role workflows validate `source_repo` against the config allowlist before generating tokens; (4) CODEOWNERS enforcement ensures workflow changes require human review.
1 change: 1 addition & 0 deletions internal/forge/github/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func AgentAppConfig(org, role string) AppConfig {
Contents: "write",
PullRequests: "write",
Checks: "read",
Workflows: "write",
}
base.Events = []string{"issues", "issue_comment", "pull_request", "check_run", "check_suite"}

Expand Down
3 changes: 3 additions & 0 deletions internal/forge/github/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func TestAgentAppConfig_Triage(t *testing.T) {
assert.Equal(t, "myorg-triage", cfg.Name)
assert.Equal(t, "write", cfg.Permissions.Issues)
assert.Empty(t, cfg.Permissions.Contents)
assert.Empty(t, cfg.Permissions.Workflows)

assert.Contains(t, cfg.Events, "issues")
assert.Contains(t, cfg.Events, "issue_comment")
Expand All @@ -54,6 +55,7 @@ func TestAgentAppConfig_Coder(t *testing.T) {
assert.Equal(t, "write", cfg.Permissions.Contents)
assert.Equal(t, "write", cfg.Permissions.PullRequests)
assert.Equal(t, "read", cfg.Permissions.Checks)
assert.Equal(t, "write", cfg.Permissions.Workflows)

assert.Contains(t, cfg.Events, "issues")
assert.Contains(t, cfg.Events, "issue_comment")
Expand All @@ -70,6 +72,7 @@ func TestAgentAppConfig_Review(t *testing.T) {
assert.Equal(t, "read", cfg.Permissions.Contents)
assert.Equal(t, "read", cfg.Permissions.Checks)
assert.Equal(t, "read", cfg.Permissions.Issues)
assert.Empty(t, cfg.Permissions.Workflows)

assert.Contains(t, cfg.Events, "pull_request")
}
Expand Down
Loading