为什么要使用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 worksgosu 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 upsavedAptMark="$(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 signatureexport 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 dependenciesapt-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 worksgosu --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 signatureexport 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 dependenciesapk del --no-network .gosu-deps; chmod +x /usr/local/bin/gosu;
# verify that the binary worksgosu --version; gosu nobody true
如何使用gosu?
一般是在entrypoint.sh
使用。
例如,Postgres Official Image使用以下脚本作为其ENTRYPOINT:
#!/bin/bash
set -eif [ "$1" = 'postgres' ]; thenchown -R postgres "$PGDATA"if [ -z "$(ls -A "$PGDATA")" ]; thengosu postgres initdbfiexec gosu postgres "$@"
fiexec "$@"
关于 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,大家可以查阅我之前的文章。