Elasticsearch 보호하기
Elasticsearch 보호하기 (Securing Elasticsearch)
이 문서는 Elasticsearch 인식 보안 정책을 적용하도록 Cilium을 사용하는 방법을 소개해요. 머신에서 단일 노드 Cilium 환경을 실행하는 과정을 자세히 안내하며, 15-30분 정도 걸리는 튜토리얼입니다. 최소 권한(least privilege) 원칙에 따라 클라이언트별로 인덱스 접근과 연산을 제한하는 방법을 다룹니다.
본문
아직 Cilium & Hubble 소개를 읽지 않았다면 먼저 읽어보는 것을 권장해요.
막히는 부분이 있다면 Cilium Slack에 질문을 올리는 것이 가장 좋은 방법이에요. 전 세계에 Cilium 기여자들이 있기 때문에 도움을 줄 사람이 거의 항상 있답니다.
Cilium 설정
아직 Cilium을 설정하지 않았다면 Cilium 빠른 설치 가이드를 따라 Kubernetes 클러스터를 빠르게 부트스트랩하고 Cilium을 설치하세요. 확실하지 않다면 minikube 경로를 선택하면 5분 이내에 준비될 거예요.
데모 애플리케이션 배포
Cilium 전통을 따라 Star Wars에서 영감을 받은 예제를 사용할 거예요. 제국(Empire)은 다양한 데이터를 저장하는 대규모 Elasticsearch 클러스터를 보유하고 있어요:
index: troop_logs: 모든 전초기지(outpost)에서 수집된 Stormtroopers 성과 로그. 약한 수행자를 식별·제거하는 데 사용됩니다!index: spaceship_diagnostics: 모든 우주선에서 수집된 우주선 진단 데이터. 우주선의 R&D와 개선에 사용됩니다.
모든 전초기지에는 Stormtroopers 로그를 업로드하는 Elasticsearch 클라이언트 서비스가 있고, 모든 우주선에는 진단을 업로드하는 서비스가 있어요. 마찬가지로 제국 본부에는 troop 로그와 우주선 진단 데이터를 검색·분석하는 서비스가 있습니다. 보안 우려를 살펴보기 전에 먼저 이 애플리케이션 시나리오를 minikube에 만들어 봅시다.
아래 명령으로 앱을 배포하세요. 그러면 다음이 생성됩니다:
- 셀렉터 라벨
component:elasticsearch와 Elasticsearch를 실행하는 파드가 있는elasticsearch서비스. empire-hq,outpost,spaceship각각을 위한 Elasticsearch 클라이언트 3개.
$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.20.2/examples/kubernetes-es/es-sw-app.yaml
serviceaccount "elasticsearch" created
service "elasticsearch" created
replicationcontroller "es" created
role "elasticsearch" created
rolebinding "elasticsearch" created
pod "outpost" created
pod "empire-hq" created
pod "spaceship" created
$ kubectl get svc,pods
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
svc/elasticsearch NodePort 10.111.238.254 <none> 9200:30130/TCP,9300:31721/TCP 2d
svc/etcd-cilium NodePort 10.98.67.60 <none> 32379:31079/TCP,32380:31080/TCP 9d
svc/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 9d
NAME READY STATUS RESTARTS AGE
po/empire-hq 1/1 Running 0 2d
po/es-g9qk2 1/1 Running 0 2d
po/etcd-cilium-0 1/1 Running 0 9d
po/outpost 1/1 Running 0 2d
po/spaceship 1/1 Running 0 2d
Elasticsearch 접근의 보안 위험
Elasticsearch 클러스터의 최소 권한 보안 과제는 클라이언트에게 특정 인덱스에만 접근을 허용하고, 각 클라이언트가 각 인덱스에서 수행할 수 있는 연산을 제한하는 것입니다. 이 예제에서 outpost Elasticsearch 클라이언트는 troop 로그 업로드만 필요하고, empire-hq 클라이언트는 두 인덱스 모두에 대한 검색 접근만 필요해요. 보안 관점에서 outpost는 약점이며 반란군에게 점령당하기 쉽습니다. 일단 침해되면 클라이언트를 사용해 Elasticsearch의 중요 데이터를 검색·조작할 수 있어요. 이 공격을 시뮬레이션할 수 있는데, 먼저 모든 클라이언트 서비스의 정당한 동작에 대한 명령을 실행해 봅시다.
outpost 클라이언트가 troop 로그 업로드
$ kubectl exec outpost -- python upload_logs.py
Uploading Stormtroopers Performance Logs
created : {'_index': 'troop_logs', '_type': 'log', '_id': '1', '_version': 1, 'result': 'created', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, 'created': True}
spaceship이 진단 업로드
$ kubectl exec spaceship -- python upload_diagnostics.py
Uploading Spaceship Diagnostics
created : {'_index': 'spaceship_diagnostics', '_type': 'stats', '_id': '1', '_version': 1, 'result': 'created', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, 'created': True}
empire-hq가 로그와 진단 검색 쿼리 실행
$ kubectl exec empire-hq -- python search.py
Searching for Spaceship Diagnostics
Got 1 Hits:
{'_index': 'spaceship_diagnostics', '_type': 'stats', '_id': '1', '_score': 1.0, \
'_source': {'spaceshipid': '3459B78XNZTF', 'type': 'tiefighter', 'title': 'Engine Diagnostics', \
'stats': '[CRITICAL] [ENGINE BURN @SPEED 5000 km/s] [CHANCE 80%]'}}
Searching for Stormtroopers Performance Logs
Got 1 Hits:
{'_index': 'troop_logs', '_type': 'log', '_id': '1', '_score': 1.0, \
'_source': {'outpost': 'Endor', 'datetime': '33 ABY 4AM DST', 'title': 'Endor Corps 1: Morning Drill', \
'notes': '5100 PRESENT; 15 ABSENT; 130 CODE-RED BELOW PAR PERFORMANCE'}}
이제 반란군이 점령한 outpost를 상상해 보세요. 아래 명령에서 반란군은 먼저 모든 인덱스를 검색한 다음 침해된 outpost에서 진단 데이터를 조작해요.
$ kubectl exec outpost -- python search.py
Searching for Spaceship Diagnostics
Got 1 Hits:
{'_index': 'spaceship_diagnostics', '_type': 'stats', '_id': '1', '_score': 1.0, \
'_source': {'spaceshipid': '3459B78XNZTF', 'type': 'tiefighter', 'title': 'Engine Diagnostics', \
'stats': '[CRITICAL] [ENGINE BURN @SPEED 5000 km/s] [CHANCE 80%]'}}
Searching for Stormtroopers Performance Logs
Got 1 Hits:
{'_index': 'troop_logs', '_type': 'log', '_id': '1', '_score': 1.0, \
'_source': {'outpost': 'Endor', 'datetime': '33 ABY 4AM DST', 'title': 'Endor Corps 1: Morning Drill', \
'notes': '5100 PRESENT; 15 ABSENT; 130 CODE-RED BELOW PAR PERFORMANCE'}}
반란군은 우주선 진단 데이터를 조작해서 우주선 결함이 empire-hq에 알려지지 않게 해요! (힌트: 반란군이 tiefighter 우주선의 stats를 변경했습니다. 감지하기 어렵지만 치명적인 영향을 주는 변경이에요!)
$ kubectl exec outpost -- python update.py
Uploading Spaceship Diagnostics
{'_index': 'spaceship_diagnostics', '_type': 'stats', '_id': '1', '_score': 1.0, \
'_source': {'spaceshipid': '3459B78XNZTF', 'type': 'tiefighter', 'title': 'Engine Diagnostics', \
'stats': '[OK] [ENGINE OK @SPEED 5000 km/s]'}}
Cilium으로 Elasticsearch 보호하기
최소 권한 보안 원칙에 따라 우리는 다음 정당한 작업만 허용하고 그 이상은 허용하지 않으려 해요:
outpost서비스는index: troop_logs에만 업로드 접근spaceship서비스는index: spaceship_diagnostics에만 업로드 접근empire-hq서비스는 두 인덱스 모두에 대해서만 검색 접근
다행히 제국 DevOps 팀은 Kubernetes 클러스터에 Cilium을 사용하고 있어요. Cilium은 Elasticsearch API 접근을 제어하는 L7 가시성과 보안 정책을 제공합니다. Cilium은 보안에 대해 화이트리스트, 최소 권한 모델을 따릅니다. 즉, CiliumNetworkPolicy는 허용된 요청을 정의하는 규칙 목록을 포함하며, 규칙과 일치하지 않는 요청은 거부됩니다.
이 예제에서 정책 규칙은 elasticsearch 서비스로의 인바운드 트래픽("ingress") 연결에 대해 정의돼요. 서비스의 백엔드 파드로 선택된 엔드포인트는 selector 라벨로 정의된다는 점을 참고하세요. Selector 라벨은 Kubernetes에서 서비스를 정의하는 것과 같은 개념을 사용합니다. 이 예제에서는 라벨 component: elasticsearch가 Kubernetes에서 elasticsearch 서비스의 일부인 파드를 정의해요.
아래 정책 파일에서 인덱스 접근과 수행되는 작업을 제어하는 다음 규칙을 볼 수 있어요:
- 라벨
app:spaceship인fromEndpoints는 정규식^/spaceship_diagnostics/stats/.*$와 일치하는 경로에 대해서만HTTPPUT허용 - 라벨
app:outpost인fromEndpoints는 정규식^/troop_logs/log/.*$와 일치하는 경로에 대해서만HTTPPUT허용 - 라벨
app:empire인fromEndpoints는 정규식^/spaceship_diagnostics/_search/??.*$와^/troop_logs/search/??.*$와 일치하는 경로에 대해서만HTTPGET허용
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: secure-empire-elasticsearch
namespace: default
specs:
- endpointSelector:
matchLabels:
component: elasticsearch
ingress:
- fromEndpoints:
- matchLabels:
app: spaceship
toPorts:
- ports:
- port: "9200"
protocol: TCP
rules:
http:
- method: ^PUT$
path: ^/spaceship_diagnostics/stats/.*$
- fromEndpoints:
- matchLabels:
app: empire-hq
toPorts:
- ports:
- port: "9200"
protocol: TCP
rules:
http:
- method: ^GET$
path: ^/spaceship_diagnostics/_search/??.*$
- method: ^GET$
path: ^/troop_logs/_search/??.*$
- fromEndpoints:
- matchLabels:
app: outpost
toPorts:
- ports:
- port: "9200"
protocol: TCP
rules:
http:
- method: ^PUT$
path: ^/troop_logs/log/.*$
- egress:
- toEndpoints:
- matchExpressions:
- key: k8s:io.kubernetes.pod.namespace
operator: Exists
- toEntities:
- cluster
- host
endpointSelector: {}
ingress:
- {}
kubectl로 이 Elasticsearch 인식 네트워크 보안 정책을 적용하세요:
$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.20.2/examples/kubernetes-es/es-sw-policy.yaml
ciliumnetworkpolicy "secure-empire-elasticsearch" created
보안 정책을 테스트해 봅시다. 먼저 outpost와 spaceship 모두에서 검색 접근이 차단돼요. 따라서 침해된 outpost에서 반란군은 troop과 우주선 진단에 대한 지식을 검색·획득할 수 없습니다. 둘째, outpost 클라이언트는 index: spaceship_diagnostics를 생성하거나 업데이트할 접근 권한이 없어요.
$ kubectl exec outpost -- python search.py
GET http://elasticsearch:9200/spaceship_diagnostics/_search [status:403 request:0.008s]
...
...
elasticsearch.exceptions.AuthorizationException: TransportError(403, 'Access denied\r\n')
command terminated with exit code 1
$ kubectl exec outpost -- python update.py
PUT http://elasticsearch:9200/spaceship_diagnostics/stats/1 [status:403 request:0.006s]
...
...
elasticsearch.exceptions.AuthorizationException: TransportError(403, 'Access denied\r\n')
command terminated with exit code 1
아래의 어떤 명령을 다시 실행해도 보안 정책이 모든 정당한 요청을 여전히 허용하는지(즉, 403 오류가 반환되지 않는지) 확인할 수 있어요.
$ kubectl exec outpost -- python upload_logs.py
...
$ kubectl exec spaceship -- python upload_diagnostics.py
...
$ kubectl exec empire-hq -- python search.py
...
정리 (Clean Up)
이제 Cilium을 설치하고, 데모 앱을 배포하고, 마지막으로 Elasticsearch 인식 네트워크 보안 정책을 배포·테스트했어요. 정리하려면 다음을 실행하세요:
$ kubectl delete -f https://raw.githubusercontent.com/cilium/cilium/1.20.2/examples/kubernetes-es/es-sw-app.yaml
$ kubectl delete cnp secure-empire-elasticsearch