From 3f1c198c2ac7b08352a19a4025d6d7a1828ea145 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Sun, 13 Sep 2026 15:15:45 +0200 Subject: [PATCH 1/2] Add private repository workflow authentication --- CHANGELOG.md | 6 ++++++ Guidelines/CICD.md | 31 +++++++++++++++++++++++++++++++ README.md | 4 ++-- Scripts/validate_guidelines.swift | 5 +++++ Tests/run_tests.swift | 21 +++++++++++++++++++++ VERSION | 2 +- 6 files changed, 66 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6feaeb1..bd00f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project are documented in this file. +## [0.0.31] - 2026-09-13 + +### Added + +- Added a least-privilege GitHub App authentication pattern for workflows that resolve private sibling repositories, including short-lived read-only tokens, process-scoped Git configuration, exact repository selection, and fork pull-request and self-hosted-runner security boundaries. + ## [0.0.30] - 2026-09-12 ### Changed diff --git a/Guidelines/CICD.md b/Guidelines/CICD.md index 190739d..46a9f2d 100644 --- a/Guidelines/CICD.md +++ b/Guidelines/CICD.md @@ -20,6 +20,37 @@ Use Swift for new repository-owned executable scripts in Swift-focused applicati Fall back to Python or a POSIX shell script only when the required behavior cannot be implemented with the repository's supported Swift toolchain and Foundation APIs. Document the exception in durable repository documentation in the same change, including the missing Swift capability, exact script and task scope, runtime and dependency requirements, security and maintenance impact, validation method, and condition for revisiting or removing the exception. Keep the fallback narrow; an existing non-Swift script does not authorize another one. The central `Scripts/swift_format.sh` command wrapper is the retained documented exception for invoking Xcode's `swift-format` modes. +## Private repository dependencies + +The workflow repository's `GITHUB_TOKEN` does not grant access to private dependencies in sibling repositories. When Swift Package Manager or another build tool must clone private ThatFactory repositories, use a GitHub App installed on the workflow repository and every required dependency repository. Grant the app only read access to repository contents, mint a short-lived installation token with `actions/create-github-app-token`, and list the exact dependency repositories in the action's `repositories` input. Do not use a personal access token, a long-lived machine credential, or an organization-wide token when the GitHub App can provide the required scope. + +Expose the installation token only to steps that resolve or build the private dependencies. Supply HTTPS authentication through Git's process-level `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_0`, and `GIT_CONFIG_VALUE_0` environment variables so the credential is not persisted in repository or global Git configuration. Keep the existing dependency URLs as `https://github.com//` URLs. For example: + +```yaml +- name: Create private dependency token + id: private-dependencies + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ vars.PRIVATE_DEPENDENCIES_APP_ID }} + private-key: ${{ secrets.PRIVATE_DEPENDENCIES_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: | + first-private-package + second-private-package + permission-contents: read + +- name: Test + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: url.https://x-access-token:${{ steps.private-dependencies.outputs.token }}@github.com/.insteadOf + GIT_CONFIG_VALUE_0: https://github.com/ + run: swift test +``` + +The GitHub App's installation and repository selection are part of the security boundary. Consumer documentation must name the app variable and secret, list the private repositories the workflow requires, record the required `Contents: read` permission, and identify the jobs or steps that receive the token. Keep pull-request and protected-branch workflows consistent unless a documented trust boundary requires otherwise. + +Repository secrets are unavailable to workflows triggered by pull requests from forks. A repository that accepts fork-originated pull requests must keep a secretless validation path or deliberately skip private-dependency jobs with an explicit, documented condition. Do not use `pull_request_target` to run untrusted pull-request code with the GitHub App credential. On self-hosted runners, do not expose the token to unrelated steps, caches, artifacts, logs, or persistent configuration; retain the action's default post-job token revocation. + ## `ci-pr.yml` Projects using GitHub Actions should keep pull-request validation in `.github/workflows/ci-pr.yml`, triggered by `pull_request` events for `opened`, `synchronize`, and `reopened`. diff --git a/README.md b/README.md index 821a309..0c684f2 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ From the consumer repository root, install a tagged release: git subtree add \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.30 \ + 0.0.31 \ --squash ``` @@ -147,7 +147,7 @@ Review the target release's changelog, then pull it deliberately: git subtree pull \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.30 \ + 0.0.31 \ --squash ``` diff --git a/Scripts/validate_guidelines.swift b/Scripts/validate_guidelines.swift index 3139e94..d148ecd 100755 --- a/Scripts/validate_guidelines.swift +++ b/Scripts/validate_guidelines.swift @@ -579,11 +579,16 @@ func validateExternalDependencyPolicy(_ errors: inout [String]) { if let contents = readText(cicdGuideline, errors: &errors) { let required = [ "## Tooling and automation": "CI/CD tooling policy section", + "## Private repository dependencies": "private repository dependency authentication section", "Fastlane is forbidden": "forbidden delivery tooling", "xcode-cloud-mcp": "first-party Xcode Cloud tooling", "app-store-connect-mcp": "first-party App Store tooling", "required behavior cannot be implemented": "non-Swift capability-gap threshold", "missing Swift capability": "documented non-Swift exception", + "actions/create-github-app-token@v3": "short-lived GitHub App token workflow", + "permission-contents: read": "read-only private dependency permission", + "GIT_CONFIG_KEY_0": "process-level Git authentication", + "pull_request_target": "untrusted pull-request credential boundary", ] for (value, description) in required where !contents.contains(value) { errors.append("Guidelines/CICD.md: missing \(description): '\(value)'") diff --git a/Tests/run_tests.swift b/Tests/run_tests.swift index 516c7c8..0ed1b8a 100755 --- a/Tests/run_tests.swift +++ b/Tests/run_tests.swift @@ -146,6 +146,27 @@ let tests: [(String, () throws -> Void)] = [ } } ), + ( + "repository validator rejects private dependency authentication drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/CICD.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "actions/create-github-app-token@v3", + with: "actions/create-github-app-token") + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "private dependency authentication drift unexpectedly passed") + try require( + result.output.contains("missing short-lived GitHub App token workflow"), + result.output + ) + } + } + ), ( "repository validator rejects gitignore template drift", { diff --git a/VERSION b/VERSION index f092e2b..d788d43 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.30 +0.0.31 From bc9e14df3511deaedf311fad372fd790a95f6d64 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Sun, 13 Sep 2026 15:23:34 +0200 Subject: [PATCH 2/2] Harden private dependency runner isolation --- Guidelines/CICD.md | 10 ++++++---- Scripts/validate_guidelines.swift | 5 +++++ Tests/run_tests.swift | 6 +++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Guidelines/CICD.md b/Guidelines/CICD.md index 46a9f2d..5138aca 100644 --- a/Guidelines/CICD.md +++ b/Guidelines/CICD.md @@ -22,7 +22,7 @@ Fall back to Python or a POSIX shell script only when the required behavior cann ## Private repository dependencies -The workflow repository's `GITHUB_TOKEN` does not grant access to private dependencies in sibling repositories. When Swift Package Manager or another build tool must clone private ThatFactory repositories, use a GitHub App installed on the workflow repository and every required dependency repository. Grant the app only read access to repository contents, mint a short-lived installation token with `actions/create-github-app-token`, and list the exact dependency repositories in the action's `repositories` input. Do not use a personal access token, a long-lived machine credential, or an organization-wide token when the GitHub App can provide the required scope. +The workflow repository's `GITHUB_TOKEN` does not grant access to private dependencies in sibling repositories. When Swift Package Manager or another build tool must clone private ThatFactory repositories, use a GitHub App installed on every required dependency repository. The app does not need access to the workflow repository unless that repository is also an intended token target. Grant the app only read access to repository contents, mint a short-lived installation token with `actions/create-github-app-token`, and list the exact dependency repositories in the action's `repositories` input. Do not use a personal access token, a long-lived machine credential, or an organization-wide token when the GitHub App can provide the required scope. Expose the installation token only to steps that resolve or build the private dependencies. Supply HTTPS authentication through Git's process-level `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_0`, and `GIT_CONFIG_VALUE_0` environment variables so the credential is not persisted in repository or global Git configuration. Keep the existing dependency URLs as `https://github.com//` URLs. For example: @@ -31,7 +31,7 @@ Expose the installation token only to steps that resolve or build the private de id: private-dependencies uses: actions/create-github-app-token@v3 with: - app-id: ${{ vars.PRIVATE_DEPENDENCIES_APP_ID }} + client-id: ${{ vars.PRIVATE_DEPENDENCIES_APP_CLIENT_ID }} private-key: ${{ secrets.PRIVATE_DEPENDENCIES_APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} repositories: | @@ -47,9 +47,11 @@ Expose the installation token only to steps that resolve or build the private de run: swift test ``` -The GitHub App's installation and repository selection are part of the security boundary. Consumer documentation must name the app variable and secret, list the private repositories the workflow requires, record the required `Contents: read` permission, and identify the jobs or steps that receive the token. Keep pull-request and protected-branch workflows consistent unless a documented trust boundary requires otherwise. +The GitHub App's installation and repository selection are part of the security boundary. Consumer documentation must name the app variable and secret, list the private repositories the workflow requires, record the required `Contents: read` permission, and identify the jobs or steps that receive the token. Keep pull-request and protected-branch workflows consistent unless a documented trust boundary requires otherwise. Because `GIT_CONFIG_*` values are ordinary inherited environment variables, treat the credential-bearing resolve or build step and its complete subprocess tree as privileged. Tests, build scripts, SwiftPM plugins, and other code executed beneath that step must be trusted to receive read access to every repository in the token scope. -Repository secrets are unavailable to workflows triggered by pull requests from forks. A repository that accepts fork-originated pull requests must keep a secretless validation path or deliberately skip private-dependency jobs with an explicit, documented condition. Do not use `pull_request_target` to run untrusted pull-request code with the GitHub App credential. On self-hosted runners, do not expose the token to unrelated steps, caches, artifacts, logs, or persistent configuration; retain the action's default post-job token revocation. +Repository secrets are unavailable to workflows triggered by pull requests from forks. A repository that accepts fork-originated or otherwise untrusted pull requests must keep a secretless validation path or deliberately skip private-dependency jobs with an explicit, documented condition. Untrusted code must not execute on a persistent self-hosted runner that is later reused for credential-bearing work. Use an isolated disposable or ephemeral self-hosted runner, an appropriate GitHub-hosted runner where possible, or a separate runner pool or host that never subsequently receives secrets; otherwise skip the untrusted validation. + +This trust rule is event-independent. Do not combine credentials with code that is not trusted at that privilege level under `pull_request`, `pull_request_target`, `issue_comment`, `workflow_run`, or another trigger. On self-hosted runners, do not expose the token to unrelated steps, caches, artifacts, logs, or persistent configuration; retain the action's default post-job token revocation. ## `ci-pr.yml` diff --git a/Scripts/validate_guidelines.swift b/Scripts/validate_guidelines.swift index d148ecd..655eadd 100755 --- a/Scripts/validate_guidelines.swift +++ b/Scripts/validate_guidelines.swift @@ -586,8 +586,13 @@ func validateExternalDependencyPolicy(_ errors: inout [String]) { "required behavior cannot be implemented": "non-Swift capability-gap threshold", "missing Swift capability": "documented non-Swift exception", "actions/create-github-app-token@v3": "short-lived GitHub App token workflow", + "client-id:": "current GitHub App client identifier input", "permission-contents: read": "read-only private dependency permission", "GIT_CONFIG_KEY_0": "process-level Git authentication", + "GIT_CONFIG_VALUE_0: https://github.com/": "GitHub HTTPS rewrite source", + "complete subprocess tree as privileged": "credential-bearing subprocess trust boundary", + "isolated disposable or ephemeral self-hosted runner": "untrusted-code runner isolation", + "This trust rule is event-independent": "event-independent credential boundary", "pull_request_target": "untrusted pull-request credential boundary", ] for (value, description) in required where !contents.contains(value) { diff --git a/Tests/run_tests.swift b/Tests/run_tests.swift index 0ed1b8a..3d61c70 100755 --- a/Tests/run_tests.swift +++ b/Tests/run_tests.swift @@ -155,13 +155,13 @@ let tests: [(String, () throws -> Void)] = [ let guideline = fixture.appendingPathComponent("Guidelines/CICD.md") var contents = try String(contentsOf: guideline, encoding: .utf8) contents = contents.replacingOccurrences( - of: "actions/create-github-app-token@v3", - with: "actions/create-github-app-token") + of: "isolated disposable or ephemeral self-hosted runner", + with: "self-hosted runner") try write(contents, to: guideline) let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) try require(!result.succeeded, "private dependency authentication drift unexpectedly passed") try require( - result.output.contains("missing short-lived GitHub App token workflow"), + result.output.contains("missing untrusted-code runner isolation"), result.output ) }