公众号关注「奇妙的 Linux 世界」

设为「星标」,每天带你玩转 Linux !

查看机器码指令_查看机器码_机器码查看

本文总结了业务容器的非 root 启动改造实战经验,强调了非 root 启动的重要性和一些基础知识。

文章提供了一些建议,如设置容器内进程的最小权限,使用 USER 指令或者启动脚本来切换用户,以及处理机器码获取等技巧。还包括不同容器镜像的修改和构建过程,以满足非 root 启动要求,特别提到了 CoreDNS 和 Consul 镜像的处理方式。

由来

客户安全要求业务容器改为非 root 启动,很多容器需要操作 ipset iptables 之类的,并不是纯粹 rootless docker 就可以解决的。是尽可能的把(非 k8s 管理容器之类以外)业务容器改为非 root 启动(是容器内业务的所有进程)。

我们在之前的文章 《》,介绍过在以 root 用户身份运行 Docker 会带来一些潜在的危害和安全风险,需要的读者可以翻阅查看。

改造前提须知

这里列举些基础知识

使用 root 不安全的举例

虽然 linux 有 user namespace 隔离技术,但是 docker 不支持类似 podman 那样的给每个容器设置范围性的 uidmap 映射(当然 k8s 现在也不支持),并且容器默认配置下的权限虽然去掉了一些。但是容器内还是能对挂载进去的进行修改的,比如帖子rm -rf * 前一定一定要看清当前目录[1]老哥的操作:

docker run --rm -v /mnt/sda1:/mnt/sda1 -it alpinecp /mnt/sda1/somefile.tar.gz .tar xzvf somefile.tar.gzcd somefile-v1.0ls# 看了看内容觉得不是自己想要的,回上一级目录准备删掉:cd ..rm -rf *

嗯,alpine 默认的 workdir 是/,所以删除rm -rf /*。当然还有其他不安全的,所以在业务角度上,我们需要给容器内进程设置在非 root 下最小的运行权限。

设置 USER 还是使用 docker-entrypoint.sh 入口

Dockerfile 里设置USER或者 run 的时候设置-u user:group只能针对于一些简单的进程,例如大部分 exporter 和一些只是用 http API 的进程,这几天我测试后也提交了一些 pr:

对于很多挂载目录持久化数据的,例如各种中间件,例如 mysql,redis ,单纯设置 USER 的话,需要在容器启动之前设置目录的权限。other 权限为 7 的话,很不安全,所以只能是 owner、group 权限,但是容器内的用户名和宿主机用户名是不一致的,只能设置 uid、gid。使用这些需要数据持久化的容器,会存在:

如果你提前修改目录权限,上面最后俩场景根本无法自动化,而且说不定某天新版本官方镜像里 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 并且执行的用户为 rootif [ "$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#L37um="$(umask)"if [ "$um" = '0022' ]; then    umask 0077fiexec "$@"

例如下面执行流程:

docker run -d -name redis7 -v $PWD/redis-ctr-data:/data --net host redis:7 --port 7777docker top redis7UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMDsystemd+            1041135             1041116             1                   15:47               ?                   00:00:00            redis-server *:7777docker exec redis7 id redisuid=999(redis) gid=999(redis) groups=999(redis)grep 999 /etc/passwdsystemd-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 官方镜像。

案例实战

这列梳理一些我做的案例。先说一些知识点:

机器码处理

获取机器码一般是使用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 下可以这样获取机器码:

dmidecode -t 1  < /sys/firmware/dmi/tables/DMI$ dmidecode -t 1 -u < /sys/firmware/dmi/tables/DMI

该文件内容按照 DMI 规范字节结构解析可以得到不少信息。然后找到了一个 go 库,在 linux 上尝试成功:

package mainimport (    "fmt"    "log"    "github.com/digitalocean/go-smbios/smbios")func main() {    // Find SMBIOS data in operating system-specific location.    rc, _, err := smbios.Stream()    if err != nil {        log.Fatalf("failed to open stream: %v", err)    }    // Be sure to close the stream!    defer rc.Close()    // Decode SMBIOS structures from the stream.    d := smbios.NewDecoder(rc)    ss, err := d.Decode()    if err != nil {        log.Fatalf("failed to decode structures: %v", err)    }    for _, s := range ss {        if s.Header.Type == 1 {            d := s.Formatted            fmt.Printf("UUID: %X%X%X%X-%X%X-%X%X-%X%X-%X%X%X%X%X%Xn",                 d[7], d[6], d[5], d[4],                d[9], d[8], d[11], d[10], d[12], d[13],                d[14], d[15], d[16], d[17], d[18], d[19],            )        }    }}

机器上测试:

$ dmidecode -s system-uuid | tr a-z A-Z66C0F667-71A0-xxxx-xxxx-4AC0A21F5428$ go build -o /tmp/uuid-go test.go$ chmod a+r /sys/firmware/dmi/tables/DMI$ su - guanzhangguanzhang@guan:~$ /tmp/uuid-go UUID: 66C0F667-71A0-xxxx-xxxx-4AC0A21F5428

然后把宿主机的/sys/firmware/dmi/tables挂载到/rootfs/sys/firmware/dmi/tables里,在 gosu 之前chmod a+r /rootfs/sys/firmware/dmi/tables/DMI,业务使用上面的库 hack 后,从指定路径的 DMI 信息即可获取到机器码。

etcd

没啥说的,加了 gosu 后再加启动脚本:

#!/bin/bashset -eif [ "${1:0:1}" = '-' ]; then    set -- etcd "$@"fi# RUN_USER 设置为 nobody 启动if [ "$1" = 'etcd' ] || [ "$1" = '/usr/local/bin/etcd' ];then    if [ "$(id -u)" = '0' -a -n "$RUN_USER" ]; then        find /var/lib/etcd ! -user ${RUN_USER} -exec chown ${RUN_USER} '{}' +        exec gosu ${RUN_USER} "$@"    fifiexec "$@"

为了不影响其他分支,这里我用了 env 作为开关,wurstmeister/kafka-docker[9]也是一样:

#!/bin/bashset -eif [ "${1:0:1}" = '-' ]; then    set -- start-kafka.sh "$@"fi# RUN_USER 设置为 nobody 启动if [ "$1" = 'start-kafka.sh' ] || [ "$1" = '/usr/bin/start-kafka.sh' ];then    if [ "$(id -u)" = '0' -a -n "$RUN_USER" ]; then        find $(readlink -f ${KAFKA_HOME}) ! -user ${RUN_USER} -exec chown ${RUN_USER} '{}' +        find /kafka ! -user ${RUN_USER} -exec chown ${RUN_USER} '{}' +        exec gosu ${RUN_USER} "$@"    fifiexec "$@"

其他的,例如 promtail 啥的都是一样,不再举例,自行制作

coredns

coredns 1.11.0 才开始非 root 启动,我们业务使用的是 1.10.1 的,不升级避免客户现场出现问题,所以重做镜像最稳妥:

ARG DEBIAN_IMAGE=debian:stable-slimARG BASE=gcr.io/distroless/static-debian12:nonrootFROM coredns/coredns:1.10.1 as binFROM  ${DEBIAN_IMAGE} AS buildSHELL [ "/bin/sh""-ec" ]RUN export DEBCONF_NONINTERACTIVE_SEEN=true            DEBIAN_FRONTEND=noninteractive            DEBIAN_PRIORITY=critical            TERM=linux ;     apt-get -qq update ;     apt-get -yyqq upgrade ;     apt-get -yyqq install ca-certificates libcap2-bin;     apt-get cleanCOPY --from=bin /coredns /corednsRUN setcap cap_net_bind_service=+ep /corednsFROM  ${BASE}COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/COPY --from=build /coredns /corednsUSER nonroot:nonrootEXPOSE 53 53/udpENTRYPOINT ["/coredns"]

非 root 用户是无法监听 1024 以下端口的,coredns 监听 53 端口是因为使用了setcap cap_net_bind_service=+ep /coredns,但是这个属性属于扩展属性,docker 构建多层 COPY 会不支持而丢失,必须使用 buildkit 构建,否则 cap 信息丢失,部署上去无法监听 53 端口:

DOCKER_BUILDKIT=1 docker build --platform=amd64  . -t coredns/coredns:1.10.1  --load

consul

consul 镜像也支持,但是 chown 的时候没带 -R 选项。

if [ "$(stat -c %u "$CONSUL_DATA_DIR")" != "${CONSUL_UID}" ]; then  chown ${CONSUL_UID}:${CONSUL_GID} "$CONSUL_DATA_DIR"fi

这里会存在一个问题,如果之前是覆盖了 entrypoint 使用 root 启动的,再切正确姿势下,因为 data 目录下子目录没被 chown,consul 在 data 下子目录写入 node-id 会报错没权限,所以我是这样 hack 重做镜像的:

ARG VER=1.8.3FROM consul:${VER}RUN sed -ri -e 's/(chown)(s+consul:)/1 -R2/'         -e '1s@/usr/bin/dumb-inits+@@'     /usr/local/bin/docker-entrypoint.sh

去掉dumb-init是因为客户要求容器内所有进程都是非 root,不去掉 pid 为 1 的就是 root 用户 dumb-init sh 进程

docker.sock 文件

有些进程是需要挂载/var/run为了使用宿主机的/var/run/docker.sock和宿主机 docker 通信的,这里我们使用 cadvisor 举例:

ARG VER=v0.37.5FROM gcr.m.daocloud.io/cadvisor/cadvisor:${VER}RUN set -eux;     sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories;     apk update;     apk add --no-cache       curl       su-exec;     rm -rf /var/cache/apk/* /tmp/* COPY docker-entrypoint.sh /ENTRYPOINT ["/docker-entrypoint.sh"]CMD ["cadvisor""-logtostderr"]#!/bin/shset -e[ -z "$D_SOCK" ] && D_SOCK=/var/run/docker.sockif [ "${1:0:1}" = '-' ]; then    set -- cadvisor "$@"fiif [ "$1" = 'cadvisor' ] || [ "$1" = '/usr/bin/cadvisor' ];then    if [ "$(id -u)" = '0' -a -n "$RUN_USER" ]; then        if [ -S "${D_SOCK}" ];then            group_id=`stat -c "%g" "${D_SOCK}"`            if ! getent group | cut -d: -f3 | grep -wq $group_idthen                addgroup -g ${group_id} docker            fi            group_name=$(stat -c "%G" "${D_SOCK}")            if ! id -nG ${RUN_USER} | grep -w ${group_name};then                # ensure user in docker group                adduser ${RUN_USER} ${group_name}            fi        fi        exec su-exec $RUN_USER $@    fifiexec $@

设置 “RUN_USER” 为operator,然后设置宿主机的 docker 的 data-root 下面权限(可以使用 systemd 的ExecStartPost=):

/var/lib//docker/image:750   ok/var/lib//docker/image/overlay2:750 ok/var/lib//docker/image/overlay2/layerdb:750 ok

cadvisor 参数为:

...      args:      - -docker_only=true      - -housekeeping_interval=20s      - -disable_metrics=accelerator,cpu_topology,tcp,udp,percpu,sched,process,hugetlb,referenced_memory,resctrl

cron

非 root 无法使用 cron 启动,使用go-crond[10]

引用链接

[1]rm -rf * 前一定一定要看清当前目录:

[2]danielqsj/kafka_exporter:

[3]ClickHouse/clickhouse_exporter:

[4]kubernetes addonresizer:

[5]mysql docker 镜像的官方启动脚本:

[6]gosu:

[7]su-exec:

[8]内核设置的权限:#L61

[9]wurstmeister/kafka-docker:

[10]go-crond:

[11]k8s 社区关于支持 user namespace 提议:

[12]dmi 信息规范:

[13]dmidecode 源码:#L448

本文转载自:「张馆长的博客」,原文:,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com。

最近,我们建立了一个技术交流微信群。目前群里已加入了不少行业内的大神,有兴趣的同学可以加入和我们一起交流技术,在 「奇妙的 Linux 世界」 公众号直接回复 「加群」 邀请你入群。

你可能还喜欢

点击下方图片即可阅读

如何使用 k3sup 一分钟快速搭建轻量极 Kubernetes 集群

点击上方图片,『美团|饿了么』外卖红包天天免费领

限时特惠:本站每日持续更新海量各大内部网赚创业教程,会员可以下载全站资源点击查看详情
站长微信:11082411

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。