Active Directory環境下でVPCエンドポイントを利用する

しばたです。
本記事ではActive Directory環境下でVPCエンドポイント(インターフェイスエンドポイント)を利用する際に気を付けるべきことについて説明します。

VPCエンドポイントをおさらい

はじめにVPCエンドポイントについて軽く触れておきます。

上記ユーザーガイドではVPCエンドポイントを次の様に説明しています。

VPC エンドポイントでは、PrivateLink を使用する AWS サービスや VPC エンドポイントサービスに VPC をプライベートに接続できます。
インターネットゲートウェイ、NAT デバイス、VPN 接続、または AWS Direct Connect 接続は必要ありません。
VPC のインスタンスは、サービスのリソースと通信するためにパブリック IP アドレスを必要としません。
VPC と他のサービス間のトラフィックは、Amazon ネットワークを離れません。

そしてAWS PrivateLinkの概要では

ネットワークトラフィックを AWS ネットワーク内に限定することで、AWS でホストされるサービスに簡単かつセキュアにアクセスします。

とあります。
なんともわかる様な、わからない様な説明です...

ここに記載されていない大前提として幾つかのAWSサービスは利用に際しインターネットへのアクセスを必要とします。

例えばAWS Systems Manager(SSM)であればOSにインストールされるAgentプログラムはインターネット経由でAWSの基盤と通信しSSMの各種機能を利用可能とします。
このため通常はインターネットゲートウェイの無いプライベートに閉じたVPCネットワークだとSSMの機能を利用することができません。
ここでプライベートに閉じたネットワークでもAWSのサービスを利用するための機能がAWS PrivateLinkやそれを含むVPCエンドポイントになります。

2つのエンドポイント

歴史的経緯によりVPCエンドポイントには2種類の方式があります。
こちらについては以下の記事に詳しく記載されていますのでまずはご覧ください。

ゲートウェイエンドポイント

ゲートウェイエンドポイントはVPCのルーティングテーブルにAWS内部からアクセス可能なルートを追加することでプライベートなネットワークからAWSのサービスにアクセスするための方式となります。

(上図はS3に対するルーティングテーブルが追加された例)

こちらは

  • Amazon S3
  • DynamoDB

が対象サービスとなります。

インターフェイスエンドポイント (AWS PrivateLinkを使用)

インターフェイスエンドポイントは各AWSサービスのエンドポイントを肩代わりするプライベートなIPを持つElastic Network Interface(ENI)をVPC内に作り、VPC内DNS(Amazon DNS)の名前解決でENIのIPにアクセスさせる方式となります。
例えばSSMの場合ssm.ap-northeast-1.amazonaws.comというホスト(エンドポイント)にアクセスしますが、通常VPCエンドポイントの設定が無い場合、

PS C:\> nslookup ssm.ap-northeast-1.amazonaws.com
サーバー:  ip-172-18-0-2.ap-northeast-1.compute.internal
Address:  172.18.0.2

権限のない回答:
名前:    ssm.ap-northeast-1.amazonaws.com
Address:  54.240.225.181

の様にグローバルなIPアドレスを返します。
これがVPCエンドポイントを設定したVPC内では

PS C:\> nslookup ssm.ap-northeast-1.amazonaws.com
サーバー:  ip-172-18-0-2.ap-northeast-1.compute.internal
Address:  172.18.0.2

権限のない回答:
名前:    ssm.ap-northeast-1.amazonaws.com
Addresses:  172.18.1.62
          172.18.2.83

の様にプライベートなIPアドレスを返す様になります。(この例では172.18.1.62172.18.2.83が作成したエンドポイント)
こちらは以下のサービスが対象です。

  • Amazon API Gateway
  • Amazon CloudWatch
  • Amazon CloudWatch Events
  • Amazon CloudWatch Logs
  • AWS CodeBuild
  • Amazon EC2 API
  • Elastic Load Balancing API
  • AWS Key Management Service
  • Amazon Kinesis Data Streams
  • Amazon SageMaker ランタイム
  • AWS Secrets Manager
  • AWS Service Catalog
  • Amazon SNS
  • AWS Systems Manager
  • 他の AWS アカウントによってホストされるエンドポイントサービス
  • サポートされる AWS Marketplace パートナーサービス

Active DirectoryとVPCエンドポイント

ここまでの説明でVPCエンドポイントがどの様なものでどう実装されているか大枠はつかめたかと思います。

ここでAWS環境下でActive Directoryを利用する場合を考えると、Active Directoryはその名前解決に独自にDNSを必要としますのでドメインに参加するEC2インスタンスのDNS設定はActive Directoryが使用するDNSサーバーに切り替えてやる必要があります。

当然ですがインターフェイスエンドポイントの名前解決の切り替えはActive Directoryが使用するDNSサーバーでは行われません。
何の対処もしない場合ドメインに参加した時点でVPCエンドポイント(インターフェイスエンドポイント)はアクセス不能になります。

テスト環境

ここで簡単な環境で確かめてみます。
以前紹介したVPN環境に対してSSM用のVPCエンドポイントを構成した環境を用意しました。

SSM用のVPCエンドポイントは以下の4つ必要ですのでサブネット毎に4つのENIが必要になります。

  • com.amazonaws.[region].ssm
  • com.amazonaws.[region].ec2messages
  • com.amazonaws.[region].ec2
  • com.amazonaws.[region].ssmmessages

加えてSSMでは大抵の場合S3も利用しますのでS3用のエンドポイントも作成しています。(ただし今回は使用しません)

ざっくり以下のCloudFormationテンプレートで作成しています。

CloudFormationテンプレート
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  SystemName:
    Description: "System name of each resource names."
    Type: String
    Default: "devio"
  EnvironmentName:
    Description: "Environment name of each resource names."
    Type: String
    Default: "test"
  AZ1:
    Description: "AZ1"
    Type: AWS::EC2::AvailabilityZone::Name
    Default: "ap-northeast-1a"
  AZ2:
    Description: "AZ2"
    Type: AWS::EC2::AvailabilityZone::Name
    Default: "ap-northeast-1c"
Resources:
  # VPC endpoint for SSM
  # https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/sysman-setting-up-vpc.html
  SSMEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId:
        Fn::ImportValue:
          Fn::Sub: "${SystemName}-${EnvironmentName}-vpc"
      SubnetIds:
        - Fn::ImportValue:
            Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-1"
        - Fn::ImportValue:
            Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-2"
      VpcEndpointType: Interface
      ServiceName:
        Fn::Sub: "com.amazonaws.${AWS::Region}.ssm"
      PrivateDnsEnabled: True
      SecurityGroupIds:
        - Ref: SecurityGroupSSMEndpoint
  EC2MessageEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId:
        Fn::ImportValue:
          Fn::Sub: "${SystemName}-${EnvironmentName}-vpc"
      SubnetIds:
        - Fn::ImportValue:
            Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-1"
        - Fn::ImportValue:
            Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-2"
      VpcEndpointType: Interface
      ServiceName:
        Fn::Sub: "com.amazonaws.${AWS::Region}.ec2messages"
      PrivateDnsEnabled: True
      SecurityGroupIds:
        - Ref: SecurityGroupSSMEndpoint
  EC2Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId:
        Fn::ImportValue:
          Fn::Sub: "${SystemName}-${EnvironmentName}-vpc"
      SubnetIds:
        - Fn::ImportValue:
            Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-1"
        - Fn::ImportValue:
            Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-2"
      VpcEndpointType: Interface
      ServiceName:
        Fn::Sub: "com.amazonaws.${AWS::Region}.ec2"
      PrivateDnsEnabled: True
      SecurityGroupIds:
        - Ref: SecurityGroupSSMEndpoint
  SSMMessageEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId:
        Fn::ImportValue:
          Fn::Sub: "${SystemName}-${EnvironmentName}-vpc"
      SubnetIds:
        - Fn::ImportValue:
            Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-1"
        - Fn::ImportValue:
            Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-2"
      VpcEndpointType: Interface
      ServiceName:
        Fn::Sub: "com.amazonaws.${AWS::Region}.ssmmessages"
      PrivateDnsEnabled: True
      SecurityGroupIds:
        - Ref: SecurityGroupSSMEndpoint
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId:
        Fn::ImportValue:
          Fn::Sub: "${SystemName}-${EnvironmentName}-vpc"
      RouteTableIds:
        - Fn::ImportValue:
            Fn::Sub: "${SystemName}-${EnvironmentName}-private-rtb"
      VpcEndpointType: Gateway
      ServiceName:
        Fn::Sub: "com.amazonaws.${AWS::Region}.s3"
  # Security groups
  SecurityGroupSSMEndpoint:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName:
        Fn::Sub: "${SystemName}-${EnvironmentName}-ssmendpoint-sg"
      GroupDescription:
        Fn::Sub: "${SystemName}-${EnvironmentName}-ssmendpoint-sg"
      VpcId:
        Fn::ImportValue:
          Fn::Sub: "${SystemName}-${EnvironmentName}-vpc"
      SecurityGroupIngress:
        - IpProtocol: -1
          CidrIp: 172.18.0.0/16
      Tags:
        - Key: Name
          Value:
            Fn::Sub: "${SystemName}-${EnvironmentName}-ssmendpoint-sg"

EC2インスタンスは適当なスペックで作成したWindows Server 2019です。
今回はIPアドレス172.18.1.33が割り当てられました。

ドメイン参加前

ドメイン参加前は下図の様にプライベートなネットワークでもSSMの機能を利用することができ、

SSMのエンドポイントもこのとおりです。

ドメイン参加後

手順は端折りますがここから確認用インスタンスをドメインcorp.shibata.techに参加させると以下の様にSSMの機能が利用できなくなり、

SSMのエンドポイントもパブリックなIPアドレスを返してしまいます。

(SSMセッションを張れないのでRDPで接続して確認)

Active Directory環境下でVPCエンドポイントを利用する

やっとですがここから本題に入ります。

VPCエンドポイント(インターフェイスエンドポイント)を利用できなくなったのはActive Directoryを利用することでDNSサーバーが切り替わってしまったのが直接的な原因です。
問題を解決するにはどうにかしてActive Directoryが利用するDNSサーバーからVPCエンドポイントのホスト名に対してプライベートなIPアドレスを返す様にしてやれば良いわけです。

対処法としてはActive Directoryが利用するDNSサーバーがどこにあるかによって2パターン存在します。

1. Active Directoryが利用するDNSサーバーがVPC内にある場合

こちらは今回の例とは合致しないのですが、AWSでActive Directoryを利用するに際してVPC内にドメインコントローラー(およびDNSサーバー)を配置することは多いと思います。

この場合は割とシンプルに問題を解決することが可能です。

Amazon DNSはVPC内部からであればDNSの問い合わせに回答します。
このためActive Directory側DNSのフォーワーダーにAmazon DNSを指定してやればVPCエンドポイントに対してプライベートなIPアドレスを返す様になります。
既存の設定を極力変えたくない場合は条件付きフォーワーダーを設定してやればよいでしょう。

AWS側でMicrosoft ADを利用している場合の例ですが、以下の記事が参考になります。

2. Active Directoryが利用するDNSサーバーがVPC外にある場合

こちらが今回の例に合致し、Active Directoryが利用するDNSサーバーがVPC外(オンプレ側)にあります。
この場合Amazon DNSはActive Directoryが利用するDNSサーバーからの問い合わせに回答しませんので、単純にフォーワーダーの設定をしても意味はありません。

(Amazon DNSに対しVPC外から条件付きフォーワーダーを設定しようとしてもタイムアウトになる)

このためVPC内にRoute 53 Resolverの様なDNSの問い合わせをフォーワードするサービスを設定する必要があります。

問い合わせをフォーワードできれば何でも良いので自前でVPC内にDNSサーバーを立てても構いませんが、個人的には絶対やりたくないので本記事ではRoute 53 Resolverを試します。

Route 53 Resolverを構成する

Route 53 Resolverにはインバウンド(オンプレ→VPC)アウトバウンド(VPC→オンプレ)の二方向のエンドポイントがありますが、今回必要なのはインバウンドのエンドポイントです。
下図の様に各サブネットにオンプレからの通信を受けるエンドポイントを作成します。

今回は以下のCloudFormationテンプレートでRoute 53 Resolver(とそれに必要なセキュリテグループ)を構成しました。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  SystemName:
    Description: "System name of each resource names."
    Type: String
    Default: "devio"
  EnvironmentName:
    Description: "Environment name of each resource names."
    Type: String
    Default: "test"
Resources:
  # Security groups
  # オンプレ → VPCの通信用
  SecurityGroupR53ResolverInbound:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName:
        Fn::Sub: "${SystemName}-${EnvironmentName}-r53resolver-inbound-sg"
      GroupDescription:
        Fn::Sub: "${SystemName}-${EnvironmentName}-r53resolver-inbound-sg"
      VpcId:
        Fn::ImportValue:
          Fn::Sub: "${SystemName}-${EnvironmentName}-vpc"
      SecurityGroupIngress:
        - IpProtocol: tcp
          CidrIp: 192.168.0.0/16
          FromPort: 53
          ToPort: 53
        - IpProtocol: udp
          CidrIp: 192.168.0.0/16
          FromPort: 53
          ToPort: 53
      Tags:
        - Key: Name
          Value:
            Fn::Sub: "${SystemName}-${EnvironmentName}-r53resolver-inbound-sg"
  # ResolverEndpoint
  # * Direction を INBOUND
  # * IpAddresses を2つのサブネットから自動取得
  ResolverEndpointInbound:
    Type: AWS::Route53Resolver::ResolverEndpoint
    Properties:
      Direction: INBOUND
      IpAddresses: 
        - SubnetId:
            Fn::ImportValue:
              Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-1"
        - SubnetId:
            Fn::ImportValue:
              Fn::Sub: "${SystemName}-${EnvironmentName}-private-subnet-2"
      Name:
        Fn::Sub: "${SystemName}-${EnvironmentName}-r53resolver-inbound"
      SecurityGroupIds: 
        - Ref: SecurityGroupR53ResolverInbound

結果は下図の様にIPアドレス172.18.1.132172.18.2.29を持つインバウンドエンドポイントが作成されました。

あとはActive Directoryが利用するDNSサーバーのフォーワーダーにこのエンドポイントを指定してやるだけです。

今度はタイムアウトせずにきちんと設定されました。
(今回はamazonaws.comに対して条件付きフォーワーダーを設定しましたが、AWSサービスの種類によって設定すべき内容は若干変わります)

これでドメインに参加したEC2インスタンスからVPCエンドポイントに対して名前解決を試みるとプライベートなIPアドレスを返す様になり、

ドメインに参加しつつSSMの機能も利用できる様になります。
(状況によってはSSM Agentの再起動も必要です)