目次
- 構成図
-
VPN構築
2.1. サーバー証明書とクライアント証明書を発行し、ACMにインポートする
2.2. TerraformでVPN構築 -
クライアントサイドの接続方法
3.1. VPN接続プロファイルの準備
3.2. AWS-Client-VPNをインストールし、プロファイルを登録する
3.3. 接続先のプロファイルを選択して「接続」 -
クライアント管理
4.1. クライアント追加
4.2. クライアント失効 -
証明書更新
5.1. サーバー証明書更新
5.2. クライアント証明書更新 - 費用節約のために
1. 構成図
構成上のポイント
- VPNの認証方式は相互認証
- プロジェクト要件としてVPN接続時にインターネット接続可能とし、かつインターネット接続する際のIPは固定にする必要があった。そのため、Public SubnetにNAT GWを配置し、NAT GWにElasticIPをアタッチする構成とした
- Win端末のメンバーとMac端末のメンバー両方がいた。Win端末のみVPC内部リソースのドメイン解決をできない事象が発生した。VPNエンドポイントのDNSサーバー設定に接続するVPCのDNSサーバーのリザーブドIP(VPC CIDR末尾2)を登録することで解消した
2. VPN構築
2.1. サーバー証明書とクライアント証明書を発行し、ACMにインポートする
以下の手順はAWSサイトを参考にした
サーバーおよびクライアント証明書の有効期限はデフォルトで約2年。クライアントが少ない場合は都度更新でもあまり手間がかからないかもしれないが、クライアントが多い場合は更新作業が手間なので、有効期限を長く設定しておく方が管理は楽。ただし、証明書の失効管理を適切に行い、暗号化アルゴリズムが長い期間の経過により陳腐化していないか注意する必要がある。
下記の有効期限を変更する設定は、こちらのサイトを参考にした。
git clone https://github.com/OpenVPN/easy-rsa.git
cd easy-rsa/easyrsa3
### 有効期限をデフォルトから変更したい場合は実施 ###
cp vars.example vars
echo "set_var EASYRSA_CA_EXPIRE 36500" >> vars
echo "set_var EASYRSA_CERT_EXPIRE 36500" >> vars
echo "set_var EASYRSA_CRL_DAYS 36500" >> vars
mv vars pki/
############################################
./easyrsa init-pki
./easyrsa build-ca nopass
./easyrsa build-server-full server nopass
./easyrsa build-client-full client001.domain.tld nopass
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/client001.domain.tld.crt ~/custom_folder
cp pki/private/client001.domain.tld.key ~/custom_folder
cd ~/custom_folder
aws acm import-certificate --certificate fileb://server.crt --private-key fileb://server.key --certificate-chain fileb://ca.crt
#サーバー証明書とクライアント証明書のCAが同じなので以下は省略してもOK
aws acm import-certificate --certificate fileb://client001.domain.tld.crt --private-key fileb://client001.domain.tld.key --certificate-chain fileb://ca.crt
2.2. TerraformでVPN構築
下記のコードはTerraformコードからVPNの箇所だけ抜粋したもの。
そのうち全量コードのgithubリンクを貼るかもしれません。
data "aws_acm_certificate" "server_certificate" {
domain = "server"
}
resource "aws_ec2_client_vpn_endpoint" "main" {
for_each = local.vpne
description = each.value.description
server_certificate_arn = each.value.server_certificate_arn
client_cidr_block = each.value.client_cidr_block
dns_servers = each.value.dns_servers
authentication_options {
type = each.value.authentication_options.type
root_certificate_chain_arn = each.value.authentication_options.root_certificate_chain_arn
}
connection_log_options {
enabled = each.value.connection_log_options.enabled
cloudwatch_log_group = each.value.connection_log_options.cloudwatch_log_group
cloudwatch_log_stream = each.value.connection_log_options.cloudwatch_log_stream
}
tags = {
Name = each.value.name
}
}
resource "aws_ec2_client_vpn_network_association" "main" {
for_each = local.vpn_network_association
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.main[each.value.vpne].id
subnet_id = aws_subnet.main[each.value.subnet].id
lifecycle {
ignore_changes = [subnet_id]
}
}
resource "aws_ec2_client_vpn_route" "main" {
for_each = local.vpn_route
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.main[each.value.client_vpn_endpoint].id
destination_cidr_block = each.value.destination_cidr_block
target_vpc_subnet_id = aws_subnet.main[each.value.target_vpc_subnet].id
depends_on = [
aws_ec2_client_vpn_network_association.main
]
}
resource "aws_ec2_client_vpn_authorization_rule" "main" {
for_each = local.vpn_auth
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.main[each.value.client_vpn_endpoint].id
target_network_cidr = each.value.target_network_cidr
authorize_all_groups = each.value.authorize_all_groups
depends_on = [
aws_ec2_client_vpn_network_association.main
]
}
locals {
vpne = {
example01 = {
name = "${var.prefix}-vpne-apne1-01"
description = "client vpn endpoint to connect example VPC"
server_certificate_arn = data.aws_acm_certificate.server_certificate.arn
client_cidr_block = var.cidr_blocks.client_vpn
dns_servers = [var.ip_addresses.dns]
authentication_options = {
type = "certificate-authentication"
root_certificate_chain_arn = data.aws_acm_certificate.server_certificate.arn
}
connection_log_options = {
enabled = true
cloudwatch_log_group = aws_cloudwatch_log_group.vpn.name
cloudwatch_log_stream = aws_cloudwatch_log_stream.vpn.name
}
}
}
vpn_network_association = {
example-pri-a = {
vpne = "example01"
subnet = "example-pri-a"
}
}
vpn_route = {
example01 = {
client_vpn_endpoint = "example01"
destination_cidr_block = var.cidr_blocks.global
target_vpc_subnet = "example-pri-a"
}
peering01 = {
client_vpn_endpoint = "example01"
destination_cidr_block = var.cidr_blocks.peering01
target_vpc_subnet = "example-pri-a"
}
}
vpn_auth = {
vpc = {
client_vpn_endpoint = "example01"
target_network_cidr = aws_vpc.main["example01"].cidr_block
authorize_all_groups = true
}
internet = {
client_vpn_endpoint = "example01"
target_network_cidr = var.cidr_blocks.global
authorize_all_groups = true
}
peering01 = {
client_vpn_endpoint = "example01"
target_network_cidr = var.cidr_blocks.peering01
authorize_all_groups = true
}
}
}
3. クライアントサイドの接続方法
3.1. VPN接続プロファイルの準備
3.1.1. マネコンからクライアント設定をダウンロード
VPC > クライアントVPNエンドポイント > 対象のエンドポイント
を開いて、「クライアント設定をダウンロード」
3.1.2. ダウンロードしたクライアント設定の末尾にクライアント証明書および鍵の情報を追記する
(方法A) 手順2.1で発行したクライアント証明書およびクライアント証明書鍵の中身を以下の形式でダウンロードした設定ファイルの末尾に追記する
<cert>
クライアント証明書の中身
</cert>
<key>
クライアント証明書鍵の中身
</key>
毎回手動でやるのが面倒なので、下記のようにシェル化していた
CLIENT=$1
cp -p example-vpn-client-config-base.ovpn example-vpn-client-config-${CLIENT}.ovpn
echo '<cert>' >> example-vpn-client-config-${CLIENT}.ovpn
cat client${CLIENT}.domain.tld.crt >> example-vpn-client-config-${CLIENT}.ovpn
echo '</cert>' >> example-vpn-client-config-${CLIENT}.ovpn
echo '<key>' >> example-vpn-client-config-${CLIENT}.ovpn
cat client${CLIENT}.domain.tld.key >> example-vpn-client-config-${CLIENT}.ovpn
echo '</key>' >> example-vpn-client-config-${CLIENT}.ovpn
(方法B) 手順2.1で発行したクライアント証明書およびクライアント証明書鍵を連携し、ダウンロードしたクライアント設定ファイルの末尾に以下のように追記する
cert <クライアント証明書のフルパス>
key <クライアント証明書鍵のフルパス>
こちらの場合証明書や鍵の場所が移動してしまった場合やファイル権限などの問題で、エラーとなってしまうケースもあるので、個人的には方法Aの方がお勧め
3.2. AWS Client VPNをインストールし、プロファイルを登録する
こちらからAWS Client VPNをダウンロードし、インストール
手順3.1.2で作成したプロファイルを登録する
3.3. 接続先のプロファイルを選択して「接続」
プルダウンから接続先のプロファイルを選んで接続する
4. クライアント管理
4.1. クライアント追加
メンバー追加などによりクライアントを追加したい場合はクライアント証明書を以下の手順で発行する
./easyrsa build-client-full clientXXX.domain.tld nopass
cp pki/issued/clientXXX.domain.tld.crt ~/custom_folder
cp pki/private/clientXXX.domain.tld.key ~/custom_folder
4.2. クライアント失効
メンバー離任などによりクライアントを失効させたい場合はクライアント証明書を以下の手順でリボークする
FULL_PATH_OF_CRL_PEMはcrl.pemファイルのフルパスが入る。クローンしたフォルダ下の easy-rsa/easyrsa3/pki/crl.pem
に存在する。
./easyrsa revoke clientXXX.domain.tld
./easyrsa gen-crl
aws ec2 import-client-vpn-client-certificate-revocation-list --certificate-revocation-list file://${FULL_PATH_OF_CRL_PEM} --client-vpn-endpoint-id ${VPN_ENDPOINT_ID} --region ${REGION}
5. 証明書更新
5.1. サーバー証明書更新
サーバー証明書更新の手順は以下を参考にした。
Easy-RSAのバージョンによってrenewコマンドがサポートされるかどうかが異なる。./easyrsa help
でサポートされているコマンドを確認できる。
デフォルトでは失効30日以内の証明書しか更新できないため、それよりも前に更新したい場合は、EASYRSA_CERT_RENEWの値を変更する
export EASYRSA_CERT_RENEW=365
また、証明書の有効期限は以下のコマンドで確認できる
openssl x509 -text -noout -in ${cert_file}
# renewがサポートされている場合
./easyrsa renew server nopass
# renewがサポートされていない場合
./easyrsa expire server
./easyrsa --san=DNS:server sign-req server server
mkdir ~/custom_folder2
cp pki/ca.crt ~/custom_folder2/
cp pki/issued/server.crt ~/custom_folder2/
cp pki/private/server.key ~/custom_folder2/
cd ~/custom_folder2/
aws acm import-certificate --certificate fileb://server.crt --private-key fileb://server.key --certificate-chain fileb://ca.crt --certificate-arn arn:aws:acm:${region}:${accout_id}:certificate/${certificate_id}
5.2. クライアント証明書更新
# renewがサポートされている場合
./easyrsa renew clientXXX.domain.tld nopass
# renewがサポートされていない場合(すみませんが、推測です。。。)
./easyrsa expire clientXXX.domain.tld
./easyrsa --san=DNS:clientXXX.domain.tld sign-req clientXXX.domain.tld clientXXX.domain.tld
mkdir ~/custom_folder2
cp pki/issued/clientXXX.domain.tld.crt ~/custom_folder2
cp pki/private/clientXXX.domain.tld.key ~/custom_folder2
更新後には手順3に記載のVPN接続プロファイルを更新する必要あり
6. 費用節約のために
使用しない時はNATおよびVPN Network Associationを削除し、使う時に再度terraformを実行して再構築すると多少節約になる。
担当したプロジェクトではCodeBuildでterraform実行できるように整備し、EventBridgeから定時でNATおよびVPN Network Associationの構築/削除を行なっていた。