由来
客户安全要求业务容器改为非 root 启动,很多容器需要操作 ipset iptables 之类的,并不是纯粹 rootless docker 就可以解决的。是尽可能的把(非 k8s 管理容器之类以外)业务容器改为非 root 启动(是容器内业务的所有进程)。
我们在之前的文章 《容器快了,却不安全了,Rootless 安排上》,介绍过在以 root 用户身份运行 Docker 会带来一些潜在的危害和安全风险,需要的读者可以翻阅查看。
改造
前提须知
这里列举些基础知识
使用 root 不安全的举例
虽然 linux 有 user namespace 隔离技术,但是 docker 不支持类似 podman 那样的给每个容器设置范围性的 uidmap 映射(当然 k8s 现在也不支持),并且容器默认配置下的权限虽然去掉了一些。但是容器内还是能对挂载进去的进行修改的,比如帖子 rm -rf * 前一定一定要看清当前目录[1] 老哥的操作:
docker run --rm -v /mnt/sda1:/mnt/sda1 -it alpine
cp /mnt/sda1/somefile.tar.gz .
tar xzvf somefile.tar.gz
cd somefile-v1.0
ls
# 看了看内容觉得不是自己想要的,回上一级目录准备删掉:
cd ..
rm -rf *
嗯,alpine 默认的 workdir 是 /
,所以删除 rm -rf /*
。当然还有其他不安全的,所以在业务角度上,我们需要给容器内进程设置在非 root 下最小的运行权限。
设置 USER 还是使用 docker-entrypoint.sh 入口
Dockerfile 里设置 USER
或者 run 的时候设置 -u user:group
只能针对于一些简单的进程,例如大部分 exporter 和一些只是用 http API 的进程,这几天我测试后也提交了一些 pr:
• danielqsj/kafka_exporter[2]
• ClickHouse/clickhouse_exporter[3]
• kubernetes addonresizer[4]
对于很多挂载目录持久化数据的,例如各种中间件,例如 mysql,redis ,单纯设置 USER 的话,需要在容器启动之前设置目录的权限。other 权限为 7 的话,很不安全,所以只能是 owner、group 权限,但是容器内的用户名和宿主机用户名是不一致的,只能设置 uid、gid。使用这些需要数据持久化的容器,会存在:
• 直接 -v 挂载或者 docker volume
• k8s 上使用 hostPath
• 固定 pv
• sc 下使用 pvc
• 别人的 k8s 集群或者实例上去部署
如果你提前修改目录权限,上面最后俩场景根本无法自动化,而且说不定某天新版本官方镜像里 Dockerfile 里换基础镜像的同时忘记在添加用户时候设置 uid 和 gid ,uid 和 gid 就变了,只能是加启动脚本里处理。
对此,mysql docker 镜像的官方启动脚本[5] 给了很好的参考,Dockerfile 制作镜像就创建了指定 uid、gid 的 mysql 用户,然后启动容器的时候都是 ENTRYPOINT CMD
(k8s 里对应 command、args) 的形式启动:
docker-entrypoint.sh mysqld
或者可以通过 cmdline 设置 mysql 启动端口
docker run xxx mysql:5.7 --port 4306
mysql 脚本里包含对于权限以外的信息比较多,不方便举例,这里使用 redis 举例:
#!/bin/sh
# 脚本某行报错就退出
set -e
# 脚本的第一个参数为 -开头的字符串,或者是 .conf 结尾的字符串
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
# 重新设置 $@ 为 redis-server "$@"
set -- redis-server "$@"
fi
# allow the container to be started with `--user`
# 第一个参数为 redis-server 并且执行的用户为 root
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
# 更改当前目录下的 owner 为 redis
find . ! -user redis -exec chown redis '{}' +
# 使用 gosu 切换到 redis 执行本脚本,并带上此刻的 $@参数
exec gosu redis "$0" "$@"
fi
# set an appropriate umask (if one isn't set already)
# - https://github.com/docker-library/redis/issues/305
# - https://github.com/redis/redis/blob/bb875603fb7ff3f9d19aad906bd45d7db98d9a39/utils/systemd-redis_server.service#L37
um="$(umask)"
if [ "$um" = '0022' ]; then
umask 0077
fi
exec "$@"
例如下面执行流程:
$ docker run -d -name redis7 -v $PWD/redis-ctr-data:/data --net host redis:7 --port 7777
$ docker top redis7
UID PID PPID C STIME TTY TIME CMD
systemd+ 1041135 1041116 1 15:47 ? 00:00:00 redis-server *:7777
$ docker exec redis7 id redis
uid=999(redis) gid=999(redis) groups=999(redis)
$ grep 999 /etc/passwd
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
docker top 显示的用户,是按照宿主机上 uid 显示的,gosu[6] 是 golang 实现 su-exec[7],切换指定用户执行命令,exec 是执行后面的命令,替换当前的 shell 进程,这样在 docker stop 给容器内 pid 为 1 的进程发送信号,业务进程能收到信号进行优雅退出,而没 exec 的话,pid 为 1 的进程是 shell 脚本,它不会转发信号的。
ENTRYPOINT
使用脚本当作入口的形式,最后业务切用户执行,即使使用 docker exec 还是使用镜像默认的 USER root,排查问题也方便。也推荐使用镜像之前,先看官方的启动脚本,例如 mongodb 官方镜像是支持类似 redis 这种非 root 启动的,但是我们 k8s 里是:
...
- name: {{ NODE_NAME }}
image: xxx/mongo:xxx
command:
- mongod
- "--port"
这样覆盖了 entrypoint,没有使用官方启动脚本执行,就是 root 用户,改为下面的不覆盖就行:
- name: {{ NODE_NAME }}
image: xxx/mongo:xxx
args: # <--- 这里
- mongod
- "--port"
要注意一个点,su-exec 在 alpine 里可以包管理安装,非 alpine 的基础镜像使用 gosu 可以参考 redis 官方镜像。
案例实战
这列梳理一些我做的案例。先说一些知识点:
• 产生 pid 和 sock 文件的,可以放 /tmp 下
• 业务进程非 root 对
/dev/stdxxx
没权限的,可以脚本里chmod a+w /dev/std*
• 如果自己业务镜像产生的数据会被其他容器挂载操作数据,你的业务进程最好创建用户的时候使用固定同样的
uid:gid
,例如我们的 mysql-backup 备份 mysql 数据用到的用户uid:gid
保持和 mysql 官方镜像一致,这样不需要修改 mysql 数据目录权限和 owner• 不要
chmod -R 777
目录
机器码处理
获取机器码一般是使用 dmidecode -s system-uuid
,但是容器内你以 root 执行会报错:
$ docker run --rm -ti debian:11
$ apt update && apt-get install -y dmidecode
$ dmidecode -s system-uuid
/dev/mem: No such file or directory
所以之前我们都是读取 /sys/devices/virtual/dmi/id/product_uuid
,但是非 root 后无法读取,因为该文件权限为 0400
:
$ ls -l /sys/devices/virtual/dmi/id/product_uuid
-r-------- 1 root root 4096 Nov 3 08:48 /sys/devices/virtual/dmi/id/product_uuid
且该文件是内核设置的权限[8],无法被更改。
后面尝试发现一些信息:
$ strace dmidecode -s system-uuid
...
openat(AT_FDCWD, "/sys/firmware/dmi/tables/smbios_entry_point", O_RDONLY)
...
openat(AT_FDCWD, "/sys/firmware/dmi/tables/DMI", O_RDONLY)
发现读取了这俩文件,搜索资料发现是 dmi table,例如 root 下可以这样获取机器码:
docker-entrypoint.sh mysqld
0
该文件内容按照 DMI 规范字节结构解析可以得到不少信息。然后找到了一个 go 库,在 linux 上尝试成功:
docker-entrypoint.sh mysqld
1
机器上测试:
docker-entrypoint.sh mysqld
2
然后把宿主机的 /sys/firmware/dmi/tables
挂载到 /rootfs/sys/firmware/dmi/tables
里,在 gosu 之前 chmod a+r /rootfs/sys/firmware/dmi/tables/DMI
,业务使用上面的库 hack 后,从指定路径的 DMI 信息即可获取到机器码。
etcd
没啥说的,加了 gosu 后再加启动脚本:
docker-entrypoint.sh mysqld
3
为了不影响其他分支,这里我用了 env 作为开关,wurstmeister/kafka-docker[9] 也是一样:
docker-entrypoint.sh mysqld
4
其他的,例如 promtail 啥的都是一样,不再举例,自行制作
coredns
coredns 1.11.0 才开始非 root 启动,我们业务使用的是 1.10.1 的,不升级避免客户现场出现问题,所以重做镜像最稳妥:
docker-entrypoint.sh mysqld
5
非 root 用户是无法监听 1024 以下端口的,coredns 监听 53 端口是因为使用了 setcap cap_net_bind_service=+ep /coredns
,但是这个属性属于扩展属性,docker 构建多层 COPY 会不支持而丢失,必须使用 buildkit 构建,否则 cap 信息丢失,部署上去无法监听 53 端口:
docker-entrypoint.sh mysqld
6
consul
consul 镜像也支持,但是 chown 的时候没带 -R 选项。
docker-entrypoint.sh mysqld
7
这里会存在一个问题,如果之前是覆盖了 entrypoint 使用 root 启动的,再切正确姿势下,因为 data 目录下子目录没被 chown,consul 在 data 下子目录写入 node-id 会报错没权限,所以我是这样 hack 重做镜像的:
docker-entrypoint.sh mysqld
8
去掉 dumb-init
是因为客户要求容器内所有进程都是非 root,不去掉 pid 为 1 的就是 root 用户 dumb-init sh 进程
docker.sock 文件
有些进程是需要挂载 /var/run
为了使用宿主机的 /var/run/docker.sock
和宿主机 docker 通信的,这里我们使用 cadvisor 举例:
docker-entrypoint.sh mysqld
9
• cadvisor 挂载了宿主机的 rootfs ,改为纯非 root 不行,但是 cadvisor 镜像内有个
operator
用户的 gid 是 0,利用启动脚本和 docker 权限来改造成非 root 启动。• docker.sock 权限是
0660
,利用 shell 把 operator 用户加到 docker 组里即可(必须取 gid)。这里要注意的是,不同版本 alpine 和其他 rootfs 的 adduser/addgroup 参数不一样,自行注意 shell 兼容
设置 “RUN_USER” 为 operator
,然后设置宿主机的 docker 的 data-root 下面权限(可以使用 systemd 的ExecStartPost=
):
docker run xxx mysql:5.7 --port 4306
0
cadvisor 参数为:
docker run xxx mysql:5.7 --port 4306
1
cron
非 root 无法使用 cron 启动,使用 go-crond[10]
引用链接
[1]
rm -rf * 前一定一定要看清当前目录: https://www.v2ex.com/t/976554[2]
danielqsj/kafka_exporter: https://github.com/danielqsj/kafka_exporter/pull/410[3]
ClickHouse/clickhouse_exporter: https://github.com/ClickHouse/clickhouse_exporter/pull/83[4]
kubernetes addonresizer: https://github.com/kubernetes/autoscaler/pull/6242/files[5]
mysql docker 镜像的官方启动脚本: https://github.com/docker-library/mysql/blob/master/5.7/docker-entrypoint.sh[6]
gosu: https://github.com/tianon/gosu[7]
su-exec: https://github.com/ncopa/su-exec[8]
内核设置的权限: https://github.com/torvalds/linux/blob/master/drivers/firmware/dmi-id.c#L61[9]
wurstmeister/kafka-docker: https://github.com/wurstmeister/kafka-docker[10]
go-crond: https://github.com/webdevops/go-crond[11]
k8s 社区关于支持 user namespace 提议: https://github.com/kubernetes/enhancements/issues/127[12]
dmi 信息规范: https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.3.0.pdf[13]
dmidecode 源码: https://github.com/mirror/dmidecode/blob/master/dmidecode.c#L448
免责声明:本文内容来源于网络,所载内容仅供参考。转载仅为学习和交流之目的,如无意中侵犯您的合法权益,请及时联系Docker中文社区!
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...