Joe Blog

什么是“以 root 身份运行”

以 root 身份运行容器意味着将打包在容器中的软件设置为以 root 或系统管理员用户身份启动。用户启动的软件与启动它的用户具有相同的权限。因此,如果一个普通(“非特权”)用户启动一个软件,它的功能就会受到限制。此时,如果它试图读取它没有明确权限的文件,它将失败。但是如果root用户启动同样的软件,软件就拥有root用户的超能力。

以 root 身份运行容器有什么问题?

容器是一种打包和运行软件的方式。正在运行的软件称为进程。当启动一个容器时,其中的软件将作为一个进程启动,该进程通过名为 cgroups 的 Linux 功能进行隔离。容器在主机上运行,或者用 Kubernetes 的话来说,在节点上运行。

docker容器中运行的进程,如果以root身份运行的会有安全隐患,该进程拥有容器内的全部权限,更可怕的是如果有数据卷映射到宿主机,那么通过该容器就能操作宿主机的文件夹了,一旦该容器的进程有漏洞被外部利用后果是很严重的。

如果您在 Kubernetes 上以 root 身份运行容器会发生什么?

问题在于,通过对主机或 Kubernetes 节点的不受限制的 root 访问,突破容器隔离的黑客可以查看各种秘密信息。这包括来自那里运行的所有其他容器的所有信息,以及硬盘驱动器上的各种文件。在许多云环境中,这也意味着访问云凭证。

因此,黑客不能只读取各种信息,例如数据库连接凭据,然后窃取所有数据。他们还可能在您的云帐户中启动新服务器,从而花费巨额成本并将其用作对其他目标发起新攻击的平台。您将成为用于攻击的资源的所有者。

如何停止以 root 身份运行容器

添加一个非特权用户并将其设置为进程所有者。实际上,这意味着将两行添加到您的 Dockerfile(如果您不使用 Docker 工具链来构建容器映像,则为 Containerfile)。在您需要以 root 身份运行的任何软件安装后添加它们:

RUN useradd --uid 10000 runner
USER 10000

第一行添加了一个用户,其指定的 UID(用户 ID)设置为 10000,名称为“runner”。它还添加了一个具有相同 GID(组 ID)和相同名称的组。第二行将 Dockerfile 设置为切换到新创建的用户。将其设置为 UID 而不是用户名是有道理的,我们会讲到的。

#Set the security context for a Pod
# UID 10000+ are used for user accounts.
...
spec:
  securityContext:
    runAsUser: 10000  
    runAsGroup: 10000
    fsGroup: 10000
...

  1. gosu启动命令时只有一个进程,所以docker容器启动时使用gosu,那么该进程可以做到PID=1
  2. sudo启动命令时先创建sudo进程,然后该进程作为父进程去创建子进程,PID=1sudo进程占据;

在docker的entrypoint中有如下建议:

  1. 创建group和普通账号,不要使用root账号启动进程;
  2. 如果普通账号权限不够用,建议使用gosu来提升权限,而不是sudo;
  3. entrypoint.sh脚本在执行的时候也是个进程,启动业务进程的时候,在命令前面加上exec,这样新的进程就会取代entrypoint.sh的进程,得到1号PID;
  4. exec “$@”是个保底的逻辑,如果entrypoint.sh的入参在整个脚本中都没有被执行,那么exec “$@”会把入参执行一遍,如果前面执行过了,这一行就不起作用,这个命令的细节在Stack Overflow上有详细的描述,如下图,地址是:https://stackoverflow.com/questions/39082768/what-does-set-e-and-exec-do-for-docker-entrypoint-scripts

如何在基础镜像中添加gosu?

FROM bitnami/minideb:bullseye

# grab gosu for easy step-down from root
# https://github.com/tianon/gosu/releases
ENV GOSU_VERSION 1.14
RUN set -ex; \
	\
	fetchDeps=" \
		ca-certificates \
		dirmngr \
		gnupg \
		wget \
	"; \
	apt-get update; \
	apt-get install -y --no-install-recommends $fetchDeps; \
	rm -rf /var/lib/apt/lists/*; \
	\
	dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
	wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
	wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
	export GNUPGHOME="$(mktemp -d)"; \
    key='B42F6819007F00F88E364FD4036A9C25BF357DD4'; \
    gpg  --yes --always-trust --keyserver pgp.mit.edu --recv-keys "$key" || \
    gpg  --yes --always-trust --keyserver keyserver.pgp.com --recv-keys "$key" || \
    gpg  --yes --always-trust --keyserver ha.pool.sks-keyservers.net --recv-keys "$key" ; \
	gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
	gpgconf --kill all; \
	rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
	chmod +x /usr/local/bin/gosu; \
	gosu nobody true; \
	\
	apt-get purge -y --auto-remove $fetchDeps
	
# Add local user 'ops'
RUN groupadd -r ops --gid=10001 && useradd -r -g ops --uid=10001 ops
    
RUN mkdir /app && chown ops:ops /app
VOLUME /app
WORKDIR /app

COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]

CMD ["bash"]

docker-entrypoint.sh

#!/bin/bash
set -e

# Change uid and gid of node user so it matches ownership of current dir
if [ "$MAP_NODE_UID" != "no" ]; then
    if [ ! -d "$MAP_NODE_UID" ]; then
        MAP_NODE_UID=$PWD
    fi

    uid=$(stat -c '%u' "$MAP_NODE_UID")
    gid=$(stat -c '%g' "$MAP_NODE_UID")

    echo "ops ---> UID = $uid / GID = $gid"

    export USER=ops

    usermod -u $uid ops 2> /dev/null && {
      groupmod -g $gid ops 2> /dev/null || usermod -a -G $gid ops
    }
fi

echo "**** GOSU ops $@ ..."

exec /usr/local/bin/gosu ops "$@"

具体程序镜像Dockerfile可以参考Redis官方的Dockerfile写法