Audit

개요

감사는 클러스터 조작에 관한 각종 행위들을 보안적으로 고려하여 레코드를 기록하는 행위를 말한다.
보안을 허술히 해서 클러스터가 뚫렸을 때, 어떤 경로로 들어왔으며 어떤 식으로 악용되었는지를 사후에라도 확인하기 위해서는 기록이 반드시 남아야 한다.
그래서 감사라는 행위는 다음의 질문에 답할 수 있도록 설정된다.

kube-apiserver 내부에는 이를 위해 감사 핸들러가 따로 동작하고 있으며, 관리자는 이것에 대해 다양한 설정을 하는 것이 가능하다.

E-api 서버 감사에서 관련 실습을 진행했으니 참고.

감사 스테이지

감사에는 여러 스테이지가 존재한다.
보통 한 요청에는 당연히 한 응답만 존재하는데, 위 그림은 watch 같은 지속적인 소켓 연결로 데이터가 보내질 때의 상황을 그려보았다.

감사 레벨

위 각각의 스테이지에 대해서, 각각 어떤 데이터들을 보고 싶은지 정책을 설정하는 것이 가능하다.
이때 정책 레벨을 설정하는 것이 가능하다.

- `None` - 규칙에 매칭되지 않으면 이벤트를 로깅하지 않는다. - `Metadata` - 내용은 제외하고 user, timestamp, resource, verb, 반환 코드 등의 메타데이터를 로깅한다. - `Request` - 요청의 전체 내용을 로깅한다. - `RequestResponse` - 요청과 요청에 대한 응답이 어떻게 됐는지까지 모든 내용을 로깅한다.

이것들은 각 스테이지에 지정할 수 있는 정책 레벨이다.
누가 요청을 했는지까지만 중요하다면 Metadata를 쓴다.
그 요청이 뭐였는지 내용까지 궁금하면 Request를 쓰면 된다.
그래서 어떤 응답이 나갔는지까지 궁금하다면, RequestResponse를 쓰면 되는데 이거 넣으면 내용이 장난 없이 길어진다.
(누가 get pod를 날렸는데 그래서 어떤 파드들이 표시됐는지까지 표시된다..)
그래서 필요에 따라 적절하게 사용하는 것이 필요하겠다.

참고로 아래 두 레벨은 리소스가 아닌 요청에 대해서는 로깅되지 않는다.
그리고 RequestReceived 스테이지에서는 당연히 레벨을 써도 응답 관련 데이터는 없을 것이다.

감사 정책

어떤 스테이지가 있고 어떤 레벨이 있는지 알았으니, 이제 정책을 작성할 수 있다!
이를 위해서는 정책 양식 파일을 작성하고, 이를 kube-apiserver--audit-policy-file로 인자를 주면 된다.
이게 좋은 게, 파일을 수정하면 api 서버가 알아서 동적으로 리로딩된다.

apiVersion: audit.k8s.io/v1
kind: Policy
# 이벤트를 만들지 않을 스테이지를 넣어주면 된다.
omitStages:
  - "RequestReceived"
rules:
  # Log pod changes at RequestResponse level
  - level: RequestResponse
    resources:
    - group: ""
      resources: ["pods"]
  - level: Metadata
    resources:
    - group: ""
      resources: ["pods/log", "pods/status"]
  - level: None
    resources:
    - group: ""
      resources: ["configmaps"]
      resourceNames: ["controller-leader"]
  - level: None
    users: ["system:kube-proxy"]
    verbs: ["watch"]
    resources:
    - group: "" # core API group
      resources: ["endpoints", "services"]

  # Don't log authenticated requests to certain non-resource URL paths.
  - level: None
    userGroups: ["system:authenticated"]
    nonResourceURLs:
    - "/api*" # Wildcard matching.
    - "/version"

  - level: Request
    resources:
    - group: "" # core API group
      resources: ["configmaps"]
    namespaces: ["kube-system"]

  - level: Metadata
    resources:
    - group: ""
      resources: ["secrets", "configmaps"]

  - level: Request
    resources:
    - group: "" # core API group
    - group: "extensions" # Version of group should NOT be included.

  - level: Metadata
    omitStages:
      - "RequestReceived"

이런 식으로 써주면 된다.
rules의 하위 필드는 리스트로 작성하면 된다.
이때 각 리스트는 level 필드를 가지고 있어야만 하고, 거기에 추가적으로 적고 싶은 조건들을 적으면 된다.
Pasted image 20250124142739.png
이런 식으로, 인증에 필요한 이벤트만 볼 수 있도록 설정했다.

중요한 점 중 하나는 바로 규칙이 순서대로 적용된다는 것이다.
어떤 조건에 먼저 매칭이 되버리면 그 친구는 바로 해당 조건에 따라 적용되어 버린다.

예를 들어서 하나의 서비스 어카운트가 행한 이벤트만 받고 싶다고 생각해보자.
이를 위해서는 이런 순서를 규칙을 작성해야 한다.

이렇게 하면 목표한 서비스 어카운트는 첫 규칙에 적용되어 이벤트가 감사된다.
그리고 다른 서비스 어카운트는 첫 규칙에 해당하지 않기 때문에 두번째 규칙에 걸려 이벤트가 기록되지 않는다.
만약 두 규칙의 순서를 바꾸게 된다면, 원하는 서비스 어카운트도 첫 규칙에서 None으로 걸려버려 결과를 받을 수 없게 된다!

이벤트 배치

이벤트는 굉장히 많이 발생할 것이다.
배치 처리해서 모아서 보내는 것도 가능하고, 모든 이벤트를 매번 보내는 것도 가능하다.

batch 모드일 때는 추가 설정을 넣어줄 수 있다.

감사 처리

그렇다면 이렇게 발생할 이벤트를 어디에 남길 것인가?
여기에는 두 가지 방법이 있다.

로컬 파일시스템

가장 기본적인 방법이라고 할 수 있겠다.
말 그대로 파일시스템을 마운팅해 거기에 모든 이벤트를 저장하는 것이다.

흔히 리눅스에서 사용되는 그냥 /var/log 경로를 이용한다면 이렇게 써주면 된다.
그런데 참고할 것이, 어디가지나 api서버도 결국 컨테이너이기에 필요하다면 볼륨 마운팅을 해서 넣어줘야 한다!

대충 세팅하면.. 아주 잠깐만 지나도 순식간에 데이터가 쌓인다..

웹훅 서비스

웹훅 서버를 설정해서 해당 서버가 이벤트를 받도록 설정할 수도 있다.
해당 서버는 반드시 api 서버와 mtls 통신을 해야 한다.
Pasted image 20250122213304.png
이렇게 --audit-webhook-config-file을 설정해주면 된다.

apiVersion: v1
kind: Config
# remote service
clusters:
- name: audit-webhook
  cluster:
    certificate-authority: /etc/kubernetes/pki/ca.crt
    server: https://webhook.com:8000/audit
preferences: {}
# api server
users:
- name: api-server
  user:
    client-certificate: /etc/kubernetes/pki/apiserver.crt
    client-key: /etc/kubernetes/pki/apiserver.key
current-context: audit@kubernetes
contexts:
- context:
    cluster: audit-webhook
    user: api-server
  name: audit@kubernetes

config 파일은 이렇게 kubeconfig 방식으로 구성하면 된다.
참고로 이 방식은 향후 웹훅 관련 기능을 이용할 때 전부 공통적으로 사용하는 방식이다.

Headers({'host': 'webhook.com:8000', 'user-agent': 'Go-http-client/1.1', 'content-length': '1675', 'accept': 'application/json, */*', 'content-type': 'application/json', 'referer': 'https://webhook.com:8000/audit?timeout=30s', 'accept-encoding': 'gzip'})

웹훅 서버에 들어오는 헤더는 이런 식으로 구성된다.

{'apiVersion': 'audit.k8s.io/v1',
 'items': [{'auditID': 'f53496b9-d126-4b91-9010-212c9a0aeed4',
            'level': 'Request',
            'objectRef': {'apiVersion': 'v1',
                          'namespace': 'default',
                          'resource': 'pods'},
            'requestReceivedTimestamp': '2025-01-22T13:48:55.964670Z',
            'requestURI': '/api/v1/namespaces/default/pods?limit=500',
            'sourceIPs': ['192.168.80.1'],
            'stage': 'RequestReceived',
            'stageTimestamp': '2025-01-22T13:48:55.964670Z',
            'user': {'extra': {'authentication.kubernetes.io/credential-id': ['X509SHA256=e7fcded028b20c07994cdf828460bdfa54892200e31ce4f13eb35862a887525c']},
                     'groups': ['kubeadm:cluster-admins',
                                'system:authenticated'],
                     'username': 'kubernetes-admin'},
            'userAgent': 'kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69',
            'verb': 'list'},
           {'annotations': {'authorization.k8s.io/decision': 'allow',
                            'authorization.k8s.io/reason': 'RBAC: allowed by '
                                                           'ClusterRoleBinding '
                                                           '"kubeadm:cluster-admins" '
                                                           'of ClusterRole '
                                                           '"cluster-admin" to '
                                                           'Group '
                                                           '"kubeadm:cluster-admins"'},
            'auditID': 'f53496b9-d126-4b91-9010-212c9a0aeed4',
            'level': 'Request',
            'objectRef': {'apiVersion': 'v1',
                          'namespace': 'default',
                          'resource': 'pods'},
            'requestReceivedTimestamp': '2025-01-22T13:48:55.964670Z',
            'requestURI': '/api/v1/namespaces/default/pods?limit=500',
            'responseStatus': {'code': 200, 'metadata': {}},
            'sourceIPs': ['192.168.80.1'],
            'stage': 'ResponseComplete',
            'stageTimestamp': '2025-01-22T13:48:55.966575Z',
            'user': {'extra': {'authentication.kubernetes.io/credential-id': ['X509SHA256=e7fcded028b20c07994cdf828460bdfa54892200e31ce4f13eb35862a887525c']},
                     'groups': ['kubeadm:cluster-admins',
                                'system:authenticated'],
                     'username': 'kubernetes-admin'},
            'userAgent': 'kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69',
            'verb': 'list'}],
 'kind': 'EventList',
 'metadata': {}}

이게 바디로 들어온 데이터로, 일단 이벤트 리스트를 보낸다.
배치 처리를 하게 되면 이렇게 이벤트리스트로 한번 감싸서 요청이 보내진다.

관련 문서

이름 noteType created
Audit knowledge 2025-03-12
E-api 서버 감사 topic/explain 2025-01-21

참고