kube2iamをIAM Role for Service Accountに載せ替えた

この記事は LAPRAS Advent Calendar 2019 の4日目です.

AWS上にkubernetesクラスタを構築していると,Pod内のコンテナから,AWSAPIを叩きたいことがある. そのためにはもちろんAWSの認証を通す必要があり,AWSAccess Tokenとか,IAM Roleを使って認証を通すことになる.

一般的に,productionで運用するEC2の中に,IAM UserのAccess Tokenを置くことは推奨されず,大抵の場合はIAM Roleで解決すると思う.もちろん,kubernetesクラスタであっても,ノードはただのEC2なのでInstance Profileを使えばIAM Roleでの認証は可能になる. しかし,kubernetesクラスタだ.もちろんノードはいっぱいあり,その上で様々なPodが動いている.ということは,ノードのインスタンスからIAM Roleでの認証をする場合,そのRoleは,クラスタ内で動くすべてのPodが要求する権限を保有する必要がある.これはかなり強大な権限を持つRoleになってしまう. おまけに,この状態だと,実はセキュリティ的にAWSへのアクセスを許可しないPodであっても,容易にAWSへの認証を通ってしまうことになる.

こういう状態を避けるために,kube2iamkiamといったOSSが開発されてきた.

で,このkube2iamに結構なバグがあったため,kopsで構築していたkubernetesクラスタに関しては,kiamに載せ替えたことがあった.

h3poteto.hatenablog.com

しかし,kiamはmasterノードにPodを配置する必要があり,masterノードに手を入れられないEKSでは,kiamを使うことができなかった.

これを完全に解決する方法として,今年9月にIAM Role for Service Accountが発表された.

aws.amazon.com

というわけで,EKS運用しているクラスタのIAM認証を,このIRSAに載せ替えた.

IRSAの仕組みについては,上記記事を参照してほしい.ここでは詳しい動作原理については説明しない.

載せ替え手順

各EKSクラスタにOIDC Providerを作成

EKSクラスタ自体はterraformで管理しているので,この辺の作業は全部terraformで行う.

以下のようにクラスタを定義しているとする.

resource "aws_eks_cluster" "cluster" {
  name     = "${var.name}-${var.env}"
  role_arn = data.terraform_remote_state.aws_iam.outputs.eks_master_role_arn

  vpc_config {
    security_group_ids = [
      aws_security_group.eks_master.id,
    ]

    subnet_ids = [
      module.vpc.public_subnet_1_id,
      module.vpc.public_subnet_2_id,
      module.vpc.public_subnet_3_id,
    ]
  }
}

まず,OpenID ConnectのProviderを作成する.

resource "aws_iam_openid_connect_provider" "oidc" {
  url = aws_eks_cluster.cluster.identity.0.oidc.0.issuer
  client_id_list = [
    "sts.amazonaws.com"
  ]
  thumbprint_list = [
    data.external.thumb.result.thumbprint
  ]
}

client_id_list はいいとして,このときにthumbprintを指定する.こいつはterraformのattributeとして取得することができない.

というわけで,kubergruntという,別のCLIツールを使って取得させる.

terraformでは外部コマンドを呼び出すexternalというリソースがあるので,それを使って,

data "external" "thumb" {
  program = ["kubergrunt", "eks", "oidc-thumbprint", "--issuer-url", aws_eks_cluster.cluster.identity.0.oidc.0.issuer]
}

としておく.

もちろん,kubergrunt自体をインストールする必要があるので,入れておく.

github.com

これをapplyすれば,OIDC Providerは作成できる.

OIDC ProviderごとにAssumeRoleを作成

次に,OIDCからAssumeRoleできるように,IAM Roleを作成する.

AssumeRoleに付与するPolicyはこんなの.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "${provider_arn}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringLike": {
          "${issue_hostpath}:sub": "system:serviceaccount:${namespace}:${service_account_name}"
        }
      }
    }
  ]
}

だいたいこれをOIDC Providerの数だけ作る必要があるので,これをtemplateにしておく.

data "template_file" "my_cluster_assume_role_policy" {
  # 先程定義したPolicyのパス
  template = file(
    "${path.module}/aws_iam_role_policies/irsa_assume_role_policy.json.tpl",
  )

  vars = {
    provider_arn   = "arn:aws:iam::123456789:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/A6SD4HSDFL91CDBNAKDFG67J3H"
    issue_hostpath = "oidc.eks.ap-northeast-1.amazonaws.com/id/A6SD4HSDFL91CDBNAKDFG67J3H"
    namespace = "my-namespace"
    service_account_name = "my-iam-role-for-pod"
  }
}

で,IAM Roleを作っておく.

resource "aws_iam_role" "my_pod_role" {
  name               = "my-pod-role"
  path               = "/"
  assume_role_policy = data.template_file.my_cluster_assume_role_policy.rendered
}

IRSAにより認証が成功すると,Podは my_pod_role として振る舞えるので,こいつに必要な権限を付与しておく.

resource "aws_iam_policy" "s3_read_only_policy" {
  name        = "s3-read-only"
  path        = "/"
  description = ""
  policy      = file("aws_iam_policies/s3_read_only_policy.json")
}

resource "aws_iam_policy_attachment" "s3_read_only" {
  name = "s3-read-only"

  roles = [
    aws_iam_role.my_pod_role.name,
  ]

  policy_arn = aws_iam_policy.s3_read_only_policy.arn
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowUserToS3ReadOnly",
      "Effect": "Allow",
      "Action": [
        "s3:Get*",
        "s3:List*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

これで認証が成功すると,PodからはS3のオブジェクトに対して,Get, List ができるようになる.

ServiceAccountを作成

次にkubernetes側に,先のIAM Roleを関連付けたServiceAccountを作成する.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-iam-role-for-pod
  namespace: my-namespace
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/my-pod-role

あとは,このSeviceAccountをPodで利用するだけ.

apiVersion: v1
kind: Deployment
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  replicas: 1
  template:
    spec:
      serviceAccountName: my-iam-role-for-pod
      containers:
      # ...

というような感じ.

これで,PodからはAWSへの認証が成功するようになる.

実運用上の工夫

AssumeRolePolicyの指定項目が細かすぎる

先の例では,AssumeRolePolicyの条件に,namespaceとService Account名までを指定していた. ServiceAccountごとにAssumeRolePolicyを分離するのであれば,確かにこれでも良いのだが,実運用を考えたときにそこまでやるか?というのはある.

認証するIAM Roleは分けたいが,AssumeRolePolicyまでをService Accountごとに作成するというのはかなりめんどくさい.しかも,それで守られるというのものはそこまで大きくなく,例えば,ひとつのAssumeRolePolicyでありながらConditionsで複数の条件を書いてしまっても同じなわけだ.

であるなら,そこまで厳密にPolicyを分割する必要はない.

というわけで,例えば

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "${provider_arn}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringLike": {
          "${issue_hostpath}:sub": "system:serviceaccount:*:${sa_prefix}*"
        }
      }
    }
  ]
}
data "template_file" "my_cluster_assume_role_policy" {
  # 先程定義したPolicyのパス
  template = file(
    "${path.module}/aws_iam_role_policies/irsa_assume_role_policy.json.tpl",
  )

  vars = {
    provider_arn   = "arn:aws:iam::123456789:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/A6SD4HSDFL91CDBNAKDFG67J3H"
    issue_hostpath = "oidc.eks.ap-northeast-1.amazonaws.com/id/A6SD4HSDFL91CDBNAKDFG67J3H"
    sa_prefix = "my-iam-role"
  }
}

とかしておくと,my-iam-roleが先頭についたService AccountのみがIRSAの対象となるようになる.

この例ではnamespaceも * にしてしまっているが,この辺はどの程度緩和するかを運用に合わせて決めたらいいと思う.

困ったこと

IRSAのtokenはrootユーザでマウントされる問題

IAM Role for Service Accountは,上記のようにして設定したService Accountをもとに,IAM RoleにアクセスするためのCredentialsを取得し,それをVolumeMountしてPod内から参照できるような形を提供する.

実際には,

  • AWS_WEB_IDENTITY_TOKEN_FILE
  • AWS_ROLE_ARN

という環境変数が出力され,token自体は aws-iam-tokenというVolumeがマウントされているはずだ.

その結果として何が起こるかというと,このVolume内のファイルはrootとして作成され,マウントされたあとも所有者はrootとなっている.そのため,例えばaws-sdkを実行するコンテナのユーザがrootではない場合,これらのファイルにはアクセスできないということになる.

実際適当なコンテナでやってみると,aws-sdkが認証しようとしたタイミングでaws-iam-token内のファイルにアクセスし,権限が足らずにエラーになる.

$ aws s3 ls

[Errno 13] Permission denied: '/var/run/secrets/eks.amazonaws.com/serviceaccount/token'

/var/run/secrets/eks.amazonaws.com/serviceaccount の中身はこうなっているので仕方がない.

drwxrwxrwt 3 root root 100 Nov 29 14:50 .
drwxr-xr-x 3 root root  28 Nov 29 14:50 ..
drwxr-xr-x 2 root root  60 Nov 29 14:50 ..2019_11_29_14_50_47.379855194
lrwxrwxrwx 1 root root  31 Nov 29 14:50 ..data -> ..2019_11_29_14_50_47.379855194
lrwxrwxrwx 1 root root  12 Nov 29 14:50 token -> ..data/token

これを回避するためには,fsGroupを使う.

kubernetes.io

apiVersion: v1
kind: Deployment
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  replicas: 1
  template:
    spec:
      serviceAccountName: my-iam-role-for-pod
      securityContext:
        fsGroup: 1000
      containers:
      # ...

こしてやると,

drwxrwsrwt 3 root operation 100 Nov 29 14:53 .
drwxr-xr-x 3 root root       28 Nov 29 14:53 ..
drwxr-sr-x 2 root operation  60 Nov 29 14:53 ..2019_11_29_14_53_43.467433262
lrwxrwxrwx 1 root root       31 Nov 29 14:53 ..data -> ..2019_11_29_14_53_43.467433262
lrwxrwxrwx 1 root root       12 Nov 29 14:53 token -> ..data/token

こうなり,無事にawsコマンドが使えるようになる.

ちなみに,Dockerを本番で利用するに当たって,root以外のユーザでコンテナを実行するのは割とよくやる手法で,セキュリティ的な懸念が大きい.コンテナ内のrootユーザは,ホストOS上でもrootユーザとして振る舞える.もしコンテナ内に侵入された場合,コンテナ内をrootで書き換えられることは当たり前として,VolumeMountしていたホスト側もrootで操作できるということになる. これを避けるために本番ではroot以外のユーザを使っていたりする.

要求されるaws-sdkのバージョン

ここまでくれば残りは些細な問題で,IAM Role for Service Accountを使うために必要なところまでawscliやaws-sdkのバージョンを上げるだけだ.

ただ,ここが結構大変で,当然運用中のKubernetesクラスタ上には無数のPodが動いている.複数のチームがある場合は当然それぞれアプリケーションを作っているわけで,その中で使っているaws-sdkのバージョンをすべてチェックして,バージョンアップするのはかなり骨が折れる.

また,バージョンアップを忘れたままIRSAに移行してしまうと,単純にaws-sdkが認証情報を取れなくなるので,No Credentials Errorが発生したりする.移行のときにこのようなエラーが出た場合は,まず,aws-sdkのバージョンを疑おう. 必要になるバージョンは公式ドキュメントに記述されている.

aws.amazon.com

また,自分のチームで運用しているアプリであれば,バージョンアップの労力をかければ上げることは可能だが,これがOSSなどを使っていると大変だ.OSS側でaws-sdkのバージョンが上がるのを待つ必要があり,それまではIRSAに変えることができない.

まとめ

VolumeMountがrootでされてしまう問題以外は,そこまでハマることなく移行することができた.ただ,一部OSS利用している箇所についてはまだaws-sdkのバージョンアップができておらず,kube2iamを使っている部分が残っている.

ただIRSAに変更した部分はkube2iamのようなバグを踏むことなくAWSの認証に成功している.