EKSとSecret ManagerでRDSの自動パスワードローテーションを試してみた

# はじめに
セキュリティ強化のため、定期的にデータベースのパスワードをローテーションする必要があります。長期間同じパスワードを使用すると、漏洩や攻撃によるリスクが高まるため、定期的な更新は重要です。しかし、EKS上のPodが参照するシークレットも更新する必要があるため、運用負荷が高くなりがちです。

解決策の1つとして､AWS Secrets ManagerとExternal Secrets Operatorを活用した自動パスワードパスワードローテーションを試してみました｡
 - AWS Secrets Manager
     - データベースのパスワードを安全に保管し、定期的なローテーションを自動化することができるだけでなく、RDSのパスワードの更新と保管も実施できる
 - External Secrets Operator(ESO)
     - AWS Secrets Managerに保存されたパスワードをEKS上のKubernetesシークレットに同期する

今回試した内容は公式ドキュメントの[手順](https://aws.amazon.com/jp/blogs/news/leverage-aws-secrets-stores-from-eks-fargate-with-external-secrets-operator/)を元にに､Secrets ManagerによるRDSパスワードローテーションを追加した内容となっています｡

# 構成
![eso.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936911/311f89ae-4844-b7d7-7ed4-cb827a2609c2.png)

AWS Secrets ManagerとExternal Secrets Operatorを組み合わせて、RDSのパスワードローテーションを自動化するようにしています｡Secrets Managerが定期的にパスワードをローテーションし、その変更内容がExternal Secrets Operatorを通じてEKSのKubernetesシークレットに反映されます。これにより、Podが新しいパスワードでRDSに接続できるようになり、手動作業を削減できます。
今回はPostgreSQLへ接続するシンプルなアプリを利用しました｡ヘルスチェックでDBに接続できるか確認するようになっており､DB接続に一定回数失敗するとpodが再起動し､新しいシークレットでDBに接続するようになります｡イメージは[dockerhub](https://hub.docker.com/repository/docker/jeff0525/postgre-app/general)に格納しています｡

# 前提
- RDS PostgreSQL環境があること
- RDSへ接続できるEKS環境があること
- aws,kubectl,eksctl,helmコマンドが利用できること

# 流れ
1. Secrets Manager接続用のポリシーとサービスアカウント作成
1. ESOをデプロイ
1. SecretStoreを作成
1. ExternalSecretを作成
1. アプリのデプロイ
1. パスワードローテーションをSecert Manager から実施
1. アプリが再起動することを確認


# 1.Secret Manager用のポリシーを作成

## 1.1 IAM OIDCプロバイダー作成
(作成済みの場合は実行不要)
```bash
eksctl utils associate-iam-oidc-provider --cluster=[クラスター名] --approve
```

## 1.2 secretmanagerへのアクセス許可を付与するiamポリシーを作成する

コンソールからSecret Managerのシークレットarnとシークレット名を確認します｡

![Secret Managerの確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936911/58ef737b-59c8-2114-baa2-717c7ee78284.png)



secretmanager-access-policy.jsonのSecret Managerのシークレットarnをを書き換えます｡

``` json:secretmanager-access-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret"
            ],
            "Resource": "arn:aws:secretsmanager:ap-northeast-1:[AWSアカウントID]:secret:[認証情報が保管されているSecret-managerのシークレットARN]"
        }
    ]
}
```
以下のコマンドを実行し､Secret Managerへのアクセスを許可するポリシーを作成する(あるいはコンソールから作成することもできます｡)

```bash
aws iam create-policy --policy-name SecretsManagerRDSPostgreReadWrite --policy-document file://secretmanager-access-policy.json
```

## 1.3 Secret ManagerへアクセスするIAMロールとサービスアカウントを作成する
アカウントIDとクラスター名を書き換えてから実行します｡
``` bash
eksctl create iamserviceaccount \
    --name secretmanager-service-account \
    --namespace default \
    --cluster クラスター名 \
    --role-name "secretmanager-service-account" \
    --attach-policy-arn arn:aws:iam::[AWS アカウントID]:policy/SecretsManagerRDSPostgreReadWrite \
    --approve \
    --override-existing-serviceaccounts
```
正しく作成されているか確認します｡

``` bash 
kubectl get sa
kubectl describe sa secretmanager-service-account
```

# 2.ESO をデプロイ
今回はHelmを使用してExternal Secrets OperatorをEKS環境にデプロイします。
``` bash
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets \
    external-secrets/external-secrets \
    -n external-secrets \
    --create-namespace \
    --set installCRDs=true \
    --set webhook.port=9443 
kubectl get pods -n external-secrets
```



# 3.SecretStoreを作成
SecretStoreは、ESOがシークレットデータを取得するためのプロバイダ設定を定義するリソースです。今回は､AWS Secrets Managerをプロバイダとして使用し、新しく作成したサービスアカウントを利用して認証を行います。

``` yaml:secretStore.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: secretmanager-secret-store
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-1
      auth:
        jwt:
          serviceAccountRef:
            name: secretmanager-service-account
```
作成

``` bash
kubectl apply -f secretstore.yaml
```

動作確認 

``` bash
kubectl get secretstore
kubectl describe secretstore secretmanager-secret-store 
```
※権限が不足している場合やシークレットARNの指定が正しくない場合にエラーが発生するので､エラー内容を確認して修正してください｡

# 4.ExternalSecretを作成
ExternalSecretは、AWS Secrets Managerから取得したシークレットをEKSのKubernetesシークレットとして同期するための設定を行うリソースです。refreshIntervalはシークレットの同期間隔を指定し、dataでは取得するプロパティ（例: username や password）を定義します。
Secret-managerのシークレットARNを書き換えてから実行してください｡

``` yaml:externalSecret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: secretmanager-external-secret
spec:
  refreshInterval: 30s                 # 更新間隔
  secretStoreRef:
    name: secretmanager-secret-store
    kind: SecretStore
  target:
    name: secret-manager-value
    creationPolicy: Owner
  data:
  - secretKey: postgre-mydb-username   # シークレットに登録する名称
    remoteRef:
      key: rds!db-xxx                  # [認証情報が保管されているSecret-managerのシークレットARN]
      property: username               # [Secret-managerに保管されているプロパティ名]
  - secretKey: postgre-mydb-password   
    remoteRef:
      key: rds!db-xxx                  
      property: password               
```
作成
```
kubectl apply -f externalsecret.yaml
```
動作確認
``` bash
  kubectl get externalsecrets
  kubectl describe externalsecrets secretmanager-external-secret
```

secret manager から取得したユーザ名, パスワードがシークレットに登録されていることを確認します｡
``` bash
kubectl get secrets secret-manager-value
```

デコードしてSecret-managerとシークレットの値が一致することを確認します｡
``` bash
kubectl get secret secret-manager-value -o jsonpath='{.data.postgre-mydb-username}' | base64 -d
kubectl get secret secret-manager-value -o jsonpath='{.data.postgre-mydb-password}' | base64 -d
```

# 5.アプリのデプロイ
PostgreSQLに接続するサンプルアプリをデプロイします。デプロイしたアプリは、Kubernetesシークレットに保存されたRDSの接続情報を環境変数として読み込みます。deploymentのヘルスチェックの設定で、定期的にデータベースに接続できるかを確認します。RDSのパスワードが更新されると、ヘルスチェックが失敗してPodが再起動し、更新されたシークレットを更新される仕組みです｡

``` yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-app
  labels:
    app: flask-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: flask-app
  template:
    metadata:
      labels:
        app: flask-app
    spec:
      containers:
      - name: flask-app
        image: jeff0525/postgre-app:v1.0                 # postgre-appイメージのパス
        ports:
        - containerPort: 5000
        env:
        - name: DB_HOST
          value: xxxx.ap-northeast-1.rds.amazonaws.com  # DBエンドポイント
        - name: DB_NAME
          value: xxx                                    # データベース名
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: secret-manager-value
              key: postgre-mydb-username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: secret-manager-value
              key: postgre-mydb-password
        livenessProbe:
          httpGet:
            path: /health
            port: 5000
          initialDelaySeconds: 10
          periodSeconds: 10
```
デプロイ

``` bash
kubectl apply -f deployment.yaml
```
podの起動確認
``` bash
kubectl get pods
```
pod起動後にシークレットを確認
``` bash
kubectl exec [flask-app-ポッド名] -- printenv |grep DB
```
healthチェックが成功していることを確認
```
kubectl logs [flask-app-ポッド名] -f
```

![ヘルスチェック1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936911/0a7adf41-f87e-fcff-d8af-3c85f1694455.png)



# 6. DBのパスワードローテーションを実施
Secrets Managerでパスワードをローテーションすると、External Secrets Operatorがその変更を検知し、Kubernetesシークレットを自動的に更新します。その後、Podが再起動することで、新しいパスワードが環境変数に反映されます。この一連の動作により、手動操作を必要とせずにパスワードローテーションが完結します。

Secret Managerからローテーションタブを選択し､"すぐにシークレットローテーションさせる"をクリックで実行できます｡

![シークレットローテーション実施.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936911/fcd25a2f-02d8-e96b-a478-3f40bf4e0361.png)


Secret Manager上のシークレット値が更新されていることを確認します｡
![シークレットローテーション後のパスワード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936911/a3845ad5-d30a-84ad-0744-00ae99aed3ab.png)


# 7. アプリが再起動することを確認
パスワードローテーションを実行すると､podのヘルスチェックでエラーが発生するようになります｡しばらくすると､自動でpodが再起動し､db接続できるようになります｡

![ヘルスチェック-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936911/7a6e66dc-987f-2fd1-ccb7-56b0f4ebcf38.png)


シークレットが更新されていることを確認します｡
``` bash
kubectl get secret secret-manager-value -o jsonpath='{.data.postgre-mydb-username}' | base64 -d
kubectl get secret secret-manager-value -o jsonpath='{.data.postgre-mydb-password}' | base64 -d
```



# おわりに
AWS Secrets ManagerとExternal Secrets Operatorを組み合わせることで、EKS環境におけるデータベースのパスワードローテーションを自動化することができました。これにより、運用負荷が軽減されるだけでなく、手動操作によるミスのリスクも低減することができます。また、パスワードローテーションをより頻繁に実施できるようになり、セキュリティが向上するメリットもあります。

# Appendix
## 作成したリソースの削除
``` bash
kubectl delete -f deployment.yaml
kubectl delete -f externalsecret.yaml
kubectl delete -f secretstore.yaml
eksctl delete iamserviceaccount \
    --name secretmanager-service-account \
    --namespace default \
    --cluster [クラスター名]
helm uninstall external-secrets -n external-secrets
```


# 参考資料

- [external-secret github](https://external-secrets.io/v0.7.1/introduction/getting-started/)
- [External Secrets Operatorとは](https://zenn.dev/nameless_gyoza/articles/external-secrets-operator)
- [External Secrets Operatorを試す](https://techstep.hatenablog.com/entry/2023/02/02/182127)
- [Kubernetes External SecretsからExternal Secrets Operatorに移行した話 〜他ツールとの比較・移行戦略・工夫したポイント〜](https://techblog.zozo.com/entry/kubernetes-external-secrets-to-external-secrets-operator)