기존에는 시크릿을 그냥 yaml파일로서 생성해서 개인 컴퓨터에 가지고 있었습니다. 이 때는 사내에서 쿠버네티스를 관리하는 사람이 저밖에 없었기 때문에 크게 문제가 되지 않았습니다. 하지만, 어느새 쿠버네티스를 관리하는 팀원이 3명으로 늘어났습니다. 물론 시크릿 파일을 코드 저장소에 올리진 않았지만, 시크릿을 계속 파일로 관리하는 것은 보안적으로도 시스템적으로도 문제가 된다는 판단을 했습니다. 시크릿이 수정될 때마다 관리하는 팀원들에게 계속 공유를 해줘야했고, 누군가 해당 부분을 적용 안 한 상태로 수정사항을 적용할 경우 해당 시크릿이 누락되는 문제가 발생할 수 있었습니다.
이런 상황을 인지하고 AWS의 Secrets Manager를 활용하여 쿠버네티스의 Secret을 동기화할 수 있게하는 작업을 진행했습니다. 이번에는 그 과정에서 배운 것들을 공유하는 포스팅을 작성해보겠습니다.
Secrets Manager란?
AWS Secrets Manager는 보안 암호를 코드상에 직접 입력하는 것이 아니라, 원격에서 안전하게 관리할 수 있도록 도와주는 AWS 서비스입니다. 이렇게 하면 코드가 노출될 때 시크릿키가 노출되는 위험을 방지할 수 있습니다. 이 외에도 여러 보안암호에 대해 특정 사람 또는 그룹별로 접근 권한을 통제하거나, AWS 서비스에 대한 시크릿키의 로테이션 등의 기능을 제공해주어 보안 수준을 높여줍니다.
쿠버네티스 시크릿
쿠버네티스의 시크릿에 대한 이야기를 간략하게 해 보겠습니다. 쿠버 네티스의 시크릿은 쿠버네티스에서 보안 정보를 보관하는 오브젝트입니다. 이를 활용하면 파드와 독립적으로 보안 정보를 상대적으로 안전하게 관리할 수 있습니다. 하지만, 쿠버네티스 시크릿은 암호화되어 보관되지는 않습니다. 보통 Base64로 인코딩 되어서 보관되는데, 이는 쉽게 디코딩할 수 있습니다. 따라서 이를 단순히 파일로서 관리하고 동기화하는 것은 해당 파일을 가진 모든 사람이 해당 시크릿 파일에 접근하고, 열어볼 수 있다는 점에서 굉장히 보안적으로 취약한 보관 방법입니다.
*시크릿에 대한 더 자세한 설명은 공식문서를 참고해주세요.
*본문에서 다룰 Secrets Manager를 통해서 Secret을 주입하더라도, Secret에 접근할 권한을 가진 사람은 해당 Secret을 디코딩해서 볼 수 있기 때문에 RBAC룰을 설정해 시크릿에 대한 접근을 제한해야 합니다.
Secrets Store CSI Driver
Secrets Store CSI Driver는 AWS, Azure, GCP등의 각 Secret Store 서비스의 시크릿을 쿠버네티스의 파드에 마운트 할 수 있게 해주는 도구입니다.
*CSI 드라이버에 대한 자세한 설명은 Kubernetes Container Storage Interface (CSI) Documentation을 참고 바랍니다.
그럼 지금부터 Secrets Store CSI Driver를 통해 AWS의 시크릿을 쿠버네티스에 주입하는 것을 알아보겠습니다.
1. Secrets Store CSI Driver와 AWS Secret Provider 설치하기
Helm을 통해서 설치해보도록 하겠습니다. (Helm을 처음 들어보신 분들은 우선 Helm에 대해서 먼저 공부해보세요!) 우선 아래와 같이 values.secrets-store.yaml 파일을 만들어줍니다.
syncSecret:
enabled: true
enableSecretRotation: true
syncSecret은 클라우드의 시크릿과 쿠버네티스 시크릿을 동기화시키는 것을 허용하는 옵션입니다. (즉, 최종 결과 쿠버네티스 시크릿이 생성됩니다.) enableSecretRotation은 주기적으로 (기본 값: 2분) 시크릿을 확인하여, 시크릿에 변경점이 있을 때 이를 재반영하는 옵션입니다. 이 옵션이 있어야, 시크릿을 수정하거나 새롭게 추가할 때 해당 변경사항이 쿠버네티스 시크릿에도 적용됩니다.
helm repo add secrets-store-csi-driver https://raw.githubusercontent.com/kubernetes-sigs/secrets-store-csi-driver/master/charts
helm install -n kube-system csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver
AWS의 Secrets Manager를 사용하기 위해서는 Secrets Store CSI Driver와 함께 AWS Secret Provider를 사용해야 합니다. 아래 명령어를 통해 AWS Secret Provider를 설치해줍시다. (실제 사용할 때는 아래 yaml파일을 직접 로컬로 가져와주시는 것을 권장드립니다.)
kubectl apply -f https://raw.githubusercontent.com/aws/secrets-store-csi-driver-provider-aws/main/deployment/aws-provider-installer.yaml
2. AWS Secrets Manager에 Secret 등록
시크릿을 등록하는 방법은 크게 두가지가 있습니다. AWS Console을 통해 하는 방법이 있고, 터미널을 활용하는 방법이 있습니다. 저는 개인적으로 시크릿 같은 경우는 AWS Console을 활용하는 것이 편한 것 같습니다. (CLI를 통해 하고 싶으신 분은 여기를 참고해주세요.)
- 우선 AWS에 로그인 → Secrets Manager → 새 보안 암호 저장을 클릭해주세요.
- 다른 유형의 보안 암호를 선택하고 아래 키/값에 시크릿 키와 값을 입력해주세요. 여기서는 USERNAME=hello, PASSWORD=world로 입력하겠습니다. 입력을 마치셨다면 다음을 눌러주세요.
- 보안 암호 이름을 입력해주세요. 여기서는 my/secret으로 입력하겠습니다. 실제 환경에서는 <환경>/<시크릿 네임>으로 만들 수 있습니다. 입력을 완료하셨다면 계속 다음을 눌러주시고 저장을 눌러주세요.
3. 쿠버네티스 IAM ServiceAccount 생성 (feat. eksctl)
저희가 방금 만든 시크릿에 접근할 수 있도록 iam serviceaccount를 만들어 줘야합니다.eksctl로 ESK를 구성한 상태이기 때문에 eskctl utils를 활용하여 만들어주겠습니다.
- EKS의 REGION과 CLUSTERNAME 변수 구성
REGION=<REGION>
CLUSTERNAME=<CLUSTERNAME>
- 우선 iam serviceaccount를 만들기 위해서는 OIDC를 활성화해주셔야 합니다. (이미 설정되어 있다면 또 할 필요는 없습니다.)
eksctl utils associate-iam-oidc-provider --region="$REGION" --cluster="$CLUSTERNAME" --approve
- 해당 secret에 접근 가능한 IAM Policy 만들어 봅시다. 아래와 같은 명령어를 입력해주세요.
POLICY_ARN=$(aws --region "$REGION" --query Policy.Arn --output text iam create-policy --policy-name nginx-deployment-policy --policy-document '{
"Version": "2012-10-17",
"Statement": [ {
"Effect": "Allow",
# 시크릿을 Get / Describe할 수 있는 권한을 허용함
"Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
# 2번 과정에서 생성한 my/secret에 대해서 해당 권한 적용
"Resource": ["arn:*:secretsmanager:*:*:secret:my/secret-??????"]
} ]
}')
- 해당 IAM Policy를 부여한 iam serviceaccount 생성해주세요. 여기서는 nginx-deployment-sa라는 이름으로 서비스 어카운트를 생성하였습니다. 만약 Secret을 적용할 파드의 네임스페이스가 디폴트가 아니라면 --namespace <namespace>를 추가로 입력해주시면 됩니다. 여기서는 디폴트 네임스페이스를 활용하겠습니다.
eksctl create iamserviceaccount --name nginx-deployment-sa --region="$REGION" --cluster "$CLUSTERNAME" --attach-policy-arn "$POLICY_ARN" --approve --override-existing-serviceaccounts
*eskctl이 아니라 다른 방법 (e.g., terraform)을 활용하고 계신분들은 각자의 방법으로 해당 정책에 대한 권한을 가진 iam serviceaccount를 만들어주시면 될 것 같습니다.
*eksctl config파일을 만들어서 생성할 수도 있습니다. 같은 환경을 다시 구축할 때, config 파일만을 읽어서 실행하면 되기 때문에 권장드리는 방법입니다.
4. Secret Provider 생성
1번에서 설치한 Secrets Store CSI Driver를 활용해보도록 합시다. 다음과 같은 secret-provider.yaml을 만들어주세요.
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: my-secret-provider
spec:
provider: aws
secretObjects:
- secretName: my-secret # 쿠버네티스에 등록할 시크릿 네임
type: Opaque
data:
- key: "USERNAME" # 시크릿에 주입할 키 (my-secret이라는 시크릿에 USERNAME 키와 그 값이 주입된다.)
objectName: "USERNAME" # 볼륨에 마운트되어 있는 파일이름 (아래의 jmesPath/objectAlias)
- key: "PASSWORD"
objectName: "PASSWORD"
parameters:
objects: |
- objectName: "my/secret" # 2번에서 등록한 시크릿 네임
objectType: "secretsmanager"
jmesPath: # secret키에서 각 키를 분리해주기 위해 각 키마다 파일을 분리
- path: "USERNAME" # 시크릿 매니저에 등록되어 있는 키
objectAlias: "USERNAME" # 볼륨에 마운트할 이름
- path: "PASSWORD"
objectAlias: "PASSWORD"
*secretObjects 부분은 쿠버네티스에 Secret을 등록할 때 사용하는 부분입니다. 단순히 secret을 volume에 마운팅 하고 싶다면 parameters 부분만 사용해주시면 됩니다.
5. 파드에 마운트시키고 환경변수로 해당 시크릿 주입
다음과 같은 nginx-example.yaml 파일을 만들고, 이를 적용하여 시크릿이 잘 주입되었는지 확인해보겠습니다. 제가 주석을 단 부분을 주의 깊게 살펴보세요.
kind: Service
apiVersion: v1
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
serviceAccountName: nginx-deployment-sa
volumes: # 볼륨 정의
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "nginx-deployment-aws-secrets" # 위에서 생성한 secret provider 이름
containers:
- name: nginx-deployment
image: nginx
ports:
- containerPort: 80
envFrom:
- secretRef:
name: my-secret # secret provider에서 설정한 시크릿 이름
volumeMounts: # 마운트 path 정의
- name: secrets-store-inline
mountPath: "/mnt/secrets-store"
readOnly: true
volume 마운트는 반드시 필요합니다. secret provider가 작동하는 원리가, 먼저 volume을 마운트시키고 해당 volume으로부터 쿠버네티스 secret을 생성하기 때문입니다. 아래와 같은 명령어를 입력하여 해당 사항을 적용하고 시크릿을 확인해보도록 하겠습니다.
- 변경사항 적용하기
kubectl apply -f nginx-example.yaml
일정 시간이 지나면 해당 부분이 적용됩니다.
- 해당 쉘 접속하기
kubectl exec -it $(kubectl get pods | awk '/nginx-deployment/{print $1}' | head -1) -- /bin/sh`
- 해당 쉘에서 Volume 마운트 확인하기
cat /mnt/secrets-store/my_secret
{"USERNAME":"hello","PASSWORD":"WORLD"}
- 환경변수 확인하기
echo $USERNAME;
echo $PASSWORD;
hello
world
- 쉘에서 나와서 쿠버네티스 시크릿 확인하기
kubectl get secret my-secret
이렇게 secrets manager를 통해 쿠버네티스의 시크릿을 주입하는 과정을 알아봤습니다.더 자세한 설정방법이나 가이드는 공식문서를 참고해주시면 감ㅅ하겠습니다.
Q&A
1. 그냥 secrets manager를 어플리케이션에서 설정해서 사용하면 되지 않나요?
물론, 이렇게 해도 오답이라고 말할 수는 없습니다. 다만, 인프라와 관련된 부분이 어플리케이션에 '결합'되는 것은 지양하는 것이 일반적으로 좋다고 여겨집니다. 그렇지 않다면 AWS를 GCloud로 바꾸거나, Vault를 사용하게 될 때 어플리케이션의 코드를 수정해야 하기 때문입니다.
2. Hashicorp의 Vault를 왜 사용하지 않았나요?
AWS Secrets Manager가 Hashicorp의 Vault 같은 오픈소스와 다른 점은 이 모든 것을 복잡하게 관리/세팅할 필요가 없다는 점입니다. 즉, 백업 등을 신경 쓰지 않더라도, AWS가 관리해주고 보다 손쉽고 빠르게 환경을 세팅할 수 있습니다. 만약 회사의 규모가 작은 스타트업이고 AWS나 기타 클라우드 서비스를 이용하고 있다면 관리형 솔루션을 이용하고, 그 시간을 다른 곳에 쓰는 것이 더 좋은 선택지 일 수 있습니다.
만약 Vault를 활용하고 싶다면 Vault의 Agent Sidecar Injector 페이지를 확인해보세요.
3. 설정과정에서 문제가 생겼어요.
일반적으로 pod의 로그나 이벤트를 확인해보시면 문제를 알 수 있습니다.
kubectl get logs -f $(kubectl get pods | awk '/nginx-deployment/{print $1}' | head -1)
kubectl describe pod/$(kubectl get pods | awk '/nginx-deployment/{print $1}' | head -1)
공식문서에서 Troubleshooting 페이지도 제공하고 있으니 이 부분도 참고해보시길 바랍니다.
Referernce
댓글