EKSの外でIAM Roles for Service Accountsっぽいことを実現するMutating Admission Webhookを作った
Sun, Sep 4, 2022
Kubernetes eks irsa-emu localstack vault AWS iamTable of Contents
バニラなKubernetesクラスタの上でMutating Admission Webhookとして動いて、IAM Roles for Service Accounts (IRSA)をエミュレートするアプリを作った話。
背景
EKSで動くサービスを開発している場合、開発環境を開発メンバ各自でデプロイしているとAWSサービスのコストがかさむし、AWSのサービスクォータにひっかかったりする問題がある。
手元のKubernetesクラスタにサービスをデプロイしていじれたら捗りそうだけど、そのためにはAWSサービスに依存している部分をなんとか何かで代用しないといけなくて、一方開発用のデプロイのためにソースやk8sマニフェストにあまり手を入れたくはない。
LocalStackでAWSサービスをモックすればいい感じにできそうだけど、IRSAはどうすればいいんだろ? LocalStackのAWS Service Feature Coverageをよく見ると、IAMのOIDCプロバイダをサポートしてないのでIRSAなんともならないんじゃないの?
と思ったのがこの記事に書いた取り組みのきっかけ。
あとでよく調べて考え直したら、Amazon EKS Pod Identity Webhookでもっとシュッと解決できそうだったので、それについてはまた別の記事で書きたい。 (書いたけど、あまりシュッとはしなかった。)
IAM Roles for Service Accounts (IRSA)とは
IAM Roles for Service Accounts (IRSA)は、IAMロールをEKSのServiceAccountに紐づける機能。
IRSAを使うには、IAMにOIDCプロバイダの設定をちょろっとして、特定の信頼ポリシーを付けたIAMロールを作っておいて、ServiceAccountのアノテーションにeks.amazonaws.com/role-arn: <IAMロールのARN>
を付けるだけ。
そうしておくと、そのServiceAccountを付けたPodがそのIAMロールの権限でAWSサービスにアクセスできる。
IRSAの仕組みは、EKSで動くとあるMutating Admission Webhookが、Service Account Token Volume Projectionを利用してPodにOIDCのIDトークンを挿入して、Pod上のAWS SDKがそのIDトークンでAssumeRoleWithWebIdentityしてIAMロールのアクセスクレデンシャルを取得する、という感じ。 詳しくは別の記事で書いた。
LocalStackとは
LocalStackはAWSサービスのモックを提供するアプリ。 IAM、S3、KMS、DynamoDB、Kinesisとかのモックが無料で使える。 有料のPro版ならEKS、ECR、ELB、EFSとかのモックもできてすごそうだけど、どこまでちゃんとエミュレートしてくれるか若干怪しいのと、どうせお金払うならAWSに払えばいい気はする。
意外(?)にもLocalStackはコンテナ一つで動いて、Helm chartが公式から提供されてるので、デプロイが簡単なのがうれしい。
やりたいこと
EKS上でHashiCorp VaultをPodで動かして利用するシステムがあるとする。 Vaultは秘匿情報を管理するアプリで、このシステムのVaultはAWS KMSと連携して秘匿情報の暗号化キーを暗号化している(awskms Seal)。 VaultからAWS KMSへのアクセスはIRSAを使っている。
このシステムの開発環境として、ローカルのバニラなKubernetesクラスタでVaultのPodを動かし、LocalStackでKMSをモックしたい。 主な課題は、EKSじゃないバニラなKubernetesクラスタでIRSAをどう実現するかということ。 あとVaultとLocalStackをどう連携させるかというのもちょっとある。
IRSAをエミュレートするirsa-emu
バニラなKubernetesクラスタでIRSAを実現するために、irsa-emuというのを作った。
irsa-emuは、Podに対するMutating Admission Webhookと、それによってPodに挿入され、AWSのアクセスクレデンシャルを管理するサイドカーで構成される。 webhookのほうはKubewebhookというGoのフレームワークを使った。(このフレームワークの詳細は別の記事で書くかも。) サイドカーはAWS SDK使おうかとも思ったけど、面倒^H^Hそれほど複雑なこともしないのでawsコマンドを叩くだけのbashスクリプトで済ました。
irsa-emuは以下のように動作する。
- PodをKubernetesに登録すると、
- irsa-emuのwebhookにそのPodが送られる。
- irsa-emuのwebhookは、そのPodのServiceAccountを見て、アノテーションに
eks.amazonaws.com/role-arn: <IAMロールのARN>
が付いてたらサイドカーをPodに挿入してKubernetesに返す。 - KubernetesがそのPodをデプロイすると、
- サイドカーがAssumeRoleを実行してアクセスクレデンシャルを取得する。
- Vaultはそのアクセスクレデンシャルを使ってKMSにアクセスする。
#5
で、サイドカーのコンテナからVaultのコンテナにアクセスクレデンシャルを渡すために、両者で共有するempyDirにサイドカー側からcredentialsファイルを書き込む。
Vaultコンテナにはirsa-emuのwebhookが環境変数AWS_SHARED_CREDENTIALS_FILEを挿入して、サイドカーが挿入したcredentialsファイルを読ませるようにする。
irsa-emuの動作確認
以下、バニラKubernetesクラスタに実際にirsa-emuをデプロイして、Vaultのawskms Sealを動かしてみる。
AWS側の設定
上でLocalStackに触れたんだけど、まずはAWSの本物のKMSでawskms Sealするところからやる。 そのためにAWS側にいくつかリソースを作っておく必要がある。
AWS側には、KMSで対称キーを一つ作っておいて、そのキーをVaultが使うためのIAMロールを作っておく。
すなわち、キーのIDがf4f7a2e2-c5c6-ea6d-dc42-526ef280e794
だとすると、以下のようなIAMポリシーをつけたIAMロールを作っておく。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/f4f7a2e2-c5c6-ea6d-dc42-526ef280e794"
}]
}
このIAMロールの名前はVault-AWSKMS
とする。
このIAMロールには、本物のIRSAを使う場合には、以下のような信頼ポリシーを付けて、特定のServiceAccountからAssumeRoleWithWebIdentityされることを許可する。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/11223344456677889900AABBCCDDEEFF"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/11223344456677889900AABBCCDDEEFF:sub": "system:serviceaccount:kube-system:vault"
}
}
}]
}
今回はirsa-emuを使うので、代わりに以下のようにAssumeRoleされることを許可する雑な信頼ポリシーを付ける。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
"Action": "sts:AssumeRole",
"Condition": {}
}]
}
あとは、適当なIAMユーザを作って、そのユーザに以下のようなIAMポリシーを紐づけて、Vault-AWSKMS
に対するAssumeRoleの実行を許可しておく。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::123456789012:role/Vault-AWSKMS"
}]
}
このIAMユーザの名前はIRSA-emu
とする。
irsa-emuのデプロイ
irsa-emuをデプロイしてみる環境として、ノートPCでVMWare Playerで作ったCentOS 7.9のVMに、手製のAnsible Playbookを使って1ノードのKubernetes 1.21.2のクラスタを構築した。
irsa-emuのwebhook(irsa-emu-webhook)とサイドカー(irsa-emu-creds-injector)のコンテナイメージはDocker Hubにおいてあって、 Helm chartも書いたので、irsa-emuのデプロイはそれをinstallするだけでいい。
まず、サイドカーに渡すAWSアクセスクレデンシャル(IAMユーザIRSA-emu
のやつ)をBase64エンコードしてどこかのYAMLファイルに以下のように書いておく。
/tmp/irsa-emu-values.yaml:
sidecar:
awsAccessKeyId: RG8gMjAgc2l0LXVwcyBvbmNlIHlvdSBzZWUgdGhpcyBtZXNzYWdl
awsSecretAccessKey: ICBfICAgICAgXyAgICAgIF8KPiguKV9fIDwoLilfXyA9KC4pCiAoX19fLyAgKF9fXy8gIChfX18vCgo=
awsDefaultRegion: dXMtZWFzdC0x
これを読ませてhelm installすればirsa-emuのwebhookのpodが動き、Mutating Admission Webhookとして登録される。
[root@vm-1 ~]# git clone https://github.com/kaitoy/irsa-emu.git
[root@vm-1 ~]# cd irsa-emu/chart
[root@vm-1 chart]# helm install irsa-emu -n kube-system -f /tmp/irsa-emu-values.yaml .
Vaultのデプロイ
VaultもHelm chartがある。
helm install
時に渡すパラメータは以下のようなもので、awskms Sealの設定としてKMSキーのIDを渡し、ServiceAccountにIRSAのアノテーションを付けてIAMロールVault-AWSKMS
を指定する。
/tmp/vault-values.yaml:
injector:
enabled: false
server:
authDelegator:
enabled: false
service:
type: NodePort
nodePort: 30000
dataStorage:
enabled: false
auditStorage:
enabled: false
standalone:
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
storage "file" {
path = "/home/vault"
}
seal "awskms" {
region = "us-east-1"
kms_key_id = "f4f7a2e2-c5c6-ea6d-dc42-526ef280e794"
}
serviceAccount:
create: true
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/Vault-AWSKMS
これを渡してhelm install
すると、Vaultをデプロイできる。
[root@vm-1 ~]# helm repo add hashicorp https://helm.releases.hashicorp.com
[root@vm-1 ~]# helm install vault -n kube-system -f /tmp/vault-values.yaml hashicorp/vault
あとはVaultの/sys/init
APIで初期化してやると、データの暗号化キーが生成され、そのキーがVaultのrootキーで暗号化され、そのrootキーがKMSキーで暗号化される。
rootキーはまたすぐに復号化されて、Vaultが使える状態(i.e. unsealed)になる。
[root@vm-1 ~]# curl -XPOST http://$(hostname -i):30000/v1/sys/init -d '{"secret_shares": 3, "secret_threshold": 3, "recovery_shares": 3, "recovery_threshold": 3}'
{"keys":[],"keys_base64":[],"recovery_keys":["1e21b0caae70195a93eecda2f2023c3ffda475fe3975a79c789530b09a392fec2b","a26d3b1aa7f78efd374f267a29fe137f2436c6a535b38621942397f35e5c4ae4c5","c782a47cb89783962cd85743c263aefb180d4fdbed68055c73e401257ba64745b4"],"recovery_keys_base64":["HiGwyq5wGVqT7s2i8gI8P/2kdf45daeceJUwsJo5L+wr","om07Gqf3jv03TyZ6Kf4TfyQ2xqU1s4YhlCOX815cSuTF","x4KkfLiXg5Ys2FdDwmOu+xgNT9vtaAVcc+QBJXumR0W0"],"root_token":"hvs.T9qb52cDHJFlWEdexj86Nem6"}
[root@vm-1 ~]# curl -s http://$(hostname -i):30000/v1/sys/health | jq .sealed
false
これでawskms Sealが動いたことが確認できた。
Vaultのpodの中をのぞくと、以下のようにサイドカーのirsa-emu-creds-injectorが追加されて、credentialsファイルが挿入されてるのが見れる。
[root@vm-1 ~]# kubectl get po -n kube-system vault-0 -o jsonpath='{.spec.containers[*].name}{"\n"}'
vault irsa-emu-creds-injector-798b0e7e69
[root@vm-1 ~]# kubectl exec -it -n kube-system vault-0 -c vault -- sh -c 'ls -l $AWS_SHARED_CREDENTIALS_FILE'
-rw-r--r-- 1 vault vault 879 Sep 4 07:08 /var/run/secrets/irsa-emu.kaitoy.xyz/shared_credentials/credentials
LocalStackのデプロイ
次に、LocalStackでIRSAしてawskms Sealを動かす。
LocalStackもHelmで簡単にデプロイできる。
[root@vm-1 ~]# helm repo add localstack-repo https://helm.localstack.cloud
[root@vm-1 ~]# helm install localstack localstack-repo/localstack
LocalStackのアカウントIDは000000000000
で、適当なクレデンシャルでアクセスするとrootユーザあつかいになる。
[root@vm-1 ~]# export LS_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services localstack)
[root@vm-1 ~]# export LS_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
[root@vm-1 ~]# echo ${LS_IP}:${LS_PORT}
192.168.11.201:31566
[root@vm-1 ~]# export AWS_ACCESS_KEY_ID=hoge
[root@vm-1 ~]# export AWS_SECRET_ACCESS_KEY=huga
[root@vm-1 ~]# aws --endpoint-url=http://${LS_IP}:${LS_PORT} sts get-caller-identity --region us-east-1
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
無料版LocalStackの認証はこんな感じで適当で、IAMポリシーによるアクセス制御も働かないので、Vaultのために事前にIAMリソースを作っておく必要はない。 KMSのキーだけ作っておく。
[root@vm-1 ~]# aws --endpoint-url=http://$LS_IP:$LS_PORT kms create-key --region us-east-1 --key-spec SYMMETRIC_DEFAULT --origin AWS_KMS
{
"KeyMetadata": {
"AWSAccountId": "000000000000",
"KeyId": "e054ced2-6879-4a6d-9fcf-4a02a1a62d1e",
"Arn": "arn:aws:kms:us-east-1:000000000000:key/e054ced2-6879-4a6d-9fcf-4a02a1a62d1e",
"CreationDate": "2022-09-04T17:33:55.416098+09:00",
"Enabled": true,
"Description": "",
"KeyState": "Enabled",
"Origin": "AWS_KMS",
"KeyManager": "CUSTOMER",
"CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
"KeySpec": "SYMMETRIC_DEFAULT",
"EncryptionAlgorithms": [
"SYMMETRIC_DEFAULT"
],
"SigningAlgorithms": [
"RSASSA_PKCS1_V1_5_SHA_256",
"RSASSA_PKCS1_V1_5_SHA_384",
"RSASSA_PKCS1_V1_5_SHA_512",
"RSASSA_PSS_SHA_256",
"RSASSA_PSS_SHA_384",
"RSASSA_PSS_SHA_512"
]
}
}
irsa-emuとVaultをLocalStackに向ける
いったんVaultはuninstallしておく。
[root@vm-1 ~]# helm uninstall -n kube-system vault
irsa-emuは、stsEndpointURL
というパラメータにLocalStackのURLを渡してデプロイしなおせば、LocalStackに対して動くようになる。
/tmp/irsa-emu-values.local.yaml:
sidecar:
awsAccessKeyId: RG8gMjAgc2l0LXVwcyBvbmNlIHlvdSBzZWUgdGhpcyBtZXNzYWdl
awsSecretAccessKey: ICBfICAgICAgXyAgICAgIF8KPiguKV9fIDwoLilfXyA9KC4pCiAoX19fLyAgKF9fXy8gIChfX18vCgo=
awsDefaultRegion: dXMtZWFzdC0x
stsEndpointURL: http://192.168.11.201:31566
helm upgrade
でパラメータを反映する。
[root@vm-1 chart]# helm upgrade irsa-emu -n kube-system -f /tmp/irsa-emu-values.local.yaml .
Vaultはendpoint
というパラメータにLocalStackのURLを渡せばいい。
/tmp/vault-values.local.yaml:
injector:
enabled: false
server:
authDelegator:
enabled: false
service:
type: NodePort
nodePort: 30000
dataStorage:
enabled: false
auditStorage:
enabled: false
standalone:
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
storage "file" {
path = "/home/vault"
}
seal "awskms" {
region = "us-east-1"
kms_key_id = "e054ced2-6879-4a6d-9fcf-4a02a1a62d1e"
endpoint = "http://192.168.11.201:31566"
}
serviceAccount:
create: true
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::000000000000:role/Hoge
このYamlでhelm install
して、そのあとVaultの初期化もする。
[root@vm-1 ~]# helm install vault -n kube-system -f /tmp/vault-values.local.yaml hashicorp/vault
[root@vm-1 ~]# curl -XPOST http://$(hostname -i):30000/v1/sys/init -d '{"secret_shares": 3, "secret_threshold": 3, "recovery_shares": 3, "recovery_threshold": 3}'
{"keys":[],"keys_base64":[],"recovery_keys":["0db24458560242cc9b66c8bd4e83fbe3568cece1095115e9388489ec9fd8c8b472","aff3e5050bd0cd5f751de1c5f6cfa8c000e5619f825dcfe8644be0efe9e5c58421","10fdcc8c1bc3410e3325a80380d1d781a0a1b74b29b9a216eb45fd6633d8fa9be9"],"recovery_keys_base64":["DbJEWFYCQsybZsi9ToP741aM7OEJURXpOISJ7J/YyLRy","r/PlBQvQzV91HeHF9s+owADlYZ+CXc/oZEvg7+nlxYQh","EP3MjBvDQQ4zJagDgNHXgaCht0spuaIW60X9ZjPY+pvp"],"root_token":"hvs.MDMdapW5VEFlLk2IBh1kG8K2"}
[root@vm-1 ~]# curl -s http://$(hostname -i):30000/v1/sys/health | jq .sealed
false
LocalStackでもちゃんと動いた。