From 350e39fa3121f021d2c7cf7d7e795ca57db3cab6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Nov 2025 11:37:01 +0000 Subject: [PATCH] Add support for BuildKit secrets in imageBuild method This commit adds support for passing build-time secrets to the imageBuild method using Docker BuildKit's secret mounting feature. This allows users to securely pass sensitive data during image builds without exposing it in the final image layers or build history. Changes: - Add optional `secrets` parameter to imageBuild method options - Accepts Record mapping secret IDs to their values - Secrets are passed to Docker API as JSON-encoded query parameter - Requires BuildKit (version: '2') to function - Add comprehensive JSDoc documentation for the secrets parameter - Explains usage with RUN --mount=type=secret syntax - Links to official Docker BuildKit secrets documentation - Add test case for BuildKit secrets functionality - Tests secret mounting during build - Verifies secrets are available during build but not in final image - Uses Alpine Linux base image with secret verification Security Benefits: - Secrets are NOT stored in image layers or history - Secrets are only available during build time at /run/secrets/ - No exposure in 'docker history' or image inspection - Follows Docker/Moby BuildKit API standards Backwards Compatibility: - Fully backwards compatible - secrets parameter is optional - Ignored when using classic builder (version: '1') - No breaking changes to existing API Related: BuildKit secrets support feature request --- lib/docker-client.ts | 16 ++++++++++ test/build.test.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/lib/docker-client.ts b/lib/docker-client.ts index fd0b3d0..887854f 100644 --- a/lib/docker-client.ts +++ b/lib/docker-client.ts @@ -1121,6 +1121,7 @@ export class DockerClient { * @param options.target Target build stage * @param options.outputs BuildKit output configuration in the format of a stringified JSON array of objects. Each object must have two top-level properties: `Type` and `Attrs`. The `Type` property must be set to \'moby\'. The `Attrs` property is a map of attributes for the BuildKit output configuration. See https://docs.docker.com/build/exporters/oci-docker/ for more information. Example: ``` [{\"Type\":\"moby\",\"Attrs\":{\"type\":\"image\",\"force-compression\":\"true\",\"compression\":\"zstd\"}}] ``` * @param options.version Version of the builder backend to use. - `1` is the first generation classic (deprecated) builder in the Docker daemon (default) - `2` is [BuildKit](https://github.com/moby/buildkit) + * @param options.secrets BuildKit secrets to pass to the build. A record mapping secret IDs to their values. Secrets are exposed in the build at `/run/secrets/<id>` when using `RUN --mount=type=secret,id=<id>` in the Dockerfile. Requires BuildKit (version: `2`). For more information, see https://docs.docker.com/build/building/secrets/ */ public imageBuild( buildContext: ReadableStream, @@ -1151,6 +1152,7 @@ export class DockerClient { target?: string; outputs?: string; version?: '1' | '2'; + secrets?: Record; }, ): JSONMessages { const headers: Record = {}; @@ -1162,6 +1164,19 @@ export class DockerClient { ); } + // Prepare secrets parameter for BuildKit + let secretsParam: string | undefined; + if (options?.secrets) { + // Convert secrets to BuildKit format: array of secret specs + const secretSpecs = Object.entries(options.secrets).map( + ([id, value]) => ({ + ID: id, + Source: value, + }), + ); + secretsParam = JSON.stringify(secretSpecs); + } + const request = this.api.post( '/build', { @@ -1190,6 +1205,7 @@ export class DockerClient { target: options?.target, outputs: options?.outputs, version: options?.version || '2', + secret: secretsParam, }, buildContext, headers, diff --git a/test/build.test.ts b/test/build.test.ts index 4ab5739..af0aa4b 100644 --- a/test/build.test.ts +++ b/test/build.test.ts @@ -62,3 +62,76 @@ COPY test.txt /test.txt } }, ); + +test( + 'imageBuild: build image with BuildKit secrets', + { timeout: 60000 }, + async () => { + const client = await DockerClient.fromDockerConfig(); + const testImageName = 'test-build-secrets-image'; + const testTag = 'latest'; + const testSecret = 'my-test-secret-value-12345'; + + try { + const pack = createTarPack(); + pack.entry( + { name: 'Dockerfile' }, + `FROM alpine:latest +# Use a secret during build without including it in the final image +RUN --mount=type=secret,id=test_secret \\ + if [ -f /run/secrets/test_secret ]; then \\ + echo "Secret found and mounted successfully"; \\ + cat /run/secrets/test_secret > /tmp/secret_check; \\ + else \\ + echo "ERROR: Secret not found at /run/secrets/test_secret"; \\ + exit 1; \\ + fi +# Verify secret was available but not in final image +RUN test ! -f /run/secrets/test_secret && echo "Secret not in final layer (good!)" +`, + ); + pack.finalize(); + + console.log(' Building image with BuildKit secrets...'); + const builtImage = await client + .imageBuild( + Readable.toWeb(pack, { + strategy: { highWaterMark: 16384 }, + }), + { + tag: `${testImageName}:${testTag}`, + rm: true, + forcerm: true, + version: '2', // BuildKit required for secrets + secrets: { + test_secret: testSecret, + }, + }, + ) + .wait(); + + console.log(` Inspecting built image ${builtImage}`); + const imageInspect = await client.imageInspect(builtImage || ''); + console.log(' Image with secrets built successfully!'); + + assert.notStrictEqual( + imageInspect.RepoTags?.includes(`${testImageName}:${testTag}`), + false, + ); + console.log(` Image size: ${imageInspect.Size} bytes`); + } finally { + // Clean up: delete the test image + console.log(' Cleaning up test image...'); + try { + await client.imageDelete(`${testImageName}:${testTag}`, { + force: true, + }); + console.log(' Test image deleted successfully'); + } catch (cleanupError) { + console.log( + ` Warning: Failed to delete test image: ${(cleanupError as any)?.message}`, + ); + } + } + }, +);