Skip to main content

Amazon EKSのセキュリティを強化する4つのステップ

著者
Headshot of Kamil Potrec

Kamil Potrec

blog feature amazon eks security

2021年7月21日

0 分で読めます

このブログシリーズの第1回では、Terraformを使ったAmazon EKSのデプロイを取り上げ、初期のRBAC実装とインスタンスメタデータサービスを保護する方法を紹介しました。第2回となる今回は、Amazon EKSのセキュリティを強化するベストプラクティスとして、継続的デリバリー専用のIAMロール、Amazon EKSクラスターを分離するマルチアカウントアーキテクチャ、そしてコントロールプレーンでシークレットを暗号化する方法の重要性を解説します。最後に、CDパイプラインに静的解析ツールを導入して、本番環境に到達する前にこれらの問題を検出する方法をご紹介します。

クラスターをデプロイしたユーザーを把握する

Amazon EKSサービスはIdentity Access Managementサービスと連携し、クラスターへのユーザー認証を行います。認可の判断には、Kubernetes標準のRBACモデルが使われます。RBAC設定はaws-auth ConfigMapを介してIAMアイデンティティにマッピングされます。ただし、設定ファイルや設定画面には表示されない特別なマッピングがあります。クラスターの作成に使用したIAMエンティティは、自動的にsystem:mastersというKubernetes組み込みグループにマッピングされます。この動作はユーザードキュメントに明記されています。しかし、クラスター権限の監査可能性には重大な影響があります。

この設定の影響を示すため、デモクラスターからすべてのRolesとClusterRolesを削除します。

dev@pwnbox:~$ kubectl delete clusterroles --all
clusterrole.rbac.authorization.k8s.io "admin" deleted
clusterrole.rbac.authorization.k8s.io "aws-node" deleted
clusterrole.rbac.authorization.k8s.io "cluster-admin" deleted
clusterrole.rbac.authorization.k8s.io "edit" deleted
clusterrole.rbac.authorization.k8s.io "eks:addon-manager" deleted
clusterrole.rbac.authorization.k8s.io "eks:fargate-manager" deleted
clusterrole.rbac.authorization.k8s.io "eks:node-bootstrapper" deleted
clusterrole.rbac.authorization.k8s.io "eks:node-manager" deleted
clusterrole.rbac.authorization.k8s.io "eks:podsecuritypolicy:privileged" deleted
<OMITTED>
dev@pwnbox:~$ kubectl delete roles --all-namespaces --all
role.rbac.authorization.k8s.io "system:controller:bootstrap-signer" deleted
role.rbac.authorization.k8s.io "eks:addon-manager" deleted
role.rbac.authorization.k8s.io "eks:certificate-controller" deleted
role.rbac.authorization.k8s.io "eks:fargate-manager" deleted
role.rbac.authorization.k8s.io "eks:node-manager" deleted
<OMITTED>
dev@pwnbox:~$ kubectl auth can-i --list
Resources                                       Non-Resource URLs   Resource Names   Verbs
*.*                                               []                  []               [*]
                                                [*]                 []               [*]
selfsubjectaccessreviews.authorization.k8s.io   []                  []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                  []               [create]
                                                [/api/*]            []               [get]
                                                [/api]              []               [get]
                                                [/apis/*]           []               [get]
                                                [/apis]             []               [get]
                                                [/healthz]          []               [get]
                                                [/healthz]          []               [get]
                                                [/livez]            []               [get]
                                                [/livez]            []               [get]
                                                [/openapi/*]        []               [get]
                                                [/openapi]          []               [get]
                                                [/readyz]           []               [get]
                                                [/readyz]           []               [get]
                                                [/version/]         []               [get]
                                                [/version/]         []               [get]
                                                [/version]          []               [get]
                                                [/version]          []               [get]
dev@pwnbox:~$ kubectl get configmap --all-namespaces
NAMESPACE     NAME                                 DATA   AGE
kube-system   coredns                              1      7h59m
kube-system   cp-vpc-resource-controller           0      7h58m
kube-system   eks-certificates-controller          0      7h59m
kube-system   extension-apiserver-authentication   6      7h59m
kube-system   kube-proxy                           1      7h59m
kube-system   kube-proxy-config                    1      7h59m

Amazon EKSはIAMとの連携にWebhookトークン認証を使用します。設定はAPIサーバーの引数で確認できます。

FLAG: --authentication-token-webhook-cache-ttl="7m0s"
FLAG: --authentication-token-webhook-config-file="/etc/kubernetes/authenticator/apiserver-webhook-kubeconfig.yaml"
FLAG: --authentication-token-webhook-version="v1beta1"

AWS IAMには一意の識別子という概念があり、特定のサービスで名前の再利用による設定ミスを防ぐために利用できます。Amazon EKSのドキュメントからは、system:masters権限がエンティティの一意の識別子に紐づくのか、表示名に紐づくのかは明確ではありません。元のエンティティを削除すればクラスターの権限がすべて失効するかを確かめるため、一時的なロールを削除して再作成しました。

dev@pwnbox:~$ aws iam get-role --role-name ci-eks-test
{
    "Role": {
        "Path": "/",
        "RoleName": "ci-eks-test",
        "RoleId": "AROAYREY3WYOOSHLPO3W6",
        "Arn": "arn:aws:iam::123456789012:role/ci-eks-test",
        "CreateDate": "2021-06-20T21:09:01+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "AWS": "arn:aws:iam::123456789012:user/ci"
                    },
                    "Action": "sts:AssumeRole",
                    "Condition": {}
                }
            ]
        },
        "MaxSessionDuration": 3600,
        "RoleLastUsed": {}
    }
}
dev@pwnbox:~$ kubectl auth can-i --list
Resources                                       Non-Resource URLs   Resource Names     Verbs
*.*                                             []                  []                 [*]
                                                [*]                 []                 [*]
selfsubjectaccessreviews.authorization.k8s.io   []                  []                 [create]
selfsubjectrulesreviews.authorization.k8s.io    []                  []                 [create]
                                                [/api/*]            []                 [get]
                                                [/api]              []                 [get]
                                                [/apis/*]           []                 [get]
                                                [/apis]             []                 [get]
                                                [/healthz]          []                 [get]
                                                [/healthz]          []                 [get]
                                                [/livez]            []                 [get]
                                                [/livez]            []                 [get]
                                                [/openapi/*]        []                 [get]
                                                [/openapi]          []                 [get]
                                                [/readyz]           []                 [get]
                                                [/readyz]           []                 [get]
                                                [/version/]         []                 [get]
                                                [/version/]         []                 [get]
                                                [/version]          []                 [get]
                                                [/version]          []                 [get]
podsecuritypolicies.policy                      []                  [eks.privileged]   [use]

dev@pwnbox:~$ aws iam get-role --role-name ci-eks-test
{
    "Role": {
        "Path": "/",
        "RoleName": "ci-eks-test",
        "RoleId": "AROAYREY3WYOAK6QMGSUT",
        "Arn": "arn:aws:iam::123456789012:role/ci-eks-test",
        "CreateDate": "2021-06-20T21:14:28+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "AWS": "arn:aws:iam::123456789012:user/ci"
                    },
                    "Action": "sts:AssumeRole",
                    "Condition": {}
                }
            ]
        },
        "MaxSessionDuration": 3600,
        "RoleLastUsed": {}
    }
}
dev@pwnbox:~$ kubectl auth can-i --list
Resources                                       Non-Resource URLs   Resource Names     Verbs
*.*                                             []                  []                 [*]
                                                [*]                 []                 [*]
selfsubjectaccessreviews.authorization.k8s.io   []                  []                 [create]
selfsubjectrulesreviews.authorization.k8s.io    []                  []                 [create]
                                                [/api/*]            []                 [get]
                                                [/api]              []                 [get]
                                                [/apis/*]           []                 [get]
                                                [/apis]             []                 [get]
                                                [/healthz]          []                 [get]
                                                [/healthz]          []                 [get]
                                                [/livez]            []                 [get]
                                                [/livez]            []                 [get]
                                                [/openapi/*]        []                 [get]
                                                [/openapi]          []                 [get]
                                                [/readyz]           []                 [get]
                                                [/readyz]           []                 [get]
                                                [/version/]         []                 [get]
                                                [/version/]         []                 [get]
                                                [/version]          []                 [get]
                                                [/version]          []                 [get]
podsecuritypolicies.policy                      []                  [eks.privileged]   [use]

ご覧のとおり、RoleIdが異なるまったく新しいロールになっています。しかし、Amazon EKSの認証サービスはロールの表示名を使うため、同じsystem:masters権限が付与されます。

この問題を緩和する最も効果的な方法は、デプロイを一元管理するシステムを使い、デプロイ専用のロールを用意して、個人の認証情報を絶対に使わないことです。aws-vaultなどのツールを使えば、複数のロールを安全に切り替えられます。

次に、ノードがコントロールプレーンに対してどのように認証するかを見てみましょう。

クラスターをマルチアカウントで分離する

Amazon EKSでは、すべてのノードに次の2つのポリシーを持つインスタンスプロファイルをアタッチする必要があります。AmazonEKSWorkerNodePolicyとAmazonEC2ContainerRegistryReadOnlyです。

dev@pwnbox:$ aws iam get-policy-version --policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy --version-id v1
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": [
                        "ec2:DescribeInstances",
                        "ec2:DescribeRouteTables",
                        "ec2:DescribeSecurityGroups",
                        "ec2:DescribeSubnets",
                        "ec2:DescribeVolumes",
                        "ec2:DescribeVolumesModifications",
                        "ec2:DescribeVpcs",
                        "eks:DescribeCluster"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                }
            ]
        },
        "VersionId": "v1",
        "IsDefaultVersion": true,
        "CreateDate": "2018-05-27T21:09:01+00:00"
    }
}

dev@pwnbox:$ aws iam get-policy-version --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly --version-id v3
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "ecr:GetAuthorizationToken",
                        "ecr:BatchCheckLayerAvailability",
                        "ecr:GetDownloadUrlForLayer",
                        "ecr:GetRepositoryPolicy",
                        "ecr:DescribeRepositories",
                        "ecr:ListImages",
                        "ecr:DescribeImages",
                        "ecr:BatchGetImage",
                        "ecr:GetLifecyclePolicy",
                        "ecr:GetLifecyclePolicyPreview",
                        "ecr:ListTagsForResource",
                        "ecr:DescribeImageScanFindings"
                    ],
                    "Resource": "*"
                }
            ]
        },
        "VersionId": "v3",
        "IsDefaultVersion": true,
        "CreateDate": "2019-12-10T20:56:32+00:00"
    }
}

これらのポリシーは、デフォルトでAWSアカウント内のすべてのリソースに対する権限を持ちます。そのため、このクラスター内のポッドが侵害されると、アカウント内のあらゆるリソース、VPC、サブネット、イメージに関する情報を列挙できるようになります。

Amazon EKSノードには、AmazonEKS_CNI_Policyというもう1つの管理ポリシーも必要です。このポリシーにより、ノードはIPアドレスを割り当て、ネットワーク関連の機能を実行できます。

dev@pwnbox:$ aws iam get-policy-version --policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy --version-id v4
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "ec2:AssignPrivateIpAddresses",
                        "ec2:AttachNetworkInterface",
                        "ec2:CreateNetworkInterface",
                        "ec2:DeleteNetworkInterface",
                        "ec2:DescribeInstances",
                        "ec2:DescribeTags",
                        "ec2:DescribeNetworkInterfaces",
                        "ec2:DescribeInstanceTypes",
                        "ec2:DetachNetworkInterface",
                        "ec2:ModifyNetworkInterfaceAttribute",
                        "ec2:UnassignPrivateIpAddresses"
                    ],
                    "Resource": "*"
                },
                {
                    "Effect": "Allow",
                    "Action": [
                        "ec2:CreateTags"
                    ],
                    "Resource": [
                        "arn:aws:ec2:*:*:network-interface/*"
                    ]
                }
            ]
        },
        "VersionId": "v4",
        "IsDefaultVersion": true,
        "CreateDate": "2020-04-20T20:52:01+00:00"
    }
}

このポリシーを使うと、呼び出し元はアカウント内のあらゆるインスタンスのネットワーク設定を変更できます。AWSは、このポリシーをaws-node Kubernetesサービスアカウントに直接アタッチすることを推奨しています。Terraformモジュールでは、デフォルトでこのポリシーがIAMプロファイルにアタッチされます。attach_worker_cni_policy属性をfalseに設定すれば、この動作を変更できます。

シークレットを暗号化する

Amazon EKSサービスが管理するコントロールプレーンのコンポーネントの1つに、etcdキーバリューストアがあります。Kubernetes APIサーバーは、すべてのオブジェクトと設定の保存にこれを使用します。デフォルトでは、Kubernetesはこのキーバリューストアに保存されたデータを暗号化しません。コントロールプレーンのコンポーネントのセキュリティはAWSが担いますが、さらに多層防御を適用することで、データのセキュリティを強化できます。

AWS KMSは暗号化プロバイダーの1つであり、クラスターに保存されたシークレットオブジェクトにエンベロープ暗号化を適用できます。`cluster_encryption_config`オプションを設定すると、KMSキーを指定して中間データ暗号化キーを暗号化できます。このキーは、個々のオブジェクトの暗号化に使用されます。AWSによるAmazon EKSのブログ記事では、仕組みの概要が詳しく説明されています。これ以降は、etcdクラスターにアクセスできても、KMSキーにもアクセスできなければシークレットを読み取れません。このプロセスはエンドユーザーから完全に透過的です。

デプロイパイプラインの問題を検出する

Infrastructure as Codeを使えば、Amazon EKSクラスターの望ましい状態を宣言的に記述できます。そのため、デプロイ前にこうした問題の一部を静的に検出できます。

Terraformでは、デプロイする設定のプランを生成できます。

dev@pwnbox:$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.aws_eks_cluster.cluster will be read during apply
  # (config refers to values not yet known)
 <= data "aws_eks_cluster" "cluster"  {
      + arn                       = (known after apply)
      + certificate_authority     = (known after apply)
      + created_at                = (known after apply)
      + enabled_cluster_log_types = (known after apply)
      + endpoint                  = (known after apply)
      + id                        = (known after apply)
      + identity                  = (known after apply)
      + kubernetes_network_config = (known after apply)
      + name                      = (known after apply)
      + platform_version          = (known after apply)
      + role_arn                  = (known after apply)
      + status                    = (known after apply)
      + tags                      = (known after apply)
      + version                   = (known after apply)
      + vpc_config                = (known after apply)
    }

<OMITTED>

# module.vpc.aws_vpc.this[0] will be created
  + resource "aws_vpc" "this" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "10.0.0.0/16"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)
      + default_security_group_id        = (known after apply)
      + dhcp_options_id                  = (known after apply)
      + enable_classiclink               = (known after apply)
      + enable_classiclink_dns_support   = (known after apply)
      + enable_dns_hostnames             = false
      + enable_dns_support               = true
      + id                               = (known after apply)
      + instance_tenancy                 = "default"
      + ipv6_association_id              = (known after apply)
      + ipv6_cidr_block                  = (known after apply)
      + main_route_table_id              = (known after apply)
      + owner_id                         = (known after apply)
      + tags                             = {
          + "Name" = "eks-threat-modelling-ddddaaaa"
        }
      + tags_all                         = {
          + "Name" = "eks-threat-modelling-ddddaaaa"
        }
    }

Plan: 44 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + cluster_endpoint          = (known after apply)
  + cluster_name              = (known after apply)
  + cluster_security_group_id = (known after apply)
  + config_map_aws_auth       = []
  + kubectl_config            = (known after apply)
  + region                    = "eu-west-1

このプランをJSON形式に変換すれば、すべての属性を静的解析できます。

dev@pwnbox:$ terraform show -json tfplan > tfplan.json
dev@pwnbox:$ jq '.resource_changes[] | select((.type == "aws_eks_cluster") and .mode == "managed").change.after' tfplan.json
{
  "enabled_cluster_log_types": [
    "audit",
    "authenticator"
  ],
  "encryption_config": [
    {
      "provider": [
        {}
      ],
      "resources": [
        "secrets"
      ]
    }
  ],
  "kubernetes_network_config": [
    {}
  ],
  "name": "eks-threat-modelling-ddddaaaa",
  "tags": null,
  "timeouts": {
    "create": "30m",
    "delete": "15m",
    "update": null
  },
  "version": "1.19",
  "vpc_config": [
    {
      "endpoint_private_access": true,
      "endpoint_public_access": false,
      "public_access_cidrs": [
        "0.0.0.0/0"
      ]
    }
  ]
}

Terraformプランの構造を把握したら、Open Policy Agentとその宣言型言語を使って簡単なテストを記述できます。

package play

deny[msg]{
    input.vpc_config[0].endpoint_public_access == true
    msg := sprintf("The public endpoint is enabled on the cluster", [])
}

オンラインのRego Playgroundで、この簡単な例を試せます。

こうしたルールの作成は煩雑になりがちです。Terraformの出力構造の解析やファイルのパース、ライブラリの保守、結果のわかりやすい報告が必要になります。そこで役立つのが、Snyk IaCのような製品です。以下は、デフォルトのデプロイに対して実施したセキュリティスキャンの例です。

dev@pwnbox:$ snyk iac test tfplan.json

Testing tfplan.json...

Infrastructure as code issues:
  ✗ EKS cluster allows public access [High Severity] [SNYK-CC-TF-94] in EKS
    introduced by aws_eks_cluster[this] > vpc_config

  ✗ Non-Encrypted root block device [Medium Severity] [SNYK-CC-TF-53] in EC2
    introduced by aws_launch_configuration[workers] > root_block_device > encrypted

  ✗ Public IPs are automatically mapped to instances [Low Severity] [SNYK-CC-AWS-427] in VPC
    introduced by aws_subnet[public_0] > map_public_ip_on_launch

  ✗ EKS control plane logging insufficient [Low Severity] [SNYK-CC-TF-131] in EKS
    introduced by aws_eks_cluster[this] > enabled_cluster_log_types

  ✗ Public IPs are automatically mapped to instances [Low Severity] [SNYK-CC-AWS-427] in VPC
    introduced by aws_subnet[public_2] > map_public_ip_on_launch

  ✗ Public IPs are automatically mapped to instances [Low Severity] [SNYK-CC-AWS-427] in VPC
    introduced by aws_subnet[public_1] > map_public_ip_on_launch

Organization:      p0tr3c
Type:              Terraform
Target file:       tfplan.json
Project name:      hardened
Open source:       no
Project path:      tfplan.json

Tested tfplan.json for known issues, found 6 issues

dev@pwnbox:/$

SnykのIaCスキャンを使えば、本番環境へのデプロイ前にTerraform設定の問題を検出し、本番環境の安全性を確保できます。https://app.snyk.ioで無料アカウントにご登録ください。また、コードのセキュリティを手軽に確認できる、Snykの無料コードチェッカーツールもお試しいただけます。

ソースコードの段階からインフラを保護

Snykはワークフロー内のIaCセキュリティとコンプライアンスを自動化し、構成ドリフトや不足しているリソースを検出します。

カテゴリー:

続きを読む

blog feature pypi spoof
Article

オープンソースのメンテナーに、Snyk AI Security Platformをすべて無料で提供

オープンソースのメンテナーは、実際の脆弱性レポートに追われ、優先順位付けや修正、修正版のリリースを迅速に進めるための支援を必要としています。SnykのSecure Developer Programでは、条件を満たすプロジェクトにSnyk AI Security Platformを無料で提供します。

Article

メンテナーアカウント侵害が疑われる中、悪意のあるnode-ipcのバージョンがnpmに公開

2026年5月14日、人気のnpmパッケージnode-ipcの複数の悪意あるバージョンがnpmレジストリに公開されました。現在の公開情報では、node...

blog feature toolkit
Blog

elementary-dataのPyPIパッケージに悪意あるリリース、データエンジニアのクラウド認証情報を窃取

攻撃者はGitHub Actionsのスクリプトインジェクション脆弱性を悪用し、悪意あるバージョンのelementary-data Python CLI(v0.23.3)を公開しました。データエンジニアリング環境のdbtプロファイル、クラウドプロバイダーのキー、SSHシークレットを標的とする認証情報窃取バックドアが埋め込まれていました。