Implement Vault Operator on Kubernetes (Integrated with Auto Unsealing)

selfieblue
8 min readAug 11, 2019

--

สำหรับบทความนี้ เราจะพูดถึงวิธีการ Implement Sensitive Data Management บน Kubernetes กันนะครับ ซึ่ง solution ที่ผมจะหยิบมา เพื่ออธิบายในหัวข้อนี้ Vault Engine by Hashicorp

แต่เนื่องจากการ provision vault cluster บน kubernetes เพื่อให้ตอบโจทย์เรื่อง security, monitoring และ high availability นั้น มีความยุ่งยาก ทางทีมงาน CoreOS จึงได้คิดค้นวิธีการ เพื่อช่วยให้การ provision vault cluster บน kubernetes ทำได้ง่ายขึ้น นวัตกรรมนั้น ชื่อ ว่า Vault Operator ซึ่งความหมายก็ค่อนข้างตรงไปตรงมาคือ ทำหน้าที่ีแทน Operator ในการ proviosion/update/upgrade/remove ของ vault cluster บน kubernetes ทำได้อย่าง่ายดาย

สำหรับ Tools เพิ่มเติม ที่เราจะใช้ในบทความนี้ มีดังนี้
1) helm cli
2) vault cli
3) terraform cli
4) git cli

ณ จุดนี้ เราจะมาศึกษาถึง architecture ของ vault cluster ซึ่งเราจะทำการ provision ผ่าน vault operator กันก่อนนะครับ

จากภาพด้านบนนี้ เราจะเห็นว่า vault cluster มี etcd cluster ทำหน้าที่เป็น backend storage และทั้ง 2 clusters ถูก proviosion และ manage ด้วย operators ทั้ง 4 ซึ่งอยู่ซ้ายมือของภาพ เพราะฉะนั้นเราจะมาทำความเข้าใจถึงขั้นตอนการ provion vault cluster กันก่อน นั่นก็คือ
ขั้นตอนที่ 1 Deploy vault-operator ผ่าน helm หลังจาก ที่ deploy เรียบร้อยแล้วเราจะได้ Pod มา 4 pods รันอยู่บน secrets namespace ของ kubernetes ดังนี้
1) vault-opertator ทำหน้าที่ provision and manage vault cluster
2) etcd-operator ทำหน้าที่ provision and manage etcd cluster
3) etcd-backup-operator ทำหน้าที่ backup data ที่อยู่ภายใน etcd cluster
4) etcd-restore-operator ทำหน้าที่ restore data ไปที่ etcd cluster
ขั้นตอนที่ 2 Provision ทั้ง etcd cluster และ vault cluster ผ่าน vault-operator

อีกหนึ่งสิ่งที่คุณควรทราบคือหลังจาก Deploy vault-operator ผ่าน helm ในขั้นตอนที่ 1 เรียบร้อยแล้ว จะมี Kubernetes’s Custom Resource Definition (CRD) ถูกสร้างใน Kubernetes Cluster มา 4 apigroups ซึ่ง apigroups เหล่านี้จะถูกกล่าวถึงต่อไป
etcdbackups.etcd.database.coreos.com
etcdclusters.etcd.database.coreos.com
etcdrestores.etcd.database.coreos.com
vaultservices.vault.security.coreos.com

สำหรับตัวอย่างที่เราจะหยิบมาพูดถึงในวันนี้ จะมี requirement เพิ่มเติมนั่นก็คือ
- Data ที่อยู่ใน ETCD Cluster จะต้องสามารถ Backup และ Restore จาก AWS S3 ได้
- Vault Cluster จะต้องสาวมารถ Configure Auto Unseal ผ่าน AWS KMS ได้
- Vault ต้องสามารถ Enable/Disable Web UI ได้

จาก requirement เพิ่มเติมข้างต้น ดังนั้นเราต้องมีการเตรียมข้อมูลพื้นฐานก่อนทำการเริ่ม provision vault cluster ดังนี้

1) AWS S3 Bucket and Security Credential
Create new aw s3 bucket and iam user, Then attach this policy to that user, then create credential to get Access key ID and Secret Access key ID

# Use this policy to attached to user
# Replace backup name : example-vault-backup-bucket
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::example-vault-backup-bucket",
"arn:aws:s3:::example-vault-backup-bucket/*"
]
}
]
}

2) AWS KMS Key Id and Security Credential

# Replace your <AWS_ACCOUNT_ID>{
"Version": "2012-10-17",
"Id": "vault-auto-unseal-policy",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<AWS_ACCOUNT_ID>:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow access for Key Administrators",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<AWS_ACCOUNT_ID>:user/kms-admin"
},
"Action": [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:TagResource",
"kms:UntagResource",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
],
"Resource": "*"
},
{
"Sid": "Allow use of the key",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<AWS_ACCOUNT_ID>:user/vault-unsealer"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
},
{
"Sid": "Allow attachment of persistent resources",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::arn:aws:iam::<AWS_ACCOUNT_ID>:user/kms-admin"
},
"Action": [
"kms:CreateGrant",
"kms:ListGrants",
"kms:RevokeGrant"
],
"Resource": "*",
"Condition": {
"Bool": {
"kms:GrantIsForAWSResource": "true"
}
}
}
]
}

คุณสามารถใช้ terraform command ในการ Provision Bucket, KMS และ Users ดังไฟล์นี้

หลังจาก provision ด้วย terraform เรียบร้อยแล้ว ผลลัพธ์ที่ได้คือ
1) New AWS User : kms-admin
2) New AWS User : vault-unsealer
3) New AWS User : vault-data-manage
4) New AWS S3 Bucket : example-vault-backup-bucket
5) New AWS KMS Alias Key : vault-unseal-key
แต่ยังไม่จบแค่นี้ เราต้อง create aws user security credential สำหรับ 2 users นี้ คือ
1) vault-unsealer
2) vault-data-manage
เพื่อที่เราจะนำ credentail ทั้ง 2 ไปใช้ในการ configure credential ระหว่าง provision vault cluster โดยสามารถอ้างอิงวิธีการสร้าง security credential ผ่าน AWS Console ได้ดังเว็บไซต์นี้ <Here> (***Remark : อย่าลืม download .csv file ในขั้นตอนการสร้าง credential บนหน้า AWS Console ไว้ด้วยนะครับ***)

ถึงคราว เรามาดู High-Level Overview หลังจากที่เราประกอบร่างกันระหว่าง AWS Services และ Vault Operators เป็นที่เรียบร้อยแล้วกันบ้าง

จากภาพด้านบนจะเห็นได้ว่า Vault Cluster จะเชื่อมต่อไปยัง AWS KMS เพื่อที่จะทำการ Unseal ผ่าน AWS KMS Service และในส่วนของ Backup และ Restore เราจะมี etcd-backup-operator และ etcd-restore-operator เป็นผู้รับผิดชอบในส่วนหน้าที่นี้อยู่ ซึ่งทั้งสองจะเชื่อมต่อกับ AWS S3 Service ดังก่อนการเชื่อมต่อไปยัง AWS KMS Service และ AWS S3 Service ได้นั้น เราจะต้องทำการ สร้าง kubernetes secrets ไว้ที่ secrets namespace เพื่อจะเก็บ credentials ซึ่งจะใช้ในการ access ไปที่ 2 AWS Services ดังกล่าว โดย credentails ทั้งสองจะได้จาก .csv file ที่เราได้ทำการ download มาแล้วในขั้นตอนการสร้าง create aws user security credential
เรามาเริ่มจากการสร้าง kubernetes secret เพื่อเชื่อมไปยัง AWS S3 Bucket กันก่อน

rm -rf mkdir /tmp/aws
mkdir /tmp/aws
cat <<EOF > /tmp/aws/credentials
[default]
aws_access_key_id = <REPLACE_ACCESS_KEY_ID_FROM_VAULT_DATA_MANAGER_USER>
aws_secret_access_key = <REPLACE_SECRET_ACCESS_KEY_FROM_VAULT_DATA_MANAGER_USER>
EOF
cat <<EOF > /tmp/aws/config
[default]
region = <REPLACE_YOUR_REGION_ID>
EOF
cd /tmp/aws
kubectl create secret generic vault-backup-bucket-aws-s3-secret --from-file=credentials --from-file=config -n secrets

จากนั้นเราจะมาสร้าง kubernetes secret เพื่อเชื่อมไปยัง AWS KMS กันต่อเลยนะครับ
Note : AWS KMS KEY ID สามารถหาได้จาก AWS Console

kubectl create secret generic vault-auto-unseal-awskms-secret \
--from-literal=region=ap-southeast-1 \
--from-literal=access_key=<REPLACE_ACCESS_KEY_ID_FROM_VAULT_UNSEALER_USER> \
--from-literal=secret_key=<REPLACE_SECRET_ACCESS_KEY_FROM_VAULT_UNSEALER_USER> \
--from-literal=kms_key_id=<REPLACE_WITH_YOUR_KMS_KEY_ID> \
-n secrets

ในขั้นตอนต่อไปเราจะมาเตรียม prerequisites ฝั่ง kubernetes cluster ก่อนที่จะเริ่ม deploy vault-operator กันบ้าง

  1. Check จำนวน Worker Node ในการ provision vault cluster ใน High Availability Mode คุณต้องมี worker nodes อย่างน้อย 3 nodes และต้องตรวจสอบด้วยว่า 3 nodes นั้น มี node label ที่ชื่อว่า kubernetes.io/role: node
  2. สร้าง Namespace, Service Account และ Pod Security Policy ไว้สำหรับ Vault Services และที่สำคัญมากคือ ต้อง Assign ClusterRole ที่ชื่อ system:auth-delegator ให้กับ Service Account ที่เราเพิ่งสร้าง ผ่าน Clusterrolebinding
==== Step #1 : Create secrets namespace
cat <<EOF > /tmp/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
labels:
name: secrets
name: secrets
spec:
finalizers:
- kubernete
EOF
kubectl apply -f /tmp/namespace.yaml==== Step #2 : Create Service Account For Vaultcat <<EOF > /tmp/vault-authen-tokenviewer-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-authen-tokenviewer
namespace: secrets
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: vault-authen-tokenviewer-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault-authen-tokenviewer
namespace: secrets
EOF
kubectl apply -f /tmp/vault-authen-tokenviewer-rbac.yaml==== Step #3 : Create SecurityCotext For Vaultcat <<EOF > /tmp/vault-psp.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: vault-pod-security
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
spec:
privileged: false
allowPrivilegeEscalation: false
allowedCapabilities:
- '*'
volumes:
- configMap
- downwardAPI
- emptyDir
- persistentVolumeClaim
- projected
- secret
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'MustRunAs'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
EOF
kubectl apply -f /tmp/vault-psp.yamlkubectl create role psp:vault-pod-security --verb=use --resource=podsecuritypolicy --resource-name=vault-pod-security -n secretskubectl create rolebinding rolebinding:psp:vault-pod-security --role=psp:vault-pod-security --serviceaccount=secrets:vault-authen-tokenviewer -n secrets

เราพร้อมที่จะ deploy vault-operator กันแล้ว โดยวิธีการ deploy vault-operator บน kubernetes นั้น เราจะใช้ helm command ซึ่งจะมีการ override parameters บางตัวในไฟล์ values.yaml ซึ่งแตกต่างไปจาก origin values.yaml ที่พบได้ใน official repo ดังนั้น คุณจำเป็นต้อง clone repository มาไว้ที่เครื่องของคุณเอง

cd /tmp
git clone https://github.com/devops-genuine/provision-vault-cluster.git

ซึ่งถ้าดูเนื้อหาในไฟล์ชื่อ custom-values.yaml จากเนื้อหาของไฟล์จะพบว่า ส่วนที่ Highlight ตัวหนาคือส่วนที่ custom เพิ่มขึ้นมา

## Default values for the image
name: vault-operator
replicaCount: 1
image:
repository: selfieblue/vault-operator
tag: v1.0.1
pullPolicy: Always
## Install Default RBAC roles and bindings
rbac:
create: true
## Service account names and whether to create them
serviceAccount:
create: true
name: vault-operator-sa
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
## additional command arguments go here; will be translated to `--key=value` form
## e.g., analytics: true
commandArgs: {}
## Configurable health checks against the /readyz endpoint that vault-operator exposes
readinessProbe:
enabled: false
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
livenessProbe:
enabled: false
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
nodeSelector: {
kubernetes.io/role: node
}
tolerations: []affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
kubernetes.io/role: node
topologyKey: kubernetes.io/hostname
###
# All of the config variables related to setting up the etcd-operator
# Disabled by default
# If you want more information about the variables exposed, please visit:
# https://github.com/kubernetes/charts/tree/master/stable/etcd-operator#configuration
###
etcd-operator:
enabled: true
etcdCluster:
name: "vault-cluster-01-etcd"
size: 3
pod:
antiAffinity: true
nodeSelector: {
kubernetes.io/role: node
}
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
deployments:
etcdOperator: true
backupOperator: true
restoreOperator: true
serviceAccount:
etcdOperatorServiceAccount:
create: true
backupOperatorServiceAccount:
create: true
restoreOperatorServiceAccount:
create: true
etcdOperator:
image:
repository: "selfieblue/etcd-operator"
tag: "v1.0.0"
nodeSelector: {
kubernetes.io/role: node
}
backupOperator:
image:
repository: "selfieblue/etcd-operator"
tag: "v1.0.0"
nodeSelector: {
kubernetes.io/role: node
}
restoreOperator:
image:
repository: "selfieblue/etcd-operator"
tag: "v1.0.0"
nodeSelector: {
kubernetes.io/role: node
}

เรามาเริ่ม deploy vault-operator กันเลย

cd /tmp/provision-vault-cluster
helm install stable/vault-operator \
--name vault-cluster-01 \ # Set Vault Cluster Name
--values ./custom-values.yaml \ # Override default values
--set etcd-operator.enabled=true \ # Force Create ETCD Operator
--tiller-namespace kube-system \ # Specify Helm's Namespace
--namespace secrets # Specify Target Namespace

หลังจาก deploy success คุณจะเห็น pod มาทั้งหมด 4 pods ที่ secrets namespace

kubectl -n secrets get pod -l release=vault-cluster-01
----output----
vault-cluster-01-etcd-operator-etcd-backup-operator-xxxxxxxxxxx
vault-cluster-01-etcd-operator-etcd-operator-xxxxxxxxxxx
vault-cluster-01-etcd-operator-etcd-restore-operator-xxxxxxxxxxx
vault-cluster-01-vault-operator-xxxxxxxxxxx

ซึ่งจะเห็นได้ ว่า ไม่ใช่เพียงแค่ vault-operator pod ด้ถูก deploy เท่านั้น แต่มี etcd-operators ได้ถูก deploy ด้วย

ขั้นตอนต่อไป เราจะทำ provision vault cluster กัน ซึ่งเราตั้งชื่อ vault cluster นี้ว่า vault-cluster-01 ซึ่งจะถูก provision ที่ secrets namespace แต่ก่อนเราจะรัน command เพื่อ provision vault cluster นั้น ผมอยาก recap ถึงผลลัพธ์ที่จะเกิดขึ้นกันก่อนตามลำดับกันก่อนนะครับ
1) etcd-operator : Create Self-Signed TLS Certificates and keys
2) vault-operator : Create Self-Signed TLS Certificates and keys
3) etcd-operator : Create ETCD Cluster & Services
4) vault-operator : Create Vault Cluster & Services
สำหรับท่านที่มี CA Server อยู่แล้ว และต้องการจะ generate key และ certificate เอง ก็สามารถทำได้ โดยการศึกษาได้ ที่นี่

เอาล่ะพูดมาซะเยอะ ถึงเวลาที่เราจะ provision vault cluster การสร้าง vault cluster จะสร้างผ่าน apiVersion: “vault.security.coreos.com/v1alpha1”

cat <<EOF > /tmp/vault.yaml
apiVersion: "vault.security.coreos.com/v1alpha1"
kind: "VaultService"
metadata:
name: "vault-cluster-01"
namespace: "secrets"
spec:
nodes: 3
BaseImage: "selfieblue/vault-engine"
version: "1.1.2"
ServiceAccountName: "vault-authen-tokenviewer"
enableWebUI: true
enableAutoUnsealing: true
autoUnsealProvider: "awskms"
autoUnsealProviderSecret: "vault-auto-unseal-awskms-secret"
PodAntiAffinityNodeRole: "node"
TLS:
static:
serverSecret: "vault-cluster-01-default-vault-server-tls"
clientSecret: "vault-cluster-01-default-vault-client-tls"
EOF
kubectl delete -f /tmp/vault.yaml
kubectl apply -f /tmp/vault.yaml

อะอะ ยังไม่จบนะจ้ะ เราต้องทำการ Initial Vault กันก่อน

==== Initial Vault ====You need to open two terminals
Terminal one : Run command to proxy vault port to localhost at port 8200
Terminal two : Run commands to initial vault and unseal
==== Terminal 1 ====NAMESPACE="secrets"
VAULTSERVICE="vault-cluster-01"
kubectl -n ${NAMESPACE} get vault ${VAULTSERVICE} -o jsonpath='{.status.vaultStatus.sealed[0]}' | xargs -0 -I {} kubectl -n ${NAMESPACE} port-forward {} 8200
==== Terminal 2 ====
mkdir -p $HOME/vault
vault operator init -tls-skip-verify > /$HOME/vault/vault_secret.txt
NAMESPACE="secrets"
kubectl delete pod -l app=vault,vault_cluster=vault-cluster-01 -n ${NAMESPACE}

Verify ว่า Vault Initialization สำเร็จหรือไม่
หาก Vault Initialization สำเร็จ เมื่อเรารัน command vault status แล้ว output ที่ได้จะต้อง
1) initialized status จะต้องเท่ากับ true
2) sealed status จะต้องเท่ากับ null
3) จะต้องมี vault 1 pod ที่อยู่ใน status active
4) จะต้องมี vault 2 pods ที่อยู่ใน status standby
ดังตัวอย่างที่ highlight ตัวหน้าด้านล่างนี้

==== Terminal 2 ====
NAMESPACE="secrets"
VAULTSERVICE="vault-cluster-01"
export VAULT_SKIP_VERIFY=1
kubectl -n ${NAMESPACE} get vault ${VAULTSERVICE} -o yaml
==== status must be shown like this ====
status:
clientPort: 8200
initialized: true
phase: Running
serviceName: vault-cluster-01
updatedNodes:
- vault-cluster-01-aaaaa-bbbbbb
- vault-cluster-01-ccccc-dddddd
- vault-cluster-01-eeeee-gggggg
vaultStatus:
active: vault-cluster-01-aaaaa-bbbbbb
sealed: null
standby:
- vault-cluster-01-ccccc-dddddd
- vault-cluster-01-eeeee-gggggg

Enjoy! นะครับ

เราจะมาพูดถึงวิธีการ Integrate vault กับ Kubernetes Auth Method และวิธีการ manage sensitive data <Available>

--

--