惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
罗磊的独立博客
H
Help Net Security
I
Intezer
G
Google Developers Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
Troy Hunt's Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
U
Unit 42
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
News and Events Feed by Topic
J
Java Code Geeks
S
Security Affairs
T
The Blog of Author Tim Ferriss
Recent Commits to openclaw:main
Recent Commits to openclaw:main
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
D
Docker
The GitHub Blog
The GitHub Blog
F
Full Disclosure
N
News and Events Feed by Topic
Webroot Blog
Webroot Blog
S
Security @ Cisco Blogs
腾讯CDC
人人都是产品经理
人人都是产品经理
M
MIT News - Artificial intelligence
Blog — PlanetScale
Blog — PlanetScale
T
Threatpost
D
DataBreaches.Net
Recent Announcements
Recent Announcements
博客园 - 三生石上(FineUI控件)
MongoDB | Blog
MongoDB | Blog
博客园 - 【当耐特】
L
LINUX DO - 最新话题
Google Online Security Blog
Google Online Security Blog
S
Schneier on Security
S
Securelist
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Help Net Security
Help Net Security
P
Proofpoint News Feed
Project Zero
Project Zero
S
SegmentFault 最新的问题
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
Google DeepMind News
Google DeepMind News
宝玉的分享
宝玉的分享
Y
Y Combinator Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 叶小钗

陈少文的网站

巨变与机遇的未来十年 Kubernetes 平台管理软件压力测试方案 使用镜像部署 Hexo 静态页面 终于等到你 - GitHub 镜像仓库服务(ghcr.io) 一起来学 Go --(6)Interface 一起来学 Go --(5)Goroutine 和 Channel 什么是函数式编程 如何在 Kubernetes 集群集成 Kata 柯里化与偏函数 使用 PyGithub 自动创建 Label 软件产品是团队能力的输出 Helm 2 、Helm 3 比较 IoT 变现 Kubernetes 中的 DNS 服务 国内的 Helm 镜像源 Harbor 使用自签证书支持 Https 访问 DevOps 工具链之 Prow 如何使用 kfctl 安装 Kubeflow VS Code 无法下载 Go 插件的工具包 工程师更应具有服务精神 你不知道的 Docker 使用技巧 使用 Docker 运行 Tensorflow 论中国 什么是左移 如何清空 Git 仓库全部历史记录 一禅小和尚 有风吹过厨房 时间的玫瑰 如何在 CentOS 安装 GPU 驱动 开发 Tips(19) 使用 Velero 备份 Kubernetes 集群 Kubernetes Cheat Sheet 开发 Tips(18) 如何构建一个 Java 工程 开发 Tips(17) KubeSpray 安装 Kubernetes 报错 ip in ansible_all_ipv4_addresses 基于 Kubernetes 和 Jenkins 搭建自动化测试系统 在 Kubernetes 上动态创建 Jenkins Slave 使用 Jenkins 进行服务拨测 开发 Tips(16) Kubernetes 签发 Ingress 证书及日常故障运维 Kubernetes 中 Deployment 的基本操作 Kubernetes 中的证书 如何使用 KubeBuilder 开发一个 Operator Kubernetes 1.6.0 安装问题汇总 镜像管理工具 -- Harbor 开发 Tips(15) Docker 如何拉取镜像 开发 Tips(14) 使用 Helm 安装 harbor 开发 Tips(13) 使用 S2I 构建云原生应用 在 Kubernetes 中使用 emptyDir、hostPath、localVolume 开发 Tips(12) 开发 Tips(11) 代码质量分析工具 SonarQube 使用 Kubeadm 安装 Kubernetes 集群 一起来学 Go --(4)常用函数 Kubernetes 中的 Ceph Kubernetes 之 Volumes Kubernetes 之 Labels、Selectors 开发 Tips(10) 开源正在重构商业模式 Kubernetes 之网络 Kubernetes 之 API 使用 Helm 和 Operator 快速部署 Prometheus Kubernetes 复杂有状态应用管理框架 -- Operator Kubernetes 的包管理器 -- Helm 一起来学 Go --(3)Go Modules 如何一步一步地优化博客方案 kubectl 实用指南 Kubernetes 中的基本概念 搭建远程 Kubernetes 开发环境 大公司和小公司的 ToB 思路 开发 Tips(9) Go 入门指南 一起来学 Go --(2)数据与逻辑结构 如何预防 Web 富文本中的 XSS 攻击 django-xss-cleaner 云工作时代 一起来学 Go --(1)背景与特点 SaaS 开发团队的不同阶段 你不知道的 Git 使用技巧 输出既服务 微服务设计 继续奔跑 开发 Tips(8) 从账户安全到二次验证 Django 性能之数据库查询优化 Django 性能之分库分表 敏捷开发之研发流程 打造一致性的团队 开发 Tips(7) Pytest 进阶学习之 Mock PaaS 部署之 buildpack Go 开发配置 领域输出才是 PaaS 的核心竞争力 Pytest 入门学习 开发 Tips(6) 如何使用 Jenkins、Docker、GitLab 搭建 Django 自动化部署流程
如何在主机上调试容器、在容器中操作主机
微信公众号 · 2021-01-11 · via 陈少文的网站

1. 一个奇怪的需求

老板有个奇怪的需求,通过一个 kubeconfig 文件,获取主机的各种状态信息,比如进程列表、进程状态等。

第一反应就是,老板是不是不懂容器,容器怎么能这样玩,这样玩还要什么容器,内心万马奔腾。

直到最近遇到了一个命令行工具,才发现原来小丑是我自己。下面一起来看看,我发现了什么吧。

2. 容器的原理

沙箱是一个虚拟环境,在沙箱内部进行的操作对外部没有影响。沙箱与沙箱之间是隔离的,也是不可见的,看不到彼此的存在。

我们常说的容器就是基于 Linux 的 Cgroups 和 Namespace 技术构建的一个沙箱环境。

从图中,可以看到,容器与容器的边界就是通过 Cgroups 和 Namespace 这两种技术控制的。下面简单描述一下这两种技术:

  1. Namespace

不同 Namespace 下的资源相互独立、不可见。Linux 从 2.4.19 完成了支持 Mount Namespace,2.6.19 完成了支持 UTS、IPS Namespace,2.6.24 完成了支持 PID Namespace,2.6.29 完成了支持 Network Namespace,3.8 完成了支持 User Namespace 。其中,除了 User Namespace ,其他都需要以 root 权限创建。同时,在 4.6 中已经新增了 Cgroup namespace,目前 RunC(Docker 提供的运行时) ,已经合并了相关的 PR: https://github.com/opencontainers/runc/pull/1916 。下面是其中的 7 种 Namespace。

  • Mount namespace,隔离文件系统挂载点。一个 Namespace 中,程序对文件的修改,只影响自身的文件系统,而对其他 Namespace 没有影响。

  • UTS namespace,隔离主机名和域名信息。每个 Namespace 中,主机和域名信息相互独立。

  • IPC namespace,隔离进程通信的行为。只有一个 Namespace 中的进程可以互相通信。

  • PID namespace,隔离进程的 PID 空间。不同 Namespace 中的进程 PID 可以重复,互不影响。PID 为 1 的进程是其他所有进程的父进程,因此这个 Namespace 非常有意义。

  • Network namespace,隔离网络资源。每个 Namespace 都具有独立的网络栈信息,容器运行时仿佛在一个独立的网络中。

  • User namespace,隔离用户和用户组。同一个用户在不同的 Namespce,可以具有不同的角色,用来保障安全性。

  • Cgroup namespace,隔离 Cgroup 的可见性。每个 Namespace 中,都具有独立的 cgroupns root 和 cgroup filesystem 视图。

  1. Cgroups

上面将一组进程放置到一个 Namespace,对外隔离,对内共享资源,接着使用 Cgroups 对其进行资源的控制。Cgroups 提供了四个功能:

  • 资源限制。针对一个进程或进程组,设置资源消耗限制。比如内存超出限制,会导致申请内存失败。
  • 资源统计。统计 CPU 使用时长、内存用量等。
  • 任务控制。控制进程的状态,可以挂起、恢复进程。
  • 优先级分配。设置进程的优先级。

利用 Namespcae 和 Cgroups 提供的沙箱环境,再加上文件系统技术,就支持起了容器技术。

3. 一个调试工具: nsenter

nsenter 是一个用来进入指定程序,所在 Namespace,并执行命令的工具。在容器场景下,很多容器为了轻量化,而裁剪了很多基础的命令,比如 iptcpdump 等。这样给调试容器带来了一定的困难,通过 nsenter 共享 Namespace 进行调试,可以很好地解决这个问题。

实际上,RunC 在创建容器时,也是调用的 nsenter ,在 libcontainer 的代码中可以看到。

  1. 安装 nsenter

大部分的 Linux 操作系统,已经内置了 nsenter 命令。如果没有,以 CentOS 为例,执行如下命令,安装 util-linux 包即可:

1
yum install -y util-linux
  1. nsenter 的版本和参数

由于不同的 Linux Kernel 对 Namespace 支持的程度不一样,nsenter 的版本会有所差异。这里以 CentOS 7 为例:

  • 查看系统内核版本
1
2
3
uname -a

Linux i-x29a8rdc 3.10.0-1127.10.1.el7.x86_64 #1 SMP Wed Jun 3 14:28:03 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
  • 查看 nsenter 版本
1
2
3
nsenter -V

nsenter from util-linux 2.23.2
  • 查看 nsenter 的参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
nsenter -h

Usage:
 nsenter [options] <program> [<argument>...]

Run a program with namespaces of other processes.

Options:
 -t, --target <pid>     target process to get namespaces from
 -m, --mount[=<file>]   enter mount namespace
 -u, --uts[=<file>]     enter UTS namespace (hostname etc)
 -i, --ipc[=<file>]     enter System V IPC namespace
 -n, --net[=<file>]     enter network namespace
 -p, --pid[=<file>]     enter pid namespace
 -U, --user[=<file>]    enter user namespace
 -S, --setuid <uid>     set uid in entered namespace
 -G, --setgid <gid>     set gid in entered namespace
     --preserve-credentials do not touch uids or gids
 -r, --root[=<dir>]     set the root directory
 -w, --wd[=<dir>]       set the working directory
 -F, --no-fork          do not fork before exec'ing <program>
 -Z, --follow-context   set SELinux context according to --target PID

 -h, --help     display this help and exit
 -V, --version  output version information and exit

这里需要注意的是 -t 参数,指定一个进程,用于获取 Namepace 参数。其他参数主要是使能、设置参数。

由于非沙箱环境下,并不容易体现 nsenter 的功能,我们在容器环境下进一步实验。

4. nsenter 在 Docker 容器环境下的应用

4.1 主机下,进入容器的 Namespace 环境

  • 选择一个容器
1
2
3
4
docker ps

CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS                          PORTS                    NAMES
9addecf82c5e        sonarqube:7.9.4-community   "./bin/run.sh"           3 weeks ago         Up 3 weeks                      0.0.0.0:9000->9000/tcp   sonarqube_sonarqube_1
  • 获取容器的 PID

每个容器内都有一个 PID=1 的进程,如同宿主机上的 init 进程一样,是其他进程的父进程。但是在主机上,容器进程具有另外一个 PID ,可以用于管理容器。

1
2
3
docker inspect --format "{{ .State.Pid }}" 9addecf82c5e

3969
  • 进入容器的 Namespace 环境

这里以进入网络空间为例:

1
nsenter -t 3969  -n /bin/bash

如果宿主机上的默认 shell,在容器中存在,可以省略 /bin/bash,否则需要显式指定一个容器中的 shell。

  • 执行主机上的命令行工具,调试容器环境
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0
       valid_lft forever preferred_lft forever

从执行结果可以看到,显示的是容器上的网卡地址信息,但 ip 命令却来自宿主机。

4.2 容器下,进入主机的 Namespace 环境

  • 以特权模式,使用主机的 Namespace 创建容器
1
docker run --privileged --net=host --ipc=host --pid=host -it --rm docker.io/alpine:3.12 /bin/sh
  • 进入 PID=1 进程的 Namespace 环境
1
nsenter -t 1 -m -u -i -n
  • 执行命令,操作主机环境
1
2
3
4
5
docker ps

CONTAINER ID        IMAGE                       COMMAND                  CREATED              STATUS                          PORTS                    NAMES
2cd99b9d7b5a        alpine:3.12                 "/bin/sh"                About a minute ago   Up About a minute                                        trusting_khorana
9addecf82c5e        sonarqube:7.9.4-community   "./bin/run.sh"           3 weeks ago          Up 3 weeks                      0.0.0.0:9000->9000/tcp   sonarqube_sonarqube_1

从执行结果可以看到,命令显示的是主机上的容器信息,但却是在容器下执行的命令。

5. nsenter 在 Kubernetes 容器环境下的应用

这部分的内容和上一个章节类似,只不过在进入容器时,需要借道 Pod 获取 PID;在主机上执行命令时,需要借道 Pod 创建容器。

5.1 从主机进入 Kubernetes Pod 中,调试容器环境

  • 选择一个 Pod
1
2
3
4
kubectl get pod -o wide

NAME                     READY   STATUS             RESTARTS   AGE     IP             NODE               NOMINATED NODE   READINESS GATES
nginx-6db489d4b7-589bd   1/1     Running            0          11s     10.233.76.91   tf-cd-allinone-0   <none>           <none>
  • 获取容器 ID
1
2
3
kubectl describe pod nginx-6db489d4b7-589bd | grep -A10 "^Containers:" | grep -Eo 'docker://.*$' | head -n 1 | sed 's/docker:\/\/\(.*\)$/\1/'

981c94ef07abfbeca548e9e36cd70a7369d1cf38a50754c2dc4f87fbc27601d1
  • 切换到主机所在节点
1
ssh root@tf-cd-allinone-0
  • 获取容器的 PID
1
2
3
docker inspect --format "{{.State.Pid}}" 981c94ef07abfbeca548e9e36cd70a7369d1cf38a50754c2dc4f87fbc27601d1

6954
  • 进入容器的 Namespace
  • 执行主机命令,调试容器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if100: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default
    link/ether 16:a3:44:dc:58:ce brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.233.76.91/32 scope global eth0
       valid_lft forever preferred_lft forever

这里需要注意的是,容器和节点是绑定在一起的,对于多节点环境,获取容器 ID 之后,需要切换到所在主机进行操作。

5.2 在 Kubernetes Pod 中,直接操作主机

  • 新建一个 pod-test.yaml 文件,内容如下
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
  name: pod-test
  namespace: default
spec:
  containers:
    - command:
        ["sh", "-c", 'echo "Hello, wwww.chenshaowen.com !" && sleep 3600']
      image: docker.io/alpine:3.12
      name: pod-test
      securityContext:
        privileged: true
  hostIPC: true
  hostNetwork: true
  hostPID: true
  • 创建 Pod
1
kubectl apply -f pod-test.yaml
  • 进入 Pod 中
1
kubectl exec -it pod-test /bin/sh
  • 进入 PID=1 进程的 Namespace
1
nsenter -t 1 -m -u -i -n -p
  • 执行主机命令测试
1
2
3
4
5
6
docker ps

CONTAINER ID   IMAGE                                         COMMAND                   CREATED         STATUS         PORTS     NAMES
f6bd778c3172   389fef711851                                  "sh -c 'echo \"Hello,…"   4 minutes ago   Up 4 minutes             k8s_pod-test_pod-test_default_3a496075-419e-477a-b03c-a423677a90be_0
4e197fd98294   kubesphere/pause:3.1                          "/pause"                  4 minutes ago   Up 4 minutes             k8s_POD_pod-test_default_3a496075-419e-477a-b03c-a423677a90be_0
...

以特权模式启动容器,通过 PID=1 的进程共享 Namespace,直接执行主机上的命令。

6. 总结

本篇主要介绍了在容器环境下,如何逃逸到主机执行命令;在主机下,如何进入容器调试环境。同时,还给出了在 Container 和 Kubernetes 两种场景下的实践示例。

其中有两点对我有所启发,一个是 nsenter 命令,加深了对容器技术的理解。另外一个是特权模式启动的容器,权限十分大,需要谨慎,业务应该尽量采用 rootless 的方式运行容器。

在以特权模式启动的 Docker Daemon 中,创建 Kuberntes 集群,通过 nsenter 命令,可以 nodeSelector 到任意节点,然后执行 kubectl/docker/systemctl 等命令进行破坏活动。

7. 参考