Kubernetes는 컨테이너 오케스트레이션을 수행하기 위해 각자의 목적을 가진 오브젝트들을 관리합니다.
대표적으로 Pod의 집합을 관리해주는 Deployment, 설정 파일을 관리해주는 Configmap, 인증을 위한 ServiceAccount 같은 것들이 존재하죠.
모든 오브젝트들이 각자의 목적을 가지고 있지만, 특히 프로덕션 수준에서 Kubernetes를 운영하기 위해서는 보안과 관련된 오브젝트도 있어야 합니다.
그래서 Kubernetes에서는 키,암호,토큰과 같은 중요한 데이터를 노출시키지 않고도 Pod에 포함시킬 수 있는 Seceret이라는 오브젝트를 제공하고 있습니다.
이번 포스팅에서는 이 Secret을 어떻게 사용할 수 있는지, Kubernetes에서는 Secret을 어떻게 해야 잘 활용할 수 있는지 알아보겠습니다.
1. Kubernetes Secret 개요
기본적으로 Kubernetes Secret은 Namespace단위의 오브젝트이며, Pod에 볼륨으로 마운트하거나 환경 변수로 지정해 사용할 수 있습니다.
Secret에 담기는 데이터는 기본적으로 키:밸류 쌍으로 이루어져 있으며 아래와 같은 유형의 Secret을 사용할 수 있습니다.
Opaque | 임의의 사용자 정의 데이터 |
kubernetes.io/service-account-token | 서비스 어카운트 토큰 |
kubernetes.io/dockercfg | 직렬화 된(serialized) ~/.dockercfg 파일 |
kubernetes.io/dockerconfigjson | 직렬화 된 ~/.docker/config.json 파일 |
kubernetes.io/basic-auth | 기본 인증을 위한 자격 증명(credential) |
kubernetes.io/ssh-auth | SSH를 위한 자격 증명 |
kubernetes.io/tls | TLS 클라이언트나 서버를 위한 데이터 |
bootstrap.kubernetes.io/token | 부트스트랩 토큰 데이터 |
Kubernetes에서는 보통 TLS 인증서, pem키, 서비스 어카운트 토큰, 계정 정보 등 기밀이 중요시되는 데이터를 Secret에 담도록 권장하고 있습니다.
하지만 Secret 자체가 기밀성을 보장하지는 않는데요.
이는 Secret에 저장되는 데이터는 암호화되는 것이 아닌, 단지 base64로 인코딩(encoding)되는 것이기 때문입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
apiVersion: v1
data:
ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVMRENDQXBTZ0F3SUJBZ0lRYzhxT2Fl...
kind: Secret
metadata:
annotations:
kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: 2b0357cc-da4a-4ddf-9942-9948661d7c2c
creationTimestamp: "2022-04-08T06:53:36Z"
name: default-token-r9rc5
namespace: test
resourceVersion: "4679273"
uid: 2365bedd-1a0a-4223-8236-1dd99b2b2302
type: kubernetes.io/service-account-token
|
cs |
위 Secret 매니페스트에서 data 항목의 ca.crt 키의 값은 LS0tLS1..과 같은 난해한 문자열로 이루어져 있는데요. 이 문자열이 기존 데이터를 base64로 인코딩한 값입니다.
데이터를 base64로 인코딩하는 이유는 SSL Cert와 같은 Binary 형식의 데이터를 문자열로 담기 위해서 인데요.
만약 오브젝트에 Binary 데이터를 그대로 저장하면 알 수 없는 문자가 등장해 사용할 수 없을 것입니다.
이 데이터는 아래와 같이 base64 --decode 명령어를 통해 쉽게 해독해서 원문을 확인할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
kubectl get secret default-token-r9rc5 -o jsonpath='{.data.ca\.crt}' | base64 --decode
-----BEGIN CERTIFICATE-----
MIIELDCCApSgAwIBAgIQc8qOaeVrl7p/Cn+kgGGu1jANBgkqhkiG9w0BAQsFADAv
MS0wKwYDVQQDEyQzOWUxZGFmMS05NDJlLTQ5MjYtOTVlMS0wMjIxZTA3ZDM4NWQw
IBcNMjIwMzMwMDIzOTAyWhgPMjA1MjAzMjIwMzM5MDJaMC8xLTArBgNVBAMTJDM5
ZTFkYWYxLTk0MmUtNDkyNi05NWUxLTAyMjFlMDdkMzg1ZDCCAaIwDQYJKoZIhvcN
AQEBBQADggGPADCCAYoCggGBAL5x4prGfV+sRSmpR2YnPnnMqTB+5xLyCZGa1WkN
2o++3WTMmAE9IZ/HleOmC3TF6uzVL7psCqvvUfHlx5pEdyNhVwKFI9mXOGNhTENE
cF94fR6S2WmD2zWU6Q/HW6GK011J0F4ao63rmu/7sCj2Yva4+q4m9BpSVJ4j0/On
...
VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUbFhRa9xPskuWaCb8alYVkYXCMI8wDQYJ
KoZIhvcNAQELBQADggGBAEHzT00DvOw9nPr7SToZybKgrkQtoBCCGz35juIp4l7u
u3PRZ96I4ipCbTbfqXTdqXWBzr6Fx4Lz79OO7vBgM8wFR+QRo1dsyrz6VxQSTTDT
8Dpvk6aYzbHTKUDFVO77hU57j/i4SSHkqvqdT977qVZZQOy7j32nltRCfpXEba+0
HN0yer0AX81W66Wm3YOD1Lc38BFW5zpRnecppZpB2wbncK+9M63z0Cl9Cz/9d9nu
O6512iQeaatw6AigeWYcprg4ctXkjGTdSXE1LzkEX1KQCkvdlJ2gba01rV6sBHx8
jW4ngdgg6OJ3q3FA55PZ4SEN4TPRthRdf+2oYIA86LGVB+FdMlapTJl9UDwSAb0X
/B+J+4Mgl/KxIla2j261bYHlKfHgVt1e27RBMzPLy8rovgxXw3mEMkJ+DN4HwwU9
8FuLFMeRn/uOpGyuRCpiMHYasuZCxCWopDHF2LyGcaQVBMdgaCN7GjYOHnA3iizB
6A72YxjvnylJBDQvVil7Vg==
-----END CERTIFICATE-----
|
cs |
위와 같이 base64는 암호화를 위한 것도 아니고, 쉽게 복호화할 수 있는데 왜 Kuberenetes는 Secret에 기밀성 정보를 저장하도록 권장하는 걸까요?
첫번째로 Secret 오브젝트는 다른 데이터와 달리 메모리에 저장되기 때문에 접근이 힘들다는 점에서 보안성이 높습니다.
이러한 특징 때문에 각 Secret의 최대 크기는 1M로 제한되며 Secret 오브젝트가 과다해지면 OOM 에러가 발생할 수 있다는 점에 유의해야 합니다.
두번째로 Secret의 목적이 데이터를 암호화해서 기밀을 보장하는 것보다는, Pod 매니페스트에 데이터를 노출하지 않도록 하려는 목적이 더 강합니다.
이 같은 특징 때문에 Secret을 보안성 높게 사용하기 위해서는 다음과 같은 항목을 지켜야 합니다.
첫번쨰로 RBAC을 이용해 Secret에 접근할 수 있는 User를 제한해야 합니다.
두번째로 데이터가 평문화되어서 저장되어 있는 etcd에 admin만 접근이 가능하게 하거나, etcd에 데이터 저장 시 암호화를 활성화해야 합니다.
세번째로 외부에서 제공하는 Secret manager를 활용해야 합니다.
Secret manager는 Secret을 암호화해 저장할 수 있는 외부 저장소를 말하는데요.
본 포스팅의 4장에서 위 3가지 중 마지막 Secret manager를 활용한 방법을 이용해 Secret을 어떻게 안전하게 이용할수 있는지 알아보겠습니다.
2. Kuberentes Secret 생성하기
kubernetes secret을 생성하는 가장 간단한 방법은 데이터를 기반으로 kubectl 명령어를 사용하는 것입니다.
예를 들어 test_secret.txt 파일을 Secret으로 생성하고 싶다면 아래와 같이 kubectl create secret generic 명령어를 사용할 수 있습니다.
1
|
kubectl create secret generic test-secret --from-file ./test_secret.txt
|
cs |
명령어를 실행하면 아래와 같이 Secret이 생성된 것을 확인할 수 있습니다.
Secret 내용을 확인해보면 기존 test_secret.txt.의 "secret contents!"라고 적혀있던 내용이 아래와 같이 인코딩되어 있는 것을 확인할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
|
apiVersion: v1
data:
test_secret.txt: c2VjcmV0IGNvbnRlbnRzIQo=
kind: Secret
metadata:
creationTimestamp: "2022-04-08T07:36:12Z"
name: test-secret
namespace: test
resourceVersion: "4694700"
uid: c20dcbbe-3fcc-464c-bce3-8dd646c65dd8
type: Opaque
|
cs |
물론 위와 같은 Secret 매니페스트 yaml파일을 사용해서 kubectl apply하는 것으로 Secret을 생성할 수도 있습니다.
3. Kubernetes Secret 사용하기
Kubernetes에서 Secret을 사용하는 방법은 총 3가지가 존재합니다.
1. 하나 이상의 컨테이너에 마운트된 볼륨 내의 파일로써 사용.
2. 컨테이너 환경 변수로써 사용
3. 파드의 이미지를 가져올 때 kubelet에 의해 사용.
3번 방법은 컨테이너의 Image를 Pull할때 필요한 Credential을 Kubernetes Secret으로 제공해 Private repository의 이미지를 가져올 수 있는 방법을 말합니다.
본 포스팅에서는 1번과 2번 사용 방법에 대해서 자세히 알아보겠습니다.
3-1. 마운트된 볼륨 내의 파일로써 Secret 사용
Kubernetes에는 Volume이라는 개념이 있어 특정 파일을 컨테이너 내의 파일시스템에 마운트할 수 있습니다.
이 마운트할 수 있는 데이터에는 Secret도 포함되는데요. 이를 이용하면 기밀성이 있는 데이터를 컨테이너 내부의 특정 폴더에 넣을 수 있습니다.
해당 사용 방법을 테스트해보기 위해 아래와 같이 Pod 매니페스트를 생성합니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: mypod
resources:
requests:
cpu: 100m
volumeMounts:
- mountPath: "/var/secrets"
name: secret
volumes:
- name: secret
secret:
secretName: test-secret
|
cs |
매니페스트를 보면 volumeMounts 항목에 secret 이름과 경로를 지정했으며, volumes 항목에 생성할 secret 오브젝트를 지정한 것을 볼 수 있습니다.
이는 곧 "/var/secrets" 경로에 test-secret이 포함한 파일을 넣겠다는 말이죠.
Pod를 생성한 뒤 정말 지정한 경로에 Secret 파일이 존재하는지 확인해보겠습니다.
해당 Pod로 들어가보니 정말 "/var/secrets/" 폴더에 Secret 오브젝트가 담고 있던 test_secret.txt 파일이 생성되어 있는 것을 볼 수 있었습니다.
예제에서는 단일 키만 존재하는 데이터를 사용했지만 여러 키를 사용해 데이터를 담는다면 키를 지정해서 어떤 데이터를 생성할지도 지정할 수 있습니다.
이렇게 Pod의 볼륨 마운트를 이용해서 특정 경로에 Secret을 파일로 넣을 수 있습니다.
3-2. 컨테이너 환경 변수로써 Secret 사용
위처럼 파일 자체를 경로에 넣는 방법도 있지만 환경 변수(env)에 Secret 값을 넣어서 사용하는 방법도 존재합니다.
다시 pod 매니페스트 파일을 생성하되 약간 다르게 수정합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: mypod
resources:
requests:
cpu: 100m
env:
- name: SECRET_ENV
valueFrom:
secretKeyRef:
name: test-secret
key: test_secret.txt
|
cs |
위 매니페스트와 바뀐 점은 "env" 속성을 사용해서 valueFrom으로 secretKeyRef를 사용해 secret 오브젝트의 키를 참조한다는 점입니다.
이 pod 또한 생성한 뒤 정말 환경 변수에 secret 값이 들어갔는지 확인해보겠습니다.
Pod에 들어가서 설정된 환경 변수 리스트를 확인해보니 제가 설정한 SECRET_ENV 변수에 secret 오브젝트의 데이터가 들어가 있는 것을 확인할 수 있었습니다.
이렇게 환경 변수를 설정하고 애플리케이션에서 해당 변수를 참조하게 만드는 방법으로 Secret을 사용할 수 있습니다.
4. Secret manager로 Secret 안전하게 활용하기
말씀드렸듯이 Secret은 이름처럼 기밀성이 보장되지 않습니다. Secret은 데이터를 암호화하는 것이 아니라 인코딩만 하기 때문입니다.
그렇기 때문에 정말 소중한 데이터들을 Kubernetes에만 저장하기에는 위험 부담이 따르는 것도 사실이죠.
이런 Kubernetes Secret의 단점을 극복하기 위해 사용할 수 있는 도구가 있습니다.
바로 Secret manager인데요.
이 Secret manager는 현재 다양한 곳에서 제공하고 있습니다. 아래는 대표적인 Secret manager들의 목록입니다.
- AWS Secrets Manager
- AWS Parameter Store
- Akeyless
- Hashicorp Vault
- Google Cloud Secrets Manager
- Azure Key Vault
- IBM Cloud Secrets Manager
- Yandex Lockbox
- Gitlab Project Variables
- Alibaba Cloud KMS
- Oracle Vault
위 업체들의 Secret manager들의 공통점은 외부 저장소를 이용해 기밀성이 필요한 데이터들을 안전하게 지켜주는 서비스라는 것입니다.
이 서비스를 사용해서 중요한 데이터들을 암호화해서 보관할 수 있는데요.
물론 보관만 할 수 있으면 안되겠죠. 이 Secret manager에 존재하는 데이터를 Kubernetes secret으로 연동까지 할 수 있어야 제대로 사용할 수 있을 것입니다.
이 Secret manager들을 Kubernetes와 연동할 수 있는 도구가 존재하는데요. 대표적으로 쓰이는 도구로는
External Secret operator(ESO)와 Kubernetes Secrets Store CSI Driver(SSCSID) 2개가 있습니다.
이번 포스팅에서는 위의 두 도구가 어떤 차이점이 있는지 알아보고, 두 도구 중 External Secret Operator(ESO)를 이용해 Kubernetes와 Secret manager를 연동하는 방법에 대해 알아보도록 하겠습니다.
4-1. ESO와 SSCSID의 차이점
위에서 말씀드렸듯이 현재 대표적인 Secret manager & Kubernetes 연동 도구는 External Secret operator(ESO)와 Kubernetes Secrets Store CSI Driver(SSCSID) 2개가 존재합니다.
이 두 개의 도구 중 어떤 상황에 어떤 것을 사용해야 할지 잘 모르겠는데요. 고맙게도 Github issue에 이와 같은 의문에 달린 답변이 존재합니다.
https://github.com/external-secrets/external-secrets/issues/478
위 링크에 SEO와 SSCID의 차이점을 조목조목 정리해놓은 리스트가 있는데, 꼭 읽어보시길 권장합니다.
답변을 간단히 정리하자면 다음과 같습니다.
Kuberntes Secret 오브젝트 생성 없이 pod에 External secret을 바로 넣고 싶다면 SSCSID를, 그 외에의 상황에는 대부분 ESO를 사용하는 것이 좋다.
개인적으로 위 차이점 리스트를 읽어본 느낌으로는 ESO가 더 활용하기 편리하다는 느낌을 받았습니다. 그래서 본 포스팅에서도 ESO를 활용한 Secret manager 연동을 소개하려고 합니다.
4-2. Secret manager 사용
Kubernetes와 Secret manager를 연동하기 전에 Secret manager에 Secret을 저장해야 합니다.
이번 포스팅에서는 많은 Secret manager 중 Google Cloud Platform의 Secret manager 서비스를 사용해보겠습니다.
GCP Secret manager에서 Secret은 Security 탭의 Secret manager에서 생성할 수 있습니다.
Secret 생성 페이지에서 원하는 Secret 파일을 업로드함으로써 Secret을 구성할 수 있습니다.
Secret manager는 단순 Secret의 저장 뿐만 아니라 암호화 방식, 복제 정책, 로테이션 등의 다양한 Secret 관리 수단을 제공하고 있습니다. 하단의 CREATE 버튼을 클릭해 Secret을 생성할 수 있습니다.
생성된 Secret의 내용은 IAM 정책을 통해 허가된 사용자만 확인할 수 있으며, Secret 수정, 사용 등의 기록 Log도 확인할 수 있습니다.
이렇게 Secret manager는 중요한 파일에 대한 기밀성 및 Compliance를 지킬 수 있는 관리를 제공합니다.
4-3. External Secret operator(ESO) 사용
ESO는 AWS,GCP,Azure 등..에서 제공하는 Secret manager 서비스와 Kubernetes Secret을 연동하기 위해 존재하는 도구입니다. 이를 위한 ESO 아키텍쳐는 다음과 같습니다.
ESO는 SecretStore라는 CRD 오브젝트를 통해 Secret manager를 추상화해 인증 및 인가에 필요한 행위를 SecretStore에 맡깁니다.
이후 외부 Secretmanager에 존재하는 Secret을 의미하는 ExternalSecret 오브젝트에 명시된 데이터를 SecretStore에서 가져오는 것으로 Kubernetes Secret과 연동하는 구조입니다.
ESO를 사용하기 위해 먼저 Kubernetes 클러스터에 ESO를 설치해야 합니다. 아래 명령어로 helm 패키지 매니저를 사용해 ESO를 설치합니다.
1
2
3
4
5
6
|
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets \
--create-namespace \
|
cs |
설치가 완료되었다면 kubectl get pods -n external-secrets 명령어를 실행해 operator pod가 정상적으로 동작하는지 확인합니다.
다음으로 Secretmanager에서 Secret을 가져올 수 있는 권한을 가진 Serviceaccount를 생성합니다. Serviceaccount는 GCP의 Workload Identity 기능을 사용해서 GCP의 Serviceaccount와 바인딩해야 합니다.
Workload identity에 대한 자세한 내용은 아래 링크를 참고해 주세요.
https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity
저는 GCP의 seo-sa@lswoo-int-210304.iam.gserviceaccount.com 계정과 바인딩된 secretmanager-sa 서비스 어카운트를 생성했습니다.
1
2
3
4
5
6
7
8
|
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
iam.gke.io/gcp-service-account: seo-sa@lswoo-int-210304.iam.gserviceaccount.com
name: secretmanager-sa
|
cs |
GCP 서비스 어카운트에는 "Secret manager Secret Accessor"와 "Service Account Token Creator" 역할이 부여되어 있어야 함에 주의합니다.
이제 GCP Secretmanager를 추상화한 secretstore 오브젝트를 생성합니다. 아래와 같은 매니페스트를 작성 후, 적용합니다.
ServiceAccountRef 항목에 위에서 생성한 서비스 어카운트를 입력합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: test-secretstore
spec:
provider:
gcpsm:
projectID: PROJECT_JD
auth:
workloadIdentity:
clusterLocation: CLUSTER_ZONE
clusterName: CLUSTER_NAME
clusterProjectID: PROJECT_ID
serviceAccountRef:
name: secretmanager-sa
|
cs |
이후 kubectl get secretstore 명령어로 생성을 확인합니다.
마지막으로 SecretStore에서 어떤 데이터를 가져올지 지정하는 ExternalSecret 오브젝트를 생성하겠습니다.
secretStoreRef 항목에 생성한 SecretStore 정보를, target 항목에 생성할 Secret 정보를 , data 항목에 Secret manager에서 가져올 데이터를 지정합니다. 아래와 같이 매니페스트를 작성후, 적용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: example
spec:
refreshInterval: 1m
secretStoreRef:
kind: SecretStore
name: test-secretstore
target:
name: created-secret
creationPolicy: Owner
data:
- secretKey: test-secretmanager-secret
remoteRef:
key: test-secretmanager-secret
|
cs |
kubectl get externalsecret 명령어로 생성을 확인할 수 있습니다.
kubectl get secret 명령어로 secret 오브젝트를 확인해보면 ExternalSecret의 target 란에 입력한대로 "created-secret" Secret이 생성된 것을 볼 수 있습니다.
앞에서 ESO가 하는 역할은 Secret manager와 Kubernetes Secret과의 연동이라고 했습니다. 정말 연동이 잘 되는지 확인하기 위해 Secret manager에서 Secret의 데이터를 변경해보겠습니다.
변경 후 refreshInterval에서 지정한 1분이 지나면 아래와 같이 Secret의 내용도 자동으로 변경된 것을 확인할 수 있습니다.
이렇게 ESO는 외부 Secret manager의 데이터를 Kubernetes Secret과 쉽게 연동할 수 있다는 장점이 있지만, 결과적으로 Kuberentes Secret 오브젝트를 생성하기 때문에 보안을 위해서 RBAC으로 Secret 접근 권한을 신경써야 한다는 단점이 존재합니다.
만약 Secret 오브젝트 생성 없이 Pod에 바로 Secret manager의 데이터를 넣고 싶다면 위에서 소개한 SSCID를 사용하는 것을 권장합니다.
https://github.com/kubernetes-sigs/secrets-store-csi-driver
5. 마무리
지금까지 Kubernetes Secret이 무엇인지, 어떻게 사용할 수 있는지, Secret manager와 ESO를 어떻게 연동할 수 있는지 알아봤습니다.
Kubernetes의 Secret는 그 이름이 의미하는 것과 달리 기밀성을 보장하지 않기 때문에 중요한 데이터를 Secret 오브젝트로 방치해두면 보안성이 심하게 위배될 것입니다.
이를 위해 앞으로는 비밀번호, API 키 등의 중요한 데이터는 Secret manager에 저장하고 ESO, SSCID 등의 도구를 사용해 RBAC으로 접근이 제어된 Kubernetes Secret을 연동하는 것이 좋아 보입니다.