AWS Client VPN[Federation authentication]を使用する


# 方法
AWS ClientVPN と Azure Active Directory (Azure AD) を統合する

# 構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/e0bd9c3c-e018-4d09-251a-77f4cd995744.png)

## IdP

* Microsoft Azure


  1. AWS ClientVPNへの接続ユーザをAzureADで制御
  2. Azure ADアカウントを用いてAWS ClientVPN に自動的にサインインさせる

## SP

* AWS Client VPN

  1. セルフサービスポータル URLを作成することで運用コストを下げる
  2. 認証はfederated-authenticationを選択
  3. VPNを作成するVPCはDNS解決、DNSホストを有効にしておく

# 構築
## Azure
[Azure Active Directory シングル サインオン (SSO) と AWS ClientVPN の統合](https://docs.microsoft.com/ja-jp/azure/active-directory/saas-apps/aws-clientvpn-tutorial)を参照する


- Azure Active Directory > Enterprise applications > New application
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/d5d32036-6fe0-a20e-13b8-cfd94bca0df1.png)

- aws clientvpnで検索しアプリケーションを選択し作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/7ed73ea1-d8cb-5d95-347f-4ecc983b89c0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/79eafbae-90ad-a8ff-9de7-0416b0ab2135.png)


- ユーザを登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/6bcba9ac-900e-fee6-74f9-725c0af631a1.png)

- Set up single sign on > SAML
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/a581fc47-b718-61cd-9e33-6c73e1f8467c.png)

- パラメータ入力
![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/56987ef7-b760-0709-936f-a84d872921f8.png)

- App registrations > AWS ClientVPN
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/b822d59d-bea5-0618-5552-43326d603b57.png)

- Manifest >  > 応答URLをhttpに更新
![名称未設定2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/0fc718a6-5dae-e3d6-829c-b64d9ac3dbcf.png)

- Federation Metadata XMLをダウンロードする
![名称未設定3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/d9979936-cce5-4bcb-5f7b-c9543ee17fd6.png)

---

## AWS
ダウンロードしたFederation Metadata XMLでSAML用のIAM Identity Providerを作成する

- IAM設定ファイル
```sh
vpn-config
├── common
│   ├── iam
│   │   ├── azure.xml
│   │   ├── iam_saml_provider.tf
│   │   └── main.tf
│   └── server-crt
│       ├── README.md
(snip)
```

<details><summary>aws_iam_saml_provider.tf</summary>

```hcl
resource "aws_iam_saml_provider" "example_azure" {
  name                   = "azure"
  saml_metadata_document = file("azure.xml")
}
```
</details>

- AWS ClientVPNで使用するサーバ証明書を作成
  - OpenVPN easy-rsa リポジトリのクローンをローカルコンピュータに作成して、easy-rsa/easyrsa3 フォルダに移動させる
```zsh
% git clone https://github.com/OpenVPN/easy-rsa.git
% cd easy-rsa/easyrsa3
% ./easyrsa init-pki
% ./easyrsa build-ca nopass
% ./easyrsa build-server-full server nopass
```

- サーバー証明書とキーをカスタムフォルダにコピーしてから、カスタムフォルダに移動させる
```zsh
% mkdir ~/custom_folder/
% cp pki/ca.crt ~/custom_folder/
% cp pki/issued/server.crt ~/custom_folder/
% cp pki/private/server.key ~/custom_folder/
% cp pki/issued/client1.domain.tld.crt ~/custom_folder
% cp pki/private/client1.domain.tld.key ~/custom_folder/
% cd ~/custom_folder/
```

- サーバー証明書とキーを ACM にアップロードする
```zsh
% aws acm import-certificate --certificate fileb://server.crt --private-key fileb://server.key --certificate-chain fileb://ca.crt
```

VPN設定ファイル
```sh
vpn-config
(snip)
│
└── network
    ├── cloudwatch_log_group.tf
    ├── cloudwatch_metric_alarm.tf
    ├── ec2_client_vpn_endpoint.tf
    ├── eip.tf
    ├── internet_gateway.tf
    ├── main.tf
    ├── nat_gateway.tf
    ├── route.tf
    ├── route_table.tf
    ├── route_table_association.tf
    ├── security_group.tf
    ├── subnet.tf
    ├── variables.tf
    └── vpc.tf
```

<details><summary>cloudwatch_log_group.tf</summary>

```hcl
resource "aws_cloudwatch_log_group" "client_vpn" {
  name              = "client-vpn"
  retention_in_days = 365

  tags = {
    Name        = "client-vpn"
  }
}

resource "aws_cloudwatch_log_stream" "client_vpn" {
  name           = "client-vpn-logstream"
  log_group_name = aws_cloudwatch_log_group.client_vpn.name
}
```
</details>

<details><summary>cloudwatch_metric_alarm.tf</summary>

```hcl
resource "aws_cloudwatch_metric_alarm" "client_vpn" {
  for_each = var.client_vpn_cloudwatch_metrics

  alarm_name          = each.value.alarm_name
  comparison_operator = each.value.comparison_operator
  evaluation_periods  = "2"
  treat_missing_data  = "notBreaching"
  metric_name         = each.value.metric_name
  namespace           = "AWS/ClientVPN"
  period              = "60"
  statistic           = each.value.statistic
  threshold           = each.value.threshold
  alarm_actions       = ["arn:aws:sns:ap-northeast-1:account-id:alert-client-vpn"]
  ok_actions          = ["arn:aws:sns:ap-northeast-1:account-id:alert-client-vpn"]

  dimensions = {
    Endpoint = "cvpn-endpoint-*********"
  }
}
```
</details>

<details><summary>ec2_client_vpn_endpoint.tf</summary>

```hcl
resource "aws_ec2_client_vpn_endpoint" "example" {
  client_cidr_block = "192.168.0.0/20"
  description       = "use tcp"
  dns_servers = [
    "1.1.1.1",
    "8.8.8.8",
  ]
  self_service_portal    = "enabled"
  server_certificate_arn = data.aws_acm_certificate.issued.arn
  split_tunnel           = 
  
  tags = {
    Name        = "example-vpn-federated-authentication"
  }
  transport_protocol = "tcp"

  authentication_options {
    saml_provider_arn              = "arn:aws:iam::account-id:saml-provider/azure"
    self_service_saml_provider_arn = "arn:aws:iam::account-id:saml-provider/azure"
    type                           = "federated-authentication"
  }

  connection_log_options {
    enabled               = true
    cloudwatch_log_group  = aws_cloudwatch_log_group.client_vpn.name
    cloudwatch_log_stream = aws_cloudwatch_log_stream.client_vpn.name
  }
}

resource "aws_ec2_client_vpn_network_association" "example_assoc" {
  for_each = var.vpn_client_nat_route_assoc

  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.example.id
  subnet_id              = aws_subnet.client_vpn[each.key].id
  security_groups        = [aws_security_group.client_vpn.id]
}


resource "aws_ec2_client_vpn_authorization_rule" "example_authorization" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.example.id
  target_network_cidr    = "0.0.0.0/0"
  authorize_all_groups   = true
}

resource "aws_ec2_client_vpn_route" "example_route_1a" {

  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.example.id
  destination_cidr_block = "0.0.0.0/0"
  target_vpc_subnet_id   = aws_subnet.client_vpn["private-1a"].id
}

resource "aws_ec2_client_vpn_route" "example_route_1c" {

  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.example.id
  destination_cidr_block = "0.0.0.0/0"
  target_vpc_subnet_id   = aws_subnet.client_vpn["private-1c"].id
}

```
</details>

<details><summary>eip.tf</summary>

```hcl
resource "aws_eip" "client_vpn" {
  for_each = var.vpn_client_natgw

  vpc = true

  tags = {
    Name        = each.value.name
  }
}
```
</details>

<details><summary>internet_gateway.tf</summary>

```hcl
resource "aws_internet_gateway" "client_vpn" {
  vpc_id = aws_vpc.client_vpn.id

  tags = {
    Name        = "client-vpn"
  }
}
```
</details>

<details><summary>nat_gateway.tf</summary>

```hcl
resource "aws_nat_gateway" "client_vpn" {
  for_each = var.vpn_client_natgw

  allocation_id = aws_eip.client_vpn[each.key].id
  subnet_id     = aws_subnet.client_vpn[each.key].id

  tags = {
    Name        = each.value.name
  }
}
```
</details>

<details><summary>route.tf</summary>

```hcl
locals {
  public-names = [
    "public-1a",
    "public-1c"
  ]
}

resource "aws_route" "client_vpn_public" {
  for_each = toset(local.public-names)

  route_table_id         = aws_route_table.client_vpn[each.key].id
  gateway_id             = aws_internet_gateway.client_vpn.id
  destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route" "client_vpn_private" {
  for_each = var.vpn_client_nat_route_assoc

  route_table_id         = aws_route_table.client_vpn[each.key].id
  nat_gateway_id         = aws_nat_gateway.client_vpn[each.value.natgw-name].id
  destination_cidr_block = "0.0.0.0/0"
}
```
</details>

<details><summary>route_table.tf</summary>

```hcl
resource "aws_route_table" "client_vpn" {
  for_each = var.client_vpn_subnets
  vpc_id   = aws_vpc.client_vpn.id

  tags = {
    Name        = each.value.name
  }
}
```
</details>

<details><summary>route_table_association.tf</summary>

```hcl
resource "aws_route_table_association" "client_vpn" {
  for_each = var.client_vpn_subnets

  subnet_id      = aws_subnet.client_vpn[each.key].id
  route_table_id = aws_route_table.client_vpn[each.key].id
}
```
</details>

<details><summary>security_group.tf</summary>

```hcl
resource "aws_security_group" "client_vpn" {
  name   = "client-vpn"
  vpc_id = aws_vpc.client_vpn.id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name        = "client-vpn"
  }
}

resource "aws_security_group_rule" "client_vpn_80" {
  type        = "ingress"
  from_port   = 80
  to_port     = 80
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]

  security_group_id = aws_security_group.client_vpn.id
}

resource "aws_security_group_rule" "client_vpn_443" {
  type        = "ingress"
  from_port   = 443
  to_port     = 443
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]

  security_group_id = aws_security_group.client_vpn.id
}
```
</details>

<details><summary>subnet.tf</summary>

```hcl
resource "aws_subnet" "client_vpn" {
    for_each = var.client_vpn_subnets
  
    vpc_id            = aws_vpc.client_vpn.id
    availability_zone = each.value.zone
  
    cidr_block              = each.value.cidr
    map_public_ip_on_launch = each.value.launch
  
    tags = {
      Name        = each.value.name
    }
  }
```
</details>

<details><summary>vpc.tf</summary>

```hcl
resource "aws_vpc" "client_vpn" {
    cidr_block       = "10.0.0.0/16"
    instance_tenancy = "default"
  
    tags = {
      Name        = "client-vpn"
    }
  }
```
</details>

<details><summary>variables.tf</summary>

```hcl
variable "vpn_client_nat_route_assoc" {
    type = map(any)
  
    default = {
      private-1a = {
        natgw-name = "public-1a"
      }
      private-1c = {
        natgw-name = "public-1c"
      }
    }
  }
  
  variable "vpn_client_natgw" {
    type = map(any)
  
    default = {
      public-1a = {
        name = "client-vpn-public-1a"
      }
      public-1c = {
        name = "client-vpn-public-1c"
      }
    }
  }
  
  variable "client_vpn_subnets" {
    type = map(any)
  
    default = {
      public-1a = {
        cidr   = "10.0.0.0/24"
        zone   = "ap-northeast-1a"
        launch = "true"
        name   = "client-vpn-public-1a"
      }
      public-1c = {
        cidr   = "10.0.1.0/24"
        zone   = "ap-northeast-1c"
        launch = "true"
        name   = "client-vpn-public-1c"
      }
      private-1a = {
        cidr   = "10.0.2.0/24"
        zone   = "ap-northeast-1a"
        launch = "false"
        name   = "client-vpn-private-1a"
      }
      private-1c = {
        cidr   = "10.0.3.0/24"
        zone   = "ap-northeast-1c"
        launch = "false"
        name   = "client-vpn-private-1c"
      }
    }
  }
  
  variable "client_vpn_cloudwatch_metrics" {
    type = map(any)
  
    default = {
      ActiveConnectionsCount = {
        alarm_name          = "cvpn-endpoint-******************-ActiveConnectionsCount"
        metric_name         = "ActiveConnectionsCount"
        statistic           = "Sum"
        threshold           = "100"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "クライアント VPN エンドポイントへのアクティブな接続数"
      }
      AuthenticationFailures = {
        alarm_name          = "cvpn-endpoint-******************-AuthenticationFailures"
        metric_name         = "AuthenticationFailures"
        statistic           = "Sum"
        threshold           = "10"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "クライアント VPN エンドポイントの認証失敗数"
      }
      CrlDaysToExpiry = {
        alarm_name          = "cvpn-endpoint-******************-CrlDaysToExpiry"
        metric_name         = "CrlDaysToExpiry"
        statistic           = "Sum"
        threshold           = "30"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "クライアント VPN エンドポイントで設定されている証明書失効リスト (CRL) の有効期限が切れるまでの日数"
      }
      SelfServicePortalClientConfigurationDownloads = {
        alarm_name          = "cvpn-endpoint-******************-SelfServicePortalClientConfigurationDownloads"
        metric_name         = "SelfServicePortalClientConfigurationDownloads"
        statistic           = "Sum"
        threshold           = "100"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "セルフサービスポータルからの Client VPN エンドポイント設定ファイルのダウンロード数"
      }
      ClientConnectHandlerTimeouts = {
        alarm_name          = "cvpn-endpoint-******************-ClientConnectHandlerTimeouts"
        metric_name         = "ClientConnectHandlerTimeouts"
        statistic           = "Sum"
        threshold           = "100"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "クライアント VPN エンドポイントへの接続用のクライアント接続ハンドラの呼び出し時のタイムアウト数"
      }
      ClientConnectHandlerInvalidResponses = {
        alarm_name          = "cvpn-endpoint-******************-ClientConnectHandlerInvalidResponses"
        metric_name         = "ClientConnectHandlerInvalidResponses"
        statistic           = "Sum"
        threshold           = "100"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "クライアント VPN エンドポイントへの接続用のクライアント接続ハンドラから返された無効な応答の数"
      }
      ClientConnectHandlerOtherExecutionErrors = {
        alarm_name          = "cvpn-endpoint-******************-ClientConnectHandlerOtherExecutionErrors"
        metric_name         = "ClientConnectHandlerOtherExecutionErrors"
        statistic           = "Sum"
        threshold           = "100"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "クライアント VPN エンドポイントへの接続用のクライアント接続ハンドラの実行中に発生した、予期されていなかったエラーの数"
      }
      ClientConnectHandlerThrottlingErrors = {
        alarm_name          = "cvpn-endpoint-******************-ClientConnectHandlerThrottlingErrors"
        metric_name         = "ClientConnectHandlerThrottlingErrors"
        statistic           = "Sum"
        threshold           = "100"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "クライアント VPN エンドポイントへの接続用のクライアント接続ハンドラの呼び出し時のスロットリングエラーの数"
      }
      ClientConnectHandlerDeniedConnections = {
        alarm_name          = "cvpn-endpoint-******************-ClientConnectHandlerDeniedConnections"
        metric_name         = "ClientConnectHandlerDeniedConnections"
        statistic           = "Sum"
        threshold           = "100"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "クライアント VPN エンドポイントへの接続用のクライアント接続ハンドラによって拒否された接続の数"
      }
      ClientConnectHandlerFailedServiceErrors = {
        alarm_name          = "cvpn-endpoint-******************-ClientConnectHandlerFailedServiceErrors"
        metric_name         = "ClientConnectHandlerFailedServiceErrors"
        statistic           = "Sum"
        threshold           = "100"
        comparison_operator = "GreaterThanOrEqualToThreshold"
        alarm_description   = "クライアント VPN エンドポイントへの接続用のクライアント接続ハンドラの実行中に発生したサービス側のエラーの数"
      }
    }
  }
```
</details>

<details><summary>main.tf</summary>

```hcl
provider "aws" {
  region  = "ap-northeast-1"

  default_tags {
    tags = {
    Service     = "client-vpn"
    Description = "Managed by Terraform"
    }
  }
}

terraform {
  required_version = ">= 0.13"

  backend "s3" {
    bucket  = "***********"
    key     = "network/terraform.tfstate"
    region  = "ap-northeast-1"
  }
}

data "aws_acm_certificate" "issued" {
  domain   = "server"
  statuses = ["ISSUED"]
}
```
</details>

## VPN Client
- AWS Client VPN Self-Service Portalにログインし、接続設定用ファイルと使用OS毎にClientVPNアプリケーションをダウンロードする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/86c7ae2c-93a0-7d37-840a-08b5a3149097.png)


- 設定手順
  - [AWS Client VPN for Windows](https://docs.aws.amazon.com/ja_jp/vpn/latest/clientvpn-user/client-vpn-connect-windows.html#client-vpn-connect-windows-connecting)
    - 3.[Add Profile (プロファイルの追加)] を選択します。の個所で保存したプロファイルを指定する
  - [AWS Client VPN for macOS](https://docs.aws.amazon.com/ja_jp/vpn/latest/clientvpn-user/client-vpn-connect-macos.html#client-vpn-connect-macos-connecting)
    - 3.[Add Profile (プロファイルの追加)] を選択します。の個所で保存したプロファイルを指定する
  - [Linux の AWS Client VPN](https://docs.aws.amazon.com/ja_jp/vpn/latest/clientvpn-user/client-vpn-connect-linux.html#client-vpn-connect-linux-connecting)
    - 3.[Add Profile (プロファイルの追加)] を選択します。の個所で保存したプロファイルを指定する

  - IPv6リーク対策のため、プロファイルの最終行に記述
```federation-auth-config.ovpn
ifconfig-ipv6 fd15:53b6:dead::2 fd15:53b6:dead::1
route-ipv6 2000::/4
```

- 確認
  - [使用中IP](https://www.cman.jp/network/support/go_access.cgi)がNatGatewayに使用しているEIPであることが確認できればOKです