Sealed Secretes

Secrets in Kubernetes

쿠버네티스를 사용하다 보면, 수 많은 YAML 파일들을 만들어 낼 것입니다. 그리고 이 YAML 파일들을 잘 관리하기 위해서 저장소를 찾게 됩니다. 일반적으로는 평소 즐겨 사용하던 git 을 저장소로 많이 사용합니다.

YAML 파일을 git 저장소에 올리는 것 자체는 별로 어렵지 않습니다. 하지만 파일에 정의된 쿠버네티스 리소스들을 하나씩 하나씩 관심있게 살펴보게 되면, 과연 이 리소스를 올리는게 맞는지 의문을 가지게 만드는 리소스가 있습니다. 바로 Secret 리소스 입니다.

쿠버네티스의 Secret 리소스는 키(key)-값(value)의 쌍으로 이루어져 있으며, 메타 데이터를 가지고 있습니다.

ConfigMap과 비슷하게 생겼지만, Secret에는 민감한 데이터를 저장하기 때문에 보호받아야하는 차이점이 존재합니다.

아래 예제는 Secret 를 YAML로 표현한것입니다.

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
  namespace: default
type: Opaque
data:
  username: YWRtaW4=
  password: aGVsbG8td3JvbGQ=

usernamepassword 라는 키가 있고, Base64 로 인코딩된 값들이 저장되어 있습니다. 문제는 Base64 인코딩이 암호화가 아니라는 점입니다. 단순히 64진수로 변환된것 뿐이라서, 아주 쉽게 디코딩을 할 수 있습니다.

예제에 나와 있는 password의 값을 단순히 디코딩하면, 원래 값을 알 수가 있는 것입니다.

$ echo aGVsbG8td3JvbGQ= | base64 --decode
hello-world

이러한 Secret 을 git에 그냥 올리게 되면, 위험한 상황에 빠질 수도 있게 되는 것입니다.

그러면 어떻게 해야 할까요?

그것은 Secret 을 암호화 해서 git에 저장하는 것입니다. 다행히도 이러한 기능을 하는 도구들이 이미 나와 있습니다. 바로 Sealed SecretsKamus 입니다.

이 문서에는 Sealed Secrets 에 대해 간단히 다룰 것입니다.

Sealed Secrets

Sealed Secrets 는 두 개의 부분으로 이루어져 있습니다. 쿠버네티스 클러스터에 설치할 sealed-secrets-controller 와 암호화 유틸리티인 kubeseal입니다.

Sealed SecretsSealedSecret라는 CR(Custom Resource) 사용해서 Secrets 을 보호합니다. 사용자는 Secrets 리소스를 직접 생성하지 않고, SealedSecret 라는 커스텀 리소스를 생성합니다. 이 SealedSecret 리소스에는 중요한 값들이 암호화 되어 저장됩니다. 이러한 중요한 값들을 암호화 할 때 사용하는 도구가 kubeseal 입니다. SealedSecret 리소스가 클러스터에 등록되면, sealed-secrets-controller 가 해당 리소스의 암호화된 값들을 복호하해서 Secrets 리소스를 생성해 주는 것입니다.

설치하기

Controller 설치

SealedSecret 이라는 CRD를 생성하고, Controller를 설치하면 됩니다.

아래 예제는 CRD를 생성하고, kube-system 네임스페이스에 Controller를 설치하는 방법입니다.

$ kubectl apply -f <https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.9.6/controller.yaml>

controller.yaml

---
apiVersion: v1
kind: Service
metadata:
  annotations: {}
  labels:
    name: sealed-secrets-controller
  name: sealed-secrets-controller
  namespace: kube-system
spec:
  ports:
  - port: 8080
    targetPort: 8080
  selector:
    name: sealed-secrets-controller
  type: ClusterIP
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  annotations: {}
  labels:
    name: sealed-secrets-service-proxier
  name: sealed-secrets-service-proxier
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: sealed-secrets-service-proxier
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:authenticated
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  annotations: {}
  labels:
    name: sealed-secrets-key-admin
  name: sealed-secrets-key-admin
  namespace: kube-system
rules:
- apiGroups:
  - ""
  resources:
  - secrets
  verbs:
  - create
  - list
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  annotations: {}
  labels:
    name: sealed-secrets-controller
  name: sealed-secrets-controller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: secrets-unsealer
subjects:
- kind: ServiceAccount
  name: sealed-secrets-controller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  annotations: {}
  labels:
    name: secrets-unsealer
  name: secrets-unsealer
rules:
- apiGroups:
  - bitnami.com
  resources:
  - sealedsecrets
  verbs:
  - get
  - list
  - watch
  - update
- apiGroups:
  - ""
  resources:
  - secrets
  verbs:
  - get
  - create
  - update
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
---
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations: {}
  labels:
    name: sealed-secrets-controller
  name: sealed-secrets-controller
  namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations: {}
  labels:
    name: sealed-secrets-controller
  name: sealed-secrets-controller
  namespace: kube-system
spec:
  minReadySeconds: 30
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      name: sealed-secrets-controller
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      annotations: {}
      labels:
        name: sealed-secrets-controller
    spec:
      containers:
      - args: []
        command:
        - controller
        env: []
        image: quay.io/bitnami/sealed-secrets-controller:v0.9.6
        imagePullPolicy: Always
        livenessProbe:
          httpGet:
            path: /healthz
            port: http
        name: sealed-secrets-controller
        ports:
        - containerPort: 8080
          name: http
        readinessProbe:
          httpGet:
            path: /healthz
            port: http
        securityContext:
          readOnlyRootFilesystem: true
          runAsNonRoot: true
          runAsUser: 1001
        stdin: false
        tty: false
        volumeMounts:
        - mountPath: /tmp
          name: tmp
      imagePullSecrets: []
      initContainers: []
      serviceAccountName: sealed-secrets-controller
      terminationGracePeriodSeconds: 30
      volumes:
      - emptyDir: {}
        name: tmp
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: sealedsecrets.bitnami.com
spec:
  group: bitnami.com
  names:
    kind: SealedSecret
    listKind: SealedSecretList
    plural: sealedsecrets
    singular: sealedsecret
  scope: Namespaced
  version: v1alpha1
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  annotations: {}
  labels:
    name: sealed-secrets-service-proxier
  name: sealed-secrets-service-proxier
  namespace: kube-system
rules:
- apiGroups:
  - ""
  resourceNames:
  - 'http:sealed-secrets-controller:'
  - sealed-secrets-controller
  resources:
  - services/proxy
  verbs:
  - create
  - get
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  annotations: {}
  labels:
    name: sealed-secrets-controller
  name: sealed-secrets-controller
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: sealed-secrets-key-admin
subjects:
- kind: ServiceAccount
  name: sealed-secrets-controller
  namespace: kube-system

kubeseal 설치

MacOS

brew install kubeseal

Linux x86-64

wget <https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.9.6/kubeseal-linux-amd64> -O kubeseal
sudo install -m 755 kubeseal /usr/local/bin/kubeseal

SealedSecret 생성하기

우선 kubectl--dry-run을 사용해서 my-secret.yaml 파일을 생성합니다.

kubectl -n default create secret generic my-secret \\
  --from-literal=username=admin \\
  --from-literal=password=hello-wrold \\
  --dry-run \\
  -o yaml > my-secret.yaml

생성한 my-secret.yaml 파일의 내용은 다음과 같습니다.

apiVersion: v1
data:
  password: aGVsbG8td3JvbGQ=
  username: YWRtaW4=
kind: Secret
metadata:
  creationTimestamp: null
  name: my-secret
  namespace: default

kubeseal을 실행해서, my-secret.yaml 파일을 my-sealed-secret.yaml 파일로 변환합니다. my-sealed-secret.yaml 파일이 생성될때, 값들은 암호화되어 저장됩니다.

kubeseal --format=yaml < my-secret.yaml > my-sealed-secret.yaml

생성한 my-sealed-secret.yaml 파일의 내용은 다음과 같습니다.

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: my-secret
  namespace: default
spec:
  encryptedData:
    password: AgAkqnrtawbZwPiOuleW5VDHTy8DtnSRqpW8xfV+um6qtqpFVOurbEXAmGWZUhv0MhvppXdHokccRDyS/iaODZGTDXALqrpfS2KMbsX3ARhUXPM5/4G8x9MrjZYGGZNYEJnY78ZvzxlMv7EWsDHTa+9UnWPAorNXd/KDjBhXujohdzs7rPmrAFQWznLovcdqSSXkBMQyf4J19wKPit9H8wBIfHkB4kftI8106NOU6NK+y9s0T9mXIGalS0h5B8KbntvnGmR1/QmxEDJeD7pRSWjAx2m8eRFUsDxRif9uJP2tkwKtQlLJrmsW9YWeDWzeszjAkcniNZtRDkak2mHs3LQZl/vq/otj3V/JuRxVYbukFkyjkvQghbfQek8CTJ7s4kZzozEEwtKWugtSxW2dh7JCewvy18W+IYjxqNDok7sA67bGTycRRBY3+db9mirdE5PxhoHQAkvDu9cUUAgM/+SR2FkpJQ9lGPKWkRiEi3Nx4ZhIJ/4PE+3swZj95BBFLK50gI32YAPnop7SO99Gt9yU7oPK7zoV9r9TNDNvZinB37r7Y26ti3ASrKYH5AY0jAqNcnK2KuzSk44NBRmQ6Hmo0/NZiONBtSckhuzpSw0jmr8KTjp3eZPjk4KLUIW/lEYdAl3kfx3Kd8+QZz6ygALLJWxPPheZm3sd1VHV7F+Bnpo4zLC8pDh9uW0spMCMYQD5adRK+FkXEgnuTw==
    username: AgBbJRLu/+ke7Ze5EgZIRbQHNgwFP/oOz8w2XlkU3oyZczvK6AqkCFUZpHAO3r02LOPpNmiKzRZKOts508LtAFcYNawDeWOS2S29x+m8DQHiYuiKQ0UDBkoSquaEAiUXBBPJomOMS2uDuOLRq2vC4KaQ7N27eQgw1JFR/GvTH0ZPf3hTZ0if0PFMOPcbm4UmCI1Qeoo0ToiOhvgOcpdy6xbcEOyOFX5L+663P5GsZJmZ3F6IoMcSI7QN/iFQXytifW/9GXKSyv4un+dtIZAmjX+sxeUr22nsrBwSJxXy8jKfnO4tNG/XzlPk91ZVu37A5nYJj2+iNVaW0j+1fu80Kcto02rFaC3eFZ+D6q/RTD0cyhJlsNDKFk93il9K7hoAJ7nkyEW4vQ+hpVBsRvRWy2yGYXs/S5tjMtBbgWPYVwlFwR1zoWAd6/t4o+u3xNcJB/LWgBOhuW+Z7TPHGffMxPe3yqgqsLy16iDytjdjsZ83v8t/Fl1OBhWH4ssSHSMu1GA830wDPeCnCQL/JZHWOpqEeTpG+/FPGpzlt+n+oJ8jHdWeErSal3JfMSOLubKkGWVB7pdAYgzl8Pn47Q/GA5QehBqJiaOME/iL93ONhOmJFyI9ErQ9sqCT6YRsvRqxKy0BZgdud2Et1/W8oVZnR6Ev0e5hG/UbJ3SCiocYusWyKwFcmO0xEydxQA85miCvu78DeaJ1aA==
  template:
    metadata:
      creationTimestamp: null
      name: my-secret
      namespace: default
status: {}

SealedSecret 를 클러스터에 등록하기

생성한 SealedSecret 리소스를 클러스터에 등록해 보겠습니다. 네임스페이스는 default입니다.

kubectl일 실행해서 secretssealedsecrets을 조회해 보면 나오지 않습니다.

$ kubectl -n default get secrets
NAME                  TYPE                                  DATA   AGE
default-token-pz9rt   kubernetes.io/service-account-token   3      20d
istio.default         istio.io/key-and-cert                 3      20d

$ kubectl -n default get sealedsecrets
No resources found in default namespace.

생성한 SealedSecret 리소스를 등록합니다.

$ kubectl apply -f my-sealed-secret.yaml
sealedsecret.bitnami.com/my-secret created

다시 secretssealedsecrets을 조회해 보면 등록한 my-secret가 보이는 것을 알 수 있습니다.

$ kubectl -n default get secrets
NAME                  TYPE                                  DATA   AGE
default-token-pz9rt   kubernetes.io/service-account-token   3      20d
istio.default         istio.io/key-and-cert                 3      20d
my-secret             Opaque                                2      4s

$ kubectl -n default get sealedsecrets
NAME        AGE
my-secret   9s

자동으로 생성된 secret의 내용을 확인해 보겠습니다.

$ kubectl -n default get secrets my-secret -o yaml
apiVersion: v1
data:
  password: aGVsbG8td3JvbGQ=
  username: YWRtaW4=
kind: Secret
metadata:
  creationTimestamp: "2020-01-25T06:37:42Z"
  name: my-secret
  namespace: default
  ownerReferences:
  - apiVersion: bitnami.com/v1alpha1
    controller: true
    kind: SealedSecret
    name: my-secret
    uid: 8ed4ea13-4f89-46e2-9149-b80fe01b46d2
  resourceVersion: "6472553"
  selfLink: /api/v1/namespaces/default/secrets/my-secret
  uid: 466f7e72-9dc0-401b-9e2f-b073d08b5540
type: Opaque

정상적으로 복호화 되어 있는 것을 확인 할 수 있습니다.

기타

인증서를 별도로 사용하기

kubeseal 은 비대칭키 암호화 알고리즘을 사용해서 값들을 암호화 합니다. 별 다른 옵션을 주지 않으면 기본 설정 파일(~/.kube/config)을 이용해서 인증서를 가져온 후, 암호화 작업을 합니다. 상황이 여의치 않는 경우에는 이 인증서 정보를 파일로 저장한 후 직접 사용하실 수 있습니다.

kubeseal --fetch-cert > pub-cert.pem

혹시라도 컨트롤러를 설치한 네임스페이스가 다르거나, 이름이 다른 경우 다음과 같은 옵션을 사용할 수도 있습니다.

kubeseal --fetch-cert \\
  --controller-namespace=kube-system \\
  --controller-name=sealed-secrets-controller \\
> pub-cert.pem

그리고 SealedSecret 리소스를 생성할때 --cert=pub-cert.pem 플래그로 인증서를 지정해 줄 수 있습니다.

kubeseal --format=yaml --cert=pub-cert.pem  < my-secret.yaml > my-sealed-secret.yaml

비대칭키 암호화 알고리즘을 사용하기 때문에, 복호화를 위해서 개인키를 사용합니다. 이 개인키를 잃어버리면, 복호화를 할 수가 없습니다. 그래서 이 개인키를 안전하게 잘 보관해야 합니다.

백업하기

kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml >master.key

복구하기

$ kubectl apply -f master.key
$ kubectl delete pod -n kube-system -l name=sealed-secrets-controller