Python镜像的Docker非root用户最佳实践?

14

最近我一直在构建一些Python Docker镜像。最佳实践显然是不要以root用户身份运行容器,并从非特权用户中删除sudo权限。

但是我一直在想最好的方法是什么。

这里是一个示例Dockerfile:

FROM python:3.10

## get UID/GID of host user for remapping to access bindmounts on host
ARG UID
ARG GID

## add a user with same GID and UID as the host user that owns the workspace files on the host (bind mount)
RUN adduser --uid ${UID} --gid ${GID} --no-create-home flaskuser
RUN usermod -aG sudo flaskuser

## install packages as root?
RUN apt update \
    && apt upgrade -y \
    && apt-get install -y --no-install-recommends python3-pip \
    #&& [... install some packages ...]
    && apt-get install -y uwsgi-plugin-python3 \
    ## cleanup
    && apt-get clean \
    && apt-get autoclean \
    && apt-get autoremove --purge  -y \
    && rm -rf /var/lib/apt/lists/*


## change to workspace folder and copy requirements.txt
WORKDIR /workspace/web
COPY ./requirements.txt /tmp/requirements.txt
RUN chown flaskuser:users /tmp/requirements.txt


## Install python packages as root?
RUN python3 -m pip install  --disable-pip-version-check --no-cache-dir -r /tmp/requirements.txt
RUN chmod -R  777 /usr/local/lib/python3.11/site-packages/*


ENV PYTHONUNBUFFERED 1
ENV PYTHONPATH "${PYTHONPATH}:/workspace/web"
ENV PYTHONPATH "${PYTHONPATH}:/usr/local/lib/python3.10/site-packages"


## change to non-priviliged user to run container
USER flaskuser
CMD ["uwsgi", "uwsgi.ini"]

我的问题如下:
  1. 使用apt-get以root用户安装软件包是否可行,或者应该使用非特权用户(使用sudo,稍后应该删除)进行安装?
  2. 最佳位置将这些软件包安装在哪里,例如/usr/local/(作为默认安装时作为root),还是更好地安装在用户主目录中?
  3. 当使用pip作为root安装Python软件包时,我收到以下警告:WARNING:使用'root'用户运行pip可能会导致权限破坏和与系统包管理器冲突的行为。建议使用虚拟环境:https://pip.pypa.io/warnings/venv (但是,由于Docker映像已经为单个服务隔离,因此我认为可以忽略该警告,不需要虚拟环境)。
  4. 还有什么其他内容我需要了解吗?
注:绑定挂载的工作区仅用于开发,对于生产映像,我将复制必要的文件/构件到映像/容器中。
谢谢

以 root 身份构建和以 root 身份运行是不同的。我认为以 root 身份构建是一种安全改进,因为在运行时非 root 用户无法修改已安装的应用程序。 - BMitch
谢谢@Bmitch,听起来完全有道理! - Phil
2个回答

23

通常,最容易且安全的方法是在 Dockerfile 中一直使用 root 用户,直到最后才声明一个替代用户 USER,在运行容器时使用该用户。

FROM ???
# Debian adduser(8); this does not have a specific known uid
RUN adduser --system --no-create-home nonroot

# ... do the various install and setup steps as root ...

# Specify metadata for when you run the container
USER nonroot
EXPOSE 12345
CMD ["my_application"]

对于你更具体的问题:

使用apt-get作为root安装包是否可以?

这是必需的;apt-get不会在非root用户下运行。如果您有一个基本镜像切换到非root用户,则需要使用“USER root”切换回来,然后才能运行apt-get命令。

最好的安装这些软件包的位置?

通常是系统位置。如果您正在使用apt-get安装软件包,它们将被放置在/usr中,这很好;pip install想要将软件包安装到系统Python site-packages目录中;等等。如果您手动安装软件包,/usr/local是一个很好的选择,特别是因为/usr/local/bin通常在$PATH中。Docker中“用户主目录”的概念不太明确,我不会尝试使用它。

以root身份使用pip安装Python软件包时,我收到以下警告...

实际上您可以忽略它,并用您所说的理由进行辩解。在Docker中使用pip有两种常见方法:一种是您展示的直接在“正常”Python环境中使用pip install安装软件包,另一种方法是使用多阶段构建创建一个完全填充的虚拟环境,然后将其COPY到运行时映像中而无需构建工具。在这两种情况下,您仍然可能想要成为root。

我还漏掉或应该知道的其他事情吗?

在您的Dockerfile中:

## get UID/GID of host user for remapping to access bindmounts on host
ARG UID
ARG GID

这不是最佳实践,因为这意味着每当有使用不同主机UID的人想要使用它时,您都必须重建图像。请使用任意UID创建非root用户,独立于任何特定的主机用户。

RUN usermod -aG sudo flaskuser
如果您的“非root”用户具有无限制的sudo访问权限,则其等同于root。Docker中存在一些重大问题,并且永远不需要使用sudo,因为每个运行命令的路径都有一种指定要以哪个用户身份运行它的方法。
RUN chown flaskuser:users /tmp/requirements.txt

您的代码和其他源文件应该具有默认的root: root所有权。默认情况下,它们将是可读的但不可写的,这没问题。您希望防止应用程序无意或有意地覆盖其自身的源代码。

RUN chmod -R  777 /usr/local/lib/python3.11/site-packages/*

chmod 0777永远不是最佳实践。 它为非特权代码提供了一个写入其恶意软件有效载荷并执行它们的位置。 对于典型的Docker设置,您根本不需要chmod

绑定挂载的工作区仅用于开发,对于生产图像,我会将必要的文件/构建成果复制到镜像/容器中。

如果您使用绑定挂载来覆盖所有应用程序代码的内容,则实际上并没有从图像中运行代码,Dockerfile的一些或所有工作将被丢失。 这意味着,如果您在没有绑定挂载的情况下进入生产环境,则正在运行未经测试的设置。

由于您的开发环境在某种程度上几乎总是与生产环境不同,因此我建议在日常开发中使用非Docker Python虚拟环境,在容器外部运行良好的(pytest)单元测试,并在部署之前对构建的容器进行集成测试。

如果您的应用程序尝试向主机目录写出文件,则可能会出现权限问题。 在这里,最好的方法是重新设计您的应用程序以避免此问题,将数据存储在其他地方,例如关系型数据库。 在此答案中,我讨论了绑定挂载数据目录的权限设置,但那似乎与您在此处问的有点不同。


谢谢David提供的详细回答。我使用bindmount,因为我在我的NAS上运行我的“开发环境”,所以我将代码更改同步到我的NAS上,然后通过bindmount使其对容器可用。我想避免在每次更改一点代码(和重新复制代码)之后都必须重建映像。但是,考虑到您所说的原因,这种做法不是最佳实践。我也考虑过使用VScode devcontainers,但我担心在容器中更改代码,然后在容器被删除时失去它... - Phil
我正在考虑回到在我的笔记本电脑上进行完全本地开发,使用本地的docker安装,并将我的NAS docker转换为“集成环境”,以在将构建移动到生产之前测试构建。 - Phil
我还没有生产环境,我仍在思考一个小型Web应用的良好起点是什么,也许我会尝试使用GKE,因为小型应用程序应该可以免费托管(就我所了解的他们的定价/信用模型而言)。我不确定,但我想他们可能也有创建集成环境的机制。 - Phil
对于其他人来说,为了设置用户的ID,我不得不使用这个命令:RUN useradd -r -u 1001 nonroot(其中nonroot是用户名,1001是ID)。我还必须更改我的YAML文件,在你设置runAsNonRoot: true旁边,我必须添加runAsUser: 1001(其中1001与上面创建的用户ID匹配)。 - Preston Badeer
这个答案的重要部分是特定的用户ID不应该有影响,只要它不是零。如果您需要以特定用户或组的身份运行容器(可能是为了访问一些外部基于文件系统的存储),则可以使用docker run -u命令以替代用户ID运行容器,而无需编辑Dockerfile。 - David Maze

-1

再次感谢 David 的详细解释。

我不得不消化所有这些内容,经过更多关于这个主题的阅读后,我终于理解了你所说的一切(希望如此)。

我最初添加 UID/GID 与主机用户匹配的用户的原因是,当我开始时,我在我的NAS上运行容器,只允许使用root进行SSH。 因此,在项目文件夹由另一个用户拥有时运行具有root权限的容器将导致权限问题,当容器用户尝试访问绑定挂载的文件时。 那时我还不太明白这一点,所以我一直认为容器用户必须始终与主机用户id匹配。

因此,我已经按照您的建议更改了我的Dockerfile,使用任意用户,删除了所有不必要的chown/chmod命令,并且现在我可以在本地macbook和我目前正在测试的VPS上成功运行它。

## ################################################################
## WEB Builder Stage
## ################################################################
FROM python:3.10-slim-buster AS builder

## ----------------------------------------------------------------
## Install Packages
## ----------------------------------------------------------------
RUN apt-get update \
    && apt-get install -y libmariadb3 libmariadb-dev \
    && apt-get install -y gcc \
    ## cleanup
    && apt-get clean \
    && apt-get autoclean \
    && apt-get autoremove --purge  -y \
    && rm -rf /var/lib/apt/lists/*

## ----------------------------------------------------------------
## Add venv
## ----------------------------------------------------------------
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

## ----------------------------------------------------------------
## Install python packages
## ----------------------------------------------------------------
COPY ./requirements.txt /tmp/requirements.txt
RUN python3 -m pip install --upgrade pip \
 && python3 -m pip install wheel \
 && python3 -m pip install  --disable-pip-version-check --no-cache-dir -r /tmp/requirements.txt




## ################################################################
## Final Stage
## ################################################################
FROM python:3.10-slim-buster

## ----------------------------------------------------------------
## add user so we can run things as non-root
## ----------------------------------------------------------------
RUN adduser flaskuser

## ----------------------------------------------------------------
## Copy from builder and set ENV for venv
## ----------------------------------------------------------------
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

## ----------------------------------------------------------------
## Set Python ENV
## ----------------------------------------------------------------
ENV PYTHONUNBUFFERED=1 \   PYTHONPATH="${PYTHONPATH}:/workspace/web/app:/opt/venv/bin:/opt/venv/lib/python3.10/site-packages"

## ----------------------------------------------------------------
## Copy app files into container
## ----------------------------------------------------------------
WORKDIR /workspace/web
COPY . .

## ----------------------------------------------------------------
## Switch to non-priviliged user and run app
## the entrypoint script runs either uwisg or flask dev server
## depending on FLASK_ENV
## ----------------------------------------------------------------
USER flaskuser
CMD ["/workspace/web/docker-entrypoint.sh"]


如果我想在我的NAS上(从NAS主机CLI使用root)使用绑定挂载运行容器,我仍然可以通过使用包含docker-compose.override.yml的文件来实现。
 myservice:
   user: "{UID}:{GID}"

其中"{UID}:{GID}"与拥有绑定挂载文件夹的主机用户匹配。

但我也会改变这一点。现在我只在本地开发和测试,可能会将我的NAS用作第一个集成环境,在那里我将仅测试从注册表中拉出的完全构建的容器/镜像(因此不再需要绑定挂载)。

我还开始使用多阶段构建,除了使最终镜像更小之外,还应该通过不包括不必要的构建依赖项来减少攻击面。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接