使用 client-go 操作 kubernetes 的 crd 资源

最近在开发 k8s crd 相关的操作,这里简单的记录一下通过 client-go 增删改查 k8s crd 资源的方法。

示例CRD

这里我以第三方的crd: kyverno 作为例子。

kyverno 的 crd 结构体定义在这里: pkg/api/kyverno/v1,主要是一下两个文件(为了引入尽可能少的文件,编译报错时可以把某些类型改成interface)

重点的 cluster policy 和 List,以及内部的 Rule 的结构体如下:

type ClusterPolicy struct {
	metav1.TypeMeta   `json:",inline,omitempty" yaml:",inline,omitempty"`
	metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
	Spec Spec `json:"spec" yaml:"spec"`
	Status PolicyStatus `json:"status,omitempty" yaml:"status,omitempty"`
}

type ClusterPolicyList struct {
	metav1.TypeMeta `json:",inline" yaml:",inline"`
	metav1.ListMeta `json:"metadata" yaml:"metadata"`
	Items           []ClusterPolicy `json:"items" yaml:"items"`
}

type Rule struct {
	Name string `json:"name,omitempty" yaml:"name,omitempty"`
	Context []ContextEntry `json:"context,omitempty" yaml:"context,omitempty"`
	MatchResources MatchResources `json:"match,omitempty" yaml:"match,omitempty"`
	ExcludeResources ExcludeResources `json:"exclude,omitempty" yaml:"exclude,omitempty"`
	AnyAllConditions apiextensions.JSON `json:"preconditions,omitempty" yaml:"preconditions,omitempty"`
	Mutation Mutation `json:"mutate,omitempty" yaml:"mutate,omitempty"`
	Validation Validation `json:"validate,omitempty" yaml:"validate,omitempty"`
	Generation Generation `json:"generate,omitempty" yaml:"generate,omitempty"`
}

部署 kyverno 的 crd:

kubectl create -f https://raw.githubusercontent.com/kyverno/kyverno/main/definitions/release/install.yaml

查看 k8s 上的api资源有哪些:

kubectl api-resources

image-20210323090256151

List

相当于 kubectl get xxx 命令,我们通过 k8s.io/client-go/dynamic 里的 Interface 提供的方法来操作 crd 资源。

package main

import (
        "encoding/json"
        "fmt"
        "os"
        "path/filepath"

        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime/schema"
        "k8s.io/client-go/dynamic"
        "k8s.io/client-go/tools/clientcmd"
)

// getGVR :- gets GroupVersionResource for dynamic client
func getGVR(group, version, resource string) schema.GroupVersionResource {
	return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
}

func listKyverno(client dynamic.Interface, ns string) (*ClusterPolicyList, error) {
	    gvr := getGVR("kyverno.io", "v1", "clusterpolicies")

        if (ns == ''){
        	list, err := dynamicClient.Resource(gvr).List(metav1.ListOptions{})
        } else {
        	list, err := dynamicClient.Resource(gvr).Namespace(namespace).List(metav1.ListOptions{})
        }
        
	
        if err != nil {
                return nil, err
        }
        
        data, err := list.MarshalJSON()
        if err != nil {
                return nil, err
        }
        
        var list ClusterPolicyList
        if err := json.Unmarshal(data, &list); err != nil {
                return nil, err
        }
        
        return &list, nil
}

func main() {
        kubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config")
        config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
        if err != nil {
                panic(err)
        }

        client, err := dynamic.NewForConfig(config)
        if err != nil {
                panic(err)
        }
        
        list, err := listKyverno(client, "")
        if err != nil {
                panic(err)
        }
        
        for _, t := range list.Items {
                fmt.Printf("%s %s %s %s\n", t.Namespace, t.Name, t.Spec.CronSpec, t.Spec.Image)
        }
}

Get

func getKyverno (client dynamic.Interface, namespace string, name string) (*ClusterPolicy, error) {
        gvr := getGVR("kyverno.io", "v1", "clusterpolicies")
        
        // Unstructured
        utd, err := client.Resource(gvr).Namespace(namespace).Get(name, metav1.GetOptions{})
        
        // Unstructured -> cpol
        var cpol ClusterPolicy
        runtime.DefaultUnstructuredConverter.FromUnstructured(utd.UnstructuredContent(), &cpol)
        logrus.Info("kube kyverno cpol ", cpol.Name, ", version:", cpol.Generation)
        
        return &cpol, nil
}

Create

前端 Post 传输 yaml 信息,传输过来实际上是字符串。

image-20210323101755887

import "gopkg.in/yaml.v3"

func createKyvernoFrom(client dynamic.Interface, namespace string, yamlData string) (*ClusterPolicy, error) {
    gvr := getGVR("kyverno.io", "v1", "clusterpolicies")
    
    // yaml string -> cpol
	var cpol ClusterPolicy
	err := yaml.Unmarshal([]byte(yamlData), &cpol)
	if err != nil {
		logrus.Error(err)
		return nil, err
	}

    // cpol ->  []byte
	data, err := json.Marshal(&cpol)
	if err != nil {
		logrus.Error(err)
		return nil, err
	}
	
	// []byte -> Unstructured
	obj := &unstructured.Unstructured{}
	err := json.Unmarshal(data, &obj.Object)
	if err != nil {
		logrus.Error(err)
		return nil, err
	}
	
	// cpol -> unstructed 也可以直接使用 runtime.DefaultUnstructuredConverter.FromUnstructured 类似的方法做一个反转换,这部分可以从参考我另一篇blog: https://blog.kelu.org/tech/2021/03/14/go-unstructured.html
	
	// Unstructured create k8s 
    var utd *unstructured.Unstructured
	utd, err = dynamicClient.Resource(gvr).Create(obj, metav1.CreateOptions{})
	if err != nil {
		logrus.Error(err)
		return nil, err
	}
	
	// unstructured -> []byte
	result, err := utd.MarshalJSON()
	if err != nil {
		logrus.Error(err)
		return nil, err
	}

    // []byte -> cpol
	var resultCpol ClusterPolicy
	if err := json.Unmarshal(result, &resultCpol); err != nil {
		logrus.Error(err)
		return nil, err
	}
    
    return &resultCpol, nil
}

Update

utd, err = client.Resource(gvr).Namespace(namespace).Update(obj, metav1.UpdateOptions{})

Patch

 _, err := client.Resource(gvr).Namespace(namespace).Patch(name, pt, data, metav1.PatchOptions{})

Delete

client.Resource(gvr).Namespace(namespace).Delete(name, nil)

参考资料


go 主动退出进程

有时候简单调试应用,懒得写 go test,可以简单的使用 stdlib os 包,在main函数中直接退出应用程序。

package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("Hello World")

	// 正常退出
	os.Exit(0)
}


跳出Go module的泥潭 —— colobu.com

转自: https://colobu.com/2018/08/27/learn-go-module/,

参考资料https://roberto.selbach.dev/intro-to-go-modules/

官方的wiki: go/wiki/Modules

最新扩展阅读(go 1.13):Go module 再回顾

Go 1.11 前天已经正式发布了,这个版本包含了两个最重要的feature就是 moduleweb assembly。虽然也有一些简单的教程介绍了go module的特性,但是基本上都是hello world的例子,在实践的过程中, 很多人都在“拼命的挣扎”,包括我自己, 从一些qq群、github的issue, twitter上都可以看到大家茫然或者抱怨的语句。

虽然有三个帮助文件go help modgo help modulesgo help module-get可以了解一些go module的用法,但是感觉Go开发组对module这一特性还是没有很好的做一个全面的介绍,很多情况还得靠大家看源代码或者去猜,比如module下载的文件夹、版本格式的完整声明,module的最佳实践等,并且当前Go 1.11的实现中还有一些bug,给大家在使用的过程中带来了很大的困难。

我也在摸索中前行, 记录了摸索过程中的一些总结,希望能给还在挣扎中的Gopher一些帮助。

Introduction to Go Modules 是一篇很好的go module 入门介绍, 如果你仔细阅读了它,应该就不需要看本文了。

GO111MODULE

要使用go module,首先要设置GO111MODULE=on,这没什么可说的,如果没设置,执行命令的时候会有提示,这个大家应该都了解了。

既有项目

假设你已经有了一个go 项目, 比如在$GOPATH/github.com/smallnest/rpcx下, 你可以使用go mod init github.com/smallnest/rpcx在这个文件夹下创建一个空的go.mod (只有第一行 module github.com/smallnest/rpcx)。

然后你可以通过 go get ./...让它查找依赖,并记录在go.mod文件中(你还可以指定 -tags,这样可以把tags的依赖都查找到)。

通过go mod tidy也可以用来为go.mod增加丢失的依赖,删除不需要的依赖,但是我不确定它怎么处理tags

执行上面的命令会把go.modlatest版本换成实际的最新的版本,并且会生成一个go.sum记录每个依赖库的版本和哈希值。

新的项目

你可以在GOPATH之外创建新的项目。

go mod init packagename可以创建一个空的go.mod,然后你可以在其中增加require github.com/smallnest/rpcx latest依赖,或者像上面一样让go自动发现和维护。

go mod download可以下载所需要的依赖,但是依赖并不是下载到$GOPATH中,而是$GOPATH/pkg/mod中,多个项目可以共享缓存的module。

go mod命令

download    download modules to local cache (下载依赖的module到本地cache))
edit        edit go.mod from tools or scripts (编辑go.mod文件)
graph       print module requirement graph (打印模块依赖图))
init        initialize new module in current directory (再当前文件夹下初始化一个新的module, 创建go.mod文件))
tidy        add missing and remove unused modules (增加丢失的module,去掉未用的module)
vendor      make vendored copy of dependencies (将依赖复制到vendor下)
verify      verify dependencies have expected content (校验依赖)
why         explain why packages or modules are needed (解释为什么需要依赖)

有些命令还有bug, 比如go mod download -dir:

go mod download -dir /tmp
flag provided but not defined: -dir
usage: go mod download [-dir] [-json] [modules]
Run 'go help mod download' for details.

帮助里明明说可以设置dir,但是实际却不支持dir参数。

看这些命令的帮助已经比较容易了解命令的功能。

翻墙

在国内访问golang.org/x的各个包都需要翻墙,你可以在go.mod中使用replace替换成github上对应的库。

replace (
	golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
	golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
	golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)

依赖库中的replace对你的主go.mod不起作用,比如github.com/smallnest/rpcxgo.mod已经增加了replace,但是你的go.mod虽然requirerpcx的库,但是没有设置replace的话, go get还是会访问golang.org/x

所以如果想编译那个项目,就在哪个项目中增加replace

版本格式

下面的版本都是合法的:

gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
gopkg.in/vmihailenco/msgpack.v2 v2.9.1
gopkg.in/yaml.v2 <=v2.2.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
latest

go get 升级

  • 运行 go get -u 将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
  • 运行 go get -u=patch 将会升级到最新的修订版本
  • 运行 go get package@version 将会升级到指定的版本号version

go mod vendor

go mod vendor 会复制modules下载到vendor中, 貌似只会下载你代码中引用的库,而不是go.mod中定义全部的module。

go module, vendor 和 Travis CI

https://arslan.io/2018/08/26/using-go-modules-with-vendor-support-on-travis-ci/


linux 使用 chattr 进行文件保护,防止修改、删除和移动

最近在试用一些的机器,发现 apt-get update 更新源的时候,老是自动生成 unstable.list 文件,指向 debian 官方源,速度又特别慢(最后发现是自己的脚本惹的祸)。

这时候可以使用chattr +i 禁止对这个文件进行修改。

+ :在原有参数设定基础上,追加参数。
- :在原有参数设定基础上,移除参数。
= :更新为指定参数设定。

A:文件或目录的 atime (access time)不可被修改(modified), 可以有效预防例如手提电脑磁盘I/O错误的发生。
S:硬盘I/O同步选项,功能类似sync。
a:即append,设定该参数后,只能向文件中添加数据,而不能删除,多用于服务器日志文 件安全,只有root才能设定这个属性。
c:即compresse,设定文件是否经压缩后再存储。读取时需要经过自动解压操作。
d:即no dump,设定文件不能成为dump程序的备份目标。
i:设定文件不能被删除、改名、设定链接关系,同时不能写入或新增内容。i参数对于文件 系统的安全设置有很大帮助。
j:即journal,设定此参数使得当通过 mount参数:data=ordered 或者 data=writeback 挂 载的文件系统,文件在写入时会先被记录(在journal中)。如果filesystem被设定参数为 data=journal,则该参数自动失效。
s:保密性地删除文件或目录,即硬盘空间被全部收回。
u:与s相反,当设定为u时,数据内容其实还存在磁盘中,可以用于undeletion.

查看相关属性则用命令 lsattr

chattr +i /etc/passwd
chattr -i /etc/passwd
lsattr /etc/passwd

Kubernetes 安全矩阵

网站已经有很多微软发布的这篇文章《Threat matrix for Kubernetes》的翻译了,相关文章都在后边参考资料里,这篇文章就记录一些要点信息,和MITRE ATT&CK®框架。

MITRE ATT&CK

MITRE ATT&CK®框架是涉及网络攻击的已知战术和技术的知识库。从Windows和Linux,MITRE ATT&CK矩阵模型涵盖了涉及网络攻击的各个阶段(战术),并详细阐述了每个阶段的已知方法(技术)。这些矩阵可以帮助企业了解其环境中的攻击面,并确保可以充分预检和排除各种风险。

简单来说,ATT&CK是MITRE提供的“对抗战术、技术和常识”框架,它按照一种易于理解的格式将所有已知的战术和技术进行排列。攻击战术展示在矩阵顶部,每列下面列出了单独的技术。

MITRE ATT&CK®框架策略包括:

  • 初始访问
  • 执行
  • 持久化
  • 权限提升
  • 防御绕过
  • 凭据访问
  • 发现
  • 横向运动
  • 影响

微软

微软发布的“ATT&CK模仿秀”,包括:

ATT&CK 是个很好的学习入口,不过我们工程师也不能完全依赖这些威胁矩阵,毕竟新的漏洞不断出现,老的东西不一定能够覆盖所有漏洞。

参考文章:微软威胁矩阵不是雷神之锤 - 安全内参

Azure威胁矩阵遗漏的一个值得注意的组件是“命令与控制”(C2)威胁类别,该类别在原始的MITER ATT&CK矩阵中可以找到。事实证明,C2仍是Kubernetes用户关注的问题,它应该是Kubernetes威胁矩阵的一部分。

Kubernetes高度依赖DNS作为其服务发现的关键基础架构。建立隐蔽通道的常见做法是利用DNS协议消息交换中的固有弱点。因此,监视Kubernetes群集内的DNS活动非常重要,可以检测并有可能阻止C2建立隐蔽通道。

Azure Matrix在权限提升方面也存在差距。最新的CVE显示,攻击权限可以从节点提升到整个群集,也可以从群集提升到托管云环境。准入控制器和Kubernetes operator也可能遭到侵入,就前置安全性而言这是不可省略的。

Azure Matrix中缺失的另一点是Kubernetes威胁的持久性。攻击者可以直接在节点上启动容器,而Kubernetes不会管理容器,这对于DevOps来说是一个盲点。如果攻击者破坏了准入控制器,他们还可以将恶意代码注入任何一个容器中。最后,攻击者可以将脚本插入容器生命周期挂钩中来执行并持续进行攻击,这是一种Kubernetes机制,可以在预定的时间点运行脚本。

参考资料