From f567c914f2e1b2d2989d02832441d9deda21477c Mon Sep 17 00:00:00 2001 From: Emilien Escalle Date: Mon, 28 Sep 2026 20:47:49 +0200 Subject: [PATCH] fix(docker-build-images)!: restrict build-secret GitHub App tokens Limit generated build tokens to contents:read and an explicit repository list. Default to the calling repository and reject empty lists to prevent tokens from falling back to installation-wide access. BREAKING CHANGE: Build-secret GitHub App tokens no longer inherit all installation permissions or access all installation repositories. Builds using private dependencies must list those repositories with build-secret-github-app-repositories. Operations requiring other permissions must receive separately scoped credentials through build-secrets. --- .github/workflows/docker-build-images.md | 98 +++++++++++++---------- .github/workflows/docker-build-images.yml | 28 ++++++- 2 files changed, 82 insertions(+), 44 deletions(-) diff --git a/.github/workflows/docker-build-images.md b/.github/workflows/docker-build-images.md index b895ba37..bb628004 100644 --- a/.github/workflows/docker-build-images.md +++ b/.github/workflows/docker-build-images.md @@ -39,6 +39,12 @@ This includes [multi-platform](https://docs.docker.com/build/building/multi-plat +Build-secret GitHub App tokens have `contents: read` permission and access only to the calling repository by default. +For builds that fetch private dependencies from other repositories, set `build-secret-github-app-repositories` +to the repository names, for example `application,shared-library`. The repositories must belong to +`build-secret-github-app-owner`, and the app installation must have access to them. +An empty repository list is rejected when a GitHub App client ID is provided. + ## Usage @@ -149,7 +155,7 @@ jobs: # Default: `GITHUB_APP_TOKEN` build-secret-github-app-token-env: GITHUB_APP_TOKEN - # GitHub App client ID to generate GitHub token to be passed as build secret env. + # GitHub App client ID to generate a token with read-only repository contents access for build secrets. # See https://github.com/actions/create-github-app-token. build-secret-github-app-client-id: "" @@ -159,6 +165,13 @@ jobs: # Default: `${{ github.repository_owner }}` build-secret-github-app-owner: ${{ github.repository_owner }} + # Comma or newline-separated repository names accessible to the build token. + # Repositories must belong to `build-secret-github-app-owner`. + # Defaults to the calling repository. Include private dependency repositories explicitly. + # + # Default: `${{ github.event.repository.name }}` + build-secret-github-app-repositories: ${{ github.event.repository.name }} + # Cache type. Set to `false` or empty to disable cache entirely. # See https://docs.docker.com/build/cache/backends. # @@ -198,47 +211,48 @@ jobs: ### Workflow Call Inputs -| **Input** | **Description** | **Required** | **Type** | **Default** | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | -------------------------------- | -| **`runs-on`** | Runner to use. JSON array of runners. | **false** | **string** | `["ubuntu-latest"]` | -| | See . | | | | -| **`oci-registry`** | OCI registry configuration used to pull, push and cache images. | **false** | **string** | `ghcr.io` | -| | Accepts either a registry hostname string (default format) or a JSON object. | | | | -| | JSON example: `{"pull":"docker.io","pull:private":"ghcr.io","push":"ghcr.io"}` | | | | -| | JSON object keys: | | | | -| | - `pull`: registry used to pull public or default base images | | | | -| | - `pull:`: additional pull registry | | | | -| | - `push`: registry used for published images | | | | -| | - `cache`: registry used when `cache-type` is `registry` | | | | -| | If no `pull` key is provided, the `push` registry is also used for pulls. | | | | -| **`oci-registry-username`** | Username configuration used to log against OCI registries. | **false** | **string** | `${{ github.repository_owner }}` | -| | Accepts either a single username string (default format) or a JSON object using the same keys as `oci-registry`. | | | | -| | JSON example: `{"pull:private":"$\{{ github.repository_owner }}","push":"$\{{ github.repository_owner }}"}` | | | | -| | See . | | | | -| **`images`** | Images to build parameters. | **true** | **string** | - | -| | JSON array of objects. | | | | -| | Example: | | | | -| |
[
 {
 "name": "application",
 "context": ".",
 "dockerfile": "./docker/application/Dockerfile",
 "target": "prod",
 "build-args": {
 "APP_PATH": "./application/",
 "PROD_MODE": "true"
 },
 "secret-envs": {
 "GH_TOKEN": "GITHUB_TOKEN"
 },
 "platforms": [
 "linux/amd64",
 {
 "name": "darwin/amd64",
 "runs-on": "macos-latest"
 }
 ]
 }
]
| | | | -| **`lfs`** | Enable Git LFS. | **false** | **boolean** | `true` | -| | See . | | | | -| **`build-secret-github-app-token-env`** | Environment variable name(s) to pass GitHub token generated by GitHub App. | **false** | **string** | `GITHUB_APP_TOKEN` | -| | Can be a multiline string list. | | | | -| | This is useful to pass a generated token to the build, as it is not possible to share generated secrets between jobs. | | | | -| | Needs input `build-secret-github-app-client-id` and secret `build-secret-github-app-key`. | | | | -| **`build-secret-github-app-client-id`** | GitHub App client ID to generate GitHub token to be passed as build secret env. | **false** | **string** | - | -| | See . | | | | -| **`build-secret-github-app-owner`** | The owner of the GitHub App installation. | **false** | **string** | `${{ github.repository_owner }}` | -| | See . | | | | -| **`cache-type`** | Cache type. Set to `false` or empty to disable cache entirely. | **false** | **string** | `gha` | -| | See . | | | | -| **`buildkitd-config-inline`** | Inline BuildKit daemon configuration. | **false** | **string** | - | -| | See . | | | | -| | Example for insecure registry: | | | | -| |
[registry."my-registry.local:5000"]
 http = true
 insecure = true
| | | | -| **`sign`** | Sign built images. | **false** | **boolean** | `true` | -| | See [sign-images](../../actions/docker/sign-images/README.md). | | | | -| **`attest`** | Generate build provenance attestations for built images. | **false** | **boolean** | `true` | -| | See [attest-image](../../actions/docker/attest-image/README.md). | | | | +| **Input** | **Description** | **Required** | **Type** | **Default** | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | ------------------------------------- | +| **`runs-on`** | Runner to use. JSON array of runners. | **false** | **string** | `["ubuntu-latest"]` | +| | See . | | | | +| **`oci-registry`** | OCI registry configuration used to pull, push and cache images. | **false** | **string** | `ghcr.io` | +| | Accepts either a registry hostname string (default format) or a JSON object. | | | | +| | JSON example: `{"pull":"docker.io","pull:private":"ghcr.io","push":"ghcr.io"}` | | | | +| | JSON object keys: | | | | +| | - `pull`: registry used to pull public or default base images | | | | +| | - `pull:`: additional pull registry | | | | +| | - `push`: registry used for published images | | | | +| | - `cache`: registry used when `cache-type` is `registry` | | | | +| | If no `pull` key is provided, the `push` registry is also used for pulls. | | | | +| **`oci-registry-username`** | Username configuration used to log against OCI registries. | **false** | **string** | `${{ github.repository_owner }}` | +| | Accepts either a single username string (default format) or a JSON object using the same keys as `oci-registry`. | | | | +| | JSON example: `{"pull:private":"$\{{ github.repository_owner }}","push":"$\{{ github.repository_owner }}"}` | | | | +| | See . | | | | +| **`images`** | Images to build parameters. | **true** | **string** | - | +| | JSON array of objects. | | | | +| | Example: | | | | +| |
[
 {
 "name": "application",
 "context": ".",
 "dockerfile": "./docker/application/Dockerfile",
 "target": "prod",
 "build-args": {
 "APP_PATH": "./application/",
 "PROD_MODE": "true"
 },
 "secret-envs": {
 "GH_TOKEN": "GITHUB_TOKEN"
 },
 "platforms": [
 "linux/amd64",
 {
 "name": "darwin/amd64",
 "runs-on": "macos-latest"
 }
 ]
 }
]
| | | | +| **`lfs`** | Enable Git LFS. | **false** | **boolean** | `true` | +| | See . | | | | +| **`build-secret-github-app-token-env`** | Environment variable name(s) to pass GitHub token generated by GitHub App. | **false** | **string** | `GITHUB_APP_TOKEN` | +| | Can be a multiline string list. | | | | +| | This is useful to pass a generated token to the build, as it is not possible to share generated secrets between jobs. | | | | +| | Needs input `build-secret-github-app-client-id` and secret `build-secret-github-app-key`. | | | | +| **`build-secret-github-app-client-id`** | GitHub App client ID to generate a token with read-only repository contents access for build secrets. | **false** | **string** | - | +| | See . | | | | +| **`build-secret-github-app-owner`** | The owner of the GitHub App installation. | **false** | **string** | `${{ github.repository_owner }}` | +| | See . | | | | +| **`build-secret-github-app-repositories`** | Comma or newline-separated repository names accessible to the build token. Repositories must belong to `build-secret-github-app-owner`. Defaults to the calling repository. Include private dependency repositories explicitly. | **false** | **string** | `${{ github.event.repository.name }}` | +| **`cache-type`** | Cache type. Set to `false` or empty to disable cache entirely. | **false** | **string** | `gha` | +| | See . | | | | +| **`buildkitd-config-inline`** | Inline BuildKit daemon configuration. | **false** | **string** | - | +| | See . | | | | +| | Example for insecure registry: | | | | +| |
[registry."my-registry.local:5000"]
 http = true
 insecure = true
| | | | +| **`sign`** | Sign built images. | **false** | **boolean** | `true` | +| | See [sign-images](../../actions/docker/sign-images/README.md). | | | | +| **`attest`** | Generate build provenance attestations for built images. | **false** | **boolean** | `true` | +| | See [attest-image](../../actions/docker/attest-image/README.md). | | | | diff --git a/.github/workflows/docker-build-images.yml b/.github/workflows/docker-build-images.yml index 66fd9a69..fddd9725 100644 --- a/.github/workflows/docker-build-images.yml +++ b/.github/workflows/docker-build-images.yml @@ -86,7 +86,7 @@ on: # yamllint disable-line rule:truthy default: "GITHUB_APP_TOKEN" build-secret-github-app-client-id: description: | - GitHub App client ID to generate GitHub token to be passed as build secret env. + GitHub App client ID to generate a token with read-only repository contents access for build secrets. See https://github.com/actions/create-github-app-token. required: false type: string @@ -97,6 +97,14 @@ on: # yamllint disable-line rule:truthy required: false type: string default: ${{ github.repository_owner }} + build-secret-github-app-repositories: + description: | + Comma or newline-separated repository names accessible to the build token. + Repositories must belong to `build-secret-github-app-owner`. + Defaults to the calling repository. Include private dependency repositories explicitly. + required: false + type: string + default: ${{ github.event.repository.name }} cache-type: description: | Cache type. Set to `false` or empty to disable cache entirely. @@ -182,6 +190,7 @@ jobs: outputs: artifact-name: ${{ steps.define-artifact-name.outputs.artifact-name }} images: ${{ steps.define-images-by-platform.outputs.images }} + build-secret-github-app-repositories: ${{ steps.validate-inputs.outputs.build-secret-github-app-repositories }} runs-on: ${{ fromJson(inputs.runs-on) }} steps: - id: validate-inputs @@ -190,6 +199,8 @@ jobs: OCI_REGISTRY_PASSWORD: ${{ secrets.oci-registry-password }} # zizmor: ignore[secrets-outside-env] RUNS_ON_INPUT: ${{ inputs.runs-on }} IMAGES_INPUT: ${{ inputs.images }} + BUILD_SECRET_GITHUB_APP_CLIENT_ID: ${{ inputs.build-secret-github-app-client-id }} + BUILD_SECRET_GITHUB_APP_REPOSITORIES: ${{ inputs.build-secret-github-app-repositories }} with: script: | const ociRegistryPassword = process.env.OCI_REGISTRY_PASSWORD; @@ -197,6 +208,17 @@ jobs: throw new Error(`"oci-registry-password" secret is missing`); } + if (process.env.BUILD_SECRET_GITHUB_APP_CLIENT_ID) { + const repositories = (process.env.BUILD_SECRET_GITHUB_APP_REPOSITORIES || '') + .split(/[,\n]/) + .map(repository => repository.trim()) + .filter(Boolean); + if (!repositories.length) { + throw new Error('"build-secret-github-app-repositories" must contain at least one repository'); + } + core.setOutput('build-secret-github-app-repositories', repositories.join(',')); + } + const runsOnInput = process.env.RUNS_ON_INPUT; let runsOn = null; try { @@ -396,7 +418,9 @@ jobs: id: generate-token with: client-id: ${{ inputs.build-secret-github-app-client-id }} - owner: ${{ inputs.build-secret-github-app-owner }} + owner: ${{ inputs.build-secret-github-app-owner }} + repositories: ${{ needs.prepare-variables.outputs.build-secret-github-app-repositories }} + permission-contents: read private-key: ${{ secrets.build-secret-github-app-key }} # zizmor: ignore[secrets-outside-env] - id: prepare-secret-envs