Audit
개요
감사는 클러스터 조작에 관한 각종 행위들을 보안적으로 고려하여 레코드를 기록하는 행위를 말한다.
보안을 허술히 해서 클러스터가 뚫렸을 때, 어떤 경로로 들어왔으며 어떤 식으로 악용되었는지를 사후에라도 확인하기 위해서는 기록이 반드시 남아야 한다.
그래서 감사라는 행위는 다음의 질문에 답할 수 있도록 설정된다.
- 무슨 일이 일어났는가?
- 언제 일어났는가?
- 누가 했는가?
- 무엇에 의해 일어났는가?
- 어디에서 관측됐는가?
- 어디에서 이뤄졌는가?
- 어떤 식으로 진행됐는가?
kube-apiserver 내부에는 이를 위해 감사 핸들러가 따로 동작하고 있으며, 관리자는 이것에 대해 다양한 설정을 하는 것이 가능하다.
E-api 서버 감사에서 관련 실습을 진행했으니 참고.
감사 스테이지
감사에는 여러 스테이지가 존재한다.
보통 한 요청에는 당연히 한 응답만 존재하는데, 위 그림은 watch 같은 지속적인 소켓 연결로 데이터가 보내질 때의 상황을 그려보았다.
RequestReceived
- 감사 핸들러가 요청을 받았을 때의 스테이지로, 어떤 요청이든 이건 발생하기 마련이다.
ResponseStarted
- 응답 헤더가 보내지기 시작하는 시점의 스테이지이다.
- watch와 같이 오래 이어지는 요청에 대해서만 발생한다.
ResponseComplete
- 응답 바디가 전부 보내진 이후의 시점의 스테이지이다.
- 즉, 모든 데이터가 보내졌을 때의 상황이다.
Panic
- 패닉이 발생했을 때 생기는 시점의 이벤트 스테이지.
감사 레벨
위 각각의 스테이지에 대해서, 각각 어떤 데이터들을 보고 싶은지 정책을 설정하는 것이 가능하다.
이때 정책 레벨을 설정하는 것이 가능하다.
이것들은 각 스테이지에 지정할 수 있는 정책 레벨이다.
누가 요청을 했는지까지만 중요하다면 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
필드를 가지고 있어야만 하고, 거기에 추가적으로 적고 싶은 조건들을 적으면 된다.
이런 식으로, 인증에 필요한 이벤트만 볼 수 있도록 설정했다.
중요한 점 중 하나는 바로 규칙이 순서대로 적용된다는 것이다.
어떤 조건에 먼저 매칭이 되버리면 그 친구는 바로 해당 조건에 따라 적용되어 버린다.
예를 들어서 하나의 서비스 어카운트가 행한 이벤트만 받고 싶다고 생각해보자.
이를 위해서는 이런 순서를 규칙을 작성해야 한다.
system:serviceaccount:{네임스페이스}:{내 서비스 어카운트}
유저에 대해 Request로 레벨을 설정한다.- 그리고 다음에
system:serviceaccounts
그룹에 대해 None 레벨을 설정한다.
이렇게 하면 목표한 서비스 어카운트는 첫 규칙에 적용되어 이벤트가 감사된다.
그리고 다른 서비스 어카운트는 첫 규칙에 해당하지 않기 때문에 두번째 규칙에 걸려 이벤트가 기록되지 않는다.
만약 두 규칙의 순서를 바꾸게 된다면, 원하는 서비스 어카운트도 첫 규칙에서 None으로 걸려버려 결과를 받을 수 없게 된다!
이벤트 배치
이벤트는 굉장히 많이 발생할 것이다.
배치 처리해서 모아서 보내는 것도 가능하고, 모든 이벤트를 매번 보내는 것도 가능하다.
--audit-webhook-mode
에 설정을 해주면 된다.batch
- 기본값으로, 이벤트를 비동기적으로 버퍼에 저장하며 배치 처리한다.blocking
- 각 이벤트 감사가 완료돼야만 응답이 돌아간다.blocking-strict
- blocking과 같으나, RequestedReceived 스테이지에서 감사 로깅이 실패하면 전체 요청이 실패한다.
batch 모드일 때는 추가 설정을 넣어줄 수 있다.
--audit-webhook-batch-buffer-size
배치로 처리하는 버퍼의 크기- 배치 처리하기 전에 버퍼를 넘어버리면, 넘은 이벤트들은 그냥 버려지니 주의하자..
--audit-webhook-batch-max-size
한 배치에 들어갈 이벤트 최대 개수--audit-webhook-batch-max-wait
큐의 이벤트를 배치 처리할 때 최대 기다릴 시간.--audit-webhook-batch-throttle-qps
1초에 발생할 배치의 최대 평균 값 지정- 이 값이 10이면 1초에 배치 처리가 13번 필요하게 됐을 때 이후 3개의 배치는 쓰로틀이 걸려 다음 1초에 처리된다.
--audit-webhook-batch-throttle-burst
QPS가 없을 때 같은 순간에 최대로 생길 배치 수
감사 처리
그렇다면 이렇게 발생할 이벤트를 어디에 남길 것인가?
여기에는 두 가지 방법이 있다.
로컬 파일시스템
가장 기본적인 방법이라고 할 수 있겠다.
말 그대로 파일시스템을 마운팅해 거기에 모든 이벤트를 저장하는 것이다.
흔히 리눅스에서 사용되는 그냥 /var/log
경로를 이용한다면 이렇게 써주면 된다.
그런데 참고할 것이, 어디가지나 api서버도 결국 컨테이너이기에 필요하다면 볼륨 마운팅을 해서 넣어줘야 한다!
대충 세팅하면.. 아주 잠깐만 지나도 순식간에 데이터가 쌓인다..
웹훅 서비스
웹훅 서버를 설정해서 해당 서버가 이벤트를 받도록 설정할 수도 있다.
해당 서버는 반드시 api 서버와 mtls 통신을 해야 한다.
이렇게 --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 |