使用 nsenter 从 k8s Pod 逃逸到 Host

本文是读完 Detecting a Container Escape with Cilium and eBPF 和 使用 Cilium 增强 Kubernetes 网络安全 的一个简单总结。 如何从 Pod 逃逸到 Host 通常为了安全起见,生产环境的 docker image 都要求不使用 root,一般都是在 Dockerfile 中指定 USER xxx,这样启动的 container/pod 是使用非特权的 user,这样的 user 是没法用 sudo 安装软件的。 有时为了能临时 debug,需要安装 vim, curl 之类的命令,又不想改动 Dockerfile 重新 build image,该怎么办呢? 一个 k8s 原生支持的方法是在 deployment 里面指定 securityContext,如下所示 $ cat privileged.yaml apiVersion: v1 kind: Pod metadata: name: privileged-the-pod spec: hostPID: true hostNetwork: true containers: - name: privileged-the-pod image: nginx:latest ports: - containerPort: 80 securityContext: privileged: true 对于 docker container,可以在指定 docker run 命令时,设置 --user 为 0 也能获得 root 的 container。 ...

2022-04-03 · Me

k8s 切换 namespace 以及命令补全

本文可以学到 .kube/config 文件中有哪些内容 如何实现 bash 的命令补全功能 起因 在使用 kubectl 命令的过程中,经常需要查看不同 namespace 下的资源,因此命令经常需要带上 -n name。 如果不想每次都多打这些字符,也可以设置一个默认的 namespace, kubectl config set-context --current --namespace=xxxx 这样是方便了不少,但是一旦切换了 namespace 之后,又要重复上面的命令,而且经常还不记得。 有没有更好的办法呢? 有人开发了一个小工具,kubectx 专门用于方便的切换 ctx 和 namespace。 ctx 是什么呢? 其实就是哪个 k8s 集群。 说白了就是让你方便的在多个集群和 namespace 之间切换。 kubectx 有两种实现,一开始用的是最简单的 bash shell 脚本,新的版本开始用 k8s client API 开发。 下文的分析仅仅关注 namespace 的切换。 shell 版本的实现 这个实现非常简单,本质上就是调用几个 kubectl 命令实现 ns 切换。 首先需要知道的是,在 ~/.kube/config 路径下的 config 记录了你配置 kubectl 的信息,比如你用 kubectl 操作过几个 k8s 都会纪录在里面。 apiVersion: v1 clusters: - cluster: certificate-authority-data: DATA+OMITTED server: https://10.180.117.162 name: gke_hyrule-dev_us-central1_hyrule-us-central1 contexts: - context: cluster: gke_hyrule_us-central1 namespace: mi-hyrule user: gke_hyrule-dev_us-central1_hyrule-us-central1 name: gke_hyrule-dev_us-central1_hyrule-us-central1 - context: cluster: user: name: current-context: gke_hyrule_us-central1 kind: Config preferences: {} users: - name: gke_hyrule-dev_us-central1_hyrule-us-central1 user: auth-provider: config: access-token: ya29.c.Ko8BCghf9Cp47iz7usZH24k_ask9OD6E4KEh-Z cmd-args: config config-helper --format=json cmd-path: /usr/lib/google-cloud-sdk/bin/gcloud name: gcp 如果是多个 k8s 的话将会有多个 contexts 和 users 字段。 ...

2021-08-01 · Me

docker exec 是如何实现交互的

docker exec 命令的作用是进入到“容器内部”,并执行一些命令,那么它是如何实现把“容器内部”的 io 重定向到我们的终端(bash) 的呢? 基本原理 首先,要明白容器所依赖的内核 namespace 的概念,其实不存在“容器内部”,只要两个进程在相同的 namespace,那它们就相互可见,从用户的角度来说,也就是进入了容器內部。 nsenter nsenter 是一个命令行工具,它可以运行一个 binary,并且把它加入到指定的 namespace 中。 用法如下, nsenter -h nsenter -a -t <pid> <command> nsenter -m -u -i -n -p -t <pid> <command> 假设有一个 redis container 正在运行,通过 docker inspect --format {{.State.Pid}} 获取 pid, 假设为 2929。 然后运行 nsenter 命令: # nsenter -a -t 2929 ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND redis 1 0.0 0.0 52968 7744 ? Ssl May17 0:19 redis-server *:6379 root 93 0.0 0.0 7640 2748 ? R+ 05:47 0:00 ps aux 可以看到,ps 命令输出了 “容器內部” 的进程: redis 和 ps,符合我们的预期。 ...

2021-05-17 · Me

如何实现一个 kubectl-debug

借助 k8s client-go 连接 API Server,并进行简单的 list 操作,创建 deployment 如何在 pod 内创建 container 如何让 container 加入到某个 Pod 中,并共享 namespace 如何让 pod 内部的 tty 操作结果显示在用户端 docker exec 和 直接 nsenter 还是不太一样的: docker exec, OCI-O 的实现是启动一个 grpc server,重定向 IO,等于是把 container 的 IO stream 重定向到 用户 cli nsenter 则是启动一个 进程,然后加入到 container 的 ns 所以 前者不需要知道 container 的 pid,而后者需要知道,所以后者需要执行 docker insepect 命令。 观察上图,分析原理,不难发现,容器内部的进程关系已然不是树。然而,为什么总是强调“树状”关系呢?答案是:树状的继承关系,有利于容器管理。以上文《docker logs 实现剖析》中卖的关子「docker exec的标准输出不会作为容器日志」为例,Docker Daemon 创建容器主进程时,负责接管主进程的标准输出,从而保证容器主进程下所有进程的标准输出被接管,然而 Docker Daemon 在新创建 docker exec 所需执行的进程时,后者的标准输出并未与容器主进程作关联,也并未被 Docker Daemon 特殊处理,故 docker exec 所执行进程的标准输出不会进入容器的日志文件中。 ...

2021-04-24 · Me

Kubernetes Scheduler 设计与实现 (一)

Scheduler 的工作就是决定让一个 pod 在哪个 node 上运行。 scheduler 从 API Server 获得 pod 和 node 的信息,然后把它的决策信息写会 API Server, 它自己不参与具体的调度,而是运行在每个 node 上的 kubelet 主动获取更新,然后启动 pod。 scheduler 的入口函数在 cmd/kube-schduler/server.go,但实际工作都是在 pkg/scheduler/scheduler.go 里面的 Run 函数开始的。 打开 scheduler.go 文件找到结构体 Scheduler ,会发现它有很多私有函数,但只有唯一一个公开的 Run() 函数。 先从 Scheduler 结构体来说一下调度器的整体思路,其中最重要的三个成员如下: type Scheduler struct { Algorithm core.ScheduleAlgorithm NextPod func() *framework.QueuedPodInfo SchedulingQueue internalqueue.SchedulingQueue } Algorithm 就是具体调度的算法 SchedulingQueue 是等待调度的队列,它本身是一个接口,它的实现是 PriorityQueue ,位于 pkg/scheduler/internal/queue/scheduling_queue.go NextPod 获取等待调度的 pod 另外顺便提一下,kubernetes 中的调度队列是由三个队列组成,分别是: activeQueue:待调度的 pod 队列,scheduler 会监听这个队列 backoffQueue:在 kubernetes 中,如果调度失败了,就相当于一次 backoff。 backoffQueue 专门用来存放 backoff 的 pod。 unschedulableQueue:调度过程被终止的 pod 存放的队列。 然后来看 Scheduler 的 Run 函数: ...

2021-04-17 · Me

Kubernetes 中的 DNS

该文件指定如何解析主机名 cat /etc/host.conf order hosts, bind multi on order bind,hosts 指定主机名查询顺序,这里规定先使用 DNS 来解析域名,然后再查询 /etc/hosts 文件(也可以相反) multi on 指 /etc/hosts 文件中的主机可以有多个地址 nospoof on 指不允许对该服务器进行IP地址欺骗

2021-03-10 · Me

Kubernetes 的 Volume 和 StorageClass

Kubernetes 的 Pod 可以 mount 很多种 Volume,常见的 volume 有 emptyDir hostPath configMap, secret persistentVolumeClaim nfs, gitRepo, cephfs, iscsi, cinder 等 其中,emptyDir 是最简单的一种,用于挂载一些临时文件,比如同一个 Pod 中两个 container 需要通过 unix socket 通信,那么把 socket 放在 emptyDir 中是最简单的方法。 甚至可以指定把这个抽象的目录放在内存,从而加快速度。 volumes: - name: html emptyDir: medium: Memory hostPath 是把数据直接存在 kubernetes 某个 worker node 上,这种方法一般不推荐使用,因为当 Pod 被调度到其他节点上后,数据就丢失了。 那么什么样的情况适合挂载 hostPath 呢?一些系统级的组件,需要挂载 node 上系统本身自带的一些文件时,比如需要读取 host 的 cert 目录,或者 etc 目录。常见的有 kube-system 空间下的 coreDNS 组件等。 当 Pod 中的程序需要把数据持久化到外部存储时,最推荐的用法是先在系统中定义 StorageClass,然后配合 persistentVolumeClaim (PVC) 和 persistentVolume (PV) 一起动态的分配空间。 ...

2020-09-28 · Me

Kubernetes Headless Service

问题起源于我用 envoy 对 grpc 做 Layer 7 负载均衡的时候,发现 traffic 永远被转发到了一个特定的 Pod,显然是配置出错了。 环境如下: 同一个 namespace 下部署了 2 个 grpc server,一个 envoy $ kubectl get pod NAME READY STATUS RESTARTS AGE grpc-envoy-7684f49cb-9fv4h 1/1 Running 0 4h49m grpc-server-668bdd6576-2bvkz 1/1 Running 0 4h51m grpc-server-668bdd6576-tqzj4 1/1 Running 0 4h51m envoy 的 service 配置如下 apiVersion: v1 kind: Service metadata: name: grpc-envoy namespace: default labels: app: grpc-envoy spec: type: NodePort ports: - name: grpc port: 8080 targetPort: grpc nodePort: 30061 protocol: TCP selector: app: grpc-envoy grpc-server 的 service 配置如下 ...

2020-09-20 · Me

Kubernetes Pod 中的 Pause 容器

在一个运行 kubernetes 的节点上,我们能看到很多名叫 “pause” 的 container。比如 $ sudo docker ps | grep pause a4218d1d379b k8s.gcr.io/pause:3.1 "/pause" a2109bf3f0db k8s.gcr.io/pause:3.1 "/pause" 57cfa42e95d3 k8s.gcr.io/pause:3.1 "/pause" 仔细观察一下不难发现,每一个 Pod 都会对应一个 pause container。 在查阅了网上的一些资料以后,我总结了一下它大概有两个作用, 它是 Pod 中第一个启动的 container ,由它创建新的 linux namespace,其他 container 启动后再加入到这些 namespace 中。 在 Pod 的环境中充当 init process 的角色,它的 PID 是 1,负责回收所有僵尸进程。 说个题外话,在 docker 中,一个 container 启动时,Dockerfile 的 ENTRYPOINT 中指定的命令会成为这个 container 的 init process,PID 为 1. 顺便来看一下 pause 容器的实现,一共只有几十行 C 语言代码 static void sigdown(int signo) { psignal(signo, "Shutting down, got signal"); exit(0); } static void sigreap(int signo) { while (waitpid(-1, NULL, WNOHANG) > 0) ; } int main(int argc, char **argv) { if (getpid() != 1) /* Not an error because pause sees use outside of infra containers. */ fprintf(stderr, "Warning: pause should be the first process\n"); if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0) return 1; if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0) return 2; if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap, .sa_flags = SA_NOCLDSTOP}, NULL) < 0) return 3; for (;;) pause(); fprintf(stderr, "Error: infinite loop terminated\n"); return 42; } 函数 sigreap(int signo) 就是在子进程退出的时候进行回收,防止产生僵尸进程,所以是 “reap”,非常形象生动。 ...

2020-09-13 · Me

Kubernetes Dashboard 添加 Auth

组里的 k8s cluster Dashboard 一直没有设置登录,导致所有可以 ping IP 的人都可以登录管理界面,这样显然是不合理的,应该要设置几个不同权限的账户,并且开启 Dashboard 的 Basic Auth。 关于开启 Dashboard Basic Auth 网上有不少资料,但是在实际操作中还是遇到几个莫名其妙的坑。 创建用户文件 根据实际需求,并不需要使用 LDAP 等等复杂的登录方式,我们只需要一个 admin 账户,再加上低权限的只读 view 账户,以及有修改权限的 edit 账户就足够。 因此,只要添加 /etc/kubernetes/pki/basic_auth_file 文件即可。 vi /etc/kubernetes/pki/basic_auth_file password,username,1 需要注意的一个坑是用户名密码的顺序是反过来的(如上面所示),否则在 dashboard 上怎么输入都提示不对。 修改 API Server 配置 修改 /etc/kubernetes/manifests/kube-apiserver.yaml 加入一个启动参数 vim /etc/kubernetes/manifests/kube-apiserver.yaml - --basic-auth-file=/etc/kubernetes/pki/basic_auth_file 这里又遇到一个坑,在我们的 k8s Master 节点这个目录下有两个文件,第一个叫 kube-apiserver.yaml, 另一个是 kube-apiserver_xxx.yaml。 本来我以为第一个是实际的配置文件,第二个应该是其他人配置时 copy 的一个备份。 然而实际并不是,真正被使用的是第二个文件,这点让人匪夷所思,我花了好久才发现这个坑,但是我始终没找到哪里指定了让 api server 读取 kube-apiserver_xxx.yaml 而不是 kube-apiserver.yaml 。 重启 API Server 很多资料上都会把重启 api server 一笔带过,但是都不写具体怎么操作,k8s 上并不是简单删除一个 pod 就算重启的。 ...

2020-08-04 · Me