From 381f9b6d9843d94638ac3488431eed4e40998f3f Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:27:52 +0200 Subject: [PATCH 1/3] build/bake: skip registry identity validation without registry login Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/.test-bake.yml | 4 ++++ .github/workflows/.test-build.yml | 4 ++++ .github/workflows/bake.yml | 1 + .github/workflows/build.yml | 1 + .../workflows/setup-registry-identities.yml | 20 +++++++++++++++++++ README.md | 3 ++- 6 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/.test-bake.yml b/.github/workflows/.test-bake.yml index acaefa96..15f1ecf2 100644 --- a/.github/workflows/.test-bake.yml +++ b/.github/workflows/.test-bake.yml @@ -524,6 +524,10 @@ jobs: context: test output: image push: false + registry-identities: | + - type: dockerhub + username: dockereng + connection_id: ${{ vars.MISSING_DOCKERHUB_OIDC_CONNECTIONID }} set: | *.args.VERSION={{meta.version}} target: hello-cross diff --git a/.github/workflows/.test-build.yml b/.github/workflows/.test-build.yml index c50da37f..8bf2d47d 100644 --- a/.github/workflows/.test-build.yml +++ b/.github/workflows/.test-build.yml @@ -573,6 +573,10 @@ jobs: output: image platforms: linux/amd64,linux/arm64 push: false + registry-identities: | + - type: dockerhub + username: dockereng + connection_id: ${{ vars.MISSING_DOCKERHUB_OIDC_CONNECTIONID }} meta-images: ghcr.io/docker/github-builder-test meta-tags: | type=raw,value=build-${{ github.run_id }} diff --git a/.github/workflows/bake.yml b/.github/workflows/bake.yml index 5733df72..eca294f1 100644 --- a/.github/workflows/bake.yml +++ b/.github/workflows/bake.yml @@ -195,6 +195,7 @@ jobs: registry-identities: uses: ./.github/workflows/setup-registry-identities.yml with: + registry-login: ${{ inputs.push && inputs.output == 'image' }} registry-identities: ${{ inputs.registry-identities }} prepare: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a9648419..02eab0c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -206,6 +206,7 @@ jobs: registry-identities: uses: ./.github/workflows/setup-registry-identities.yml with: + registry-login: ${{ inputs.push && inputs.output == 'image' }} registry-identities: ${{ inputs.registry-identities }} prepare: diff --git a/.github/workflows/setup-registry-identities.yml b/.github/workflows/setup-registry-identities.yml index 56f57895..0043d843 100644 --- a/.github/workflows/setup-registry-identities.yml +++ b/.github/workflows/setup-registry-identities.yml @@ -7,6 +7,11 @@ on: type: string description: "Keyless registry identity configuration as YAML objects" required: false + registry-login: + type: boolean + description: "Whether registry login is enabled" + required: false + default: true outputs: aws-ecr-enabled: description: "Whether an AWS ECR registry identity was configured" @@ -86,8 +91,15 @@ jobs: INPUT_RUNTIME-MODULE: ${{ env.RUNTIME_MODULE }} INPUT_RUNTIME-INSTALL-ARGS: ${{ env.RUNTIME_INSTALL_ARGS }} INPUT_REGISTRY-IDENTITIES: ${{ inputs.registry-identities }} + INPUT_REGISTRY-LOGIN: ${{ inputs.registry-login }} with: script: | + const registryLogin = core.getBooleanInput('registry-login'); + if (!registryLogin) { + core.info('Registry authentication not required; skipping npm install'); + return; + } + const registryIdentities = core.getInput('registry-identities', {trimWhitespace: false}); if (!registryIdentities.trim()) { core.info('No registry identities configured; skipping npm install'); @@ -115,6 +127,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_REGISTRY-IDENTITIES: ${{ inputs.registry-identities }} + INPUT_REGISTRY-LOGIN: ${{ inputs.registry-login }} with: script: | const setEmptyOutputs = () => { @@ -133,6 +146,13 @@ jobs: core.setOutput('dockerhub-oidc-connection-id', ''); }; + const registryLogin = core.getBooleanInput('registry-login'); + if (!registryLogin) { + core.info('Registry authentication not required; skipping validation'); + setEmptyOutputs(); + return; + } + const registryIdentities = core.getInput('registry-identities', {trimWhitespace: false}); if (!registryIdentities.trim()) { core.info('No registry identities configured; skipping validation'); diff --git a/README.md b/README.md index 9482ddba..6572d34b 100644 --- a/README.md +++ b/README.md @@ -408,7 +408,8 @@ build, which is the SLSA isolation concern described in The `registry-identities` input configures keyless registry authentication from non-secret identity metadata. Do not put passwords, tokens, client secrets, private keys, raw cloud credential JSON, or other secret values in this input. -The workflow validates the YAML before any build work starts. +When an image push or verification needs registry access, the workflow validates +the YAML before any build work starts. `registry-identities` can be combined with the existing `registry-auths` secret. Provider-specific authentication steps are pinned in these reusable From 4166571ec58f962e601292afa96f786a9daf429c Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:37:17 +0200 Subject: [PATCH 2/3] setup-registry-identities: report input errors cleanly Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .../workflows/setup-registry-identities.yml | 176 +++++++++--------- README.md | 38 ++-- 2 files changed, 111 insertions(+), 103 deletions(-) diff --git a/.github/workflows/setup-registry-identities.yml b/.github/workflows/setup-registry-identities.yml index 0043d843..d3f3f610 100644 --- a/.github/workflows/setup-registry-identities.yml +++ b/.github/workflows/setup-registry-identities.yml @@ -170,11 +170,13 @@ jobs: const fail = message => { throw new Error(`Invalid registry-identities input: ${message}`); }; + const ensureObject = (value, path) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { fail(`${path} must be an object`); } }; + const requireString = (entry, key, path) => { const value = entry[key]; if (typeof value !== 'string' || !value.trim()) { @@ -182,104 +184,110 @@ 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); - } catch (error) { - fail(error.message); - } - if (parsed === null || parsed === undefined) { - setEmptyOutputs(); - return; - } + let parsed; + try { + parsed = yaml.load(registryIdentities); + } catch (error) { + fail(error.message); + } + if (parsed === null || parsed === undefined) { + setEmptyOutputs(); + return; + } - const entries = Array.isArray(parsed) ? parsed : [parsed]; - if (entries.length === 0) { - setEmptyOutputs(); - return; - } + const entries = Array.isArray(parsed) ? parsed : [parsed]; + if (entries.length === 0) { + setEmptyOutputs(); + return; + } - let awsEcr; - let gcpWif; - let dockerhubOidc; - entries.forEach((entry, index) => { - const path = `registry-identities[${index}]`; - ensureObject(entry, path); - const type = requireString(entry, 'type', path); - switch (type) { - case 'aws-ecr': { - const allowedKeys = new Set(['type', 'registry', 'role-to-assume', 'region']); - for (const key of Object.keys(entry)) { - if (!allowedKeys.has(key)) { - fail(`${path}.${key} is not supported for aws-ecr`); + let awsEcr; + let gcpWif; + let dockerhubOidc; + entries.forEach((entry, index) => { + const path = `registry-identities[${index}]`; + ensureObject(entry, path); + const type = requireString(entry, 'type', path); + switch (type) { + case 'aws-ecr': { + const allowedKeys = new Set(['type', 'registry', 'role-to-assume', 'region']); + for (const key of Object.keys(entry)) { + if (!allowedKeys.has(key)) { + fail(`${path}.${key} is not supported for aws-ecr`); + } } - } - if (awsEcr) { - fail('only one aws-ecr registry identity is supported'); - } - awsEcr = { - registry: requireString(entry, 'registry', path), - roleToAssume: requireString(entry, 'role-to-assume', path), - region: requireString(entry, 'region', path) - }; - break; - } - case 'gcp-wif': { - const allowedKeys = new Set(['type', 'registry', 'workload_identity_provider', 'service_account', 'project_id']); - for (const key of Object.keys(entry)) { - if (!allowedKeys.has(key)) { - fail(`${path}.${key} is not supported for gcp-wif`); + if (awsEcr) { + fail('only one aws-ecr registry identity is supported'); } + awsEcr = { + registry: requireString(entry, 'registry', path), + roleToAssume: requireString(entry, 'role-to-assume', path), + region: requireString(entry, 'region', path) + }; + break; } - if (gcpWif) { - fail('only one gcp-wif registry identity is supported'); - } - gcpWif = { - registry: requireString(entry, 'registry', path), - workloadIdentityProvider: requireString(entry, 'workload_identity_provider', path), - serviceAccount: requireString(entry, 'service_account', path), - projectId: requireString(entry, 'project_id', path) - }; - 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`); + case 'gcp-wif': { + const allowedKeys = new Set(['type', 'registry', 'workload_identity_provider', 'service_account', 'project_id']); + for (const key of Object.keys(entry)) { + if (!allowedKeys.has(key)) { + fail(`${path}.${key} is not supported for gcp-wif`); + } + } + if (gcpWif) { + fail('only one gcp-wif registry identity is supported'); } + gcpWif = { + registry: requireString(entry, 'registry', path), + workloadIdentityProvider: requireString(entry, 'workload_identity_provider', path), + serviceAccount: requireString(entry, 'service_account', path), + projectId: optionalString(entry, 'project_id', path, '') + }; + break; } - if (dockerhubOidc) { - fail('only one dockerhub registry identity is supported'); + 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; } - 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}`); } - default: - fail(`${path}.type has unsupported provider ${type}`); - } - }); + }); - core.setOutput('aws-ecr-enabled', awsEcr ? 'true' : 'false'); - core.setOutput('aws-ecr-registry', awsEcr?.registry || ''); - core.setOutput('aws-ecr-role-to-assume', awsEcr?.roleToAssume || ''); - core.setOutput('aws-ecr-region', awsEcr?.region || ''); - core.setOutput('gcp-wif-enabled', gcpWif ? 'true' : 'false'); - core.setOutput('gcp-wif-registry', gcpWif?.registry || ''); - 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 || ''); + core.setOutput('aws-ecr-enabled', awsEcr ? 'true' : 'false'); + core.setOutput('aws-ecr-registry', awsEcr?.registry || ''); + core.setOutput('aws-ecr-role-to-assume', awsEcr?.roleToAssume || ''); + core.setOutput('aws-ecr-region', awsEcr?.region || ''); + core.setOutput('gcp-wif-enabled', gcpWif ? 'true' : 'false'); + core.setOutput('gcp-wif-registry', gcpWif?.registry || ''); + 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 || ''); + } catch (error) { + core.setFailed(error.message || error); + } diff --git a/README.md b/README.md index 6572d34b..279b8f65 100644 --- a/README.md +++ b/README.md @@ -445,12 +445,12 @@ jobs: 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. | +| Name | Type | Required | Description | +|-----------------|--------|----------|-----------------------------------------------------------------------------| +| `type` | String | Yes | Registry identity provider type. Must be `dockerhub`. | +| `registry` | String | No | Registry hostname passed to `docker/login-action`. Defaults to `docker.io`. | +| `username` | String | Yes | Docker Hub username or organization passed to `docker/login-action`. | +| `connection_id` | String | Yes | 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 @@ -483,12 +483,12 @@ jobs: region: us-east-1 ``` -| Name | Type | Description | -|------------------|--------|---------------------------------------------------------------------------------------------------------------------------------| -| `type` | String | Registry identity provider type. Must be `aws-ecr`. | -| `registry` | String | Registry server passed to `docker/login-action`, such as `public.ecr.aws` for public ECR or a private ECR host for private ECR. | -| `role-to-assume` | String | IAM role ARN assumed through GitHub OIDC. | -| `region` | String | AWS region passed to `aws-actions/configure-aws-credentials` when assuming the role. Use `us-east-1` for ECR Public. | +| Name | Type | Required | Description | +|------------------|--------|----------|---------------------------------------------------------------------------------------------------------------------------------| +| `type` | String | Yes | Registry identity provider type. Must be `aws-ecr`. | +| `registry` | String | Yes | Registry server passed to `docker/login-action`, such as `public.ecr.aws` for public ECR or a private ECR host for private ECR. | +| `role-to-assume` | String | Yes | IAM role ARN assumed through GitHub OIDC. | +| `region` | String | Yes | AWS region passed to `aws-actions/configure-aws-credentials` when assuming the role. Use `us-east-1` for ECR Public. | The `registry` value is required for AWS ECR. Use the registry server that `docker/login-action` should log in to, such as `public.ecr.aws` for public ECR @@ -520,13 +520,13 @@ jobs: project_id: my-project ``` -| Name | Type | Description | -|------------------------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | String | Registry identity provider type. Must be `gcp-wif`. | -| `registry` | String | Google Artifact Registry host passed to `docker/login-action`, such as `us-docker.pkg.dev`. Do not use the full repository path. | -| `workload_identity_provider` | String | Workload Identity Provider resource name passed to `google-github-actions/auth`. | -| `service_account` | String | Service account email passed to `google-github-actions/auth` for Workload Identity Federation. | -| `project_id` | String | Google Cloud project ID passed to `google-github-actions/auth`. | +| Name | Type | Required | Description | +|------------------------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | String | Yes | Registry identity provider type. Must be `gcp-wif`. | +| `registry` | String | Yes | Google Artifact Registry host passed to `docker/login-action`, such as `us-docker.pkg.dev`. Do not use the full repository path. | +| `workload_identity_provider` | String | Yes | Workload Identity Provider resource name passed to `google-github-actions/auth`. | +| `service_account` | String | Yes | Service account email passed to `google-github-actions/auth` for Workload Identity Federation. | +| `project_id` | String | No | Google Cloud project ID passed to `google-github-actions/auth` when provided. | The `registry` value is required for GCP WIF. Use the Artifact Registry host that `docker/login-action` should log in to, such as `us-docker.pkg.dev`, not From 874b69b7cc104b5103a211827463e3a3b2110ca9 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:06:12 +0200 Subject: [PATCH 3/3] setup-registry-identities: log validation decisions Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/setup-registry-identities.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/setup-registry-identities.yml b/.github/workflows/setup-registry-identities.yml index d3f3f610..71060851 100644 --- a/.github/workflows/setup-registry-identities.yml +++ b/.github/workflows/setup-registry-identities.yml @@ -106,6 +106,7 @@ jobs: return; } + core.info('Registry identities configured; installing validation dependencies'); const npmArgs = ['install', ...core.getMultilineInput('runtime-install-args'), core.getInput('runtime-module')]; const maxAttempts = 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { @@ -200,15 +201,18 @@ jobs: fail(error.message); } if (parsed === null || parsed === undefined) { + core.info('Registry identities input is empty after parsing; disabling registry identity outputs'); setEmptyOutputs(); return; } const entries = Array.isArray(parsed) ? parsed : [parsed]; if (entries.length === 0) { + core.info('No registry identity entries parsed; disabling registry identity outputs'); setEmptyOutputs(); return; } + core.info(`Validating ${entries.length} registry identity ${entries.length === 1 ? 'entry' : 'entries'}`); let awsEcr; let gcpWif; @@ -233,6 +237,7 @@ jobs: roleToAssume: requireString(entry, 'role-to-assume', path), region: requireString(entry, 'region', path) }; + core.info(`Configured aws-ecr registry identity for ${awsEcr.registry}`); break; } case 'gcp-wif': { @@ -251,6 +256,7 @@ jobs: serviceAccount: requireString(entry, 'service_account', path), projectId: optionalString(entry, 'project_id', path, '') }; + core.info(`Configured gcp-wif registry identity for ${gcpWif.registry}`); break; } case 'dockerhub': { @@ -268,6 +274,7 @@ jobs: username: requireString(entry, 'username', path), connectionID: requireString(entry, 'connection_id', path) }; + core.info(`Configured dockerhub registry identity for ${dockerhubOidc.registry}`); break; } default: