Access KeyなしでMinIOを使う in Kubernetes with MinIO Operator
[2025-05-04 追記]
minio-operator v7.1.0より、ServiceAccount Tokenのaudienceに sts.min.io を指定しなければならなくなったので記載を修正しました。
github.com
はじめに
以下の記事を読んで、MinIOのSTSを使うとAccess Keyを毎回作って保存してとかしなくても済むことを知りました。
ちょうど家にMinIO Operatorを使って構築したクラスタが居たので、上記の記事では検証されていない MinIO Operator を使ったSTSをやってみました。
STSとは
AWSに存在するSecurity Token Serviceの略です。 OpenID ConnectやSAMLなどの別のIdentity Providerへのフェデレーションを行い、 それらの認証トークンからAWSへアクセスするための短命なトークンを発行する方法が主流です。必要なときに都度リクエストして短命なトークンを生成するので、Access Keyなどを作って保存しておく必要がなくなります。
MinIO Operatorでは、KubernetesのService Account Tokenを使ってAssumeRoleWithWebIdentityを行います。Service AccountのトークンはKubernetesによって署名されており、operatorはその署名を検証してService Account Tokenの真正性を確認します。
事前にMinIO Operatorのカスタムリソースである PolicyBinding を作っておくことで、MinIOクラスタ内のアクセスポリシーとServiceAccountの紐付けを行います。operatorはService Account Tokenから得られたアカウント名に対応するPolicyBindingのアクセスポリシーを知り、MinioのAPIを使って一時的なトークンを発行します。

そのため、Kubernetesクラスタ内でAccess KeyやAccess SecretをSecretに保存する必要はなくなります。MinioのAPIへのアクセスをするアプリケーションをデプロイする前に、対応するPolicyとPolicyBinding・ServiceAccountを用意してアプリケーションの環境変数として与えるだけでアクセスできるようになります。 正しいService Account以外からのリクエストに対してはトークンが発行されず、MinIOへのアクセスはできません。
準備
今回はkindで作ったクラスタで試します。STSのためにはTLSが必須となり、署名検証を無効にせずに使うためにはcert-managerおよびtrust-managerを用いるのがよいです。いずれもhelmで簡単にインストールできます。
k8sクラスタをセットアップ
kindで建てたクラスタにhelmを使ってcert-managerとtrust-managerをインストールします。trust-managerについて詳しくは過去記事をご覧下さい。 簡単に言うとクラスタ内にCA証明書をばらまく機能を持ったカスタムコントローラです。
kind create cluster --name minio-sts helm repo add jetstack https://charts.jetstack.io --force-update helm upgrade cert-manager jetstack/cert-manager \ --install \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true \ --wait # テストのために secretTargets.authorizedSecretsAll=true を付けているが、 # 本来は secretTargets.authorizedSecrets で許可するsecretを指定した方が良い helm upgrade trust-manager jetstack/trust-manager \ --install \ --namespace cert-manager \ --wait --set secretTargets.enabled=true \ --set secretTargets.authorizedSecretsAll=true
CAとClusterIssuerをセットアップ
このk8sクラスタ内で使えるCAをセットアップし、各種コンポーネントの証明書はこのCAを使って発行します。tls/issuer.yaml に記述します。
--- apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsign spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: internal-ca-2025 spec: isCA: true commonName: internal-ca-2025 secretName: internal-ca-2025-secret duration: 87600h renewBefore: 336h # 14d privateKey: algorithm: ECDSA size: 256 issuerRef: name: selfsign kind: ClusterIssuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: internal spec: ca: secretName: internal-ca-2025-secret
kubectl apply -f tls/issuer.yaml -n cert-manager
Operator用の証明書とCA証明書をセットアップ
先にMinIO OperatorのSTSエンドポイント用証明書を発行しておきます。このとき、secretNameは sts-tls とする必要があります*1。minio-operator/cert.yaml として記述します。
--- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: sts-certmanager-cert namespace: minio-operator spec: dnsNames: - sts - sts.minio-operator.svc - sts.minio-operator.svc.cluster.local secretName: sts-tls # MUST issuerRef: kind: ClusterIssuer name: internal
kubectl create ns minio-operator
kubectl apply -f minio-operator/cert.yaml
今回MinIO本体でもcert-managerが発行した証明書を使うのですが、operatorからMinIOへのリクエストでも署名検証できるようにするためにCA証明書をマウントします。Operatorには特定の名前パターンでsecretを作ると自動で検知してくれる仕組み*2があるのでこれを使います。
CA証明書はtrust-managerに配布させます。tls/bundle-for-minio.yaml として記述します。
--- apiVersion: trust.cert-manager.io/v1alpha1 kind: Bundle metadata: name: operator-ca-tls-internal # operator-ca-tls-*という名前にする namespace: cert-manager spec: sources: # 作成したクラスタ内CAの証明書を同梱する - secret: name: "internal-ca-2025-secret" key: "ca.crt" target: secret: key: "ca.crt" namespaceSelector: # minio-operatorのnamespaceだけにインストールする matchLabels: kubernetes.io/metadata.name: minio-operator
kubectl apply -f tls/bundle-for-minio.yaml
MinIO Operatorをセットアップ
MinIO Operatorはアップストリームの手順に従ってインストールします。具体的にはリポジトリのアーカイブをダウンロードし、kustomizeでマニフェストをビルド・一部パッチを当ててクラスタにapplyします。
MINIO_OPERATOR_VERSION="7.1.1" mkdir -p minio-operator/upstream # minio-operatorのupstreamのマニフェストをダウンロードする wget -O minio-operator/upstream/v${MINIO_OPERATOR_VERSION}.tar.gz \ https://github.com/minio/operator/archive/refs/tags/v${MINIO_OPERATOR_VERSION}.tar.gz cd "minio-operator/upstream" tar xf "v${MINIO_OPERATOR_VERSION}.tar.gz" # リポジトリ丸ごとは要らないので、一旦素のマニフェストをビルドして書き出しておく kustomize build operator-${MINIO_OPERATOR_VERSION}/ > "operator.yaml" # 不要なファイルを削除する rm -r operator-${MINIO_OPERATOR_VERSION} v${MINIO_OPERATOR_VERSION}.tar.gz cd ../../
通常MinIO Operatorは自己署名証明書を自動的に作成しますが、これはあらゆる署名検証を無効にする必要があり面倒です。そのため、自動作成する機能を切ってcert-managerで発行した証明書を使わせます。前のセクションで証明書は既に用意したので、ここでは自動作成する機能をオフにします。minio-operator/kustomization.yaml でパッチを当てます。
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: minio-operator resources: - upstream/operator.yaml - cert.yaml patches: - patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: minio-operator namespace: minio-operator spec: template: spec: containers: - name: minio-operator env: - name: OPERATOR_STS_AUTO_TLS_ENABLED value: "off" - name: OPERATOR_STS_ENABLED value: "on"
kustomize build minio-operator/ | kubectl apply -f -
MinIO用の証明書をセットアップ
MinIOのWebコンソールおよびS3互換APIのエンドポイントで利用される証明書を発行します。
--- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: minio-tenant-cert namespace: objectstorage spec: dnsNames: - "minio.objectstorage" - "minio.objectstorage.svc" - "minio.objectstorage.svc.cluster.local" - "*.minio-hl.objectstorage.svc.cluster.local" - "*.objectstorage.svc.cluster.local" - "*.minio.objectstorage.svc.cluster.local" secretName: minio-tenant-tls issuerRef: kind: ClusterIssuer name: internal
kubectl create ns objectstorage
kubectl apply -f objectstorage/cert.yaml
MinIOをセットアップ
MinIO Operatorに対して、MinIO本体はTenantと呼ばれます。カスタムリソースであるTenantを作ることでOpertorによってMinIOがセットアップされます。 今回はテスト用にErasure Codingの設定を限界まで緩めているので冗長性がありません。本番環境でそのまま利用しないでください。
--- apiVersion: minio.min.io/v2 kind: Tenant metadata: name: minio namespace: objectstorage spec: image: 'minio/minio:RELEASE.2025-03-12T18-04-18Z' # Disable default tls certificates. requestAutoCert: false # Use certificates generated by cert-manager. externalCertSecret: # 先に作っておいた証明書 - name: minio-tenant-tls type: cert-manager.io/v1 configuration: # 後で作る name: storage-configuration pools: - servers: 1 name: pool-0 volumesPerServer: 1 volumeClaimTemplate: apiVersion: v1 kind: persistentvolumeclaims spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi
いくつかの追加設定をsecretを使って設定します。 MINIO_STORAGE_CLASS_STANDARD でErasure Codingの設定をしています。本番環境では正しい設定をしてください*3。パスワードも同様にランダムかつ十分長い文字列を設定するようにし、リポジトリにはpushしないようにしてください。
--- apiVersion: v1 kind: Secret metadata: name: storage-configuration namespace: objectstorage stringData: config.env: | export MINIO_ROOT_USER="minio" export MINIO_ROOT_PASSWORD="password" export MINIO_STORAGE_CLASS_STANDARD="EC:0" export MINIO_BROWSER="on"
これらを適用します。
cat << 'EOF' > objectstorage/base/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: objectstorage resources: - storage-configuration.yaml - tenant.yaml - tenant-cert.yaml EOF kustomize build objectstorage/ | kubectl apply -f -
最終的に以下の様にPodが立っていればOKです。
❯ kubectl get po -n objectstorage NAME READY STATUS RESTARTS AGE minio-pool-0-0 2/2 Running 0 6h24m
BucketとPolicyを作る
ポートフォワードしてコンソールにアクセスします。証明書のエラーが出ますが、これはクラスタ内CAの証明書がブラウザ側にはインストールされていないためです。本番環境ではコンソールを公開するためのIngressリソースなどを用意し、クラスタ外でもvalidな証明書を使いましょう。今回は単に警告を無視してアクセスします。
kubectl port-forward svc/minio-console -n objectstorage 9443:9443
https://localhost:9443 にアクセスすると設定したユーザ名・パスワードでログインできます。

Create Bucket してテスト用のbucketを作ります。

最後にこの test bucketのためのPolicyを作成します。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:DeleteObject", "s3:GetObject", "s3:ListBucket", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::test/*" ] } ] }

MinIOのクライアント用CA証明書を配布する
MinIOの証明書の署名検証のため、CA証明書をクラスタ内の全namespaceに配布します。各Podはこれをマウントすることで正しく署名検証できます。
--- apiVersion: trust.cert-manager.io/v1alpha1 kind: Bundle metadata: name: internal-ca-bundle spec: sources: - useDefaultCAs: true - secret: name: "internal-ca-2025-secret" key: "tls.crt" target: configMap: key: "trust-bundle.pem"
kubectl apply -f tls/bundle.yaml
全namespaceで internal-ca-bundle というConfigMapが作られているはずです。
❯ kubectl get cm internal-ca-bundle NAME DATA AGE internal-ca-bundle 1 6h4m
STSを使ってMinioにアクセスする
ようやく長いセットアップが終わったので実際にSTSを使ってMinIOのAPIを呼び出します。
PolicyBindingを作る
MinIO Operatorはこのカスタムリソースを元にService Accountとそれに許可するPolicyを識別します。このPolicy BindingはMinIO Tenantと同じnamespaceに配置する必要があります。今回はdefault namespaceの test というService Accountに test-rw Policyを許可します。
--- apiVersion: sts.min.io/v1alpha1 kind: PolicyBinding metadata: name: default-test namespace: objectstorage spec: application: namespace: default serviceaccount: test policies: - test-rw
kubectl apply -f objectstorage/policybindings.yaml
awscliで操作する
基本的には環境変数を使ってアクセス先や認証方法を設定します。awscliやAWS SDKはいずれも同じ環境変数で設定を変更できるようになっています。
awscliの入ったPodを立ち上げます。
--- apiVersion: v1 kind: ServiceAccount metadata: name: test --- apiVersion: v1 kind: Pod metadata: name: bastion spec: containers: - name: bastion # 古いバージョンでは環境変数からの設定変更をサポートしていないので注意 image: amazon/aws-cli:latest command: - sleep - infinity volumeMounts: - name: internal-ca mountPath: /tls/internal readOnly: true # audienceを指定したトークンをマウント - name: minio-sts-token mountPath: /var/run/secrets/sts.min.io/serviceaccount readOnly: true volumes: - name: internal-ca configMap: name: internal-ca-bundle - name: minio-sts-token projected: sources: - serviceAccountToken: # audienceを指定しなければならない audience: "sts.min.io" expirationSeconds: 86400 path: token serviceAccountName: test
kubectl apply -f default/bastion.yaml
kubectl exec を使って上記コンテナ内のbashを立ち上げます。
kubectl exec -it bastion -- bash
コンテナの中で以下の環境変数を設定します。
# マウントしたCA証明書のパス。STSとMinioのエンドポイントの署名検証に必要 export AWS_CA_BUNDLE=/tls/internal/trust-bundle.pem # ServiceAccountのトークンを認証キーとして使う export AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/sts.min.io/serviceaccount/token # consoleではなくMinioのAPIエンドポイント export AWS_ENDPOINT_URL_S3=https://minio.objectstorage.svc:443 # STSのエンドポイントはサービス名の末尾に `sts/テナントのnamesapce` を入れる export AWS_ENDPOINT_URL_STS=https://sts.minio-operator.svc:4223/sts/objectstorage # Minioでは使われないのだが、awscliはそれっぽい値を入れないとリクエストを蹴るので適当に入れる export AWS_ROLE_ARN=arn:aws:iam::dummy:role/test
これだけでawscliを使ってMinIOのS3互換APIを利用できます。
bash-4.2# aws s3 ls s3://test/ bash-4.2# echo hello > hello.txt bash-4.2# aws s3 cp hello.txt s3://test/ upload: ./hello.txt to s3://test/hello.txt bash-4.2# aws s3 ls s3://test/ 2025-03-21 06:14:38 6 hello.txt bash-4.2#
AWS SDKから操作する
同じ環境変数を用いると、以下の様なコードでMinIOへの操作を記述できます。今回はGoのAWS SDK v2を使っています。
package main import ( "context" "flag" "fmt" "log/slog" "os" "os/signal" "syscall" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" ) var ( bucketName string pathToList string ) func listBucketItems() error { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer cancel() cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return fmt.Errorf("failed to load configuration, %w", err) } bucket := s3.NewFromConfig(cfg, func(o *s3.Options) { o.UsePathStyle = true }) outputs, err := bucket.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ Bucket: &bucketName, Prefix: &pathToList, }) if err != nil { return fmt.Errorf("failed to list bucket items, %w", err) } for _, item := range outputs.Contents { slog.Info("get", "path", item.Key, "size", item.Size, "bucket", bucketName) } return nil } func main() { flag.StringVar(&bucketName, "bucket", "", "bucket name") flag.StringVar(&pathToList, "path", "", "path to list") flag.Parse() if err := listBucketItems(); err != nil { slog.Error("failed to list bucket items", "error", err) os.Exit(1) } }
同じようにAWS SDKを使っているアプリケーションでは同様の環境変数の設定でアクセス出来るようになるため非常に便利です。
例えば筆者のクラスタにはmocoというMySQLのオペレータがインストールされており、これはAWS SDKを使ってMySQLクラスタのバックアップをS3にアップロードする仕組みを持っています。MySQLBackupというカスタムリソースでその設定を書くのですが、以下の様に書いて適切なBucket・Policy・PolicyBindingを用意するだけでバックアップのデータをMinIOにアップロードできます。
apiVersion: moco.cybozu.com/v1beta2 kind: BackupPolicy metadata: name: daily spec: schedule: "5 0 * * *" concurrencyPolicy: Forbid jobConfig: serviceAccountName: moco-mysql env: - name: AWS_CA_BUNDLE value: /tls/internal/trust-bundle.pem - name: AWS_WEB_IDENTITY_TOKEN_FILE value: /var/run/secrets/sts.min.io/serviceaccount/token - name: AWS_ENDPOINT_URL_STS value: https://sts.minio-operator.svc:4223/sts/objectstorage # AWS_ROLE_ARN is required by the AWS SDK, but it is not mandatory in Minio. - name: AWS_ROLE_ARN value: arn:aws:iam::dummy:role/test bucketConfig: bucketName: moco-mysql-backup region: us-east-1 endpointURL: https://minio.objectstorage.svc:443 usePathStyle: true workVolume: ephemeral: volumeClaimTemplate: spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 10Gi volumeMounts: - name: internal-ca mountPath: /tls/internal readOnly: true - name: minio-sts-token mountPath: /var/run/secrets/sts.min.io/serviceaccount readOnly: true volumes: - name: internal-ca configMap: name: internal-ca-bundle - name: minio-sts-token projected: sources: - serviceAccountToken: audience: "sts.min.io" expirationSeconds: 86400 path: token
気になったところ
STSのエンドポイントが /sts/{{Tenantのnamespace}} となるのですが、 Tenant リソース自体は一つのnamespaceに複数作成できそうな気がします。
すると、あるPolicyBindingがどのTenantのものなのか判別できない気がします。実用的にこれで困ることがあるかは不明です。
今回は自分しか管理する人が居ないクラスタなのでよいのですが、複数の開発者が同じクラスタを共有するマルチテナント構成の場合、PolicyBindingリソースの操作権限をどう与えるか悩ましいなと思いました。 いくらでも作成・更新できてしまうと他チームのbucketを勝手に操作する権限を付与できてしまいますし、逆に全く作れないようにしてMinIO Tenantの管理チームだけに絞ってしまうと都度依頼対応が必要になったりして手間が増えそうです。
また、Policyを宣言的に管理する方法がないのもつらいところです。どのように権限分担をするのかが悩ましい仕組みだなと思いました。
まとめ
Kubernetesのin clusterなMinIOへのアクセスでは、MinIO OperatorのSTSを利用することで面倒なAccess Keyの管理なしにMinIOへのアクセス権を付与出来ます。
MinIO OperatorのSTSはリクエストに付与されたKubernetesのService Account Tokenを元に、PolicyBindingカスタムリソースを用いてMinIO上のPolicyを識別し、MinIOから短命なトークンを発行します。
awscliやAWS SDKは環境変数経由で認証方法やアクセス先をカスタマイズでき、他者のアプリケーションであってもAWS SDKを使っていればSTSを使ってMinIOにアクセス出来るます(たぶん)。
*1:https://min.io/docs/minio/kubernetes/upstream/operations/cert-manager/cert-manager-operator.html#id4
*2:https://min.io/docs/minio/kubernetes/gke/operations/cert-manager/cert-manager-tenants.html#trust-the-tenant-s-ca-in-minio-operator
*3:https://min.io/docs/minio/linux/operations/concepts/erasure-coding.html