Skip to content

Add support for BuildKit secrets in imageBuild method #57

@SpeedySH

Description

@SpeedySH

This PR adds support for passing build secrets to the imageBuild method using BuildKit's --mount type=secret feature. Currently, the method lacks this capability, forcing users to rely on less secure alternatives like build arguments.

Problem

  • Users cannot use Docker BuildKit secrets (--mount type=secret) when building images through this library
  • Sensitive data (API keys, credentials, SSH keys) cannot be securely passed during build without exposing them in the image history
  • The only current workaround is using buildargs, which stores values in the image metadata

Solution

Add a new optional parameter secrets to the imageBuild method that accepts secret configurations and passes them to the Docker daemon via the appropriate API headers.

Changes

API Changes

Add new parameter to imageBuild options:

secrets?: Record<string, string>;

Implementation Details

  1. New parameter: secrets - a Record mapping secret names to their values

    • Example: { "github_token": "ghp_...", "ssh_key": "..." }
  2. Header encoding: Secrets should be passed via X-Registry-Config or a new X-BuildKit-Secrets header (following Docker API conventions)

  3. Dockerfile usage: Allows the Dockerfile to mount secrets like:

    RUN --mount=type=secret,id=github_token cat /run/secrets/github_token > ~/.credentials

Code Changes

public imageBuild(
    buildContext: ReadableStream,
    options?: {
        dockerfile?: string;
        tag?: string;
        extrahosts?: string;
        remote?: string;
        quiet?: boolean;
        nocache?: boolean;
        cachefrom?: string;
        pull?: string;
        rm?: boolean;
        forcerm?: boolean;
        memory?: number;
        memswap?: number;
        cpushares?: number;
        cpusetcpus?: string;
        cpuperiod?: number;
        cpuquota?: number;
        buildargs?: string;
        shmsize?: number;
        squash?: boolean;
        labels?: string;
        networkmode?: string;
        credentials?: Record<string, AuthConfig>;
        platform?: string;
        target?: string;
        outputs?: string;
        version?: '1' | '2';
        secrets?: Record<string, string>;  // NEW
    },
): JSONMessages<JSONMessage, string> {
    const headers: Record<string, string> = {};
    headers['Content-Type'] = 'application/x-tar';

    if (options?.credentials) {
        headers['X-Registry-Config'] = this.authCredentials(
            options.credentials,
        );
    }

    // NEW: Handle secrets
    if (options?.secrets) {
        headers['X-BuildKit-Secrets'] = Buffer.from(
            JSON.stringify(options.secrets)
        ).toString('base64');
    }

    const request = this.api.post(
        '/build',
        {
            dockerfile: options?.dockerfile,
            t: options?.tag,
            extrahosts: options?.extrahosts,
            remote: options?.remote,
            q: options?.quiet,
            nocache: options?.nocache,
            cachefrom: options?.cachefrom,
            pull: options?.pull,
            rm: options?.rm,
            forcerm: options?.forcerm,
            memory: options?.memory,
            memswap: options?.memswap,
            cpushares: options?.cpushares,
            cpusetcpus: options?.cpusetcpus,
            cpuperiod: options?.cpuperiod,
            cpuquota: options?.cpuquota,
            buildargs: options?.buildargs,
            shmsize: options?.shmsize,
            squash: options?.squash,
            labels: options?.labels,
            networkmode: options?.networkmode,
            platform: options?.platform,
            target: options?.target,
            outputs: options?.outputs,
            version: options?.version || '2',
            secrets: options?.secrets,  // NEW
        },
        buildContext,
        headers,
    );

    return {
        messages: async function* (): AsyncGenerator<
            types.JSONMessage,
            void,
            undefined
        > {
            const response = await request;
            yield* jsonMessages<types.JSONMessage>(response);
        },
        wait: async function (): Promise<string> {
            let id = '';
            const response = await request;
            for await (const message of jsonMessages<types.JSONMessage>(
                response,
            )) {
                if (message.errorDetail) {
                    throw new Error(message.errorDetail?.message);
                }
                if (message.id === 'moby.image.id') {
                    id = message?.aux?.ID || '';
                }
            }
            return id;
        },
    };
}

Usage Example

const secrets = {
    github_token: process.env.GITHUB_TOKEN,
    npm_token: process.env.NPM_TOKEN,
    ssh_key: fs.readFileSync('/path/to/id_rsa', 'utf-8'),
};

const build = docker.imageBuild(tarStream, {
    tag: 'myapp:latest',
    secrets: secrets,
    version: '2',  // BuildKit required
});

const imageId = await build.wait();

Dockerfile:

FROM node:18

RUN --mount=type=secret,id=github_token \
    --mount=type=secret,id=npm_token \
    cat /run/secrets/github_token > /tmp/token && \
    npm install

# Secrets are NOT included in the final image layer

Security Benefits

  • ✅ Secrets are NOT stored in image layers or history
  • ✅ Secrets are only available during build time in /run/secrets/
  • ✅ No exposure in docker history or image inspection
  • ✅ Follows Docker/Moby API standards

Backwards Compatibility

  • ✅ Fully backwards compatible - secrets parameter is optional
  • ✅ Requires BuildKit (version: '2') - will be ignored in BuildKit v1

Testing

it('should pass secrets to build', async () => {
    const secrets = { 'github_token': 'test-token' };
    
    docker.imageBuild(tarStream, {
        tag: 'test:latest',
        secrets: secrets,
        version: '2',
    });
    
    // Verify headers contain encoded secrets
    expect(mockRequest.headers['X-BuildKit-Secrets']).toBeDefined();
});

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions