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