本文主要通过如何在 Docker Build 时使用 SSH 私钥进行认证,比如拉取私有仓库时就很有用。包括 18.09 版本之前的使用多阶段构建方式,以及 18.09 版本后的 --ssh 方式。

在实际工作中,Build Docker 镜像时,经常碰上需要在 Docker 镜像内用到 SSH Private Key 的场景。比如构建镜像时要从 GitHub、GitLab 的私有库 Clone 代码,或者要安装私有库的 Gem、NPM Package 等。

如果直接把自己的 SSH Private Key 打包到 Docker 镜像中的话,是存在很大安全风险的。如何解决这个问题?

通过参数将私钥传递到容器里,同时配合多阶段构建以解决直接把私钥打包进容器带来的安全风险。

使用多阶段构建,只要私钥不出现在最后一阶段,都是比较安全的,中间过程的镜像只会存放在本机,不会公开,因此问题也不大。

Dockerfile 如下:

分为三个阶段:

  • 阶段一:拿到私钥并写入到 / root/.ssh/id_rsa 文件用于认证
  • 阶段二:yarn build
  • 阶段三:将编译好的产物 COPY 到 node 环境运行
# Stage 1: get sources from npm and git over ssh
FROM node:carbon AS sources
ARG SSH_KEY
ARG SSH_KEY_PASSPHRASE
RUN mkdir -p /root/.ssh && \
    chmod 0700 /root/.ssh && \
    ssh-keyscan bitbucket.org > /root/.ssh/known_hosts && \
    echo "${SSH_KEY}" > /root/.ssh/id_rsa && \
    chmod 600 /root/.ssh/id_rsa
WORKDIR /app/
COPY package*.json yarn.lock /app/
RUN eval `ssh-agent -s` && \
    printf "${SSH_KEY_PASSPHRASE}\n" | ssh-add $HOME/.ssh/id_rsa && \
    yarn --pure-lockfile --mutex file --network-concurrency 1 && \
    rm -rf /root/.ssh/

# Stage 2: build minified production code
FROM node:carbon AS production
WORKDIR /app/
COPY --from=sources /app/ /app/
COPY . /app/
RUN yarn build:prod

# Stage 3: include only built production files and host them with Node Express server
FROM node:carbon
WORKDIR /app/
RUN yarn add express
COPY --from=production /app/dist/ /app/dist/
COPY server.js /app/
EXPOSE 33330
CMD ["node", "server.js"]

build 命令

docker build -t ezze/geoport:0.6.0 \
  --build-arg SSH_KEY="$(cat ~/.ssh/id_rsa)" \
  --build-arg SSH_KEY_PASSPHRASE="my_super_secret" \
  ./

同时 Docker 在 18.09 版本后,推出了 BuildKit 的 SSH mount type,我们也可以用这个特性来解决该问题。

由于是 BuildKit 的特性,因此需要设置这个环境来开启 BuildKit

或者修改 /etc/docker/daemon.json 文件并重启 docker 服务永久开启 BuildKit,添加内容如下所示:

{
  "features": {
    "buildkit" : true
  }
}

首先需要在 Dockerfile 首行开启特性:

# syntax=docker/dockerfile:1

这句话意思是用 docker/dockerfile:1 这个镜像来解析 Dockerfile

然后添加下面的内容,下载对应网站的公钥:

# Download public key for github.com
RUN --mount=type=ssh mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts

注意替换域名

然后,在 Dockerfile 中需要使用 SSH Private Key 的地方都加上--mount=type=ssh

这个 flag 指定该命令运行时有权限访问对应的 ssh 私钥,即:其他没有指定的命令是无法使用该私钥的。

比如 go 下载依赖就像这样:

RUN --mount=type=ssh go mod download

比如,Rails 项目安装有私有库的 Gem 包时,就写成这样:

RUN --mount=type=ssh bundle install

然后 docker build 时通过 --ssh指定私钥:

docker build -f Dockerfile -t helloworld:1.0.0 . --ssh default=/root/.ssh/id_rsa

这种方式,既能正常使用上 SSH Private Key,又能使其在镜像中不留痕迹。完美!

完整 Dockerfile 内容如下:

# syntax=docker/dockerfile:1

# Build the manager binary
FROM golang:1.19 as builder
ARG TARGETOS
ARG TARGETARCH

ENV GOPROXY=https://goproxy.cn

WORKDIR /workspace
# Copy the go source
COPY . /workspace
# Download public key for github.com
RUN --mount=type=ssh mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN --mount=type=ssh  go mod download

# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot

WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]

使用以下命令进行构建:

docker build -t ${IMG} . --ssh default=~/.ssh/id_rsa

关闭 SSH 严格模式

在测试时出现以下错误:

#0 0.831   Host key verification failed.
#0 0.831   fatal: Could not read from remote repository.

这是因为 ssh 不能识别远程主机提供的秘钥,默认情况下会询问是否信任该秘钥,但是在非交互的环境里会直接拒绝掉。

Dockerfile 里的 ssh-keycan github.com > known_hosts 这句就是添加到信任列表,也有可能没生效。可以试试加上下面这句,直接关闭 SSH 的严格模式:

# Configure ssh to trust unknown host keys:
RUN sed /^StrictHostKeyChecking/d /etc/ssh/ssh_config; \
  echo StrictHostKeyChecking no >> /etc/ssh/ssh_config

buildx

使用 docker buildx 进行多架构编译时也可以使用同样的方式对 Dockerfile 进行修改,然后在 build 时指定私钥。

docker buildx build --push --platform "linux/amd64,linux/arm64" -t helloworld:1.0.0  --ssh default=~/.ssh/id_rsa  -f cross.Dockerfile .  

多私钥

如果有多个私有仓库并且需要不同的私钥进行认证的话也是支持的,需要额外处理一下。

首先是 Dockerfile 里每条命令需要指定使用的私钥

RUN --mount=type=ssh,id=github_ssh_key go mod download
RUN --mount=type=ssh,id=gitlab_ssh_key bundle install

相应的需要信任多个 hosts

ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-keyscan gitlab.com >> ~/.ssh/known_hosts

最后 build 时需要传递多个私钥

docker build --ssh github_ssh_key=/path/to/.ssh/github_ssh_id_rsa --ssh gitlab_ssh_key=/path/to/.ssh/gitlab_ssh_id_rsa .

在 Docker build 时使用 SSH 私钥进行认证有两种比较好的解决方案:

  • 1)Docker 18.09 之前的版本,使用多阶段构建,将私钥以参数形式传递进容器,需要保证私钥不出现在最后一阶段即可
  • 2)Docker 18.09 及以后,原生支持 --ssh 参数,推荐使用

如果不使用 Docker 的话就只能用方案一了。

using-ssh-keys-inside-docker-container

docker 官方文档

dockerfile-run-mount-type-ssh-doesnt-seem-to-work

Build secrets and SSH forwarding in Docker 18.09