以 root 身份运行容器意味着将打包在容器中的软件设置为以 root 或系统管理员用户身份启动。用户启动的软件与启动它的用户具有相同的权限。因此,如果一个普通(“非特权”)用户启动一个软件,它的功能就会受到限制。此时,如果它试图读取它没有明确权限的文件,它将失败。但是如果root用户启动同样的软件,软件就拥有root用户的超能力。
容器是一种打包和运行软件的方式。正在运行的软件称为进程。当启动一个容器时,其中的软件将作为一个进程启动,该进程通过名为 cgroups 的 Linux 功能进行隔离。容器在主机上运行,或者用 Kubernetes 的话来说,在节点上运行。
docker容器中运行的进程,如果以root身份运行的会有安全隐患,该进程拥有容器内的全部权限,更可怕的是如果有数据卷映射到宿主机,那么通过该容器就能操作宿主机的文件夹了,一旦该容器的进程有漏洞被外部利用后果是很严重的。
问题在于,通过对主机或 Kubernetes 节点的不受限制的 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
...
这里就需要讲到的gosu 正是解决使用非root用户运行业务进程的一种最佳实践方法。linux中本身是有su
和sudo
用来给普通用户提升权限操作需要特定权限才能运行的命令,但是su
和sudo
具有非常奇怪且经常令人讨厌的TTY和信号转发行为的问题。su
和sudo
的设置和使用也有些复杂(特别是在sudo
的情况下),虽然它们有很大的表达力,但是如果您所需要的只是“以特定用户身份运行特定应用程序”,那么它们将不再那么适合。
gosu
处理完用户/组后,我们将切换到指定用户,然后执行指定的进程,gosu
本身不再驻留或完全不在进程生命周期中。这避
免了信号传递和TTY的所有问题。
$ docker run -it --rm ubuntu:trusty su -c 'exec ps aux'
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 46636 2688 ? Ss+ 02:22 0:00 su -c exec ps a
root 6 0.0 0.0 15576 2220 ? Rs 02:22 0:00 ps aux
$ docker run -it --rm ubuntu:trusty sudo ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 3.0 0.0 46020 3144 ? Ss+ 02:22 0:00 sudo ps aux
root 7 0.0 0.0 15576 2172 ? R+ 02:22 0:00 ps aux
$ docker run -it --rm -v $PWD/gosu-amd64:/usr/local/bin/gosu:ro ubuntu:trusty gosu root ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 7140 768 ? Rs+ 02:22 0:00 ps aux
以上的演示可以看出,假如一个普通用户 通过su
和sudo
能够实现需要特定权限的命令,但是存在信号传递的问题,可以看出通过su
和sudo
之后,ps aux
这个进程的PID
分别是6
和7
,这个不符合容器优雅停机的要求。通过gosu
这个切换到root之后,可以看到ps aux
这个进程的PID=1
宿主机执行docker stop命令时,该
PID=1
进程可以收到SIGTERM
信号量,于是应用可以做一些退出前的准备工作,例如保存变量、退出循环等,也就是优雅停机(Gracefully Stopping);
通过上面对可以小结:
gosu
启动命令时只有一个进程,所以docker容器启动时使用gosu
,那么该进程可以做到PID=1
;sudo
启动命令时先创建sudo
进程,然后该进程作为父进程去创建子进程,PID=1
被sudo
进程占据;在docker的entrypoint中有如下建议:
如何在基础镜像中添加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写法