为什么要使用 gosu?
Docker 容器中运行的进程,如果以 root 身份运行话会有安全隐患,该进程拥有容器内的全部权限,更可怕的是如果有数据卷映射到宿主机,那么通过该容器就能操作宿主机的文件夹了,一旦该容器的进程有漏洞被外部利用后果是很严重的。
因此,容器内使用非 root 账号运行进程才是安全的方式,这也是我们在制作镜像时要注意的地方。
而我们今天讲到的 gosu 正是解决使用非 root 用户运行业务进程的一种最佳实践方法。
su
和 sudo
具有非常奇怪且经常令人讨厌的 TTY 和信号转发行为的问题。su
和 sudo
的设置和使用也有些复杂(特别是在 sudo
的情况下),虽然它们有很大的表达力,但是如果您所需要的只是“以特定用户身份运行特定应用程序”,那么它们将不再那么适合。
处理完用户 / 组后,我们将切换到指定用户,然后执行指定的进程,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
安装 gosu
对于 debian:
Debian 9 (“Debian Stretch”) or newer:
RUN set -eux; \
apt-get update; \
apt-get install -y gosu; \
rm -rf /var/lib/apt/lists/*; \
# verify that the binary works
gosu nobody true
Older Debian releases (or newer gosu
releases):
ENV GOSU_VERSION 1.12
RUN set -eux; \
# save list of currently installed packages for later so we can clean up
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates wget; \
if ! command -v gpg; then \
apt-get install -y --no-install-recommends gnupg2 dirmngr; \
elif gpg --version | grep -q '^gpg (GnuPG) 1\.'; then \
# "This package provides support for HKPS keyservers." (GnuPG 1.x only)
apt-get install -y --no-install-recommends gnupg-curl; \
fi; \
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"; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
command -v gpgconf && gpgconf --kill all || :; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
# clean up fetch dependencies
apt-mark auto '.*' > /dev/null; \
[-z "$savedAptMark"] || apt-mark manual $savedAptMark; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
\
chmod +x /usr/local/bin/gosu; \
# verify that the binary works
gosu --version; \
gosu nobody true
对于 alpine(3.7+):
ENV GOSU_VERSION 1.12
RUN set -eux; \
\
apk add --no-cache --virtual .gosu-deps \
ca-certificates \
dpkg \
gnupg \
; \
\
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"; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
command -v gpgconf && gpgconf --kill all || :; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
# clean up fetch dependencies
apk del --no-network .gosu-deps; \
\
chmod +x /usr/local/bin/gosu; \
# verify that the binary works
gosu --version; \
gosu nobody true
如何使用 gosu?
一般是在 entrypoint.sh
使用。
例如,Postgres Official Image 使用以下脚本作为其 ENTRYPOINT:
#!/bin/bash
set -e
if ["$1" = 'postgres']; then
chown -R postgres "$PGDATA"
if [-z "$(ls -A"$PGDATA")" ]; then
gosu postgres initdb
fi
exec gosu postgres "$@"
fi
exec "$@"
关于 exec,大家可以查阅我之前写的文章,其作用主要是会将 gosu postgres 后面命令运行的进程替换 entrypoint.sh 进程作为 1 号进程。并且运行该进程的用户为 postgres,而不是 root。
拿我们线上的一个容器来举例:
entrypoint.sh 为:
#! /bin/bash
set -e
chown -R xxxuser:xxxgroup /data/logs
exec gosu xxxuser tini -- myprogram -config /etc/config.prod.yaml
exec 到容器执行 whoami:
sh-4.2# whoami
root
可以看到整个容器当前的用户是 root。
然后查看运行我们 tini 和 myprogram 进程的用户:
sh-4.2# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
xxxuser 1 0.0 0.0 4372 368 ? Ss 18:17 0:00 tini -- myprogram -config /etc/config.prod.yaml
xxxuser 14 2.6 0.4 1015768 315868 ? Sl 18:17 1:20 myprogram -config /etc/config.prod.yaml
到了这里可能大家已经非常清楚了。
至于 tini,大家可以查阅我之前的文章。