こんにちは、広野です。
EC2 に関する初投稿です。実は私は AWS で初めてつくったリソースが OpenVPN サーバでした。オンプレで VPN 環境を構築していた知識をそのままに EC2 で作っただけでしたが。当時は EC2 が持つ IP アドレスの特性に衝撃を受けたものでした。
さて、先日そんな思い出のある VPN サーバをリニューアルする機会がありまして、そのタイミングで IaC 化しましたので紹介します。紹介する AWS CloudFormation テンプレートは簡易化していますが、テンプレートを流せば一撃で VPN 内にアクセスできる OpenVPN サーバが出来上がります。
完成品はこの後説明する設計思想に基づいて作られていますので、適宜カスタマイズしてご利用頂けましたら嬉しいです。
やりたかったこと
- AWS VPC 内のリソースにリモートから VPN で自由にアクセスしたい。
- 接続元ネットワークからは固定の IP アドレスにしか VPN 通信できない制約がある。(AWS VPN で実現できなくはないかもしれないが、トリッキーな構成になりそうで嫌)
- VPC 側も接続元ソース IP アドレスを限定したい。(セキュリティグループで指定)
こりゃ Elastic IP で固定グローバル IP アドレスを付与した EC2 インスタンスで VPN サーバ作るしかないな!となりました。
それ以外にやりたかったことは以下。
- 手作業で構築するのは面倒なので AWS CloudFormation だけで動く状態にまで持っていきたい。
- VPN サーバ内で作成した証明書、鍵ファイルを SCP 等で取得するのは面倒、S3経由で受け取りたい。
前提事項
- VPC は一般的な構成で完成している。
- Elastic IP も取得までは終わっている。(未割当)
- キーペアは既存のものを使用する。
上記も AWS CloudFormation に混ぜ込むことはできますが、VPC やキーペアは既に存在していることが多そうですし、Elastic IP は今後同じアドレスで使い回しすることがありそうなので、テンプレートには含めませんでした。
アーキテクチャ
こんな設計でつくってみました。IP アドレスは例です。
EC2 的な設定は以下のようにします。
- OS は Amazon Linux 2 とする。AMI イメージは自動取得。
- 送信元/送信先チェックを無効にする。
- ユーザーデータを使って EC2 初回立ち上げ時に OpenVPN を構築する。
- ユーザからの通信はソース IP アドレスが OpenVPN サーバのプライベート IP アドレスに NAT されるため、通信先のリソースにセキュリティグループによるアクセス制御をかける場合は OpenVPN サーバのセキュリティグループからの通信を許可する設定を入れておくこと。
OpenVPN 的な設定は、以下のようにします。
- 証明書による認証。証明書は複数人で共用可能。
- VPN 接続したクライアントの通信は全て OpenVPN サーバ経由とする。インターネット接続さえも。
その弊害として、ユーザは VPN 接続中は接続元ネットワーク内のリソース(例えば社内ポータル等)にアクセス不可能となる。
これを避けるために VPN 接続に流す通信を分ける「スプリットトンネル」にする手法は本記事後半で紹介する。 - その他設定はデフォルト。
AWS CloudFormation テンプレート
補足説明はテンプレート内にコメントします。
AWSTemplateFormatVersion: "2010-09-09" Description: CloudFormation template that creates an EC2 instance, a S3 bucket, an EC2 Security Group, an IAM role and an EIP association for OpenVPN. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System name. (e.g. AAA) Default: AAA MaxLength: 10 MinLength: 1 GroupName: Type: String Description: Group name. (e.g. OVPN) Default: OVPN MaxLength: 30 MinLength: 1 HostName: Type: String Description: Host name. (e.g. OVPN) Default: OVPN MaxLength: 30 MinLength: 1 S3BucketName: Type: String Description: System name - Host name in lower case. (e.g. aaa-ovpn) Default: aaa-ovpn MaxLength: 50 MinLength: 1 EipAllocationId: Type: String Default: eipalloc-xxxxxxxxxxxxxxxxx Description: Fill an Elastic IP allocation ID you allocate with the EC2 instance. VpcId: Type: AWS::EC2::VPC::Id Description: Choose a existing VPC ID you deploy the EC2 instance in. InstanceSubnet: Type: AWS::EC2::Subnet::Id Description: Choose a existing Subnet ID you deploy the EC2 instance in. ImageID: Type: AWS::SSM::Parameter::Value Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 Description: Choosed the latest Amazon Linux 2 AMI ID automatically. InstanceType: Type: String Default: t3.small KeyPairName: Type: AWS::EC2::KeyPair::KeyName Description: Choose a existing key pair you associate with the EC2. OpenVpnVersion: Type: String Default: 2.4.12 Description: Fill the latest OpenVPN version. 2.4.12 as of Oct 2022. ClientsVirtualNetworkAddress: Type: String Default: 172.16.179.0 Description: Fill the network address without a subnet mask. The fixed value /24 will be used in this template. Resources: # ------------------------------------------------------------# # EC2 # ------------------------------------------------------------# Ec2Instance: Type: AWS::EC2::Instance Properties: IamInstanceProfile: !Ref Ec2InstanceProfile ImageId: !Ref ImageID KeyName: !Ref KeyPairName InstanceType: !Ref InstanceType BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeType: gp2 VolumeSize: 30 SecurityGroupIds: - !Ref Ec2SecurityGroup SourceDestCheck: false SubnetId: !Ref InstanceSubnet Tags: - Key: Cost Value: !Ref SystemName - Key: Name Value: !Sub ${SystemName}-${HostName} # UserDataに、OpenVPNを構築するためのスクリプトが書かれています。EC2初回立ち上げ時のみ実行されます。 UserData: Fn::Base64: !Sub | #!/bin/bash -xe yum update -y --exclude=amazon-ssm-agent amazon-linux-extras install -y epel yum install -y openvpn easy-rsa --enablerepo=epel cd /usr/share/easy-rsa/3 ./easyrsa init-pki echo CA | ./easyrsa build-ca nopass ./easyrsa gen-dh ./easyrsa build-server-full server nopass ./easyrsa build-client-full client001 nopass cp /usr/share/doc/openvpn-${OpenVpnVersion}/sample/sample-config-files/server.conf /etc/openvpn/ sed -i "s/server 10.8.0.0 255.255.255.0/server ${ClientsVirtualNetworkAddress} 255.255.255.0/g" /etc/openvpn/server.conf sed -i "s/;duplicate-cn/duplicate-cn/g" /etc/openvpn/server.conf echo -e "\n\n" >> /etc/openvpn/server.conf echo push \"redirect-gateway def1\" >> /etc/openvpn/server.conf openvpn --genkey --secret /etc/openvpn/ta.key cp /usr/share/easy-rsa/3/pki/ca.crt /etc/openvpn/ cp /usr/share/easy-rsa/3/pki/issued/server.crt /etc/openvpn/ cp /usr/share/easy-rsa/3/pki/private/server.key /etc/openvpn/ cp /usr/share/easy-rsa/3/pki/dh.pem /etc/openvpn/dh2048.pem aws s3 cp /usr/share/easy-rsa/3/pki/ca.crt s3://ec2files-${S3BucketName}/ aws s3 cp /usr/share/easy-rsa/3/pki/private/client001.key s3://ec2files-${S3BucketName}/ aws s3 cp /usr/share/easy-rsa/3/pki/issued/client001.crt s3://ec2files-${S3BucketName}/ aws s3 cp /etc/openvpn/ta.key s3://ec2files-${S3BucketName}/ iptables -t nat -A POSTROUTING -s ${ClientsVirtualNetworkAddress}/24 -o eth0 -j MASQUERADE echo iptables -t nat -A POSTROUTING -s ${ClientsVirtualNetworkAddress}/24 -o eth0 -j MASQUERADE >> /etc/rc.d/rc.local chmod +x /etc/rc.d/rc.local echo net.ipv4.ip_forward = 1 >> /etc/sysctl.conf sysctl -p systemctl start openvpn@server systemctl enable openvpn@server DependsOn: - S3Bucket - Ec2InstanceProfile - Ec2SecurityGroup # ------------------------------------------------------------# # EC2 Security Group # ------------------------------------------------------------# Ec2SecurityGroup: Type: AWS::EC2::SecurityGroup Properties: VpcId: !Ref VpcId GroupName: !Sub SG-${SystemName}-${GroupName} GroupDescription: Allow OpenVPN access via port UDP 1194 and TCP 22 only # セキュリティグループは、特定のIPアドレスからのみVPN(1194)およびSSH(22)通信を許可する設定にしています。 SecurityGroupIngress: - CidrIp: 99.99.99.99/32 FromPort: 1194 IpProtocol: udp ToPort: 1194 - CidrIp: 99.99.99.99/32 FromPort: 22 IpProtocol: tcp ToPort: 22 Tags: - Key: Cost Value: !Ref SystemName - Key: Name Value: !Sub SG-${SystemName}-${GroupName} # ------------------------------------------------------------# # EC2 Role / Instance Profile (IAM) # ------------------------------------------------------------# Ec2Role: Type: AWS::IAM::Role Properties: RoleName: !Sub Ec2Role-${SystemName}-${GroupName} Description: This role allows EC2 instance to invoke S3, CloudWatch Logs and SSM. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore Policies: - PolicyName: !Sub Ec2S3Policy-${SystemName}-${GroupName} PolicyDocument: Version: "2012-10-17" Statement: - Action: - "s3:GetObject" - "s3:PutObject" - "s3:ListBucket" Resource: - !Sub "arn:aws:s3:::ec2files-${S3BucketName}*" Effect: Allow - Action: - "s3:PutObject" Resource: - "arn:aws:s3:::icmo-sessionmanager-logs-ap-northeast-1/*" Effect: Allow - Action: - "s3:GetEncryptionConfiguration" - "kms:Decrypt" - "kms:GenerateDataKey" - "logs:CreateLogStream" - "logs:PutLogEvents" - "logs:DescribeLogGroups" - "logs:DescribeLogStreams" Resource: "*" Effect: Allow Ec2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: InstanceProfileName: !Ref Ec2Role Path: / Roles: - !Ref Ec2Role DependsOn: - Ec2Role # ------------------------------------------------------------# # EIP Association # ------------------------------------------------------------# EIPAssociation: Type: AWS::EC2::EIPAssociation Properties: AllocationId: !Ref EipAllocationId InstanceId: !Ref Ec2Instance DependsOn: - Ec2Instance # ------------------------------------------------------------# # S3 # ------------------------------------------------------------# # このS3バケットに、OpenVPN構築により生成されたクライアント用証明書、鍵ファイルが保存されます。 S3Bucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ec2files-${S3BucketName} PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: Cost Value: !Ref SystemName # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: # S3 Ec2S3BucketName: Value: !Ref S3Bucket
スタック作成後の確認
EC2 インスタンスはすぐにプロビジョニング完了しますが、しばらくはユーザーデータに記載されたスクリプトが実行されています。その結果までは AWS CloudFormation は面倒見てくれません。初回作成時は、必ずスクリプトの実行結果にエラーがないか確認してください。実行結果ログは EC2 インスタンス内の以下にあります。
/var/log/cloud-init-output.log
ユーザーデータ内のスクリプト解説
OpenVPN を構築するためのスクリプトが書いてあるので、簡単に説明します。
yum update -y --exclude=amazon-ssm-agent amazon-linux-extras install -y epel yum install -y openvpn easy-rsa --enablerepo=epel
その時点で最新のパッチを適用しているのと、openvpn、および openvpn 構築に必要な easy-rsa をインストールしています。
cd /usr/share/easy-rsa/3 ./easyrsa init-pki echo CA | ./easyrsa build-ca nopass ./easyrsa gen-dh ./easyrsa build-server-full server nopass ./easyrsa build-client-full client001 nopass
easy-rsa で証明書や鍵ファイルを生成しています。
cp /usr/share/doc/openvpn-${OpenVpnVersion}/sample/sample-config-files/server.conf /etc/openvpn/ sed -i "s/server 10.8.0.0 255.255.255.0/server ${ClientsVirtualNetworkAddress} 255.255.255.0/g" /etc/openvpn/server.conf sed -i "s/;duplicate-cn/duplicate-cn/g" /etc/openvpn/server.conf echo -e "\n\n" >> /etc/openvpn/server.conf echo push \"redirect-gateway def1\" >> /etc/openvpn/server.conf
OpenVPN のサーバー側設定ファイルをサンプルからコピーし、コピーしたものを編集しています。
openvpn --genkey --secret /etc/openvpn/ta.key cp /usr/share/easy-rsa/3/pki/ca.crt /etc/openvpn/ cp /usr/share/easy-rsa/3/pki/issued/server.crt /etc/openvpn/ cp /usr/share/easy-rsa/3/pki/private/server.key /etc/openvpn/ cp /usr/share/easy-rsa/3/pki/dh.pem /etc/openvpn/dh2048.pem
OpenVPN のコマンドでもう1つ鍵ファイルを生成しています。
また、easy-rsa で生成した証明書や鍵ファイルを /etc/openvpn 配下にまとめるようコピーしています。
aws s3 cp /usr/share/easy-rsa/3/pki/ca.crt s3://ec2files-${S3BucketName}/ aws s3 cp /usr/share/easy-rsa/3/pki/private/client001.key s3://ec2files-${S3BucketName}/ aws s3 cp /usr/share/easy-rsa/3/pki/issued/client001.crt s3://ec2files-${S3BucketName}/ aws s3 cp /etc/openvpn/ta.key s3://ec2files-${S3BucketName}/
AWS CLI コマンドで、EC2 インスタンス内に生成されたクライアント用の証明書・鍵ファイルを S3 バケットに保存しています。
AWS CLI はデフォルトでバージョン 1 が EC2 インスタンスにインストールされています。
iptables -t nat -A POSTROUTING -s ${ClientsVirtualNetworkAddress}/24 -o eth0 -j MASQUERADE echo iptables -t nat -A POSTROUTING -s ${ClientsVirtualNetworkAddress}/24 -o eth0 -j MASQUERADE >> /etc/rc.d/rc.local chmod +x /etc/rc.d/rc.local echo net.ipv4.ip_forward = 1 >> /etc/sysctl.conf sysctl -p
EC2 インスタンスを NAT サーバにするためのコマンドを入れています。
2種類ありますが、両方ともインスタンス再起動時には無効になってしまうため、再起動後にも有効になるように設定ファイルに書き込んでいます。
systemctl start openvpn@server systemctl enable openvpn@server
最後に、OpenVPN を起動し、インスタンス再起動時に自動起動するよう設定しています。
クライアントからの VPN 接続
クライアントデバイスには、OpenVPN クライアントソフトをインストールします。
以下の設定ファイル内の IP アドレス部分 (remote 111.111.111.111 のところ) だけ、割り当てた Elastic IP アドレスに書き換えて使用します。
設定ファイルは OpenVPN クライアントにインポートし、S3 バケットに出力された証明書、鍵ファイル(全4つ)を設定ファイルと同じディレクトリに保存します。後は接続するだけです。証明書、鍵ファイルの取り扱いにはお気を付けください。
- 設定ファイル(拡張子は .ovpn にする)
############################################## # Sample client-side OpenVPN 2.0 config file # # for connecting to multi-client server. # # # # This configuration can be used by multiple # # clients, however each client should have # # its own cert and key files. # # # # On Windows, you might want to rename this # # file so it has a .ovpn extension # ############################################## # Specify that we are a client and that we # will be pulling certain config file directives # from the server. client # Use the same setting as you are using on # the server. # On most systems, the VPN will not function # unless you partially or fully disable # the firewall for the TUN/TAP interface. ;dev tap dev tun # Windows needs the TAP-Win32 adapter name # from the Network Connections panel # if you have more than one. On XP SP2, # you may need to disable the firewall # for the TAP adapter. ;dev-node MyTap # Are we connecting to a TCP or # UDP server? Use the same setting as # on the server. ;proto tcp proto udp # The hostname/IP and port of the server. # You can have multiple remote entries # to load balance between the servers. remote 111.111.111.111 # Choose a random host from the remote # list for load-balancing. Otherwise # try hosts in the order specified. ;remote-random # Keep trying indefinitely to resolve the # host name of the OpenVPN server. Very useful # on machines which are not permanently connected # to the internet such as laptops. resolv-retry infinite # Most clients don't need to bind to # a specific local port number. nobind # Downgrade privileges after initialization (non-Windows only) ;user nobody ;group nobody # Try to preserve some state across restarts. persist-key persist-tun # If you are connecting through an # HTTP proxy to reach the actual OpenVPN # server, put the proxy server/IP and # port number here. See the man page # if your proxy server requires # authentication. ;http-proxy-retry # retry on connection failures ;http-proxy [proxy server] [proxy port #] # Wireless networks often produce a lot # of duplicate packets. Set this flag # to silence duplicate packet warnings. ;mute-replay-warnings # SSL/TLS parms. # See the server config file for more # description. It's best to use # a separate .crt/.key file pair # for each client. A single ca # file can be used for all clients. ca ca.crt cert client001.crt key client001.key # Verify server certificate by checking # that the certicate has the nsCertType # field set to "server". This is an # important precaution to protect against # a potential attack discussed here: # http://openvpn.net/howto.html#mitm # # To use this feature, you will need to generate # your server certificates with the nsCertType # field set to "server". The build-key-server # script in the easy-rsa folder will do this. ;ns-cert-type server remote-cert-tls server # If a tls-auth key is used on the server # then every client must also have the key. tls-auth ta.key 1 # Select a cryptographic cipher. # If the cipher option is used on the server # then you must also specify it here. cipher AES-256-CBC # Enable compression on the VPN link. # Don't enable this unless it is also # enabled in the server config file. ;comp-lzo # Set log file verbosity. verb 3 # Silence repeating messages ;mute 20 auth-nocache
スプリットトンネルにしたいとき
VPN 接続先 (ここでは AWS VPC 内) に流す通信を限定したいときは、OpenVPN の設定を編集する必要があります。
AWS CloudFormation テンプレート内の EC2 ユーザーデータに書かれているスクリプトを書き換えてスタックを作成することになります。AWS というよりは OpenVPN の世界の話です。
意味的には、指定したネットワークへの通信だけ VPN に流すようルーティングするということです。
- スクリプト変更箇所抜粋(変更前)
echo push \"redirect-gateway def1\" >> /etc/openvpn/server.conf
- スクリプト変更箇所抜粋(変更後)
echo push \"route 10.0.1.0 255.255.255.0\" >> /etc/openvpn/server.conf echo push \"route 10.0.2.0 255.255.255.0\" >> /etc/openvpn/server.conf echo push \"route 10.0.101.0 255.255.255.0\" >> /etc/openvpn/server.conf echo push \"route 10.0.102.0 255.255.255.0\" >> /etc/openvpn/server.conf
本記事の例では、VPC 内に存在するサブネットの分だけ記述しています。VPC に割り当てた、まるっと大きなネットワーク全てにルーティングするのでもかまいません。
まとめ
いかがでしたでしょうか?
実運用ではセキュリティを会社のルール等に従って強化して頂く必要はあると思いますが、VPN の基本的な機能は簡単に実装頂けると思います。価格的にも、使わないときは停止しておけば結構安上がりだと思っております。
本記事が皆様のお役に立てれば幸いです。