├── .github └── workflows │ ├── build_workshop.yaml │ └── release_build.yaml ├── .gitignore ├── .goreleaser.yaml ├── Dockerfile ├── README.md ├── README_zh.md ├── cmd └── inspector │ ├── app │ └── app.go │ └── main.go ├── demo.gif ├── go.mod ├── go.sum ├── pkg ├── inspector │ ├── inspector.go │ ├── patterns.go │ └── sensitive_keyword.txt ├── kubeclient │ └── kubeclient.go └── utils │ └── utils.go ├── slides └── discover the secrets hidden in apis.pdf └── workshop ├── Dockerfile ├── cmd ├── app │ ├── app.go │ └── options │ │ └── options.go └── main.go ├── examples ├── apiserviceservice.yaml ├── etcd │ ├── deploy.sh │ ├── etcd-deployment.yaml │ ├── etcd-service.yaml │ └── generate-certs.sh ├── namespace.yaml ├── tenant │ ├── cluster-1.yaml │ ├── cluster-2.yaml │ ├── tenant-1-serviceaccount.yaml │ ├── tenant-2-serviceaccount.yaml │ ├── tenant-clusterrole.yaml │ └── tenant-clusterrolebinding.yaml ├── workshop-apiserver-clusterrolebinding.yaml ├── workshop-apiserver-deployment.yaml └── workshop-apiserver-sa.yaml ├── go.mod ├── go.sum └── pkg ├── apis └── workshop │ ├── registry.go │ └── v1alpha1 │ ├── doc.go │ ├── registry.go │ ├── types.go │ ├── zz_generated.deepcopy.go │ └── zz_generated.openapi.go └── server ├── config.go ├── registry └── cluster │ └── rest.go └── server.go /.github/workflows/build_workshop.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Push Workshop Docker Image 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Environment variables available to all jobs and steps in the workflow 12 | env: 13 | REGISTRY: ghcr.io 14 | IMAGE_NAME: yeahx/kubeapi-inspector:workshop-apiserver 15 | 16 | jobs: 17 | build-and-push-image: 18 | runs-on: ubuntu-latest # Use the latest Ubuntu runner 19 | 20 | # Grant permissions for actions to push to ghcr.io 21 | permissions: 22 | contents: read # Needed to check out the repository 23 | packages: write # Needed to push Docker images to ghcr.io 24 | 25 | steps: 26 | - name: Get version 27 | id: get_version 28 | run: | 29 | echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 30 | 31 | - name: Checkout repository 32 | uses: actions/checkout@v3 # Checks out your repository code 33 | 34 | - name: Log in to the Container registry 35 | uses: docker/login-action@v3 36 | with: 37 | registry: ${{ env.REGISTRY }} 38 | username: ${{ github.actor }} # Use the GitHub Actions token user 39 | password: ${{ secrets.REGISTRY }} # Use the automatically generated token 40 | 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v3 # Sets up the buildx builder instance 43 | 44 | # - name: Extract metadata (tags, labels) for Docker 45 | # id: meta # Assign an ID to refer to the outputs of this step 46 | # uses: docker/metadata-action@v5 47 | # with: 48 | # images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} # Full image name 49 | # # Generate tags based on the event 50 | # tags: | 51 | # type=ref,event=pr # Example: Add PR number tag (e.g., pr-123) 52 | # type=sha,format=short # Add short git commit SHA as tag (e.g., a1b2c3d) 53 | # type=raw,value=latest,enable={{is_default_branch}} 54 | 55 | - name: Build and push Docker image 56 | uses: docker/build-push-action@v6 57 | with: 58 | context: ./workshop # IMPORTANT: Set build context to the workshop directory 59 | file: ./workshop/Dockerfile # IMPORTANT: Specify the Dockerfile path relative to repo root 60 | push: true # Push the image after building 61 | tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-${{ steps.get_version.outputs.VERSION }} # Use tags generated by the metadata step 62 | # labels: ${{ steps.meta.outputs.labels }} # Add labels generated by the metadata step 63 | cache-from: type=gha # Enable build cache from GitHub Actions cache 64 | cache-to: type=gha,mode=max # Write build cache to GitHub Actions cache (mode=max for potentially better performance) -------------------------------------------------------------------------------- /.github/workflows/release_build.yaml: -------------------------------------------------------------------------------- 1 | name: Release Inspector 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # triggers only if push new tag version, like `0.8.4` or else 7 | 8 | jobs: 9 | build: 10 | name: GoReleaser build 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 # See: https://goreleaser.com/ci/actions/ 18 | 19 | - name: Set up Go 1.23 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.23 23 | id: go 24 | 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v3 27 | with: 28 | version: latest 29 | args: release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | # custom 28 | .idea/ 29 | local/ 30 | bin/ 31 | KubeAPI-Inspector-CFT.txt 32 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | project_name: KubeAPI-Inspector 4 | 5 | before: 6 | hooks: 7 | - go mod tidy 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | - GO111MODULE=on 12 | goos: 13 | - linux 14 | - darwin 15 | main: ./cmd/inspector/ 16 | binary: inspector 17 | goarch: 18 | - amd64 19 | - arm64 20 | # ensures mod timestamp to be the commit timestamp 21 | mod_timestamp: "{{ .CommitTimestamp }}" 22 | flags: 23 | # trims path 24 | - -trimpath 25 | ldflags: 26 | # use commit date instead of current date as main.date 27 | # only needed if you actually use those things in your main package, otherwise can be ignored. 28 | - -s -w 29 | 30 | # proxies from the go mod proxy before building 31 | # https://goreleaser.com/customization/gomod 32 | 33 | # config the checksum filename 34 | # https://goreleaser.com/customization/checksum 35 | checksum: 36 | name_template: "checksums.txt" 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.23 as builder 3 | 4 | WORKDIR /workspace 5 | ARG TARGETOS=linux 6 | ARG TARGETARCH=amd64 7 | 8 | # Copy the Go Modules manifests 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | 12 | # Copy the go source 13 | COPY cmd/ cmd/ 14 | COPY pkg/ pkg/ 15 | 16 | ENV GO111MODULE on 17 | ENV DEBUG true 18 | ENV GOPROXY http://goproxy.cn,direct 19 | 20 | # Build workshop-apiserver 21 | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GO111MODULE=on go build -o inspector cmd/inspector/main.go 22 | 23 | FROM gcr.io/distroless/static-debian12:debug 24 | WORKDIR / 25 | COPY --from=builder /workspace/inspector . 26 | 27 | ENTRYPOINT ["/inspector"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KubeAPI-Inspector:discover the secrets hidden in apis 2 | English | [简体中文](https://github.com/yeahx/KubeAPI-Inspector/blob/main/README_zh.md) 3 | ## Description 4 | 5 | A tool specifically designed for Kubernetes environments aims to efficiently and automatically discover hidden vulnerable APIs within clusters. It reveals and demonstrates a common error through a workshop format, which could lead to API endpoint authentication failures and potentially compromise the entire cluster. The workshop can be deployed using Kubernetes resource YAML files. 6 | 7 | ![demo](https://github.com/yeahx/KubeAPI-Inspector/blob/main/demo.gif) 8 | ## Features 9 | ### Implemented in Inspector 10 | * 【✅】Automatically parse OpenAPI to identify sensitive fields 11 | * 【✅】Automatically detect potential authentication bypass APIs 12 | * 【✅】Automatically load credentials from the environment 13 | ### To be Implemented in Inspector 14 | * 【 】Automatically discover services and detect potential vulnerabilities in extension API servers 15 | * 【 】exploitation of known control plane components? 16 | ### Implemented in Workshop 17 | * 【✅】Flawed implementation of the REST layer 18 | ### To be Implemented in Workshop 19 | * 【 】Typical vulnerabilities involving operator controllers 20 | 21 | ## Usage 22 | ### in-cluster 23 | 1. download binary in pod 24 | 2. run binary `./inspector` 25 | ### out-of-cluster 26 | 1. `./inspector -kubeconfig path/to/kubeconfig` 27 | 2. test other namespace `./inspector -kubeconfig path/to/kubeconfig -namespace kube-system` 28 | 3. skip sensitive field test `./inspector -kubeconfig path/to/kubeconfig -skipCheckSensitiveField=true` 29 | 30 | ## Installation 31 | ### Requirements 32 | 1. golang>1.22 33 | 2. kubernetes and docker 34 | 3. linux-amd64, linux-arm 35 | ### build kubeapi-inspector 36 | CWD: /repo/ 37 | 1. use go build `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -o inspector cmd/inspector/main.go` 38 | 2. or use docker to build `docker build . -t inspector:latest` 39 | ### build & deploy workshop steps 40 | CWD: /repo/workshop/ 41 | 1. setup a kubernetes cluster, maybe you should use minikube, e.g. `minikube start --kubernetes-version='v1.23.17'` 42 | 2. build workshop image with docker `docker build . -t workshop-apiserver:latest` 43 | 3. deploy etcd for workshop-apiserver `cd workshop/examples/etcd && ./generate-certs.sh && deploy.sh` 44 | 4. create workshop k8s resource `cat examples/{namespace,apiserviceservice,workshop-apiserver-sa,workshop-apiserver-clusterrolebinding,workshop-apiserver-deployment}.yaml | kubectl apply -f -` 45 | 5. create demo cluster resource and tenant accounts `kubectl apply -f examples/tenant` 46 | 47 | ## License 48 | MIT License -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # KubeAPI-Inspector 2 | [English](https://github.com/yeahx/KubeAPI-Inspector/blob/main/README.md) | 简体中文 3 | ## 概述 4 | 5 | 一个专为 Kubernetes 环境设计的工具,旨在高效且自动地发现集群中隐藏的漏洞 API。 6 | 附赠一个靶场可以学习到自定义 apiserver 的一个经典漏洞,这一设计会导致 API 端点鉴权失效,并可能危及整个集群。 7 | 8 | ![demo](https://github.com/yeahx/KubeAPI-Inspector/blob/main/demo.gif) 9 | ## 功能 10 | ### inspector 已实现 11 | * 【✅】自动解析 openapi 发现敏感字段 12 | * 【✅】自动探测潜在的认证绕过的 api 13 | * 【✅】自动加载环境内的凭证 14 | ### inspector 待实现 15 | * 【 】自动服务发现并探测潜在的缺陷 extension apiserver 16 | * 【 】集成已知控制面组件利用? 17 | ### workshop 已实现 18 | * 【✅】REST层的缺陷实现 19 | ### workshop 待实现 20 | * 【 】带有 operator 控制器的典型漏洞 21 | ## 使用方法 22 | ### 集群内 23 | 1. 在 pod 中下载二进制文件 24 | 2. 运行二进制文件 `./inspector` 25 | ### 集群外 26 | 1. ./inspector -kubeconfig path/to/kubeconfig 27 | 2. 测试其他命名空间 ./inspector -kubeconfig path/to/kubeconfig -namespace kube-system 28 | 3. 跳过敏感字段测试 ./inspector -kubeconfig path/to/kubeconfig -skipCheckSensitiveField=true 29 | ## 安装 30 | ### 要求 31 | 1. golang>1.22 32 | 2. kubernetes 和 docker 33 | 3. linux-amd64, linux-arm 34 | ### 构建 kubeapi-inspector 35 | * 当前工作目录:/repo/ 36 | 1. 使用 `go build CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -o inspector cmd/inspector/main.go` 37 | 2. 或使用 docker 构建 `docker build . -t inspector:latest` 38 | ### 构建和部署 workshop 步骤 39 | * 当前工作目录:/repo/workshop/ 40 | 41 | 1. 设置一个 Kubernetes 集群,可以使用 minikube,如 `minikube start --kubernetes-version='v1.23.17'` 42 | 2. 使用 docker 构建 workshop 镜像 `docker build . -t workshop-apiserver:latest` 43 | 3. 部署 workshop-apiserver 使用的etcd `cd workshop/examples/etcd && ./generate-certs.sh && deploy.sh` 44 | 4. 创建 workshop k8s 资源 `cat examples/{namespace,apiserviceservice,workshop-apiserver-sa,workshop-apiserver-clusterrolebinding,workshop-apiserver-deployment}.yaml | kubectl apply -f -` 45 | 5. 创建 demo 的集群 resource 及租户的服务账号 `kubectl apply -f examples/tenant` -------------------------------------------------------------------------------- /cmd/inspector/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/yeahx/kubeapi-inspector/pkg/inspector" 8 | "github.com/yeahx/kubeapi-inspector/pkg/kubeclient" 9 | "github.com/yeahx/kubeapi-inspector/pkg/utils" 10 | ) 11 | 12 | func Run() { 13 | var kubeconfig, namespace, token, server string 14 | var skipCheckSensitiveField, insecureSkipTLS bool 15 | 16 | flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") 17 | //flag.BoolVar(&skipNativeAPI, "skip-native-api", true, "") 18 | flag.StringVar(&namespace, "namespace", "", "") 19 | flag.StringVar(&token, "token", "", "token for access apiserver. Only required if out-of-cluster.") 20 | flag.StringVar(&server, "server", "", "target apiserver address.") 21 | flag.BoolVar(&skipCheckSensitiveField, "skipCheckSensitiveField", false, "if true skip check resource sensitive field") 22 | flag.BoolVar(&insecureSkipTLS, "insecure-skip-tls-verify", false, "if true, skip TLS verification for Kubernetes API server") 23 | flag.Parse() 24 | 25 | kubeClient, err := kubeclient.NewKubeClient(kubeconfig, namespace, insecureSkipTLS) 26 | if err != nil { 27 | fmt.Printf("[-] Failed to create kubeclient: %s\nmake sure kubeconfig is valided.", err) 28 | return 29 | } 30 | 31 | doc, err := kubeClient.DownloadOpenApiSchema() 32 | if err != nil { 33 | fmt.Printf("[-] Failed to download openapi schema: %s\n, will be skip sensitive field test.", err) 34 | skipCheckSensitiveField = true 35 | } 36 | 37 | err = kubeClient.FetchCRDApis() 38 | if err != nil { 39 | fmt.Printf("[-] Failed to fetch CRD apis: %s\n", err) 40 | } 41 | 42 | scan := inspector.NewInspector(kubeClient, nil) 43 | err = scan.ParseDocument(doc) 44 | if err != nil { 45 | fmt.Printf("[-] Failed to parse document: %s\n, will be skip sensitive field test.", err) 46 | skipCheckSensitiveField = true 47 | } 48 | 49 | _, err = scan.DiscoveryAPIServiceBySRV() 50 | if err != nil { 51 | fmt.Printf("[-] Failed to discovery apiservice by srv: %s\n", err) 52 | } 53 | 54 | for k, v := range kubeClient.Resources { 55 | if utils.IsStatusSubresource(v.Name) { 56 | continue 57 | } 58 | 59 | fmt.Printf("[*] Starting validation for %s, group: %s, version: %s, resource: %s,\n", k, v.GroupName, v.Version, v.Name) 60 | if !skipCheckSensitiveField { 61 | err := scan.DetectSensitiveField(v.GroupName, v.Version, v.Name) 62 | if err != nil { 63 | fmt.Printf("[-] Failed to detect sensitive field: %s\n", err) 64 | } 65 | } 66 | err = scan.DetectObjectLeak(v.GroupName, v.Version, v.Name) 67 | if err != nil { 68 | fmt.Printf("[-] Detect err: %v", err) 69 | return 70 | } 71 | } 72 | 73 | fmt.Println("[*] Done") 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /cmd/inspector/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yeahx/kubeapi-inspector/cmd/inspector/app" 5 | ) 6 | 7 | func main() { 8 | app.Run() 9 | } 10 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeahx/KubeAPI-Inspector/5e1b498c2fea608011e1326215c84ee908d0ba7f/demo.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yeahx/kubeapi-inspector 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/google/gnostic-models v0.6.9 9 | k8s.io/api v0.32.3 10 | k8s.io/apimachinery v0.32.3 11 | k8s.io/client-go v0.32.3 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 16 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 17 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 18 | github.com/go-logr/logr v1.4.2 // indirect 19 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 20 | github.com/go-openapi/jsonreference v0.21.0 // indirect 21 | github.com/go-openapi/swag v0.23.1 // indirect 22 | github.com/gogo/protobuf v1.3.2 // indirect 23 | github.com/golang/protobuf v1.5.4 // indirect 24 | github.com/google/go-cmp v0.7.0 // indirect 25 | github.com/google/gofuzz v1.2.0 // indirect 26 | github.com/google/uuid v1.6.0 // indirect 27 | github.com/josharian/intern v1.0.0 // indirect 28 | github.com/json-iterator/go v1.1.12 // indirect 29 | github.com/mailru/easyjson v0.9.0 // indirect 30 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 31 | github.com/modern-go/reflect2 v1.0.2 // indirect 32 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 33 | github.com/pkg/errors v0.9.1 // indirect 34 | github.com/spf13/pflag v1.0.6 // indirect 35 | github.com/x448/float16 v0.8.4 // indirect 36 | golang.org/x/net v0.38.0 // indirect 37 | golang.org/x/oauth2 v0.28.0 // indirect 38 | golang.org/x/sys v0.31.0 // indirect 39 | golang.org/x/term v0.30.0 // indirect 40 | golang.org/x/text v0.23.0 // indirect 41 | golang.org/x/time v0.11.0 // indirect 42 | google.golang.org/protobuf v1.36.6 // indirect 43 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 44 | gopkg.in/inf.v0 v0.9.1 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | k8s.io/klog/v2 v2.130.1 // indirect 47 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 48 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 49 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 50 | sigs.k8s.io/randfill v1.0.0 // indirect 51 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 52 | sigs.k8s.io/yaml v1.4.0 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 6 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 7 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 8 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 9 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 10 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 11 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 12 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 13 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 14 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 15 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 16 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 17 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 18 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 19 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 20 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 21 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 22 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 23 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 24 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 25 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 26 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 27 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 28 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 29 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 30 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 32 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 33 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 34 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 35 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 36 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 37 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 38 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 39 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 40 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 41 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 42 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 43 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 44 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 45 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 46 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 47 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 50 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 51 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 52 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 53 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 54 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 55 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 56 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 57 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 58 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 59 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 60 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 61 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 62 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 64 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 65 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 66 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 69 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 70 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 71 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 72 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 73 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 74 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 75 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 76 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 77 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 78 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 79 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 80 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 81 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 82 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 83 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 84 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 85 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 86 | golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 87 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 88 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 89 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 90 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 91 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 92 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 95 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 96 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 97 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 98 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 99 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 100 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 101 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 102 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 103 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 104 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 105 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 106 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 107 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 108 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 109 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 110 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 111 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 112 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 113 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 114 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 115 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 116 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 117 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 118 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 119 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 120 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 121 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 122 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 123 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 124 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 125 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 126 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 127 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 128 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 129 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 130 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 131 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 132 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 133 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 134 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 135 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 136 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 137 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 138 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 139 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 140 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 141 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 142 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 143 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 144 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 145 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 146 | -------------------------------------------------------------------------------- /pkg/inspector/inspector.go: -------------------------------------------------------------------------------- 1 | package inspector 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/yeahx/kubeapi-inspector/pkg/kubeclient" 10 | "github.com/yeahx/kubeapi-inspector/pkg/utils" 11 | 12 | openapi_v2 "github.com/google/gnostic-models/openapiv2" 13 | apierrors "k8s.io/apimachinery/pkg/api/errors" 14 | 15 | "net" 16 | ) 17 | 18 | type Inspector struct { 19 | client *kubeclient.KubeClient 20 | schemaMap map[string]*openapi_v2.Schema 21 | sensitiveInfoRegexps []*regexp.Regexp 22 | sensitiveCheckFunc func(p string) bool 23 | pathBodyParameterMap map[string]*openapi_v2.BodyParameter // map[pathItem.Name]*BodyParameter 24 | pathPathParameterMap map[string]*openapi_v2.NonBodyParameter // map[pathItem.Name]*NonBodyParameter 25 | } 26 | 27 | // DNSServiceInfo 28 | type DNSServiceInfo struct { 29 | Target string 30 | Port uint16 31 | Priority uint16 32 | Weight uint16 33 | } 34 | 35 | func NewInspector(client *kubeclient.KubeClient, sensitiveCheckFunc func(string) bool) *Inspector { 36 | regs := compileRegexps(sensitivePatterns) 37 | fmt.Printf("[*] Load %d sensitive pattern\n", len(regs)) 38 | 39 | i := &Inspector{client: client, sensitiveInfoRegexps: regs, sensitiveCheckFunc: sensitiveCheckFunc} 40 | 41 | if i.sensitiveCheckFunc == nil { 42 | i.sensitiveCheckFunc = func(p string) bool { 43 | for _, re := range i.sensitiveInfoRegexps { 44 | if r := re.MatchString(p); r { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | } 51 | 52 | return i 53 | } 54 | 55 | // DiscoveryAPIServiceBySRV 56 | func (i *Inspector) DiscoveryAPIServiceBySRV() ([]DNSServiceInfo, error) { 57 | fmt.Printf("[*] Starting discovery api service by coredns\n") 58 | 59 | dnsPatterns := []string{ 60 | "any.any.svc.cluster.local.", 61 | "any.any.any.svc.cluster.local.", 62 | } 63 | 64 | var results []DNSServiceInfo 65 | for _, dns := range dnsPatterns { 66 | _, srvs, err := net.LookupSRV("", "", dns) 67 | if err != nil { 68 | fmt.Printf("[*] DNS Query Eror %s: %s\n", dns, err.Error()) 69 | continue 70 | } 71 | 72 | for _, srv := range srvs { 73 | 74 | results = append(results, DNSServiceInfo{ 75 | Target: srv.Target, 76 | Port: srv.Port, 77 | Priority: srv.Priority, 78 | Weight: srv.Weight, 79 | }) 80 | } 81 | } 82 | 83 | // print result 84 | for _, r := range results { 85 | fmt.Printf("[+] Service: %s:%d\n", 86 | r.Target, r.Port) 87 | } 88 | 89 | return results, nil 90 | } 91 | 92 | func (i *Inspector) DetectObjectLeak(group, version, resource string) error { 93 | var errors []error 94 | 95 | // is subresource should be skip. 96 | if len(strings.Split(resource, "/")) > 1 { 97 | return nil 98 | } 99 | 100 | uri := utils.MakeUri(group, version, resource) 101 | 102 | baseRes, err := i.client.List(uri) 103 | if err != nil { 104 | // 403 105 | if !apierrors.IsForbidden(err) { 106 | errors = append(errors, err) 107 | fmt.Printf("[-] verb List access apiserver failed: %v", err) 108 | } 109 | } 110 | 111 | // diff object? 112 | // baseObj, baseLen, err := bytesToUnstructuredList(baseRes) 113 | _, baseLen, err := utils.BytesToUnstructuredList(baseRes) 114 | //if err != nil { 115 | // errors = append(errors, err) 116 | //} 117 | 118 | watchRes, err := i.client.Watch(uri) 119 | if err != nil { 120 | if !apierrors.IsForbidden(err) { 121 | errors = append(errors, err) 122 | fmt.Printf("[-] verb Watch access apiserver failed: %v", err) 123 | } 124 | } 125 | 126 | watchObj, watchLen, err := utils.WatchResToUnstructuredList(watchRes) 127 | // diff 128 | 129 | if watchLen > baseLen { 130 | utils.RemoveObjectFields(watchObj, uri) 131 | utils.PrintResult(utils.MakeUri(group, version, resource), "Watch", watchObj) 132 | return nil 133 | } 134 | 135 | dcRes, err := i.client.DeleteCollection(uri) 136 | if err != nil { 137 | if !apierrors.IsForbidden(err) { 138 | errors = append(errors, err) 139 | fmt.Printf("[-] verb DeleteCollection access apiserver failed: %v", err) 140 | } 141 | // 403 skip 142 | } 143 | 144 | dcObj, dcLen, err := utils.BytesToUnstructuredList(dcRes) 145 | 146 | if dcLen > baseLen { 147 | utils.RemoveObjectFields(dcObj, uri) 148 | utils.PrintResult(utils.MakeUri(group, version, resource), "DeleteCollection", watchObj) 149 | return nil 150 | } 151 | 152 | // lres wres 153 | 154 | return nil 155 | } 156 | 157 | func (i *Inspector) DetectSensitiveField(group, version, resource string) error { 158 | uri := utils.MakeUri(group, version, resource) 159 | sensitiveFields := make(map[string]bool) 160 | path := []string{"$"} 161 | 162 | bodyParameter, ok := i.pathBodyParameterMap[uri] 163 | if !ok { 164 | return errors.New(fmt.Sprintf("%s not found body parameter", uri)) 165 | } 166 | refName := strings.TrimPrefix(bodyParameter.GetSchema().XRef, "#/definitions/") 167 | refSchema, exists := i.schemaMap[refName] 168 | if !exists { 169 | return errors.New(fmt.Sprintf("%s not found ref %s schema", uri, refName)) 170 | } 171 | 172 | resolveSchema(refSchema, i.schemaMap, path, sensitiveFields, i.sensitiveCheckFunc) 173 | //schemaMap := make(map[string]*openapi_v2.Schema) 174 | 175 | return nil 176 | } 177 | 178 | func (i *Inspector) ParseDocument(doc *openapi_v2.Document) error { 179 | i.schemaMap = make(map[string]*openapi_v2.Schema) 180 | i.pathBodyParameterMap = make(map[string]*openapi_v2.BodyParameter) 181 | i.pathPathParameterMap = make(map[string]*openapi_v2.NonBodyParameter) 182 | 183 | if doc.GetDefinitions() == nil { 184 | return errors.New("openapi document definitions is nil") 185 | } 186 | 187 | if properties := doc.GetDefinitions().GetAdditionalProperties(); properties != nil { 188 | for _, p := range doc.GetDefinitions().GetAdditionalProperties() { 189 | i.schemaMap[p.GetName()] = p.GetValue() 190 | } 191 | } 192 | 193 | if doc.GetPaths() == nil { 194 | return errors.New("openapi document paths is nil") 195 | } 196 | 197 | for _, pathItem := range doc.GetPaths().GetPath() { 198 | if pathItem.GetValue() != nil && pathItem.GetValue().GetPost() != nil { 199 | bodyParam, err := getPostBodyParameter(pathItem.GetValue().GetPost().GetParameters()) 200 | if err != nil { 201 | continue 202 | } 203 | i.pathBodyParameterMap[pathItem.Name] = bodyParam 204 | } 205 | 206 | } 207 | 208 | fmt.Printf("[*] Parse openapi schema success.\n") 209 | 210 | return nil 211 | } 212 | 213 | // resolveSchema 解析schema,处理可能的$xref引用 214 | func resolveSchema(schema *openapi_v2.Schema, definitions map[string]*openapi_v2.Schema, 215 | path []string, sensitiveFields map[string]bool, checkFunc func(ppName string) bool) { 216 | if schema == nil { 217 | return 218 | } 219 | 220 | // 解析properties 221 | if schema.Properties != nil { 222 | for _, pair := range schema.Properties.AdditionalProperties { 223 | propertyName := pair.Name 224 | propertySchema := pair.Value 225 | 226 | // 更新路径 227 | currentPath := append(path, propertyName) 228 | 229 | // 检查是否为敏感字段 230 | if checkFunc(propertyName) { 231 | fullPath := strings.Join(currentPath, ".") 232 | fmt.Printf("[+] sensitive field found: %s\n", fullPath) 233 | sensitiveFields[fullPath] = true 234 | } 235 | 236 | // check schema has xref 237 | if propertySchema.XRef != "" { 238 | refName := strings.TrimPrefix(propertySchema.XRef, "#/definitions/") 239 | refSchema, exists := definitions[refName] 240 | if exists { 241 | resolveSchema(refSchema, definitions, currentPath, sensitiveFields, checkFunc) 242 | } 243 | } else { 244 | // 递归解析非引用的schema 245 | resolveSchema(propertySchema, definitions, currentPath, sensitiveFields, checkFunc) 246 | } 247 | } 248 | } 249 | } 250 | 251 | func getPostBodyParameter(items []*openapi_v2.ParametersItem) (*openapi_v2.BodyParameter, error) { 252 | if items == nil { 253 | return nil, errors.New("ParametersItem is nil") 254 | } 255 | 256 | for _, item := range items { 257 | if item.GetParameter() != nil && item.GetParameter().GetBodyParameter() != nil && 258 | item.GetParameter().GetBodyParameter().GetName() == "body" { 259 | return item.GetParameter().GetBodyParameter(), nil 260 | } else { 261 | print(item.GetParameter()) 262 | } 263 | } 264 | 265 | return nil, nil 266 | } 267 | 268 | func compileRegexps(parttens []string) []*regexp.Regexp { 269 | var regexps []*regexp.Regexp 270 | for _, partten := range parttens { 271 | re, err := regexp.Compile(partten) 272 | if err != nil { 273 | 274 | } 275 | regexps = append(regexps, re) 276 | } 277 | 278 | return regexps 279 | } 280 | -------------------------------------------------------------------------------- /pkg/inspector/patterns.go: -------------------------------------------------------------------------------- 1 | package inspector 2 | 3 | // 定义敏感字段的正则表达式模式 4 | var sensitivePatterns = []string{ 5 | `(?i)password`, // match "password",not case-sensitive 6 | `(?i)passwd`, 7 | `(?i)pwd`, 8 | `(?i)secret`, 9 | `(?i)token`, 10 | `(?i)api[_-]?key`, 11 | `(?i)auth[_-]?token`, 12 | `(?i)kubeconfig`, 13 | `(?i)private[_-]?key`, 14 | `(?i)certificate`, 15 | `(?i)access[_-]?key`, 16 | `(?i)secret[_-]?key`, 17 | `(?i)aws[_-]?access[_-]?key[_-]?id`, 18 | `(?i)aws[_-]?secret[_-]?access[_-]?key`, 19 | `(?i)rsa[_-]?private[_-]?key`, 20 | `(?i)dsa[_-]?private[_-]?key`, 21 | `(?i)ecdsa[_-]?private[_-]?key`, 22 | `(?i)config$`, 23 | `(?i)env`, 24 | `(?i)credentials?`, 25 | `(?i)dockerconfigjson`, 26 | `(?i)^.*[_-]?config$`, 27 | `(?i)^.*[_-]?secret$`, 28 | `(?i)^.*[_-]?file$`, 29 | `(?i)^.*[_-]?key$`, 30 | `(?i)^.*[_-]?cert$`, 31 | `(?i)^.*[_-]?credential$`, 32 | `(?i)host`, 33 | `(?i)port`, 34 | `(?i)url`, 35 | `(?i)connection[_-]?string`, 36 | `(?i)database[_-]?url`, 37 | `(?i)command`, 38 | } 39 | -------------------------------------------------------------------------------- /pkg/inspector/sensitive_keyword.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeahx/KubeAPI-Inspector/5e1b498c2fea608011e1326215c84ee908d0ba7f/pkg/inspector/sensitive_keyword.txt -------------------------------------------------------------------------------- /pkg/kubeclient/kubeclient.go: -------------------------------------------------------------------------------- 1 | package kubeclient 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "github.com/yeahx/kubeapi-inspector/pkg/utils" 13 | 14 | openapi_v2 "github.com/google/gnostic-models/openapiv2" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/client-go/discovery" 17 | 18 | authorizationv1 "k8s.io/api/authorization/v1" 19 | rbacv1 "k8s.io/api/rbac/v1" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/client-go/kubernetes" 22 | authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" 23 | "k8s.io/client-go/rest" 24 | "k8s.io/client-go/tools/clientcmd" 25 | ) 26 | 27 | type KubeClient struct { 28 | clientset *kubernetes.Clientset 29 | doc *openapi_v2.Document 30 | namespace string 31 | DiscoveryClient discovery.DiscoveryInterface 32 | AuthClient authorizationv1client.AuthorizationV1Interface 33 | Rules []rbacv1.PolicyRule 34 | // [accounts.space.test.io] = Resource{}, [accounts.space.test.io/status] = Resource{} 35 | Resources map[string]Resource 36 | } 37 | 38 | type APIGroup string 39 | 40 | type Resource struct { 41 | GroupName string 42 | GroupVersion string 43 | Remote bool 44 | *metav1.APIResource 45 | } 46 | 47 | // NewKubeClient creates a Kubernetes client. 48 | func NewKubeClient(kubeconfig, namespace string, insecureSkipTLS bool) (*KubeClient, error) { 49 | var config *rest.Config 50 | var err error 51 | var inCluster bool 52 | 53 | const namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" 54 | 55 | if namespace == "" { 56 | ns, err := os.ReadFile(namespaceFile) 57 | if err != nil { 58 | namespace = "default" 59 | } else { 60 | namespace = string(ns) 61 | } 62 | } 63 | 64 | if kubeconfig == "" { 65 | //home := os.Getenv("HOME") 66 | //kubeconfig = fmt.Sprintf("%s/.kube/config", home) 67 | 68 | config, err = rest.InClusterConfig() 69 | if err != nil { 70 | return nil, err 71 | } 72 | inCluster = true 73 | } else { 74 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 75 | if err != nil { 76 | return nil, fmt.Errorf("failed to build config from path %s: %w", kubeconfig, err) 77 | } 78 | 79 | // Set Insecure to true (skip TLS verification) only if the flag is set 80 | // If the flag is not set, set Insecure to false if certificate data or files are provided 81 | if insecureSkipTLS { 82 | config.Insecure = true 83 | } else if config.CAData != nil || config.CertData != nil || config.CAFile != "" || config.CertFile != "" || config.KeyFile != "" { 84 | config.Insecure = false 85 | } else { 86 | config.Insecure = true 87 | } 88 | } 89 | 90 | clientset, err := kubernetes.NewForConfig(config) 91 | if err != nil { 92 | return nil, fmt.Errorf("failed to create kubeclient: %w", err) 93 | } 94 | 95 | kc := &KubeClient{clientset: clientset, namespace: namespace} 96 | 97 | kc.DiscoveryClient = kc.clientset.Discovery() 98 | kc.AuthClient = kc.clientset.AuthorizationV1() 99 | 100 | //ssr := &authenticationv1.SelfSubjectReview{} 101 | //res, err := kc.clientset.AuthenticationV1().SelfSubjectReviews().Create(context.TODO(), ssr, metav1.CreateOptions{}) 102 | //if err != nil { 103 | // return nil, err 104 | //} 105 | 106 | kc.Resources = make(map[string]Resource) 107 | 108 | // check rule review 109 | if err = kc.loadRBACPolicy(); err != nil { 110 | return nil, err 111 | } 112 | 113 | if inCluster { 114 | fmt.Printf("[*] Use in-cluster mode, Host: %s, Token %s", config.Host, config.BearerToken) 115 | } else { 116 | fmt.Printf("[*] Load %s config, Host: %s, Token: %s\n", kubeconfig, config.Host, config.BearerToken) 117 | } 118 | 119 | return kc, nil 120 | } 121 | 122 | func (kc *KubeClient) Get(ctx context.Context, uri string) ([]byte, error) { 123 | return kc.clientset.RESTClient().Get().RequestURI(uri).DoRaw(ctx) 124 | } 125 | 126 | func (kc *KubeClient) Watch(uri string) ([]byte, error) { 127 | params := "?watch=true&timeoutSeconds=2" 128 | 129 | uri = fmt.Sprintf("%s%s", uri, params) 130 | 131 | b, err := kc.Get(context.TODO(), uri) 132 | //fmt.Printf("%s", string(b)) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | return b, nil 138 | } 139 | 140 | func (kc *KubeClient) List(uri string) ([]byte, error) { 141 | b, err := kc.Get(context.TODO(), uri) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | return b, nil 147 | } 148 | 149 | // DeleteCollection use deletecollection verb to test, make sure target apiserver support dryRun mode. 150 | func (kc *KubeClient) DeleteCollection(uri string) ([]byte, error) { 151 | params := "?dryRun=All" 152 | uri = fmt.Sprintf("%s%s", uri, params) 153 | 154 | b, err := kc.clientset.RESTClient().Delete().RequestURI(uri).DoRaw(context.TODO()) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | return b, nil 160 | } 161 | 162 | func (kc *KubeClient) GetClientSet() *kubernetes.Clientset { 163 | if kc.clientset == nil { 164 | return nil 165 | } 166 | 167 | return kc.clientset 168 | } 169 | 170 | // loadRBACPolicy load current accounts rbac rules. 171 | func (kc *KubeClient) loadRBACPolicy() error { 172 | sar := &authorizationv1.SelfSubjectRulesReview{ 173 | Spec: authorizationv1.SelfSubjectRulesReviewSpec{ 174 | Namespace: kc.namespace, 175 | }, 176 | } 177 | 178 | res, err := kc.AuthClient.SelfSubjectRulesReviews().Create(context.TODO(), sar, metav1.CreateOptions{}) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | // Verbs: [Get,List]; APIGroup: space.test.io; Resources: [accounts, accounts/status] 184 | kc.Rules = utils.ConvertToPolicyRule(res.Status) 185 | return nil 186 | } 187 | 188 | func (kc *KubeClient) FetchCRDApis() error { 189 | fmt.Printf("[*] Starting to discovery apis\n") 190 | body, err := kc.clientset.RESTClient().Get(). 191 | AbsPath("/apis"). 192 | SetHeader("Accept", runtime.ContentTypeJSON). 193 | Do(context.TODO()). 194 | Raw() 195 | 196 | if err != nil { 197 | return err 198 | } 199 | 200 | apiGroupList := &metav1.APIGroupList{} 201 | 202 | err = json.Unmarshal(body, apiGroupList) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | for _, group := range apiGroupList.Groups { 208 | if utils.IsNativeAPI(group.Name) { 209 | continue 210 | } 211 | for _, version := range group.Versions { 212 | resourceList, err := kc.DiscoveryClient.ServerResourcesForGroupVersion(version.GroupVersion) 213 | if err != nil { 214 | log.Printf("could not retrieve resource list for group version %s: %v", version.GroupVersion, err) 215 | continue 216 | } 217 | for _, resource := range resourceList.APIResources { 218 | r := Resource{ 219 | GroupName: group.Name, 220 | GroupVersion: version.GroupVersion, 221 | Remote: isRemoteApi(resource), 222 | APIResource: resource.DeepCopy(), 223 | } 224 | 225 | r.Version = version.Version 226 | 227 | combine := utils.CombineResourceGroup(r.APIResource.Name, r.GroupName) 228 | 229 | if _, ok := kc.Resources[combine]; !ok { 230 | kc.Resources[combine] = r 231 | } 232 | } 233 | } 234 | } 235 | 236 | apisCount := len(kc.Resources) 237 | 238 | fmt.Printf("[*] Discovered %d custom apis\n", apisCount) 239 | 240 | return nil 241 | } 242 | 243 | func (kc *KubeClient) DownloadOpenApiSchema() (*openapi_v2.Document, error) { 244 | fmt.Printf("[*] Starting to download openapi definition\n") 245 | doc, err := kc.clientset.OpenAPISchema() 246 | if err != nil { 247 | return nil, errors.New(fmt.Sprintf("failed to get openapi schema: %v", err)) 248 | } 249 | kc.doc = doc 250 | return kc.doc, nil 251 | } 252 | 253 | // isRemoteApi determines whether a given API resource is served by a remote API server. 254 | func isRemoteApi(resource metav1.APIResource) bool { 255 | part := strings.Split(resource.Name, "/") 256 | 257 | return resource.StorageVersionHash == "" && len(part) == 1 258 | } 259 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | authorizationv1 "k8s.io/api/authorization/v1" 7 | rbacv1 "k8s.io/api/rbac/v1" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "strings" 11 | ) 12 | 13 | var ( 14 | nativeApiGroup = map[string]interface{}{ 15 | "apps": struct{}{}, 16 | "batch": struct{}{}, 17 | "policy": struct{}{}, 18 | "extensions": struct{}{}, 19 | "autoscaling": struct{}{}, 20 | "node.k8s.io": struct{}{}, 21 | "events.k8s.io": struct{}{}, 22 | "storage.k8s.io": struct{}{}, 23 | "cli.k8s.io": struct{}{}, 24 | "discovery.k8s.io": struct{}{}, 25 | "scheduling.k8s.io": struct{}{}, 26 | "networking.k8s.io": struct{}{}, 27 | "coordination.k8s.io": struct{}{}, 28 | "certificates.k8s.io": struct{}{}, 29 | "apiextensions.k8s.io": struct{}{}, 30 | "authorization.k8s.io": struct{}{}, 31 | "authentication.k8s.io": struct{}{}, 32 | "apiregistration.k8s.io": struct{}{}, 33 | "rbac.authorization.k8s.io": struct{}{}, 34 | "admissionregistration.k8s.io": struct{}{}, 35 | } 36 | ) 37 | 38 | func MakeUri(g, v, r string) string { 39 | return fmt.Sprintf("/apis/%s/%s/%s", g, v, strings.TrimRight(r, "/")) 40 | } 41 | 42 | func IsStatusSubresource(res string) bool { 43 | parts := strings.Split(res, "/") 44 | if len(parts) == 2 { 45 | return parts[1] == "status" 46 | } 47 | 48 | return false 49 | } 50 | 51 | func IsNativeAPI(group string) bool { 52 | var f bool 53 | 54 | _, ok := nativeApiGroup[group] 55 | 56 | if group == "" || ok { 57 | f = true 58 | } 59 | 60 | return f 61 | } 62 | 63 | func ConvertToPolicyRule(status authorizationv1.SubjectRulesReviewStatus) []rbacv1.PolicyRule { 64 | ret := []rbacv1.PolicyRule{} 65 | for _, resource := range status.ResourceRules { 66 | ret = append(ret, rbacv1.PolicyRule{ 67 | Verbs: resource.Verbs, 68 | APIGroups: resource.APIGroups, 69 | Resources: resource.Resources, 70 | ResourceNames: resource.ResourceNames, 71 | }) 72 | } 73 | 74 | for _, nonResource := range status.NonResourceRules { 75 | ret = append(ret, rbacv1.PolicyRule{ 76 | Verbs: nonResource.Verbs, 77 | NonResourceURLs: nonResource.NonResourceURLs, 78 | }) 79 | } 80 | 81 | return ret 82 | } 83 | 84 | func CombineResourceGroup(resource, group string) string { 85 | if len(resource) == 0 { 86 | return "" 87 | } 88 | parts := strings.SplitN(resource, "/", 2) 89 | combine := parts[0] 90 | 91 | if group != "" { 92 | combine = combine + "." + group 93 | } 94 | 95 | if len(parts) == 2 { 96 | combine = combine + "/" + parts[1] 97 | } 98 | return combine 99 | } 100 | 101 | // bytesToUnstructuredObj s 102 | func BytesToUnstructuredList(bytes []byte) (*unstructured.UnstructuredList, int, error) { 103 | obj := &unstructured.Unstructured{} 104 | l := &unstructured.UnstructuredList{} 105 | 106 | if err := obj.UnmarshalJSON(bytes); err != nil { 107 | return nil, 0, err 108 | } 109 | 110 | l, err := obj.ToList() 111 | if err != nil { 112 | return nil, 0, err 113 | } 114 | 115 | return l, len(l.Items), nil 116 | } 117 | 118 | func WatchResToUnstructuredList(bytes []byte) (*unstructured.UnstructuredList, int, error) { 119 | l := &unstructured.UnstructuredList{} 120 | if len(bytes) == 0 { 121 | return nil, 0, nil 122 | } 123 | parts := strings.Split(string(bytes), "\n") 124 | for _, part := range parts { 125 | if len(part) == 0 { 126 | continue 127 | } 128 | item := make(map[string]interface{}) 129 | 130 | _ = json.Unmarshal([]byte(part), &item) 131 | 132 | object, ok := item["object"] 133 | if !ok { 134 | continue 135 | } 136 | content, ok := object.(map[string]interface{}) 137 | if !ok { 138 | continue 139 | } 140 | 141 | obj := &unstructured.Unstructured{} 142 | obj.SetUnstructuredContent(content) 143 | obj.GetObjectKind().GroupVersionKind() 144 | 145 | l.Items = append(l.Items, *obj) 146 | 147 | } 148 | 149 | if len(l.Items) > 0 { 150 | i := l.Items[0] 151 | gvk := i.GetObjectKind().GroupVersionKind() 152 | gvk.Kind = fmt.Sprintf("%sList", gvk.Kind) 153 | l.SetGroupVersionKind(gvk) 154 | } 155 | 156 | return l, len(l.Items), nil 157 | } 158 | 159 | // removeOjbectFields remove redundant fields 160 | func RemoveObjectFields(list *unstructured.UnstructuredList, uri string) { 161 | _ = list.EachListItem(func(object runtime.Object) error { 162 | unstructuredObj, ok := object.(*unstructured.Unstructured) 163 | if !ok { 164 | return nil 165 | } 166 | unstructuredObj.SetManagedFields(nil) 167 | //unstructuredObj.SetAnnotations() 168 | unstructured.RemoveNestedField(unstructuredObj.UnstructuredContent(), "metadata", 169 | "annotations", "kubectl.kubernetes.io/last-applied-configuration") 170 | //fmt.Printf("%s, leak data: %v", uri, unstructuredObj) 171 | return nil 172 | }) 173 | } 174 | 175 | func PrintResult(uri, verb string, object any) { 176 | fmt.Printf("[+] Path: %s found broken access control by %s verb.\nCommand example: %s\n", uri, verb, generateCurlExample(verb, uri)) 177 | indent, _ := json.MarshalIndent(object, "", " ") 178 | fmt.Printf("[+] leak objects: %v \n", string(indent)) 179 | } 180 | 181 | func generateCurlExample(verb, uri string) string { 182 | output := "" 183 | switch verb { 184 | case "Watch": 185 | output = fmt.Sprintf("curl -H\"Authorization: Bearer $token\" -k https://kubernetes.default%s?watch=true&timeoutSeconds=2", uri) 186 | case "DeleteCollection": 187 | output = fmt.Sprintf("curl -H\"Authorization: Bearer $token\" -k https://kubernetes.default%s?dryRun=All", uri) 188 | } 189 | 190 | return output 191 | } 192 | -------------------------------------------------------------------------------- /slides/discover the secrets hidden in apis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeahx/KubeAPI-Inspector/5e1b498c2fea608011e1326215c84ee908d0ba7f/slides/discover the secrets hidden in apis.pdf -------------------------------------------------------------------------------- /workshop/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.23 as builder 3 | 4 | WORKDIR /workspace 5 | ARG TARGETOS=linux 6 | ARG TARGETARCH=amd64 7 | 8 | # Copy the Go Modules manifests 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | 12 | # Copy the go source 13 | COPY cmd/ cmd/ 14 | COPY pkg/ pkg/ 15 | 16 | ENV GO111MODULE on 17 | ENV DEBUG true 18 | 19 | # Build workshop-apiserver 20 | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GO111MODULE=on go build -o workshop-apiserver cmd/main.go 21 | 22 | FROM gcr.io/distroless/static-debian12:debug 23 | WORKDIR / 24 | COPY --from=builder /workspace/workshop-apiserver . 25 | 26 | ENTRYPOINT ["/workshop-apiserver"] -------------------------------------------------------------------------------- /workshop/cmd/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/spf13/cobra" 7 | cliflag "k8s.io/component-base/cli/flag" 8 | "k8s.io/component-base/logs" 9 | "k8s.io/component-base/term" 10 | "os" 11 | "workshop/cmd/app/options" 12 | ) 13 | 14 | func NewServerCommand(stopCh <-chan struct{}) *cobra.Command { 15 | opts := options.NewOptions() 16 | cmd := &cobra.Command{ 17 | Short: "Launch mutlicluster-server", 18 | Long: "Launch mutlicluster-server", 19 | RunE: func(c *cobra.Command, args []string) error { 20 | if err := runCommand(opts, stopCh); err != nil { 21 | return err 22 | } 23 | return nil 24 | }, 25 | } 26 | fs := cmd.Flags() 27 | nfs := opts.Flags() 28 | for _, f := range nfs.FlagSets { 29 | fs.AddFlagSet(f) 30 | } 31 | local := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 32 | logs.AddGoFlags(local) 33 | nfs.FlagSet("logging").AddGoFlagSet(local) 34 | 35 | usageFmt := "Usage:\n %s\n" 36 | cols, _, _ := term.TerminalSize(cmd.OutOrStdout()) 37 | cmd.SetUsageFunc(func(cmd *cobra.Command) error { 38 | fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine()) 39 | cliflag.PrintSections(cmd.OutOrStderr(), nfs, cols) 40 | return nil 41 | }) 42 | cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { 43 | fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine()) 44 | cliflag.PrintSections(cmd.OutOrStdout(), nfs, cols) 45 | }) 46 | fs.AddGoFlagSet(local) 47 | return cmd 48 | } 49 | 50 | func runCommand(o *options.Options, stopCh <-chan struct{}) error { 51 | 52 | errors := o.Validate() 53 | if len(errors) > 0 { 54 | return errors[0] 55 | } 56 | 57 | config, err := o.ServerConfig() 58 | 59 | if err != nil { 60 | return err 61 | } 62 | 63 | s, err := config.Complete() 64 | 65 | if err != nil { 66 | return err 67 | } 68 | 69 | return s.RunUntil(stopCh) 70 | } 71 | -------------------------------------------------------------------------------- /workshop/cmd/app/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "fmt" 5 | apiextensionsserver "k8s.io/apiextensions-apiserver/pkg/apiserver" 6 | openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" 7 | genericapiserver "k8s.io/apiserver/pkg/server" 8 | genericoptions "k8s.io/apiserver/pkg/server/options" 9 | serverstorage "k8s.io/apiserver/pkg/server/storage" 10 | "k8s.io/apiserver/pkg/storage/storagebackend" 11 | utilversion "k8s.io/apiserver/pkg/util/version" 12 | "k8s.io/client-go/rest" 13 | "k8s.io/client-go/tools/clientcmd" 14 | "k8s.io/component-base/cli/flag" 15 | "k8s.io/component-base/logs" 16 | logsapi "k8s.io/component-base/logs/api/v1" 17 | _ "k8s.io/component-base/logs/json/register" 18 | "net" 19 | generatedopenapi "workshop/pkg/apis/workshop/v1alpha1" 20 | "workshop/pkg/server" 21 | //"sigs.k8s.io/metrics-server/pkg/api" 22 | //generatedopenapi "sigs.k8s.io/metrics-server/pkg/api/generated/openapi" 23 | //"sigs.k8s.io/metrics-server/pkg/server" 24 | ) 25 | 26 | type Options struct { 27 | // genericoptions.RecomendedOptions - EtcdOptions 28 | GenericServerRunOptions *genericoptions.ServerRunOptions 29 | SecureServing *genericoptions.SecureServingOptionsWithLoopback 30 | Authentication *genericoptions.DelegatingAuthenticationOptions 31 | Authorization *genericoptions.DelegatingAuthorizationOptions 32 | Etcd *genericoptions.EtcdOptions 33 | Logging *logs.Options 34 | 35 | Kubeconfig string 36 | 37 | // Only to be used to for testing 38 | DisableAuthForTesting bool 39 | } 40 | 41 | func (o *Options) Validate() []error { 42 | var errors []error 43 | err := logsapi.ValidateAndApply(o.Logging, nil) 44 | if err != nil { 45 | errors = append(errors, err) 46 | } 47 | if errs := o.GenericServerRunOptions.Validate(); len(errs) > 0 { 48 | errors = append(errors, errs...) 49 | } 50 | return errors 51 | } 52 | 53 | func (o *Options) Flags() (fs flag.NamedFlagSets) { 54 | msfs := fs.FlagSet("mutlicluster-server") 55 | msfs.StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "The path to the kubeconfig used to connect to the Kubernetes API server and the Kubelets (defaults to in-cluster config)") 56 | 57 | o.GenericServerRunOptions.AddUniversalFlags(fs.FlagSet("generic")) 58 | o.SecureServing.AddFlags(fs.FlagSet("apiserver secure serving")) 59 | o.Authentication.AddFlags(fs.FlagSet("apiserver authentication")) 60 | o.Authorization.AddFlags(fs.FlagSet("apiserver authorization")) 61 | o.Etcd.AddFlags(fs.FlagSet("etcd")) 62 | logsapi.AddFlags(o.Logging, fs.FlagSet("logging")) 63 | 64 | return fs 65 | } 66 | 67 | // NewOptions constructs a new set of default options for metrics-server. 68 | func NewOptions() *Options { 69 | return &Options{ 70 | GenericServerRunOptions: genericoptions.NewServerRunOptions(), 71 | SecureServing: genericoptions.NewSecureServingOptions().WithLoopback(), 72 | Authentication: genericoptions.NewDelegatingAuthenticationOptions(), 73 | Authorization: genericoptions.NewDelegatingAuthorizationOptions(), 74 | Etcd: genericoptions.NewEtcdOptions(storagebackend.NewDefaultConfig("/workshop/test", nil)), 75 | Logging: logs.NewOptions(), 76 | } 77 | } 78 | 79 | func (o Options) ServerConfig() (*server.Config, error) { 80 | apiserver, err := o.ApiserverConfig() 81 | if err != nil { 82 | return nil, err 83 | } 84 | restConfig, err := o.restConfig() 85 | if err != nil { 86 | return nil, err 87 | } 88 | return &server.Config{ 89 | Apiserver: apiserver, 90 | Rest: restConfig, 91 | }, nil 92 | } 93 | 94 | func (o Options) ApiserverConfig() (*genericapiserver.Config, error) { 95 | if err := o.SecureServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, []net.IP{net.ParseIP("127.0.0.1")}); err != nil { 96 | return nil, fmt.Errorf("error creating self-signed certificates: %v", err) 97 | } 98 | 99 | serverConfig := genericapiserver.NewConfig(server.Codecs) 100 | 101 | if err := o.GenericServerRunOptions.ApplyTo(serverConfig); err != nil { 102 | return nil, err 103 | } 104 | 105 | if err := o.SecureServing.ApplyTo(&serverConfig.SecureServing, &serverConfig.LoopbackClientConfig); err != nil { 106 | return nil, err 107 | } 108 | 109 | if !o.DisableAuthForTesting { 110 | if err := o.Authentication.ApplyTo(&serverConfig.Authentication, serverConfig.SecureServing, nil); err != nil { 111 | return nil, err 112 | } 113 | if err := o.Authorization.ApplyTo(&serverConfig.Authorization); err != nil { 114 | return nil, err 115 | } 116 | } 117 | 118 | if err := o.Etcd.ApplyWithStorageFactoryTo(serverstorage.NewDefaultStorageFactory( 119 | o.Etcd.StorageConfig, 120 | o.Etcd.DefaultStorageMediaType, 121 | server.Codecs, 122 | serverstorage.NewDefaultResourceEncodingConfig(server.Scheme), 123 | apiextensionsserver.DefaultAPIResourceConfigSource(), 124 | nil, 125 | ), serverConfig); err != nil { 126 | return nil, err 127 | } 128 | 129 | // versionGet := version.Get() 130 | // enable OpenAPI schemas 131 | serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(generatedopenapi.GetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(server.Scheme)) 132 | serverConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(generatedopenapi.GetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(server.Scheme)) 133 | serverConfig.OpenAPIConfig.Info.Title = "mutlicluster-server" 134 | serverConfig.OpenAPIV3Config.Info.Title = "mutlicluster-server" 135 | serverConfig.OpenAPIConfig.Info.Version = "1" 136 | serverConfig.OpenAPIV3Config.Info.Version = "1" 137 | serverConfig.EffectiveVersion = utilversion.DefaultKubeEffectiveVersion() 138 | 139 | return serverConfig, nil 140 | } 141 | 142 | func (o Options) restConfig() (*rest.Config, error) { 143 | var config *rest.Config 144 | var err error 145 | if len(o.Kubeconfig) > 0 { 146 | loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: o.Kubeconfig} 147 | loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) 148 | 149 | config, err = loader.ClientConfig() 150 | } else { 151 | config, err = rest.InClusterConfig() 152 | } 153 | if err != nil { 154 | return nil, fmt.Errorf("unable to construct lister client config: %v", err) 155 | } 156 | // Use protobufs for communication with apiserver 157 | config.ContentType = "application/vnd.kubernetes.protobuf" 158 | err = rest.SetKubernetesDefaults(config) 159 | if err != nil { 160 | return nil, err 161 | } 162 | return config, nil 163 | } 164 | -------------------------------------------------------------------------------- /workshop/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | genericapiserver "k8s.io/apiserver/pkg/server" 8 | "k8s.io/component-base/logs" 9 | 10 | "workshop/cmd/app" 11 | ) 12 | 13 | func main() { 14 | logs.InitLogs() 15 | defer logs.FlushLogs() 16 | 17 | if len(os.Getenv("GOMAXPROCS")) == 0 { 18 | runtime.GOMAXPROCS(runtime.NumCPU()) 19 | } 20 | 21 | cmd := app.NewServerCommand(genericapiserver.SetupSignalHandler()) 22 | if err := cmd.Execute(); err != nil { 23 | panic(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /workshop/examples/apiserviceservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiregistration.k8s.io/v1 2 | kind: APIService 3 | metadata: 4 | name: v1alpha1.workshop.io 5 | spec: 6 | version: v1alpha1 7 | versionPriority: 1000 8 | group: workshop.io 9 | groupPriorityMinimum: 10000 10 | insecureSkipTLSVerify: true 11 | service: 12 | name: workshop-apiservice 13 | namespace: kubeapi-inspector-workshop 14 | --- 15 | apiVersion: v1 16 | kind: Service 17 | metadata: 18 | name: workshop-apiservice 19 | namespace: kubeapi-inspector-workshop 20 | labels: 21 | app: workshop-apiservice 22 | spec: 23 | ports: 24 | - name: apiservice 25 | port: 443 26 | protocol: TCP 27 | targetPort: 443 28 | selector: 29 | app: workshop-apiserver 30 | -------------------------------------------------------------------------------- /workshop/examples/etcd/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # 检查证书是否已经生成 5 | if [ ! -d "./certs" ]; then 6 | echo "error: certs directory not found" 7 | echo "plz run ./generate-certs.sh to generate certificates first" 8 | exit 1 9 | fi 10 | 11 | # 确保命名空间存在 12 | kubectl create namespace kubeapi-inspector-workshop --dry-run=client -o yaml | kubectl apply -f - 13 | 14 | # 创建服务账号 15 | kubectl create serviceaccount workshop-apiserver-sa -n kubeapi-inspector-workshop --dry-run=client -o yaml | kubectl apply -f - 16 | 17 | # 创建证书的Secret 18 | kubectl create secret generic etcd-certs \ 19 | --from-file=ca.crt=certs/ca.crt \ 20 | --from-file=server.crt=certs/server.pem \ 21 | --from-file=server.key=certs/server-key.pem \ 22 | --from-file=etcd-client.crt=certs/etcd-client.pem \ 23 | --from-file=etcd-client.key=certs/etcd-client-key.pem \ 24 | -n kubeapi-inspector-workshop \ 25 | --dry-run=client -o yaml | kubectl apply -f - 26 | 27 | # 创建etcd服务器地址的ConfigMap 28 | kubectl create configmap etcd-config \ 29 | --from-literal=etcd-servers=https://etcd.kubeapi-inspector-workshop.svc.cluster.local:2379 \ 30 | -n kubeapi-inspector-workshop \ 31 | --dry-run=client -o yaml | kubectl apply -f - 32 | 33 | # 部署etcd 34 | kubectl apply -f etcd-deployment.yaml 35 | kubectl apply -f etcd-service.yaml 36 | 37 | # 部署workshop-apiserver 38 | kubectl apply -f ../workshop-apiserver-deployment.yaml 39 | 40 | echo "deployment completed!" 41 | echo "you can use the following command to check the deployment status:" 42 | echo "kubectl get pods,svc -n kubeapi-inspector-workshop" -------------------------------------------------------------------------------- /workshop/examples/etcd/etcd-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: etcd 5 | namespace: kubeapi-inspector-workshop 6 | labels: 7 | app: etcd 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: etcd 12 | replicas: 1 13 | template: 14 | metadata: 15 | labels: 16 | app: etcd 17 | spec: 18 | containers: 19 | - name: etcd 20 | image: quay.io/coreos/etcd:v3.4.13 21 | command: 22 | - etcd 23 | - --advertise-client-urls=https://0.0.0.0:2379 24 | - --listen-client-urls=https://0.0.0.0:2379 25 | - --cert-file=/etc/etcd/certs/server.crt 26 | - --key-file=/etc/etcd/certs/server.key 27 | - --client-cert-auth 28 | - --trusted-ca-file=/etc/etcd/certs/ca.crt 29 | - --data-dir=/var/lib/etcd 30 | ports: 31 | - containerPort: 2379 32 | name: client 33 | volumeMounts: 34 | - name: etcd-certs 35 | mountPath: /etc/etcd/certs 36 | readOnly: true 37 | - name: etcd-data 38 | mountPath: /var/lib/etcd 39 | resources: 40 | requests: 41 | cpu: 100m 42 | memory: 100Mi 43 | limits: 44 | cpu: 200m 45 | memory: 200Mi 46 | volumes: 47 | - name: etcd-certs 48 | secret: 49 | secretName: etcd-certs 50 | defaultMode: 0400 51 | - name: etcd-data 52 | emptyDir: {} -------------------------------------------------------------------------------- /workshop/examples/etcd/etcd-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: etcd 5 | namespace: kubeapi-inspector-workshop 6 | labels: 7 | app: etcd 8 | spec: 9 | ports: 10 | - port: 2379 11 | name: client 12 | targetPort: 2379 13 | selector: 14 | app: etcd -------------------------------------------------------------------------------- /workshop/examples/etcd/generate-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | #检查cfssl是否已安装 5 | if ! command -v cfssl &> /dev/null || ! command -v cfssljson &> /dev/null; then 6 | echo "plz install cfssl and cfssljson first" 7 | echo "Linux x86_64 example:" 8 | echo "curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.1/cfssl_1.6.1_linux_amd64 -o cfssl" 9 | echo "curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.1/cfssljson_1.6.1_linux_amd64 -o cfssljson" 10 | echo "chmod +x cfssl cfssljson" 11 | echo "sudo mv cfssl cfssljson /usr/local/bin/" 12 | exit 1 13 | fi 14 | 15 | # 创建证书输出目录 16 | CERT_DIR="./certs" 17 | mkdir -p ${CERT_DIR} 18 | cd ${CERT_DIR} 19 | 20 | # 生成CA证书 21 | echo "generating CA certificate..." 22 | echo '{"CN":"CA","key":{"algo":"rsa","size":2048}}' | cfssl gencert -initca - | cfssljson -bare ca 23 | mv ca.pem ca.crt 24 | mv ca-key.pem ca.key 25 | 26 | # 创建证书配置文件 27 | cat > config.json < server-csr.json < client-csr.json <