diff --git a/.github/workflows/.test-bake.yml b/.github/workflows/.test-bake.yml index bd960a8f..acaefa96 100644 --- a/.github/workflows/.test-bake.yml +++ b/.github/workflows/.test-bake.yml @@ -187,6 +187,63 @@ jobs: const builderOutputs = JSON.parse(core.getInput('builder-outputs')); core.info(JSON.stringify(builderOutputs, null, 2)); + bake-dockerhub-oidc: + uses: ./.github/workflows/bake.yml + if: ${{ github.event_name != 'pull_request' }} + permissions: + contents: read + id-token: write + with: + job-name-prefix: bake-dockerhub-oidc + context: test + output: image + push: true + sbom: true + set: | + *.args.VERSION={{meta.version}} + target: hello + registry-identities: | + - type: dockerhub + registry: docker.io + username: ${{ vars.DOCKERHUB_OIDC_USERNAME }} + connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }} + meta-images: | + docker.io/dockereng/github-builder-test + meta-tags: | + type=raw,value=bake-ghbuilder-oidc-${{ github.run_id }} + + bake-dockerhub-oidc-verify: + uses: ./.github/workflows/verify.yml + if: ${{ github.event_name != 'pull_request' }} + needs: + - bake-dockerhub-oidc + permissions: + contents: read + id-token: write + with: + builder-outputs: ${{ toJSON(needs.bake-dockerhub-oidc.outputs) }} + registry-identities: | + - type: dockerhub + registry: docker.io + username: ${{ vars.DOCKERHUB_OIDC_USERNAME }} + connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }} + + bake-dockerhub-oidc-outputs: + runs-on: ubuntu-24.04 + if: ${{ github.event_name != 'pull_request' }} + needs: + - bake-dockerhub-oidc + steps: + - + name: Builder outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.bake-dockerhub-oidc.outputs) }} + with: + script: | + const builderOutputs = JSON.parse(core.getInput('builder-outputs')); + core.info(JSON.stringify(builderOutputs, null, 2)); + bake-dockerhub-scan: runs-on: ubuntu-24.04 if: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/.test-build.yml b/.github/workflows/.test-build.yml index a7434316..c50da37f 100644 --- a/.github/workflows/.test-build.yml +++ b/.github/workflows/.test-build.yml @@ -184,6 +184,62 @@ jobs: const builderOutputs = JSON.parse(core.getInput('builder-outputs')); core.info(JSON.stringify(builderOutputs, null, 2)); + build-dockerhub-oidc: + uses: ./.github/workflows/build.yml + if: ${{ github.event_name != 'pull_request' }} + permissions: + contents: read + id-token: write + with: + job-name-prefix: build-dockerhub-oidc + build-args: | + VERSION={{meta.version}} + file: test/hello.Dockerfile + output: image + push: true + sbom: true + registry-identities: | + - type: dockerhub + registry: docker.io + username: ${{ vars.DOCKERHUB_OIDC_USERNAME }} + connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }} + meta-images: | + docker.io/dockereng/github-builder-test + meta-tags: | + type=raw,value=build-ghbuilder-oidc-${{ github.run_id }} + + build-dockerhub-oidc-verify: + uses: ./.github/workflows/verify.yml + if: ${{ github.event_name != 'pull_request' }} + needs: + - build-dockerhub-oidc + permissions: + contents: read + id-token: write + with: + builder-outputs: ${{ toJSON(needs.build-dockerhub-oidc.outputs) }} + registry-identities: | + - type: dockerhub + registry: docker.io + username: ${{ vars.DOCKERHUB_OIDC_USERNAME }} + connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }} + + build-dockerhub-oidc-outputs: + runs-on: ubuntu-24.04 + if: ${{ github.event_name != 'pull_request' }} + needs: + - build-dockerhub-oidc + steps: + - + name: Builder outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-dockerhub-oidc.outputs) }} + with: + script: | + const builderOutputs = JSON.parse(core.getInput('builder-outputs')); + core.info(JSON.stringify(builderOutputs, null, 2)); + build-dockerhub-scan: runs-on: ubuntu-24.04 if: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/bake.yml b/.github/workflows/bake.yml index 5be2d62b..7fc9fc78 100644 --- a/.github/workflows/bake.yml +++ b/.github/workflows/bake.yml @@ -1021,6 +1021,16 @@ jobs: - registry: ${{ needs.registry-identities.outputs.gcp-wif-registry }} username: oauth2accesstoken password: ${{ steps.gcp-wif-auth.outputs.access_token }} + - + name: Login to Docker Hub with OIDC + if: ${{ needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }} + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + env: + DOCKERHUB_OIDC_CONNECTIONID: ${{ needs.registry-identities.outputs.dockerhub-oidc-connection-id }} + with: + registry-auth: | + - registry: ${{ needs.registry-identities.outputs.dockerhub-oidc-registry }} + username: ${{ needs.registry-identities.outputs.dockerhub-oidc-username }} - name: Login to registry if: ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} @@ -1263,6 +1273,16 @@ jobs: - registry: ${{ needs.registry-identities.outputs.gcp-wif-registry }} username: oauth2accesstoken password: ${{ steps.gcp-wif-auth.outputs.access_token }} + - + name: Login to Docker Hub with OIDC + if: ${{ inputs.push && inputs.output == 'image' && needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }} + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + env: + DOCKERHUB_OIDC_CONNECTIONID: ${{ needs.registry-identities.outputs.dockerhub-oidc-connection-id }} + with: + registry-auth: | + - registry: ${{ needs.registry-identities.outputs.dockerhub-oidc-registry }} + username: ${{ needs.registry-identities.outputs.dockerhub-oidc-username }} - name: Login to registry if: ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c62a8e0..608f7591 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -887,6 +887,16 @@ jobs: - registry: ${{ needs.registry-identities.outputs.gcp-wif-registry }} username: oauth2accesstoken password: ${{ steps.gcp-wif-auth.outputs.access_token }} + - + name: Login to Docker Hub with OIDC + if: ${{ needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }} + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + env: + DOCKERHUB_OIDC_CONNECTIONID: ${{ needs.registry-identities.outputs.dockerhub-oidc-connection-id }} + with: + registry-auth: | + - registry: ${{ needs.registry-identities.outputs.dockerhub-oidc-registry }} + username: ${{ needs.registry-identities.outputs.dockerhub-oidc-username }} - name: Login to registry if: ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} @@ -1125,6 +1135,16 @@ jobs: - registry: ${{ needs.registry-identities.outputs.gcp-wif-registry }} username: oauth2accesstoken password: ${{ steps.gcp-wif-auth.outputs.access_token }} + - + name: Login to Docker Hub with OIDC + if: ${{ inputs.push && inputs.output == 'image' && needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }} + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + env: + DOCKERHUB_OIDC_CONNECTIONID: ${{ needs.registry-identities.outputs.dockerhub-oidc-connection-id }} + with: + registry-auth: | + - registry: ${{ needs.registry-identities.outputs.dockerhub-oidc-registry }} + username: ${{ needs.registry-identities.outputs.dockerhub-oidc-username }} - name: Login to registry if: ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} diff --git a/.github/workflows/setup-registry-identities.yml b/.github/workflows/setup-registry-identities.yml index ff12af7c..56f57895 100644 --- a/.github/workflows/setup-registry-identities.yml +++ b/.github/workflows/setup-registry-identities.yml @@ -35,6 +35,18 @@ on: gcp-wif-project-id: description: "GCP project ID" value: ${{ jobs.setup-registry-identities.outputs.gcp-wif-project-id }} + dockerhub-oidc-enabled: + description: "Whether a Docker Hub OIDC registry identity was configured" + value: ${{ jobs.setup-registry-identities.outputs.dockerhub-oidc-enabled }} + dockerhub-oidc-registry: + description: "Docker Hub registry hostname" + value: ${{ jobs.setup-registry-identities.outputs.dockerhub-oidc-registry }} + dockerhub-oidc-username: + description: "Docker Hub username or organization to authenticate as" + value: ${{ jobs.setup-registry-identities.outputs.dockerhub-oidc-username }} + dockerhub-oidc-connection-id: + description: "Docker Hub OIDC connection ID" + value: ${{ jobs.setup-registry-identities.outputs.dockerhub-oidc-connection-id }} env: RUNTIME_MODULE: "@docker/github-builder-runtime@0.94.0" @@ -62,6 +74,10 @@ jobs: gcp-wif-workload-identity-provider: ${{ steps.validate.outputs.gcp-wif-workload-identity-provider }} gcp-wif-service-account: ${{ steps.validate.outputs.gcp-wif-service-account }} gcp-wif-project-id: ${{ steps.validate.outputs.gcp-wif-project-id }} + dockerhub-oidc-enabled: ${{ steps.validate.outputs.dockerhub-oidc-enabled }} + dockerhub-oidc-registry: ${{ steps.validate.outputs.dockerhub-oidc-registry }} + dockerhub-oidc-username: ${{ steps.validate.outputs.dockerhub-oidc-username }} + dockerhub-oidc-connection-id: ${{ steps.validate.outputs.dockerhub-oidc-connection-id }} steps: - name: Install dependencies @@ -111,6 +127,10 @@ jobs: core.setOutput('gcp-wif-workload-identity-provider', ''); core.setOutput('gcp-wif-service-account', ''); core.setOutput('gcp-wif-project-id', ''); + core.setOutput('dockerhub-oidc-enabled', 'false'); + core.setOutput('dockerhub-oidc-registry', ''); + core.setOutput('dockerhub-oidc-username', ''); + core.setOutput('dockerhub-oidc-connection-id', ''); }; const registryIdentities = core.getInput('registry-identities', {trimWhitespace: false}); @@ -142,6 +162,12 @@ jobs: } return value.trim(); }; + const optionalString = (entry, key, path, defaultValue) => { + if (!Object.prototype.hasOwnProperty.call(entry, key)) { + return defaultValue; + } + return requireString(entry, key, path); + }; let parsed; try { parsed = yaml.load(registryIdentities); @@ -161,6 +187,7 @@ jobs: let awsEcr; let gcpWif; + let dockerhubOidc; entries.forEach((entry, index) => { const path = `registry-identities[${index}]`; ensureObject(entry, path); @@ -201,6 +228,23 @@ jobs: }; break; } + case 'dockerhub': { + const allowedKeys = new Set(['type', 'registry', 'username', 'connection_id']); + for (const key of Object.keys(entry)) { + if (!allowedKeys.has(key)) { + fail(`${path}.${key} is not supported for dockerhub`); + } + } + if (dockerhubOidc) { + fail('only one dockerhub registry identity is supported'); + } + dockerhubOidc = { + registry: optionalString(entry, 'registry', path, 'docker.io'), + username: requireString(entry, 'username', path), + connectionID: requireString(entry, 'connection_id', path) + }; + break; + } default: fail(`${path}.type has unsupported provider ${type}`); } @@ -215,3 +259,7 @@ jobs: core.setOutput('gcp-wif-workload-identity-provider', gcpWif?.workloadIdentityProvider || ''); core.setOutput('gcp-wif-service-account', gcpWif?.serviceAccount || ''); core.setOutput('gcp-wif-project-id', gcpWif?.projectId || ''); + core.setOutput('dockerhub-oidc-enabled', dockerhubOidc ? 'true' : 'false'); + core.setOutput('dockerhub-oidc-registry', dockerhubOidc?.registry || ''); + core.setOutput('dockerhub-oidc-username', dockerhubOidc?.username || ''); + core.setOutput('dockerhub-oidc-connection-id', dockerhubOidc?.connectionID || ''); diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 55815af0..6c02669b 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -7,6 +7,10 @@ on: type: string description: "JSON build outputs from Docker GitHub Builder reusable workflows" required: true + registry-identities: + type: string + description: "Keyless registry identity configuration as YAML objects" + required: false secrets: registry-auths: description: "Registry authentication details as YAML objects" @@ -26,8 +30,17 @@ env: NPM_CONFIG_FETCH_RETRIES: "5" jobs: + registry-identities: + uses: ./.github/workflows/setup-registry-identities.yml + with: + registry-identities: ${{ inputs.registry-identities }} + verify: runs-on: ubuntu-24.04 + needs: + - registry-identities + env: + REGISTRY_AUTHS_PRESENT: ${{ secrets.registry-auths != '' }} steps: - name: Extract builder outputs @@ -103,9 +116,54 @@ jobs: const cosign = new Cosign(); await cosign.printVersion(); + - + name: Configure AWS credentials + if: ${{ steps.vars.outputs.signed == 'true' && steps.vars.outputs.output-type == 'image' && needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} + uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 + with: + role-to-assume: ${{ needs.registry-identities.outputs.aws-ecr-role-to-assume }} + aws-region: ${{ needs.registry-identities.outputs.aws-ecr-region }} + - + name: Login to Amazon ECR + if: ${{ steps.vars.outputs.signed == 'true' && steps.vars.outputs.output-type == 'image' && needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + with: + registry-auth: | + - registry: ${{ needs.registry-identities.outputs.aws-ecr-registry }} + - + name: Authenticate to Google Cloud + id: gcp-wif-auth + if: ${{ steps.vars.outputs.signed == 'true' && steps.vars.outputs.output-type == 'image' && needs.registry-identities.outputs.gcp-wif-enabled == 'true' }} + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 + with: + token_format: access_token + workload_identity_provider: ${{ needs.registry-identities.outputs.gcp-wif-workload-identity-provider }} + service_account: ${{ needs.registry-identities.outputs.gcp-wif-service-account }} + project_id: ${{ needs.registry-identities.outputs.gcp-wif-project-id }} + create_credentials_file: false + export_environment_variables: false + - + name: Login to Google Artifact Registry + if: ${{ steps.vars.outputs.signed == 'true' && steps.vars.outputs.output-type == 'image' && needs.registry-identities.outputs.gcp-wif-enabled == 'true' }} + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + with: + registry-auth: | + - registry: ${{ needs.registry-identities.outputs.gcp-wif-registry }} + username: oauth2accesstoken + password: ${{ steps.gcp-wif-auth.outputs.access_token }} + - + name: Login to Docker Hub with OIDC + if: ${{ steps.vars.outputs.signed == 'true' && steps.vars.outputs.output-type == 'image' && needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }} + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + env: + DOCKERHUB_OIDC_CONNECTIONID: ${{ needs.registry-identities.outputs.dockerhub-oidc-connection-id }} + with: + registry-auth: | + - registry: ${{ needs.registry-identities.outputs.dockerhub-oidc-registry }} + username: ${{ needs.registry-identities.outputs.dockerhub-oidc-username }} - name: Login to registry - if: ${{ steps.vars.outputs.signed == 'true' && steps.vars.outputs.output-type == 'image' }} + if: ${{ steps.vars.outputs.signed == 'true' && steps.vars.outputs.output-type == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 with: registry-auth: ${{ secrets.registry-auths }} diff --git a/README.md b/README.md index 2da913df..9482ddba 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ ___ * [Notes](#notes) * [Signed GitHub Actions cache](#signed-github-actions-cache) * [Registry identities](#registry-identities) + * [Docker Hub OIDC](#docker-hub-oidc) * [AWS ECR](#aws-ecr) * [Google Artifact Registry](#google-artifact-registry) * [Runner mapping](#runner-mapping) @@ -414,6 +415,48 @@ secret. Provider-specific authentication steps are pinned in these reusable workflows; callers can only select supported provider types and pass identity configuration. +The `build.yml`, `bake.yml`, and `verify.yml` reusable workflows accept this +input. Use the same identity configuration with `verify.yml` when signature +verification needs access to a private registry image. + +#### Docker Hub OIDC + +Docker Hub registry authentication can be configured with +`type: dockerhub`. Callers must grant `id-token: write` so +`docker/login-action` can exchange the GitHub OIDC token through the configured +[Docker Hub OIDC connection](https://docs.docker.com/enterprise/security/oidc-connections/): + +```yaml +jobs: + build: + uses: docker/github-builder/.github/workflows/build.yml@v1 + permissions: + contents: read # to fetch the repository content + id-token: write # for signing attestations, cache entries with GitHub OIDC and logging in to Docker Hub + with: + output: image + push: ${{ github.event_name != 'pull_request' }} + meta-images: | + docker.io/my-organization/test-github-builder + registry-identities: | + - type: dockerhub + username: my-organization + connection_id: 123e4567-e89b-42d3-a456-426614174000 +``` + +| Name | Type | Description | +|-----------------|--------|-----------------------------------------------------------------------------| +| `type` | String | Registry identity provider type. Must be `dockerhub`. | +| `registry` | String | Registry hostname passed to `docker/login-action`. Defaults to `docker.io`. | +| `username` | String | Docker Hub username or organization passed to `docker/login-action`. | +| `connection_id` | String | Docker Hub OIDC connection ID passed to `docker/login-action` in each job. | + +The workflow mints the Docker Hub access token inside each reusable workflow +job that needs Docker Hub registry access. The token is not accepted as an +input and is not passed across the reusable workflow boundary. If you call +`verify.yml` for a private Docker Hub image, pass the same +`registry-identities` value to that workflow instead of using a Docker Hub PAT. + #### AWS ECR Amazon ECR registry authentication is configured with `type: aws-ecr`. Callers