OpenVPN サーバを AWS CloudFormation 一撃で立ち上げる

こんにちは、広野です。

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 クライアントソフトをインストールします。

OpenVPN.JP
OpenVPNはオープンソースのVPNソリューションです。Windows/MacOS/LinuxなどのPCはもちろん、iOSやAndroidなどのスマートデバイスでもお使いいただけます。

以下の設定ファイル内の 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 の基本的な機能は簡単に実装頂けると思います。価格的にも、使わないときは停止しておけば結構安上がりだと思っております。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもAWSすげー!って気持ちの方が強いです。
取得資格:AWS 認定は12資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2023 Japan AWS Top Engineer 受賞
2022 - 2023 Japan AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
AWSクラウドクラウドセキュリティソリューション
シェアする
タイトルとURLをコピーしました