From 0136eeab639595689ad6ca76c68e7acf82ecfc5d Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:56:37 +0200 Subject: [PATCH] build/bake: secrets support Accept a shared YAML build-secrets mapping and pass values through private temporary files. Scope Git credentials to the build action and remove secret files after the build, including on failure. Override only declared Bake secrets in the resolved target graph using Buildx secret source overrides. Cover multiline values and replacement of environment and file sources in the workflow fixtures. Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/.test-bake.yml | 21 +++ .github/workflows/.test-build.yml | 19 +++ .github/workflows/bake.yml | 212 ++++++++++++++++++------------ .github/workflows/build.yml | 181 ++++++++++++++----------- README.md | 66 +++++++++- test/docker-bake.hcl | 10 ++ test/secret.Dockerfile | 14 ++ 7 files changed, 355 insertions(+), 168 deletions(-) create mode 100644 test/secret.Dockerfile diff --git a/.github/workflows/.test-bake.yml b/.github/workflows/.test-bake.yml index dfd5fcd0..444420ec 100644 --- a/.github/workflows/.test-bake.yml +++ b/.github/workflows/.test-bake.yml @@ -477,6 +477,27 @@ jobs: const builderOutputs = JSON.parse(core.getInput('builder-outputs')); core.info(JSON.stringify(builderOutputs, null, 2)); + bake-secret: + uses: ./.github/workflows/bake.yml + permissions: + contents: read + id-token: write + with: + artifact-upload: false + context: test + output: local + target: foosec + secrets: + build-secrets: |+ + fixture.plain: | + alpha-line + beta-line + foosec: + fixture.json: ${{ toJSON(format('gamma-line{0}delta-line{0}', fromJSON('"\n"'))) }} + fixture_fuu: standard-secret + fixture_keep: |+ + keep-line + bake-set-runner: uses: ./.github/workflows/bake.yml permissions: diff --git a/.github/workflows/.test-build.yml b/.github/workflows/.test-build.yml index 804204b2..e50863cc 100644 --- a/.github/workflows/.test-build.yml +++ b/.github/workflows/.test-build.yml @@ -523,6 +523,25 @@ jobs: const builderOutputs = JSON.parse(core.getInput('builder-outputs')); core.info(JSON.stringify(builderOutputs, null, 2)); + build-secret: + uses: ./.github/workflows/build.yml + permissions: + contents: read + id-token: write + with: + artifact-upload: false + file: test/secret.Dockerfile + output: local + secrets: + build-secrets: |+ + fixture.plain: | + alpha-line + beta-line + fixture.json: ${{ toJSON(format('gamma-line{0}delta-line{0}', fromJSON('"\n"'))) }} + fixture_fuu: standard-secret + fixture_keep: |+ + keep-line + build-set-runner: uses: ./.github/workflows/build.yml permissions: diff --git a/.github/workflows/bake.yml b/.github/workflows/bake.yml index 2afed834..1d71eccf 100644 --- a/.github/workflows/bake.yml +++ b/.github/workflows/bake.yml @@ -149,6 +149,9 @@ on: registry-auths: description: "Raw authentication to registries, defined as YAML objects (for image output)" required: false + build-secrets: + description: "YAML object mapping BuildKit secret IDs to values, with optional nested target mappings" + required: false github-token: description: "GitHub Token used to authenticate against the repository for Git context" required: false @@ -215,6 +218,7 @@ jobs: metaImages: ${{ steps.set.outputs.metaImages }} sign: ${{ steps.set.outputs.sign }} privateRepo: ${{ steps.set.outputs.privateRepo }} + targets: ${{ steps.set.outputs.targets }} ghaCacheSign: ${{ steps.set.outputs.ghaCacheSign }} proxyNetwork: ${{ steps.set.outputs.proxyNetwork }} steps: @@ -400,7 +404,7 @@ jobs: } ); await core.group(`Set envs`, async () => { - core.info(JSON.stringify(envs, null, 2)); + core.info(JSON.stringify(Object.keys(envs).sort(), null, 2)); }); const metaImages = inpMetaImages.map(image => image.toLowerCase()); @@ -414,9 +418,15 @@ jobs: try { await core.group(`Validating definition`, async () => { const bake = new Bake(); + // Resolve the graph without local secret files. The build job validates + // secrets after applying the workflow-provided source overrides. + const validationOverrides = inpSet.filter(override => { + const key = override.split('=', 1)[0].split('.')[1]; + return !['secret', 'secrets', 'secrets+'].includes(key); + }); def = await bake.getDefinition({ files: inpFiles, - overrides: inpSet, + overrides: [...validationOverrides, '*.secrets='], sbom: inpSbom ? `generator=${inpSbomImage}` : 'false', source: bakeSource, targets: [inpTarget] @@ -426,8 +436,9 @@ jobs: if (!def) { throw new Error('Bake definition not set'); } - BakeTargets.resolve(def, inpTarget); + const targets = BakeTargets.resolve(def, inpTarget); target = inpTarget; + core.setOutput('targets', JSON.stringify(targets)); }); } catch (error) { core.setFailed(error); @@ -710,6 +721,82 @@ jobs: cosignPath, `${containerName}:/usr/bin/cosign` ]); + - + name: Configure AWS credentials + if: ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} + uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 + 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: ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + env: + AWS_ACCOUNT_IDS: ${{ needs.registry-identities.outputs.aws-ecr-account-ids }} + with: + registry-auth: | + - registry: ${{ needs.registry-identities.outputs.aws-ecr-registry }} + - + name: Authenticate to Google Cloud + id: gcp-wif-auth + if: ${{ 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: ${{ needs.registry-identities.outputs.gcp-wif-enabled == 'true' }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + 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: ${{ needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + 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: Authenticate to Azure + if: ${{ needs.registry-identities.outputs.azure-acr-enabled == 'true' }} + uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 + with: + client-id: ${{ needs.registry-identities.outputs.azure-acr-client-id }} + tenant-id: ${{ needs.registry-identities.outputs.azure-acr-tenant-id }} + subscription-id: ${{ needs.registry-identities.outputs.azure-acr-subscription-id }} + - + name: Login to Azure Container Registry + if: ${{ needs.registry-identities.outputs.azure-acr-enabled == 'true' }} + env: + ACR_REGISTRY: ${{ needs.registry-identities.outputs.azure-acr-registry }} + AZURE_CORE_OUTPUT: none + run: az acr login --name "${ACR_REGISTRY%.azurecr.io}" + - + name: Set up chainctl + if: ${{ needs.registry-identities.outputs.chainguard-enabled == 'true' }} + uses: chainguard-dev/setup-chainctl@2cddd35a2f120d9973e58094dc6878c93cf58c28 # v0.5.1 + with: + identity: ${{ needs.registry-identities.outputs.chainguard-identity }} + apk-host: ${{ needs.registry-identities.outputs.chainguard-apk-host }} + libraries-host: ${{ needs.registry-identities.outputs.chainguard-libraries-host }} + - + name: Login to registry + if: ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry-auth: ${{ secrets.registry-auths }} - name: Prepare id: prepare @@ -721,6 +808,8 @@ jobs: INPUT_CACHE: ${{ inputs.cache }} INPUT_CACHE-SCOPE: ${{ inputs.cache-scope }} INPUT_CACHE-MODE: ${{ inputs.cache-mode }} + INPUT_BUILD-SECRETS: ${{ secrets.build-secrets }} + INPUT_TARGETS: ${{ needs.prepare.outputs.targets }} INPUT_CONTEXT: ${{ inputs.context }} INPUT_FILES: ${{ inputs.files }} INPUT_OUTPUT: ${{ inputs.output }} @@ -737,15 +826,15 @@ jobs: INPUT_BAKE-FILE-TAGS: ${{ steps.meta.outputs.bake-file-tags }} INPUT_BAKE-FILE-ANNOTATIONS: ${{ steps.meta.outputs.bake-file-annotations }} INPUT_BAKE-FILE-LABELS: ${{ steps.meta.outputs.bake-file-labels }} - INPUT_GITHUB-TOKEN: ${{ secrets.github-token || github.token }} INPUT_BUILDKIT-PROXY-NETWORK: ${{ inputs.buildkit-proxy-network }} with: script: | const os = require('os'); const { Build } = require('@docker/github-builder-runtime/lib/buildx/build'); + const { BuildSecrets } = require('@docker/github-builder-runtime/lib/github-builder/build-secrets'); const { GitHub } = require('@docker/github-builder-runtime/lib/github/github'); const { Util } = require('@docker/github-builder-runtime/lib/util'); - + const inpPlatform = core.getInput('platform'); const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : ''; core.setOutput('platform-pair-suffix', platformPairSuffix); @@ -756,6 +845,8 @@ jobs: const inpCache = core.getBooleanInput('cache'); const inpCacheScope = core.getInput('cache-scope'); const inpCacheMode = core.getInput('cache-mode'); + const inpBuildSecrets = core.getInput('build-secrets', {trimWhitespace: false}); + const inpTargets = core.getInput('targets'); const inpContext = core.getInput('context'); const inpFiles = Util.getInputList('files'); const inpOutput = core.getInput('output'); @@ -764,7 +855,6 @@ jobs: const inpSet = Util.getInputList('set', {ignoreComma: true, quote: false}); const inpTarget = core.getInput('target'); const inpVars = Util.getInputList('vars'); - const inpGitHubToken = core.getInput('github-token'); const inpBuildkitProxyNetwork = core.getBooleanInput('buildkit-proxy-network'); const inpMetaImages = core.getMultilineInput('meta-images'); @@ -781,7 +871,7 @@ jobs: tags: inpMetaTags }; const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta}); - + const gitContextAttrs = GitHub.context.ref.startsWith('refs/tags/') ? {checksum: GitHub.context.sha} : {'fetch-by-commit': 'true'}; const bakeSource = await new Build().gitContext({subdir: inpContext, attrs: gitContextAttrs}); await core.group(`Set source output`, async () => { @@ -799,7 +889,16 @@ jobs: core.info(sbom); core.setOutput('sbom', sbom); }); - + + let buildSecrets; + try { + buildSecrets = BuildSecrets.prepareBake(inpBuildSecrets, inpTarget, JSON.parse(inpTargets || '[]')); + } catch (err) { + core.setFailed(err.message); + return; + } + core.setOutput('secret-dir', buildSecrets.directory); + const envs = Object.assign({}, inpVars ? inpVars.reduce((acc, curr) => { const idx = curr.indexOf('='); @@ -809,12 +908,15 @@ jobs: return acc; }, {}) : {}, { - BUILDKIT_MULTI_PLATFORM: '1', - BUILDX_BAKE_GIT_AUTH_TOKEN: inpGitHubToken + BUILDKIT_MULTI_PLATFORM: '1' } ); + + // Git authentication is supplied directly to the Bake action. + delete envs.BUILDX_BAKE_GIT_AUTH_TOKEN; + await core.group(`Set envs`, async () => { - core.info(JSON.stringify(envs, null, 2)); + core.info(JSON.stringify(Object.keys(envs).sort(), null, 2)); core.setOutput('envs', JSON.stringify(envs)); }); @@ -878,85 +980,10 @@ jobs: bakeOverrides.push(`*.cache-from=type=gha,scope=${inpCacheScope || inpTarget}${platformPairSuffix}${proxyNetworkSuffix}`); bakeOverrides.push(`*.cache-to=type=gha,ignore-error=true,scope=${inpCacheScope || inpTarget}${platformPairSuffix}${proxyNetworkSuffix},mode=${inpCacheMode}`); } + bakeOverrides.push(...buildSecrets.inputs); core.info(JSON.stringify(bakeOverrides, null, 2)); core.setOutput('overrides', bakeOverrides.join(os.EOL)); }); - - - name: Configure AWS credentials - if: ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 - 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: ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - env: - AWS_ACCOUNT_IDS: ${{ needs.registry-identities.outputs.aws-ecr-account-ids }} - with: - registry-auth: | - - registry: ${{ needs.registry-identities.outputs.aws-ecr-registry }} - - - name: Authenticate to Google Cloud - id: gcp-wif-auth - if: ${{ 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: ${{ needs.registry-identities.outputs.gcp-wif-enabled == 'true' }} - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - 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: ${{ needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }} - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - 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: Authenticate to Azure - if: ${{ needs.registry-identities.outputs.azure-acr-enabled == 'true' }} - uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 - with: - client-id: ${{ needs.registry-identities.outputs.azure-acr-client-id }} - tenant-id: ${{ needs.registry-identities.outputs.azure-acr-tenant-id }} - subscription-id: ${{ needs.registry-identities.outputs.azure-acr-subscription-id }} - - - name: Login to Azure Container Registry - if: ${{ needs.registry-identities.outputs.azure-acr-enabled == 'true' }} - env: - ACR_REGISTRY: ${{ needs.registry-identities.outputs.azure-acr-registry }} - AZURE_CORE_OUTPUT: none - run: az acr login --name "${ACR_REGISTRY%.azurecr.io}" - - - name: Set up chainctl - if: ${{ needs.registry-identities.outputs.chainguard-enabled == 'true' }} - uses: chainguard-dev/setup-chainctl@2cddd35a2f120d9973e58094dc6878c93cf58c28 # v0.5.1 - with: - identity: ${{ needs.registry-identities.outputs.chainguard-identity }} - apk-host: ${{ needs.registry-identities.outputs.chainguard-apk-host }} - libraries-host: ${{ needs.registry-identities.outputs.chainguard-libraries-host }} - - - name: Login to registry - if: ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry-auth: ${{ secrets.registry-auths }} - name: Build id: bake @@ -967,7 +994,18 @@ jobs: targets: ${{ steps.prepare.outputs.target }} sbom: ${{ steps.prepare.outputs.sbom }} set: ${{ steps.prepare.outputs.overrides }} + github-token: ${{ secrets.github-token || github.token }} env: ${{ fromJson(steps.prepare.outputs.envs || '{}') }} + - + name: Remove build secrets + if: ${{ always() && steps.prepare.outputs.secret-dir != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_SECRET-DIR: ${{ steps.prepare.outputs.secret-dir }} + with: + script: | + const { BuildSecrets } = require('@docker/github-builder-runtime/lib/github-builder/build-secrets'); + BuildSecrets.cleanup(core.getInput('secret-dir', {required: true})); - name: Get image digest id: get-image-digest diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54e8337d..c941ea81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,6 +160,9 @@ on: registry-auths: description: "Raw authentication to registries, defined as YAML objects (for image output)" required: false + build-secrets: + description: "YAML object mapping BuildKit secret IDs to secret values" + required: false github-token: description: "GitHub Token used to authenticate against the repository for Git context" required: false @@ -645,6 +648,82 @@ jobs: cosignPath, `${containerName}:/usr/bin/cosign` ]); + - + name: Configure AWS credentials + if: ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} + uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 + 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: ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + env: + AWS_ACCOUNT_IDS: ${{ needs.registry-identities.outputs.aws-ecr-account-ids }} + with: + registry-auth: | + - registry: ${{ needs.registry-identities.outputs.aws-ecr-registry }} + - + name: Authenticate to Google Cloud + id: gcp-wif-auth + if: ${{ 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: ${{ needs.registry-identities.outputs.gcp-wif-enabled == 'true' }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + 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: ${{ needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + 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: Authenticate to Azure + if: ${{ needs.registry-identities.outputs.azure-acr-enabled == 'true' }} + uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 + with: + client-id: ${{ needs.registry-identities.outputs.azure-acr-client-id }} + tenant-id: ${{ needs.registry-identities.outputs.azure-acr-tenant-id }} + subscription-id: ${{ needs.registry-identities.outputs.azure-acr-subscription-id }} + - + name: Login to Azure Container Registry + if: ${{ needs.registry-identities.outputs.azure-acr-enabled == 'true' }} + env: + ACR_REGISTRY: ${{ needs.registry-identities.outputs.azure-acr-registry }} + AZURE_CORE_OUTPUT: none + run: az acr login --name "${ACR_REGISTRY%.azurecr.io}" + - + name: Set up chainctl + if: ${{ needs.registry-identities.outputs.chainguard-enabled == 'true' }} + uses: chainguard-dev/setup-chainctl@2cddd35a2f120d9973e58094dc6878c93cf58c28 # v0.5.1 + with: + identity: ${{ needs.registry-identities.outputs.chainguard-identity }} + apk-host: ${{ needs.registry-identities.outputs.chainguard-apk-host }} + libraries-host: ${{ needs.registry-identities.outputs.chainguard-libraries-host }} + - + name: Login to registry + if: ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry-auth: ${{ secrets.registry-auths }} - name: Prepare id: prepare @@ -660,6 +739,7 @@ jobs: INPUT_CACHE-SCOPE: ${{ inputs.cache-scope }} INPUT_CACHE-MODE: ${{ inputs.cache-mode }} INPUT_BUILDKIT-PROXY-NETWORK: ${{ inputs.buildkit-proxy-network }} + INPUT_BUILD-SECRETS: ${{ secrets.build-secrets }} INPUT_LABELS: ${{ inputs.labels }} INPUT_CONTEXT: ${{ inputs.context }} INPUT_OUTPUT: ${{ inputs.output }} @@ -677,9 +757,10 @@ jobs: with: script: | const { Build } = require('@docker/github-builder-runtime/lib/buildx/build'); + const { BuildSecrets } = require('@docker/github-builder-runtime/lib/github-builder/build-secrets'); const { GitHub } = require('@docker/github-builder-runtime/lib/github/github'); const { Util } = require('@docker/github-builder-runtime/lib/util'); - + const inpPlatform = core.getInput('platform'); const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : ''; core.setOutput('platform-pair-suffix', platformPairSuffix); @@ -693,6 +774,7 @@ jobs: const inpCache = core.getBooleanInput('cache'); const inpCacheScope = core.getInput('cache-scope'); const inpCacheMode = core.getInput('cache-mode'); + const inpBuildSecrets = core.getInput('build-secrets', {trimWhitespace: false}); const inpContext = core.getInput('context'); const inpLabels = core.getInput('labels'); const inpOutput = core.getInput('output'); @@ -773,6 +855,16 @@ jobs: } core.setOutput('labels', labels.join('\n')); core.setOutput('build-args', buildArgs); + + let buildSecrets; + try { + buildSecrets = BuildSecrets.prepareBuild(inpBuildSecrets); + } catch (err) { + core.setFailed(err.message); + return; + } + core.setOutput('secret-dir', buildSecrets.directory); + core.setOutput('secret-files', buildSecrets.inputs.join('\n')); if (GitHub.context.payload.repository?.private ?? false) { // if this is a private repository, we set min provenance mode @@ -781,82 +873,6 @@ jobs: // for a public repository, we set max provenance mode core.setOutput('provenance', Build.resolveProvenanceAttrs(`mode=max,version=v1`)); } - - - name: Configure AWS credentials - if: ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 - 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: ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }} - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - env: - AWS_ACCOUNT_IDS: ${{ needs.registry-identities.outputs.aws-ecr-account-ids }} - with: - registry-auth: | - - registry: ${{ needs.registry-identities.outputs.aws-ecr-registry }} - - - name: Authenticate to Google Cloud - id: gcp-wif-auth - if: ${{ 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: ${{ needs.registry-identities.outputs.gcp-wif-enabled == 'true' }} - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - 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: ${{ needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }} - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - 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: Authenticate to Azure - if: ${{ needs.registry-identities.outputs.azure-acr-enabled == 'true' }} - uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 - with: - client-id: ${{ needs.registry-identities.outputs.azure-acr-client-id }} - tenant-id: ${{ needs.registry-identities.outputs.azure-acr-tenant-id }} - subscription-id: ${{ needs.registry-identities.outputs.azure-acr-subscription-id }} - - - name: Login to Azure Container Registry - if: ${{ needs.registry-identities.outputs.azure-acr-enabled == 'true' }} - env: - ACR_REGISTRY: ${{ needs.registry-identities.outputs.azure-acr-registry }} - AZURE_CORE_OUTPUT: none - run: az acr login --name "${ACR_REGISTRY%.azurecr.io}" - - - name: Set up chainctl - if: ${{ needs.registry-identities.outputs.chainguard-enabled == 'true' }} - uses: chainguard-dev/setup-chainctl@2cddd35a2f120d9973e58094dc6878c93cf58c28 # v0.5.1 - with: - identity: ${{ needs.registry-identities.outputs.chainguard-identity }} - apk-host: ${{ needs.registry-identities.outputs.chainguard-apk-host }} - libraries-host: ${{ needs.registry-identities.outputs.chainguard-libraries-host }} - - - name: Login to registry - if: ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry-auth: ${{ secrets.registry-auths }} - name: Build id: build @@ -874,12 +890,23 @@ jobs: provenance: ${{ steps.prepare.outputs.provenance }} sbom: ${{ steps.prepare.outputs.sbom }} secret-envs: GIT_AUTH_TOKEN=GIT_AUTH_TOKEN + secret-files: ${{ steps.prepare.outputs.secret-files }} shm-size: ${{ inputs.shm-size }} target: ${{ inputs.target }} ulimit: ${{ inputs.ulimit }} env: BUILDKIT_MULTI_PLATFORM: 1 GIT_AUTH_TOKEN: ${{ secrets.github-token || github.token }} + - + name: Remove build secrets + if: ${{ always() && steps.prepare.outputs.secret-dir != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_SECRET-DIR: ${{ steps.prepare.outputs.secret-dir }} + with: + script: | + const { BuildSecrets } = require('@docker/github-builder-runtime/lib/github-builder/build-secrets'); + BuildSecrets.cleanup(core.getInput('secret-dir', {required: true})); - name: Login to registry for signing if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }} diff --git a/README.md b/README.md index 5ddd155b..12664efb 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ ___ * [Outputs](#outputs-1) * [Notes](#notes) * [BuildKit proxy network](#buildkit-proxy-network) + * [Build secrets](#build-secrets) * [Signed GitHub Actions cache](#signed-github-actions-cache) * [Registry identities](#registry-identities) * [Docker Hub OIDC](#docker-hub-oidc) @@ -262,6 +263,7 @@ jobs: | Name | Default | Description | |------------------|-----------------------|--------------------------------------------------------------------------------| | `registry-auths` | | Raw authentication to registries, defined as YAML objects (for `image` output) | +| `build-secrets` | | YAML object mapping BuildKit secret IDs to secret values | | `github-token` | `${{ github.token }}` | GitHub Token used to authenticate against the repository for Git context | ### Outputs @@ -372,10 +374,11 @@ jobs: ### Secrets -| Name | Default | Description | -|------------------|-----------------------|--------------------------------------------------------------------------------| -| `registry-auths` | | Raw authentication to registries, defined as YAML objects (for `image` output) | -| `github-token` | `${{ github.token }}` | GitHub Token used to authenticate against the repository for Git context | +| Name | Default | Description | +|------------------|-----------------------|-----------------------------------------------------------------------------------------| +| `registry-auths` | | Raw authentication to registries, defined as YAML objects (for `image` output) | +| `build-secrets` | | YAML object mapping BuildKit secret IDs to values, with optional nested target mappings | +| `github-token` | `${{ github.token }}` | GitHub Token used to authenticate against the repository for Git context | ### Outputs @@ -425,6 +428,61 @@ proxy. Enabling `buildkit-proxy-network` replaces Docker's predefined proxy build arguments for affected `RUN` operations and can bypass an application-level organizational proxy. See BuildKit's [proxy network documentation](https://github.com/moby/buildkit/blob/master/docs/proxy.md). +### Build secrets + +Both workflows accept `build-secrets` as a YAML object mapping BuildKit secret +IDs to values, not caller workspace paths. Bake requires matching secrets to be +declared in `docker-bake.hcl`: + +```hcl +target "default" { + secret = [ + "id=npm.token,env=NPM_TOKEN", + "id=aws.credentials,src=./aws-credentials", + "id=inline_config,env=INLINE_CONFIG", + ] +} +``` + +Then pass their values to the reusable workflow: + +```yaml +secrets: + build-secrets: | + npm.token: ${{ toJSON(secrets.NPM_TOKEN) }} + aws.credentials: ${{ toJSON(secrets.AWS_CREDENTIALS) }} + inline_config: | + first line + second line +``` + +Use `toJSON(...)` for GitHub secrets to preserve multiline values and YAML-sensitive +characters. For literal values with trailing blank lines, use `|+` on both the +outer `build-secrets` block and the inner value. + +In Bake, string values apply to the selected `target`. Nested mappings explicitly +select a target in the resolved build graph. For the same definition above: + +```yaml +with: + target: default +secrets: + build-secrets: | + npm.token: ${{ toJSON(secrets.NPM_TOKEN) }} + default: + aws.credentials: ${{ toJSON(secrets.AWS_CREDENTIALS) }} +``` + +Dots are always part of the secret ID, never target separators. Nested target +mappings are only supported by the bake workflow. IDs cannot be empty or contain +line breaks or `=`; the build workflow also rejects commas, double quotes, and +leading or trailing whitespace. + +Values are passed through private temporary files, not the job environment. +For Bake, these override the declared file or environment sources without adding +new secrets. Files are created after registry authentication and cleaned up after +the build, including on failure. Abrupt runner termination can prevent cleanup. + ### Signed GitHub Actions cache When the workflow has GitHub OIDC available through `id-token: write`, BuildKit diff --git a/test/docker-bake.hcl b/test/docker-bake.hcl index a782ca74..a69517f6 100644 --- a/test/docker-bake.hcl +++ b/test/docker-bake.hcl @@ -42,6 +42,16 @@ target "proxy-network" { dockerfile = "proxy-network.Dockerfile" } +target "foosec" { + dockerfile = "secret.Dockerfile" + secret = [ + "id=fixture.plain,env=FIXTURE_PLAIN", + "id=fixture.json,src=./fixture-json.txt", + "id=fixture_fuu,env=FIXTURE_FUU", + "id=fixture_keep,env=FIXTURE_KEEP", + ] +} + target "go-cross-with-contexts" { inherits = ["go-cross"] contexts = { diff --git a/test/secret.Dockerfile b/test/secret.Dockerfile new file mode 100644 index 00000000..90e2e2ec --- /dev/null +++ b/test/secret.Dockerfile @@ -0,0 +1,14 @@ +# syntax=docker/dockerfile:1 + +FROM alpine +RUN --mount=type=secret,id=fixture.plain,env=fixture_plain \ + --mount=type=secret,id=fixture.json,env=fixture_json \ + --mount=type=secret,id=fixture_fuu,env=fixture_fuu \ + --mount=type=secret,id=fixture_keep,env=fixture_keep \ + printf 'alpha-line\nbeta-line\n' > /tmp/expected && \ + printf '%s' "$fixture_plain" | cmp - /tmp/expected && \ + printf 'gamma-line\ndelta-line\n' > /tmp/expected-json && \ + printf '%s' "$fixture_json" | cmp - /tmp/expected-json && \ + test "$fixture_fuu" = 'standard-secret' && \ + printf 'keep-line\n\n' > /tmp/expected-keep && \ + printf '%s' "$fixture_keep" | cmp - /tmp/expected-keep