Kubernetes/Certificates

[CKS] Supply Chain Security

Operation CWAL 2021. 5. 16. 18:32

Image Footprint

다음과 같은 Dockerfile이 있다고 가정한다.

FROM ubuntu
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go
CMD ["./app"]

'app.go' 파일은 아래와 같다. 1초마다 현재 사용자의 이름과 UID를 출력하며, 만약 위와 같이 따로 User를 특정하지 않는다면 root를 사용할 것이다.

package main

import (
    "fmt"
    "time"
    "os/user"
)

func main () {
    user, err := user.Current()
    if err != nil {
        panic(err)
    }

    for {
        fmt.Println("user: " + user.Username + " id: " + user.Uid)
        time.Sleep(1 * time.Second)
    }
}

다음 명령어로 컨테이너 이미지를 빌드한다.

docker build -t app .

다음 명령어로 컨테이너를 생성할 수 있다.

docker run app

 

기능 자체는 문제없는 Image를 만들었지만, 여러가지 문제가 숨어있다. 가장 먼저 짚고 넘어갈건 Image의 용량이 700MB 정도로 상당히 크다는 점이다.

 

Base Image 자체가 상당히 클 뿐만 아니라, Go 컴파일러 및 기타 패키지, 소스 코드 등을 같이 포함하기 때문에 이와 같은 결과가 나왔다. 사실 우리가 필요한건 컴파일된 실행파일 뿐이며, 굳이 Ubuntu를 Base Image로 사용할 필요가 없다. Dockerfile을 다음과 같이 Multi-stage 방식으로 수정해보자.

FROM ubuntu
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go

FROM alpine
COPY --from=0 /app .
CMD ["./app"]

위 Dockerfile을 사용하여 이미지를 빌드할 경우, Image 사이즈가 무려 수십배 줄었음에도 불구하고 동일한 기능을 수행한다.

Dockerfile에는 아직 잠재적인 문제들이 남아있다. Base Image의 버전을 명시하지 않았기 때문에 항상 latest 태그를 사용(ubuntu:latest, alpine:latest)하여 빌드한다. 당장은 빌드가 될지 몰라도, 만약 몇달 뒤 Base Image가 업데이트 되었다면 기존 Dockerfile이 정상적으로 빌드될지 장담할 수 없다. 따라서 다음과 같이 Dockerfile을 수정한다.

FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go

FROM alpine:3.12.1
COPY --from=0 /app .
CMD ["./app"]

 

컨테이너의 Default User가 root로 설정되어 있는 상태이다. 해당 App을 실행하는데 굳이 root 권한이 필요하지 않으며, 보안상 문제를 일으킬 리스크가 있으므로 일반 사용자 'appuser'를 사용하도록 Dockerfile을 수정한다.

FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go

FROM alpine:3.12.1
RUN addgroup -S appgroup && adduser -S appuser -G appgroup -h /home/appuser
COPY --from=0 /app /home/appuser/
USER appuser
CMD ["./app"]

그리고 root filesystem을 변경할 수 없도록 read-only로 설정할 필요가 있다. 다음은 /etc 디렉토리에서 쓰기 권한을 제거한 Dockerfile이다.

FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go

FROM alpine:3.12.1
RUN chmod a-w /etc
RUN addgroup -S appgroup && adduser -S appuser -G appgroup -h /home/appuser
COPY --from=0 /app /home/appuser/
USER appuser
CMD ["./app"]

 

마지막으로 Shell을 통한 접근을 할 수 없도록, /bin 디렉토리 안의 모든 파일을 제거한다.

FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go

FROM alpine:3.12.1
RUN chmod a-w /etc
RUN addgroup -S appgroup && adduser -S appuser -G appgroup -h /home/appuser
RUN rm -rf /bin/*
COPY --from=0 /app /home/appuser/
USER appuser
CMD ["./app"]

 

Image를 다시 빌드하고, 컨테이너를 생성하면 다음과 같은 결과를 얻을 수 있다.

shell 접근 역시 허용하지 않는다.

 

Static Analysis

Kubernetes Manifest 파일에 대해서 Policy 위반 여부를 확인하기 위한 Static Analysis 수행이 가능하다. 물론 Admission Controller를 통해서 생성 또는 업데이트 요청에 대해 자동으로 Allow/Deny를 할 수 있지만, 그보다 먼저 위반 사항이 있는지 확인할 수 있는 방법이 존재한다.

Kubesec

  • K8s 리소스에 대한 보안 위험 분석
  • 오픈소스
  • Security Best Practice를 바탕으로 미리 정의된 Rule 사용
  • 수행 방식
    • Binary
    • Container
    • kubectl 플러그인
    • Admission Controller(kubesec-webhook)

 

Docker Container 방식으로 kubesec을 실행해보자. 명령어는 다음과 같다.

docker run -i kubesec/kubesec:512c5e0 scan /dev/stdin < kubesec-test.yaml

kubesec-test.yaml 파일은 다음과 같이 준비해보았다.

kubectl run nginx --image=nginx --dry-run=client -oyaml > kubesec-test.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: nginx
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}

 

kubesec 실행 결과이다. 분석 결과, 0점을 획득했으며 어떤 점이 잘못됐는지에 대해서 advise를 확인할 수 있다.

[
  {
    "object": "Pod/nginx.default",
    "valid": true,
    "message": "Passed with a score of 0 points",
    "score": 0,
    "scoring": {
      "advise": [
        {
          "selector": "containers[] .securityContext .readOnlyRootFilesystem == true",
          "reason": "An immutable root filesystem can prevent malicious binaries being added to PATH and increase attack cost"
        },
        {
          "selector": "containers[] .securityContext .runAsNonRoot == true",
          "reason": "Force the running image to run as a non-root user to ensure least privilege"
        },
        {
          "selector": "containers[] .securityContext .runAsUser -gt 10000",
          "reason": "Run as a high-UID user to avoid conflicts with the host's user table"
        },
        {
          "selector": "containers[] .securityContext .capabilities .drop",
          "reason": "Reducing kernel capabilities available to a container limits its attack surface"
        },
        {
          "selector": "containers[] .securityContext .capabilities .drop | index(\"ALL\")",
          "reason": "Drop all capabilities and add only those required to reduce syscall attack surface"
        },
        {
          "selector": "containers[] .resources .requests .cpu",
          "reason": "Enforcing CPU requests aids a fair balancing of resources across the cluster"
        },
        {
          "selector": "containers[] .resources .limits .cpu",
          "reason": "Enforcing CPU limits prevents DOS via resource exhaustion"
        },
        {
          "selector": "containers[] .resources .requests .memory",
          "reason": "Enforcing memory requests aids a fair balancing of resources across the cluster"
        },
        {
          "selector": "containers[] .resources .limits .memory",
          "reason": "Enforcing memory limits prevents DOS via resource exhaustion"
        },
        {
          "selector": ".spec .serviceAccountName",
          "reason": "Service accounts restrict Kubernetes API access and should be configured with least privilege"
        },
        {
          "selector": ".metadata .annotations .\"container.seccomp.security.alpha.kubernetes.io/pod\"",
          "reason": "Seccomp profiles set minimum privilege and secure against unknown threats"
        },
        {
          "selector": ".metadata .annotations .\"container.apparmor.security.beta.kubernetes.io/nginx\"",
          "reason": "Well defined AppArmor policies may provide greater protection from unknown threats. WARNING: NOT PRODUCTION READY"
        }
      ]
    }
  }
]

 

점수를 획득하기 위해선, advise 내용을 충실히 이행해야 한다. 예를 들어, 다음과 같이 runAsNonRoot를 활성화하고 다시 kubesec을 실행하면 분석 결과가 1점이 된 것을 확인할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: nginx
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    resources: {}
    securityContext:
      runAsNonRoot: true
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
[
  {
    "object": "Pod/nginx.default",
    "valid": true,
    "message": "Passed with a score of 1 points",
    "score": 1,
    "scoring": {
      "advise": [
        {
          "selector": "containers[] .securityContext .runAsUser -gt 10000",
          "reason": "Run as a high-UID user to avoid conflicts with the host's user table"
        },
        {
          "selector": ".metadata .annotations .\"container.seccomp.security.alpha.kubernetes.io/pod\"",
          "reason": "Seccomp profiles set minimum privilege and secure against unknown threats"
        },
        {
          "selector": "containers[] .resources .limits .memory",
          "reason": "Enforcing memory limits prevents DOS via resource exhaustion"
        },
        {
          "selector": ".spec .serviceAccountName",
          "reason": "Service accounts restrict Kubernetes API access and should be configured with least privilege"
        },
        {
          "selector": ".metadata .annotations .\"container.apparmor.security.beta.kubernetes.io/nginx\"",
          "reason": "Well defined AppArmor policies may provide greater protection from unknown threats. WARNING: NOT PRODUCTION READY"
        },
        {
          "selector": "containers[] .securityContext .capabilities .drop",
          "reason": "Reducing kernel capabilities available to a container limits its attack surface"
        },
        {
          "selector": "containers[] .securityContext .capabilities .drop | index(\"ALL\")",
          "reason": "Drop all capabilities and add only those required to reduce syscall attack surface"
        },
        {
          "selector": "containers[] .resources .requests .cpu",
          "reason": "Enforcing CPU requests aids a fair balancing of resources across the cluster"
        },
        {
          "selector": "containers[] .resources .limits .cpu",
          "reason": "Enforcing CPU limits prevents DOS via resource exhaustion"
        },
        {
          "selector": "containers[] .resources .requests .memory",
          "reason": "Enforcing memory requests aids a fair balancing of resources across the cluster"
        },
        {
          "selector": "containers[] .securityContext .readOnlyRootFilesystem == true",
          "reason": "An immutable root filesystem can prevent malicious binaries being added to PATH and increase attack cost"
        }
      ]
    }
  }
]

 

자세한 내용은 아래 링크에서 확인하기 바란다.

 

kubesec.io :: kubesec.io

kubesec.io – v2 Security risk analysis for Kubernetes resources Live Demo apiVersion: v1 kind: Pod metadata: name: kubesec-demo spec: containers: - name: kubesec-demo image: gcr.io/google-samples/node-hello:1.0 securityContext: readOnlyRootFilesystem: tr

kubesec.io

 

Open Policy Agent

OPA와 Admission Controller에 대한 설명은 미리 정리해둔 페이지가 있으므로, 아래 링크를 참고하기 바란다.

 

Open Policy Agent

Request Workflow 관리자나 Pod이 Kubernetes 클러스터에 접근시, API 서버 내부적으로 다음과 같은 순서를 통해 Request를 처리한다. 모든 Workflow를 통과해야만, 정상적으로 작업이 이루어진다. 1. Authentica..

cwal.tistory.com

 

OPA Conftest

Conftest는 Open Policy Agent의 구현체 중 하나로, K8s 리소스에 대한 Unit Test Framework이다. Rego language를 사용하여, Policy를 직접 정의할 수 있다.

 

Conftest for K8s

우선 K8s Deployment에 대한 Policy를 Rego로 정의해보자. 다음과 같은 파일을 'policy/deployment.rego' 경로에 저장한다. 총 2개의 테스트가 존재하는데, 하나는 'runAsNonRoot'의 true 여부를 판별하고 다른 하나는 Selector에 'app' label의 유무를 확인한다. 테스트 중 하나라도 실패할 경우 Policy를 위반한 것으로 간주한다.

package main

deny[msg] {
  input.kind = "Deployment"
  not input.spec.template.spec.securityContext.runAsNonRoot = true
  msg = "Containers must not run as root"
}

deny[msg] {
  input.kind = "Deployment"
  not input.spec.selector.matchLabels.app
  msg = "Containers must provide app label for pod selectors"
}

Deployment 리소스는 다음과 같이 conftest-sample-deployment.yaml 파일에 정의하였다.

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: test
  name: test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: test
    spec:
      containers:
        - image: httpd
          name: httpd
          resources: {}
status: {}

다음 명령어로 conftest를 Docker Container 환경에서 실행할 수 있다. Selector의 label 중 'app'은 존재하지만, 'runAsNonRoot=true' 조건은 만족시키지 못하므로 테스트는 실패해야 한다.

docker run --rm -v $(pwd):/project openpolicyagent/conftest test conftest-sample-deployment.yaml

예상한대로 2개의 테스트 중 하나만 통과하였고, 'runAsNonRoot' 조건은 실패한 것을 확인할 수 있다.

 

 

Conftest for Dockerfile

Conftest를 사용하여 Dockerfile에 대한 Static Analysis도 가능하다. 우선 아래와 같은 Dockerfile이 있다고 가정하자. 

FROM ubuntu
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go
CMD ["./app"]

2개의 Policy를 정의한다. 하나는 Base Image로 'ubuntu'를 사용하는지 여부이며, 나머지 하나는 RUN 명령어 안에서 금칙어를 사용하는지를 확인한다.

 

# from https://www.conftest.dev
package main

denylist = [
  "ubuntu"
]

deny[msg] {
  input[i].Cmd == "from"
  val := input[i].Value
  contains(val[i], denylist[_])

  msg = sprintf("unallowed image found %s", [val])
}
# from https://www.conftest.dev

package commands

denylist = [
  "apk",
  "apt",
  "pip",
  "curl",
  "wget",
]

deny[msg] {
  input[i].Cmd == "run"
  val := input[i].Value
  contains(val[_], denylist[_])

  msg = sprintf("unallowed commands found %s", [val])
}

아래 명령어로 conftest를 수행한다.

docker run --rm -v $(pwd):/project openpolicyagent/conftest test Dockerfile --all-namespaces

Image Vulnerability Scanning

우리가 사용하는 Docker Image는 보안상의 취약점이 존재할 수 있다. 예를 들어, 취약점이 알려진 App을 포함한 Image를 Base로 사용할 경우, 동일한 이유로 보안상의 허점이 발생할 수 밖에 없다. 아래 2개 사이트는 자주 사용되는 Public Image의 버전별 취약점을 별도의 Database로 구축하여 제공하고 있으니 Production 환경에서 컨테이너를 배포할 시 반드시 확인하기 바란다.

Trivy

Trivy는 오픈 소스 프로젝트로, CI에 적합한 형태로 컨테이너의 취약점을 스캔할 수 있는 툴이다. 사용법이 간단하고 빠르기 때문에 인기가 많은 편이다. Trivy를 사용하여 kube-apiserver를 비롯한 일부 Public Image를 스캔해보자.

 

다른 Tool과 마찬가지로 Docker 환경 기반으로 실행할 수 있으며, nginx:latest 이미지를 스캔하는 예시이다.

docker run ghcr.io/aquasecurity/trivy:latest image nginx

무려 159개의 취약점을 발견할 수 있다. VULNERABILITY_ID를 google로 검색하면 NVD나 CVE 사이트가 가장 먼저 나오며, 해당 취약점에 대해 자세한 설명을 확인할 수 있다. 예를 들어, Critical로 판별된 'CVE-2021-20231'에 대한 설명은 다음과 같다.

nginx:latest가 아닌 nginx:1.18-alpine을 스캔하면 어떤 결과가 나올까? 모든 nginx 이미지가 이렇게 보안에 취약하진 않을 것이다.

현재 실행중인 kube-apiserver Image의 취약점도 확인해보자. 우선 다음 명령어로 현재 kube-apiserver의 Image를 확인한다.

kubectl -n kube-system get pod kube-apiserver-master -oyaml | grep image

다음은 'k8s.gcr.io/kube-apiserver:v1.20.2'를 Trivy로 스캔한 결과이다.

이전 버전도 취약점이 없었는지 궁금해진다. v1.18.3을 스캔해보자.

무려 108개의 보안 취약점이 발견되었다고 한다. 이 결과를 보니, 클러스터 업그레이드를 부지런히 해야겠다는 생각이 든다.

Secure Supply Chain

Image Digest

다음 명령어로 현재 K8s 클러스터에 사용중인 모든 Image와 Registry를 확인할 수 있다.

kubectl get pods --all-namespaces -o jsonpath="{..image}" |\
tr -s '[[:space:]]' '\n' |\
sort |\
uniq

현재 실행중인 Pod을 yaml 형식으로 출력하면, image 외에 imageID를 확인할 수 있다. imageID는 해당 Image에 대한 sha256값을 태그 대신 사용하는 Image Digest로 태그와 달리 특정 Image에 대한 Uniqueness를 보장할 수 있다. 예를 들어, kube-apiserver에 대해 imageID를 확인하면 다음과 같다.

또한 Image 태그 대신 Digest를 사용할 수 있으며, 다음은 kube-apiserver.yaml 파일을 수정하여 Digest를 사용하도록 변경한 결과이다. 당연히 기존과 동일한 Image를 사용한다.

OPA Gatekeeper를 활용한 Registry 화이트리스팅

특정 Container Registry만 접근할 수 있도록 Policy를 추가해보자. OPA Gatekeeper가 필요하며, 설치는 링크를 참고하기 바란다.

다음과 같이 'docker.io'와 'k8s.gcr.io'만 허용하는 ContraintTemplate을 추가한다.

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8strustedimages
spec:
  crd:
    spec:
      names:
        kind: K8sTrustedImages
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8strustedimages
        violation[{"msg": msg}] {
          image := input.review.object.spec.containers[_].image
          not startswith(image, "docker.io/")
          not startswith(image, "k8s.gcr.io/")
          msg := "not trusted image!"
        }

어떤 Resource와 API에 대해 적용할지에 대해 다음과 같이 'K8sTrustedImage' CRD를 정의한다.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sTrustedImages
metadata:
  name: pod-trusted-images
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]

Policy가 Admission Controller 레벨에서 시행되는지 테스트한 결과이다. 정상적으로 적용된 것을 알 수 있다.

 

ImagePolicyWebhook

API 서버의 Admission Controller 중 ImagePolicyWebhook은 생성/변경 Request 발생시, Image를 사용해도 문제가 없는지에 대한 검증을 담당한다. 말그대로 Webhook이기 때문에, 실제 Image 검증을 수행하는 Web Server를 따로 구현해야 한다. ImagePolicyWebhook의 전체 workflow는 아래 그림과 같다.

 

Image 검증에 필요한 데이터를 포함한 Request Body는 다음과 같은 구조로 되어있다.

{
  "apiVersion":"imagepolicy.k8s.io/v1alpha1",
  "kind":"ImageReview",
  "spec":{
    "containers":[
      {
        "image":"myrepo/myimage:v1"
      },
      {
        "image":"myrepo/myimage@sha256:beb6bd6a68f114c1dc2ea4b28db81bdf91de202a9014972bec5e4d9171d90ed"
      }
    ],
    "annotations":{
      "mycluster.image-policy.k8s.io/ticket-1234": "break-glass"
    },
    "namespace":"mynamespace"
  }
}

External Service는 Request Body로 전달받은 ImageReview 내용을 확인하여, 해당 Image 사용을 승인하거나 거부할 수 있다. 이때 프로토콜은 반드시 HTTPS를 사용해야 한다.

 

Admission 관련 설정 파일 추가

/etc/kubernetes/admission 디렉토리에 아래와 같은 파일이 있다고 가정한다. 실제 시험 환경에선 Master에 해당 파일이 미리 준비된 상태이다.

  • apiserver-client-cert.pem
  • apiserver-client-key.pem
  • external-cert.pem
  • external-key.pem
  • kubeconf
    • API서버에서 External Service에 접근하기 위한 설정이 존재하는 파일로 보통 아래와 같이 구성된다.
apiVersion: v1
kind: Config

# clusters refers to the remote service.
clusters:
- cluster:
    certificate-authority: /etc/kubernetes/admission/external-cert.pem  # CA for verifying the remote service.
    server: https://external-service:1234/check-image                   # URL of remote service to query. Must use 'https'.
  name: image-checker

contexts:
- context:
    cluster: image-checker
    user: api-server
  name: image-checker
current-context: image-checker
preferences: {}

# users refers to the API server's webhook configuration.
users:
- name: api-server
  user:
    client-certificate: /etc/kubernetes/admission/apiserver-client-cert.pem     # cert for the webhook admission controller to use
    client-key:  /etc/kubernetes/admission/apiserver-client-key.pem             # key matching the cert

만약 시험 환경에서 HTTPS 연결이 실패한다면, 관련 파일 또는 URL이 제대로 설정되어있는지 확인해야 한다.

 

같은 경로에 다음과 같이 AdmissionConfiguration을 정의한 'admission_conf.yaml' 파일을 추가한다.

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
  configuration:
    imagePolicy:
      kubeConfigFile: /etc/kubernetes/admission/kubeconf
      allowTTL: 50
      denyTTL: 50
      retryBackoff: 500
      defaultAllow: true

 

만약 'defaultAllow: false'로 설정할 경우, External Service 연결 실패시 Admission Controller는 해당 Request를 무조건 거부하기 때문에 아래와 같은 에러가 발생한다.

 

API 서버 설정

/etc/kubernetes/manifests/kube-apiserver.yaml 파일을 다음과 같이 수정한다.

 

1. '--enable-admission-plugins' 옵션에 'ImagePolicyWebhook'을 추가

2. '--admission-control-config-file=/etc/kubernetes/admission/admission_conf.yaml' 옵션 추가

 

3. Volume 추가

아래와 같이 hostPath 볼륨 'k8s-admission'을 추가한다.

그리고 volumeMounts 필드에 아래 내용을 추가한다.

이제 위 파일을 저장하면, kube-apiserver Pod이 새로 생성되면서 ImagePolicyWebhook 설정이 반영된다. 다만 현재 테스트 환경에선 External Service를 구현하지 않았기 때문에 실제 동작은 이루어지지 않고 무조건 Pod 생성 요청을 승인한다.

 

One more thing!

CKS 시험의 ImagePolicyWebhook 문제에선, 실제 External Service와 관련 파일(SSL 인증서, kubeconf)이 Master에 준비된 상태이며,  AdmissionConfiguration 추가 및 API 서버 설정에 대해서만 다룬다. 다른 문제에 비해 상당히 높게 배점되어 있기 때문에, 아래 링크된 페이지 내용을 반드시 숙지하고 시험에 응시하기 바란다.

 

Using Admission Controllers

This page provides an overview of Admission Controllers. What are they? An admission controller is a piece of code that intercepts requests to the Kubernetes API server prior to persistence of the object, but after the request is authenticated and authoriz

kubernetes.io

 

'Kubernetes > Certificates' 카테고리의 다른 글

[CKS] 취득 후기  (4) 2021.05.23
[CKS] Runtime Security  (0) 2021.05.08
[CKS] Minimize Microservice Vulnerabilities  (0) 2021.05.03
[CKS] System Hardening  (0) 2021.05.02
[CKS] Cluster Hardening  (0) 2021.05.01