标签 k8s 下的文章

Kubernetes Deployment故障排除图解指南

img{512x368}


下面是一个示意图,可帮助你调试Kubernetes Deployment(你可以在此处下载它的PDF版本)。

img{512x368}

当你希望在Kubernetes中部署应用程序时,你通常会定义三个组件:

  • 一个Deployment – 这是一份用于创建你的应用程序的Pod副本的”食谱”;
  • 一个Service – 一个内部负载均衡器,用于将流量路由到内部的Pod上;
  • 一个Ingress – 描述如何流量应该如何从集群外部流入到集群内部的你的服务上。

下面让我们用示意图快速总结一下要点。

img{512x368}

在Kubernetes中,你的应用程序通过两层负载均衡器暴露服务:内部的和外部的

img{512x368}

内部的负载均衡器称为Service,而外部的负载均衡器称为Ingress

img{512x368}

Pod不会直接部署。Deployment会负责创建Pod并管理它们

假设你要部署一个简单的”HelloWorld”应用,该应用的YAML文件的内容应该类似下面这样:

// hello-world.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    track: canary
spec:
  selector:
    matchLabels:
      any-name: my-app
  template:
    metadata:
      labels:
        any-name: my-app
    spec:
      containers:
      - name: cont1
        image: learnk8s/app:1.0.0
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    name: app
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - http:
    paths:
    - backend:
        serviceName: app
        servicePort: 80
      path: /

这个定义很长,组件之间的相互关系并不容易看出来。

例如:

  • 什么时候应使用端口80,又是何时应使用端口8080?
  • 你是否应该为每个服务创建一个新端口以免它们相互冲突?
  • 标签(label)名重要吗?它们是否在每一处都应该是一样的?

在进行调试之前,让我们回顾一下这三个组件是如何相互关联的。

让我们从Deployment和Service开始。

一. 连接Deployment和Service

令人惊讶的消息是,Service和Deployment之间根本没有连接。

事实是:Service直接指向Pod,并完全跳过了Deployment。

因此,你应该注意的是Pod和Service之间的相互关系。

你应该记住三件事:

  • Service selector应至少与Pod的一个标签匹配;
  • Service的targetPort应与Pod中容器的containerPort匹配;
  • Service的port可以是任何数字。多个Service可以使用同一端口号,因为它们被分配了不同的IP地址。

下面的图总结了如何连接端口:

img{512x368}

考虑上面被一个服务暴露的Pod

img{512x368}

创建Pod时,应为Pod中的每个容器定义containerPort端口

img{512x368}

当创建一个Service时,你可以定义port和targetPort,但是哪个用来连接容器呢?

img{512x368}

targetPort和containerPort应该始终保持匹配

img{512x368}

如果容器暴露3000端口(containerPort),那么targetPort应该匹配这一个端口号

再来看看YAML,标签和ports/targetPort应该匹配:

// hello-world.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    track: canary
spec:
  selector:
    matchLabels:
      any-name: my-app
  template:
    metadata:
      labels:
        any-name: my-app
    spec:
      containers:
      - name: cont1
        image: learnk8s/app:1.0.0
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    any-name: my-app

那deployment顶部的track: canary标签呢?

它也应该匹配吗?

该标签属于deployment,service的选择器未使用它来路由流量。

换句话说,你可以安全地删除它或为其分配其他值。

matchLabels选择器呢?

它必须始终与Pod的标签匹配,并且被Deployment用来跟踪Pod。

假设你已经进行了所有正确的设置,该如何测试它呢?

你可以使用以下命令检查Pod是否具有正确的标签:

$ kubectl get pods --show-labels

或者,如果你拥有属于多个应用程序的Pod:

$ kubectl get pods --selector any-name=my-app --show-labels

any-name=my-app就是标签:any-name: my-app

还有问题吗?

你也可以连接到Pod!

你可以使用kubectl中的port-forward命令连接到service并测试连接。

$ kubectl port-forward service/<service name> 3000:80
  • service/ 是服务的名称- 在上面的YAML中是“my-service”
  • 3000是你希望在计算机上打开的端口
  • 80是service通过port字段暴露的端口

如果可以连接,则说明设置正确。

如果不行,则很可能是你填写了错误的标签或端口不匹配。

二. 连接Service和Ingress

接下来是配置Ingress以将你的应用暴露到集群外部。

Ingress必须知道如何检索服务,然后检索Pod并将流量路由给它们。

Ingress按名字和暴露的端口检索正确的服务。

在Ingress和Service中应该匹配两件事:

  • Ingress的servicePort应该匹配service的port
  • Ingress的serviceName应该匹配服务的name

下面的图总结了如何连接端口:

img{512x368}

你已经知道servive暴露一个port

img{512x368}

Ingress有一个字段叫servicePort

img{512x368}

service的port和Ingress的service应该始终保持匹配

img{512x368}

如果你为service指定的port是80,那么你也应该将ingress的servicePort改为80

实践中,你应该查看以下几行(下面代码中的my-service和80):

// hello-world.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-service   --- 需关注
spec:
  ports:
  - port: 80       --- 需关注
    targetPort: 8080
  selector:
    any-name: my-app
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - http:
    paths:
    - backend:
        serviceName: my-service --- 需关注
        servicePort: 80 --- 需关注
      path: /

你如何测试Ingress是否正常工作呢?

你可以使用与以前相同的策略kubectl port-forward,但是这次你应该连接到Ingress控制器,而不是连接到Service。

首先,使用以下命令检索Ingress控制器的Pod名称:

$ kubectl get pods --all-namespaces
NAMESPACE   NAME                              READY STATUS
kube-system coredns-5644d7b6d9-jn7cq          1/1   Running
kube-system etcd-minikube                     1/1   Running
kube-system kube-apiserver-minikube           1/1   Running
kube-system kube-controller-manager-minikube  1/1   Running
kube-system kube-proxy-zvf2h                  1/1   Running
kube-system kube-scheduler-minikube           1/1   Running
kube-system nginx-ingress-controller-6fc5bcc  1/1   Running

标识Ingress Pod(可能在其他命名空间中)并描述它以检索端口:

$ kubectl describe pod nginx-ingress-controller-6fc5bcc \
 --namespace kube-system \
 | grep Ports
Ports:         80/TCP, 443/TCP, 18080/TCP

最后,连接到Pod:

$ kubectl port-forward nginx-ingress-controller-6fc5bcc 3000:80 --namespace kube-system

此时,每次你访问计算机上的端口3000时,请求都会转发到Ingress控制器Pod上的端口80。

如果访问http://localhost:3000,则应找到提供网页服务的应用程序。

回顾Port

快速回顾一下哪些端口和标签应该匹配:

  • service selector应与Pod的标签匹配
  • service的targetPort应与Pod中容器的containerPort匹配
  • service的端口可以是任何数字。多个服务可以使用同一端口,因为它们分配了不同的IP地址。
  • ingress的servicePort应该匹配service的port
  • serivce的名称应与ingress中的serviceName字段匹配

知道如何构造YAML定义只是故事的一部分。

出了问题后该怎么办?

Pod可能无法启动,或者正在崩溃。

三. kubernetes deployment故障排除的3个步骤

在深入研究失败的deployment之前,我们必须对Kubernetes的工作原理有一个明确定义的思维模型。

由于每个deployment中都有三个组件,因此你应该自下而上依次调试所有组件。

  • 你应该先确保Pods正在运行
  • 然后,专注于让service将流量路由到到正确的Pod
  • 然后,检查是否正确配置了Ingress

img{512x368}

你应该从底部开始对deployment进行故障排除。首先,检查Pod是否已就绪并正在运行。

img{512x368}

如果Pod已就绪,则应调查service是否可以将流量分配给Pod。

img{512x368}

最后,你应该检查service与ingress之间的连接。

1. Pod故障排除

在大多数情况下,问题出在Pod本身。

你应该确保Pod正在运行并准备就绪。

该如何检查呢?

$ kubectl get pods
NAME                    READY STATUS            RESTARTS  AGE
app1                    0/1   ImagePullBackOff  0         47h
app2                    0/1   Error             0         47h
app3-76f9fcd46b-xbv4k   1/1   Running           1         47h

在上述会话中,最后一个Pod处于就绪并正常运行的状态;但是,前两个Pod既不处于Running也不是Ready。

你如何调查出了什么问题?

有四个有用的命令可以对Pod进行故障排除:

  • kubectl logs 有助于检索Pod容器的日志
  • kubectl describe pod 检索与Pod相关的事件列表很有用
  • kubectl get pod 用于提取存储在Kubernetes中的Pod的YAML定义
  • kubectl exec -ti bash 在Pod的一个容器中运行交互式命令很有用

应该使用哪一个呢?

没有一种万能的。

相反,我们应该结合着使用它们。

常见Pod错误

Pod可能会出现启动和运行时错误。

启动错误包括:

  • ImagePullBackoff
  • ImageInspectError
  • ErrImagePull
  • ErrImageNeverPull
  • RegistryUnavailable
  • InvalidImageName

运行时错误包括:

  • CrashLoopBackOff
  • RunContainerError
  • KillContainerError
  • VerifyNonRootError
  • RunInitContainerError
  • CreatePodSandboxError
  • ConfigPodSandboxError
  • KillPodSandboxError
  • SetupNetworkError
  • TeardownNetworkError

有些错误比其他错误更常见。

以下是最常见的错误列表以及如何修复它们的方法。

ImagePullBackOff

当Kubernetes无法获取到Pod中某个容器的镜像时,将出现此错误。

共有三个可能的原因:

  • 镜像名称无效-例如,你拼错了名称,或者image不存在
  • 你为image指定了不存在的标签
  • 你尝试检索的image属于一个私有registry,而Kubernetes没有凭据可以访问它

前两种情况可以通过更正image名称和标记来解决。

针对第三种情况,你应该将私有registry的访问凭证通过Secret添加到k8s中并在Pod中引用它。

官方文档中有一个有关如何实现此目标的示例

CrashLoopBackOff

如果容器无法启动,则Kubernetes将显示错误状态为:CrashLoopBackOff。

通常,在以下情况下容器无法启动:

  • 应用程序中存在错误,导致无法启动
  • 未正确配置容器
  • Liveness探针失败太多次

你应该尝试从该容器中检索日志以调查其失败的原因。

如果由于容器重新启动太快而看不到日志,则可以使用以下命令:

$ kubectl logs <pod-name> --previous

这个命令打印前一个容器的错误消息。

RunContainerError

当容器无法启动时,出现此错误。

甚至在容器内的应用程序启动之前。

该问题通常是由于配置错误,例如:

  • 挂载不存在的卷,例如ConfigMap或Secrets
  • 将只读卷安装为可读写

你应该使用kubectl describe pod 命令收集和分析错误。

处于Pending状态的Pod

当创建Pod时,该Pod保持Pending状态。

为什么?

假设你的调度程序组件运行良好,可能的原因如下:

  • 集群没有足够的资源(例如CPU和内存)来运行Pod
  • 当前的命名空间具有ResourceQuota对象,创建Pod将使命名空间超过配额
  • 该Pod绑定到一个处于pending状态的 PersistentVolumeClaim

最好的选择是检查kubectl describe命令输出的“事件”部分内容:

$ kubectl describe pod <pod name>

对于因ResourceQuotas而导致的错误,可以使用以下方法检查集群的日志:

$ kubectl get events --sort-by=.metadata.creationTimestamp

处于未就绪状态的Pod

如果Pod正在运行但未就绪(not ready),则表示readiness就绪探针失败。

当“就绪”探针失败时,Pod未连接到服务,并且没有流量转发到该实例。

就绪探针失败是应用程序的特定错误,因此你应检查kubectl describe中的“ 事件”部分以识别错误。

2. 服务的故障排除

如果你的Pod正在运行并处于就绪状态,但仍无法收到应用程序的响应,则应检查服务的配置是否正确。

service旨在根据流量的标签将流量路由到Pod。

因此,你应该检查的第一件事是服务关联了多少个Pod。

你可以通过检查服务中的端点(endpoint)来做到这一点:

$ kubectl describe service <service-name> | grep Endpoints

端点是一对,并且在服务(至少)以Pod为目标时,应该至少有一个端点。

如果“端点”部分为空,则有两种解释:

  • 你没有运行带有正确标签的Pod(提示:你应检查自己是否在正确的命名空间中)
  • service的selector标签上有错字

如果你看到端点列表,但仍然无法访问你的应用程序,则targetPort可能是你服务中的罪魁祸首。

你如何测试服务?

无论服务类型如何,你都可以使用kubectl port-forward来连接它:

$kubectl port-forward service/<service-name> 3000:80

这里:

  • 是服务的名称
  • 3000 是你希望在计算机上打开的端口
  • 80 是服务公开的端口

3.Ingress的故障排除

如果你已到达本节,则:

  • Pod正在运行并准备就绪
  • 服务会将流量分配到Pod

但是你仍然看不到应用程序的响应。

这意味着最有可能是Ingress配置错误。

由于正在使用的Ingress控制器是集群中的第三方组件,因此有不同的调试技术,具体取决于Ingress控制器的类型。

但是在深入研究Ingress专用工具之前,你可以用一些简单的方法进行检查。

Ingress使用serviceName和servicePort连接到服务。

你应该检查这些配置是否正确。

你可以通过下面命令检查Ingress配置是否正确:

$kubectl describe ingress <ingress-name>

如果backend一列为空,则配置中必然有一个错误。

如果你可以在“backend”列中看到端点,但是仍然无法访问该应用程序,则可能是以下问题:

  • 你如何将Ingress暴露于公共互联网
  • 你如何将集群暴露于公共互联网

你可以通过直接连接到Ingress Pod来将基础结构问题与Ingress隔离开。

首先,获取你的Ingress控制器Pod(可以位于其他名称空间中):

$ kubectl get pods --all-namespaces
NAMESPACE   NAME                              READY STATUS
kube-system coredns-5644d7b6d9-jn7cq          1/1   Running
kube-system etcd-minikube                     1/1   Running
kube-system kube-apiserver-minikube           1/1   Running
kube-system kube-controller-manager-minikube  1/1   Running
kube-system kube-proxy-zvf2h                  1/1   Running
kube-system kube-scheduler-minikube           1/1   Running
kube-system nginx-ingress-controller-6fc5bcc  1/1   Running

描述它以检索端口:

# kubectl describe pod nginx-ingress-controller-6fc5bcc
 --namespace kube-system \
 | grep Ports

最后,连接到Pod:

$ kubectl port-forward nginx-ingress-controller-6fc5bcc 3000:80 --namespace kube-system

此时,每次你访问计算机上的端口3000时,请求都会转发到Pod上的端口80。

现在可以用吗?

  • 如果可行,则问题出在基础架构中。你应该调查流量如何路由到你的集群。
  • 如果不起作用,则问题出在Ingress控制器中。你应该调试Ingress。

如果仍然无法使Ingress控制器正常工作,则应开始对其进行调试。

目前有许多不同版本的Ingress控制器。

热门选项包括Nginx,HAProxy,Traefik等。

你应该查阅Ingress控制器的文档以查找故障排除指南。

由于Ingress Nginx是最受欢迎的Ingress控制器,因此在下一部分中我们将介绍一些有关调试ingress-nginx的技巧。

调试Ingress Nginx

Ingress-nginx项目有一个Kubectl的官方插件

你可以用kubectl ingress-nginx来:

  • 检查日志,后端,证书等。
  • 连接到ingress
  • 检查当前配置

你应该尝试的三个命令是:

  • kubectl ingress-nginx lint,它会检查 nginx.conf
  • kubectl ingress-nginx backend,以检查后端(类似于kubectl describe ingress
  • kubectl ingress-nginx logs,查看日志

请注意,你可能需要为Ingress控制器指定正确的名称空间–namespace

四. 总结

如果你不知道从哪里开始,那么在Kubernetes中进行故障排除可能是一项艰巨的任务。

你应该始终牢记从下至上解决问题:从Pod开始,然后通过Service和Ingress向上移动堆栈。

你在本文中了解到的调试技术也可以应用于其他对象,例如:

  • failing Job和CronJob
  • StatefulSets和DaemonSets

本文翻译自learnk8s上的文章A visual guide on troubleshooting Kubernetes deployments


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go语言项目的安全评估技术

在今年夏天我们对Kubernetes的评估成功之后,我们收到了大量Go项目的安全评估需求。为此,我们将在其他编译语言中使用过的安全评估技术和策略调整适配到多个Go项目中。

我们从了解语言的设计开始,识别出开发人员可能无法完全理解语言语义特性的地方。多数这些被误解的语义来自我们向客户报告的调查结果以及对语言本身的独立研究。尽管不是详尽无遗,但其中一些问题领域包括作用域、协程、错误处理和依赖管理。值得注意的是,其中许多与运行时没有直接关系。默认情况下,Go运行时本身的设计是安全的,避免了很多类似C语言的漏洞。

对根本原因有了更好地理解后,我们搜索了现有的能帮助我们快速有效检测客户端代码库的工具。结果我们找到一些静态和动态开源工具,其中包括了一些与Go无关的工具。为了配合这些工具使用,我们还确定了几种有助于检测的编译器配置。

一. 静态分析

由于Go是一种编译型语言,因此编译器在生成二进制可执行文件之前就检测并杜绝了许多潜在的错误模式。虽然对于新的Go开发人员来说,这些编译器的输出比较烦,但是这些警告对于防止意外行为以及保持代码的清洁和可读性非常重要。

静态分析趋向于捕获很多未包括在编译器错误和警告中的悬而未决的问题。在Go语言生态系统中,有许多不同的工具,例如go-vetstaticcheckanalysis包中的工具。这些工具通常会识别出诸如变量遮蔽、不安全的指针使用以及未使用的函数返回值之类的问题。调查这些工具显示警告的项目区域通常会发现可被利用(进行安全攻击)的功能特性。

这些工具绝不是完美的。例如,go-vet可能会错过非常常见的问题,例如下面例子中这种。

package main

import "fmt"

func A() (bool, error) { return false, fmt.Errorf("I get overridden!") }

func B() (bool, error) { return true, nil }

func main() {
    aSuccess, err := A()
    bSuccess, err := B()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(aSuccess, ":", bSuccess)
}

这个例子未使用A函数的err返回值,并在表达式左侧为bSuccess赋值期间立即重新对err做了赋值。编译器针对这种情况不会提供警告,而go-vet也不会检测到该问题;errcheck也不会。实际上,能成功识别这种情况的工具是前面提到的staticcheck和ineffassign,它们将A的错误返回值标识为未使用或无效。

示例程序的输出以及errcheck,go-vet,staticcheck和ineffassign的检查结果如下:

$ go run .
false : true
$ errcheck .
$ go vet .
$ staticcheck .
main.go:5:50: error strings should not be capitalized (ST1005)
main.go:5:50: error strings should not end with punctuation or a newline (ST1005)
main.go:10:12: this value of err is never used (SA4006)
$ ineffassign .
main.go:10:12: ineffectual assignment to err

当您深入研究此示例时,您可能会想知道为什么编译器没有针对此问题发出警告。当程序中有未使用的变量时,Go编译器将出错,但此示例成功通过编译。这是由“短变量声明”的语义引起的。下面是短变量声明的语法规范:

ShortVarDecl = IdentifierList ":=" ExpressionList .

根据规范,短变量声明具有重新声明变量的特殊功能,只要:

  • 重新声明在多变量短声明中。
  • 重新声明的变量在同一代码块或函数的参数列表中声明较早。
  • 重新声明的变量与先前的声明具有相同的类型。
  • 声明中至少有一个非空白变量(“_”)是新变量。

所有这些约束在上一个示例中均得到满足,从而防止了编译器针对此问题产生编译错误。

许多工具都具有类似这样的极端情况,即它们在识别相关问题或识别问题但以不同的方式描述时均未成功。使问题复杂化的是,这些工具通常需要先构建Go源代码,然后才能执行分析。如果分析人员无法轻松构建代码库或其依赖项,这将使第三方安全评估变得复杂。

尽管存在这些困难,但只要付出一点点努力,这些工具就可以很好地提示我们在项目中从何处查找问题。我们建议至少使用gosecgo-vetstaticcheck。对大多数代码库而言,这些工具具有良好的文档和人机工效。他们还提供了针对常见问题的多种检查(例如ineffassign或errcheck)。但是,要对特定类型的问题进行更深入的分析,可能必须使用更具体的分析器,直接针对SSA开发定制的工具或使用semmle

二. 动态分析

一旦执行了静态分析并检查了结果,动态分析技术通常是获得更深层结果的下一步。由于Go的内存安全性,动态分析通常发现的问题是导致硬崩溃(hard crash)或程序状态无效的问题。Go社区已经建立了各种工具和方法来帮助识别Go生态系统中这些类型的问题。此外,可以改造现有的与语言无关的工具以满足Go动态分析的需求,我们将在下面展示。

1. 模糊测试

Go语言领域中最著名的动态测试工具可能是Dimitry Vyukovgo-fuzz了。该工具使您可以快速有效地实施模糊测试,并且它已经有了不错的战利品。更高级的用户在猎错过程中可能还会发现分布式的模糊测试libFuzzer的支持非常有用。

Google还发布了一个更原生的模糊器(fuzzer),它拥有一个与上面的go-fuzz相似的名字:gofuzz。它通过初始化具有随机值的结构来帮助用户。与Dimitry的go-fuzz不同,Google的gofuzz不会生成夹具(harness)或协助提供存储崩溃时的输出信息、模糊输入或任何其他类型的信息。尽管这对于测试某些目标可能是不利的,但它使轻量级且可扩展的框架成为可能。

为了简洁起见,我们请您参考各自自述文件中这两个工具的示例。

2. 属性测试(property test)

译注:属性测试指编写对你的代码来说为真的逻辑语句(即“属性”),然后使用自动化工具来生成测试输入(一般来说,是指某种特定类型的随机生成输入数据),并观察程序接受该输入时属性是否保持不变。如果某个输入违反了某一条属性,则证明用户程序存在错误 – 摘自网络。

与传统的模糊测试方法不同,Go的testing包(通常用于单元测试和集成测试)为Go函数的“黑盒测试” 提供了testing/quick子包。换句话说,它提供了属性测试的基本原语。给定一个函数和生成器,该包可用于构建夹具,以测试在给定输入生成器范围的情况下潜在的属性违规。以下示例是直接摘自官方文档。

func TestOddMultipleOfThree(t *testing.T) {
    f := func(x int) bool {
        y := OddMultipleOfThree(x)
        return y%2 == 1 && y%3 == 0
    }
    if err := quick.Check(f, nil); err != nil {
        t.Error(err)
    }
}

上面示例正在测试OddMultipleOfThree函数,其返回值应始终为3的奇数倍。如果不是,则f函数将返回false并将违反该属性。这是由quick.Check功能检测到的。

虽然此包提供的功能对于属性测试的简单应用是可以接受的,但重要的属性通常不能很好地适合这种基本界面。为了解决这些缺点,诞生了leanovate/gopter框架。Gopter为常见的Go类型提供了各种各样的生成器,并且支持您创建与Gopter兼容的自定义生成器。通过gopter/commands子包还支持状态测试,这对于测试跨操作序列的属性是否有用很有有帮助。除此之外,当违反属性时,Gopter会缩小生成的输入。请参阅下面的输出中输入收缩的属性测试的简要示例。

Compute结构的测试夹具:

package main_test
import (
  "github.com/leanovate/gopter"
  "github.com/leanovate/gopter/gen"
  "github.com/leanovate/gopter/prop"
  "math"
  "testing"
)

type Compute struct {
  A uint32
  B uint32
}

func (c *Compute) CoerceInt () { c.A = c.A % 10; c.B = c.B % 10; }
func (c Compute) Add () uint32 { return c.A + c.B }
func (c Compute) Subtract () uint32 { return c.A - c.B }
func (c Compute) Divide () uint32 { return c.A / c.B }
func (c Compute) Multiply () uint32 { return c.A * c.B }

func TestCompute(t *testing.T) {
  parameters := gopter.DefaultTestParameters()
  parameters.Rng.Seed(1234) // Just for this example to generate reproducible results

  properties := gopter.NewProperties(parameters)

  properties.Property("Add should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Add()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.Property("Subtract should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Subtract()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.Property("Multiply should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Multiply()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.Property("Divide should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Divide()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.TestingRun(t)
}

执行测试夹具并观察属性测试的输出(除法失败):

user@host:~/Desktop/gopter_math$ go test
+ Add should never fail.: OK, passed 100 tests.
Elapsed time: 253.291µs
+ Subtract should never fail.: OK, passed 100 tests.
Elapsed time: 203.55µs
+ Multiply should never fail.: OK, passed 100 tests.
Elapsed time: 203.464µs
! Divide should never fail.: Error on property evaluation after 1 passed
   tests: Check paniced: runtime error: integer divide by zero
goroutine 5 [running]:
runtime/debug.Stack(0x5583a0, 0xc0000ccd80, 0xc00009d580)
    /usr/lib/go-1.12/src/runtime/debug/stack.go:24 +0x9d
github.com/leanovate/gopter/prop.checkConditionFunc.func2.1(0xc00009d9c0)
    /home/user/go/src/github.com/leanovate/gopter/prop/check_condition_func.g
  o:43 +0xeb
panic(0x554480, 0x6aa440)
    /usr/lib/go-1.12/src/runtime/panic.go:522 +0x1b5
_/home/user/Desktop/gopter_math_test.Compute.Divide(...)
    /home/user/Desktop/gopter_math/main_test.go:18
_/home/user/Desktop/gopter_math_test.TestCompute.func4(0x0, 0x0)
    /home/user/Desktop/gopter_math/main_test.go:63 +0x3d
# snip for brevity;

ARG_0: 0
ARG_0_ORIGINAL (1 shrinks): 117380812
ARG_1: 0
ARG_1_ORIGINAL (1 shrinks): 3287875120
Elapsed time: 183.113µs
--- FAIL: TestCompute (0.00s)
    properties.go:57: failed with initial seed: 1568637945819043624
FAIL
exit status 1
FAIL    _/home/user/Desktop/gopter_math 0.004s

3. 故障注入

在攻击Go系统时,故障注入令人惊讶地有效。我们使用此方法发现的最常见错误包括对error类型的处理。因为error在Go中只是一种类型,所以当它返回时,它不会像panic语句那样自行改变程序的执行流程。我们通过强制生成来自最低级别(内核)的错误来识别此类错误。由于Go会生成静态二进制文件,因此必须在不使用LD_PRELOAD的情况下注入故障。我们的工具之一KRF使我们能够做到这一点。

在我们最近的Kubernetes代码库评估中,我们使用KRF找到了一个vendored依赖深处的问题,只需通过随机为进程和其子进程发起的read和write系统调用制造故障。该技术对通常与底层系统交互的Kubelet十分有效。该错误是在ionice命令出现错误时触发的,未向STDOUT输出信息并向STDERR发送错误。记录错误后,将继续执行而不是将STDERR的错误返回给调用方。这导致STDOUT后续被索引,从而导致索引超出范围导致运行时panic。

下面是导致kubelet panic的调用栈信息:

E0320 19:31:54.493854    6450 fs.go:591] Failed to read from stdout for cmd [ionice -c3 nice -n 19 du -s /var/lib/docker/overlay2/bbfc9596c0b12fb31c70db5ffdb78f47af303247bea7b93eee2cbf9062e307d8/diff] - read |0: bad file descriptor
panic: runtime error: index out of range

goroutine 289 [running]:
k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs.GetDirDiskUsage(0xc001192c60, 0x5e, 0x1bf08eb000, 0x1, 0x0, 0xc0011a7188)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs/fs.go:600 +0xa86
k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs.(*RealFsInfo).GetDirDiskUsage(0xc000bdbb60, 0xc001192c60, 0x5e, 0x1bf08eb000, 0x0, 0x0, 0x0)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs/fs.go:565 +0x89
k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).update(0xc000ee7560, 0x0, 0x0)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:82 +0x36a
k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).trackUsage(0xc000ee7560)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:120 +0x13b
created by
k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).Start
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:142 +0x3f

下面例子:记录了STDERR日志但未将error返回调用方。

stdoutb, souterr := ioutil.ReadAll(stdoutp)
if souterr != nil {
    klog.Errorf("Failed to read from stdout for cmd %v - %v", cmd.Args, souterr)
}

当stdout为空,也尝试使用索引,这是运行时出现panic的原因:

usageInKb, err := strconv.ParseUint(strings.Fields(stdout)[0], 10, 64)

更完整的包含重现上述问题的步骤,可参见我们的Kubernetes最终报告附录G(第109页),那里详细介绍了针对Kubelet使用KRF的方法。

Go的编译器还允许将测量工具包含在二进制文件中,从而可以在运行时检测race状况,这对于将潜在的race识别为攻击者非常有用,但也可以用来识别对defer、panic和recover的不正确处理。我们构建了Trailofbits/on-edge来做到这一点:识别函数入口点和函数panic点之间的全局状态变化,并通过Go race检测器”泄露”此信息。有关OnEdge的更多详细信息,请参见我们以前的博客文章“在Go中选择正确panic的方式”

实践中,我们建议使用:

  • dvyukov/go-fuzz为组件解析输入建立夹具
  • google/gofuzz用于测试结构验证
  • leanovate/gopter用于增强现有的单元和集成测试以及测试规范的正确性
  • Trailofbits/krf和Trailofbits/on-edge用于测试错误处理。

除KRF外,所有这些工具在实践中都需要付出一些努力。

三. 利用编译器的优势

Go编译器具有许多内置功能和指令(directive),可帮助我们查找错误。这些功能隐藏在各种开关中中,并且需要一些配置才能达到我们的目的。

1. 颠覆类型系统

有时在尝试测试系统功能时,导出函数不是我们要测试的。要获得对所需的函数的测试访问权,可能需要重命名许多函数,以便可以将其导出,这可能会很麻烦。要解决此问题,可以使用编译器的build指令(directive)进行名称链接(name linking)以及导出系统的访问控制。作为此功能的示例,下面的程序(从Stack Overflow答案中提取)访问未导出的reflect.typelinks函数,并随后迭代类型链接表以识别已编译程序中存在的类型。

下面是使用linkname build directive的Stack Overflow答案的通用版本:

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func Typelinks() (sections []unsafe.Pointer, offset [][]int32) {
    return typelinks()
}

//go:linkname typelinks reflect.typelinks
func typelinks() (sections []unsafe.Pointer, offset [][]int32)

func Add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
    return add(p, x, whySafe)
}

//go:linkname add reflect.add
func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer

func main() {
    sections, offsets := Typelinks()
    for i, base := range sections {
        for _, offset := range offsets[i] {
            typeAddr := Add(base, uintptr(offset), "")
            typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&amp;typeAddr)))
            fmt.Println(typ)
        }
    }
}

下面是typelinks表的输出:

$ go run main.go
**reflect.rtype
**runtime._defer
**runtime._type
**runtime.funcval
**runtime.g
**runtime.hchan
**runtime.heapArena
**runtime.itab
**runtime.mcache
**runtime.moduledata
**runtime.mspan
**runtime.notInHeap
**runtime.p
**runtime.special
**runtime.sudog
**runtime.treapNode
**sync.entry
**sync.poolChainElt
**syscall.Dirent
**uint8

如果需要在运行时进行更精细的控制(即,不仅仅是linkname指令),则可以编写Go的中间汇编码,并在编译过程中包括它。尽管在某些地方它可能不完整且有些过时,但是teh-cmc/go-internals提供了有关Go如何组装函数的很好的介绍。

2. 编译器生成的覆盖图

为了帮助进行测试,Go编译器可以执行预处理以生成coverage信息。这旨在标识单元测试和集成测试的测试覆盖范围信息,但是我们也可以使用它来标识由模糊测试和属性测试生成的测试覆盖范围。Filippo Valsorda在博客文章中提供了一个简单的示例。

3. 类型宽度安全

Go支持根据目标平台自动确定整数和浮点数的大小。但是,它也允许使用固定宽度的定义,例如int32和int64。当混合使用自动宽度和固定宽度大小时,对于跨多个目标平台的行为,可能会出现错误的假设。

针对目标的32位和64位平台构建进行测试将有助于识别特定于平台的问题。这些问题通常在执行验证、解码或类型转换的时候发现,原因在于对源和目标类型属性做出了不正确的假设。在Kubernetes安全评估中就有一些这样的示例,特别是TOB-K8S-015:使用strconv.Atoi并将结果向下转换时的溢出(Kubernetes最终报告中的第42页),下面是这个示例。

// updatePodContainers updates PodSpec.Containers.Ports with passed parameters.
func updatePodPorts(params map[string]string, podSpec *v1.PodSpec) (err error) {
    port := -1
    hostPort := -1
    if len(params["port"]) > 0 {
        port, err = strconv.Atoi(params["port"]) // <-- this should parse port as strconv.ParseUint(params["port"], 10, 16)
        if err != nil {
            return err
        }
    }
       // (...)
    // Don't include the port if it was not specified.
    if len(params["port"]) > 0 {
        podSpec.Containers[0].Ports = []v1.ContainerPort{
            {
                ContainerPort: int32(port), // <-- this should later just be uint16(port)
            },
        }

错误的类型宽度假设导致的溢出:

root@k8s-1:/home/vagrant# kubectl expose deployment nginx-deployment --port 4294967377 --target-port 4294967376
E0402 09:25:31.888983    3625 intstr.go:61] value: 4294967376 overflows int32
goroutine 1 [running]:
runtime/debug.Stack(0xc000e54eb8, 0xc4f1e9b8, 0xa3ce32e2a3d43b34)
    /usr/local/go/src/runtime/debug/stack.go:24 +0xa7
k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/util/intstr.FromInt(0x100000050, 0xa, 0x100000050, 0x0, 0x0)
...
service/nginx-deployment exposed

实际上,很少需要颠覆类型系统。最需要的测试目标已经是导出了的,可以通过import获得。我们建议仅在需要助手和测试类似的未导出函数时才使用此功能。至于测试类型宽度安全性,我们建议您尽可能对所有目标进行编译,即使没有直接支持也是如此,因为不同目标上的问题可能更明显。最后,我们建议至少生成包含单元测试和集成测试的项目的覆盖率报告。它有助于确定未经直接测试的区域,这些区域可以优先进行审查。

四. 有关依赖的说明

在诸如JavaScript和Rust的语言中,依赖项管理器内置了对依赖项审核的支持-扫描项目依赖项以查找已知存在漏洞的版本。在Go中,不存在这样的工具,至少没有处于公开可用且非实验状态的。

这种缺乏可能是由于存在多种不同的依赖关系管理方法:go-modgo-getvendored等。这些不同的方法使用根本不同的实现方案,导致无法直接识别依赖关系及其版本。此外,在某些情况下,开发人员通常会随后修改其vendor的依赖的源代码。

在Go的开发过程中,依赖管理问题的解决已经取得了进展,大多数开发人员都在朝使用go mod的方向发展。这样就可以通过项目中的go.mod跟踪和依赖项并进行版本控制,从而为以后的依赖项扫描工作打开了大门。我们可以在OWASP DependencyCheck工具中看到此类工作的示例,该工具是具有实验性质的go mod插件。

五. 结论

最终,Go生态系统中有许多可以使用的工具。尽管大多数情况是完全不同的,但是各种静态分析工具可帮助识别给定项目中的“悬而未决的问题”。当寻求更深层次的关注时,可以使用模糊测试,属性测试和故障注入工具。编译器配置随后增强了动态技术,使构建测试夹具和评估其有效性变得更加容易。

本文翻译自“Security assessment techniques for Go projects”


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats