AWS Lambda(Public)からRDS(Public)へのデータ送信をIP制限してセキュリティをあげてみる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、せーのです。今日はLambdaからRDSへのデータ送信ソリューションのセキュリティについて考えた解決案をひとつご紹介したいと思います。

課題

今回考えたのはこのようなソリューションです。

case1

外から何かしらのデータが飛んできて、それを元にLambdaを動かし、VPC内にあるRDSへそのデータを格納、蓄積する、というソリューションです。よくありますね。
実際はLambdaを直接叩くケースはなく、手前にAPI GatewayやAWS IoTあたりが置かれることが多いかと思いますが、今回は単純化でこのようにしました。

さてこのような場合に気になるのはRDSのセキュリティです。LambdaがPublicにあるためここからデータを流すにはRDSもPublic Accessを許可しなくてはいけません。ただRDSでPublic Accessを許可すると万が一Host, User ID, Passwordがセットで漏れてしまった場合誰でもアクセス出来ることになってしまいます。それはセキュリティ的にちょっと不安ですね。

こういう場合一般的な解決法としてはLambdaをVPC内に入れてしまう、というものがあります。

iprestrict-1

こうすることでRDSのPublic Accessを許可する必要がなくなり、VPCという閉じた空間の中でデータを送受信することができます。というわけで解決です。ありがとうございました。

とはなかなかいかないわけです。何故ならVPC内に入れたLambdaは起動が遅くなる場合があることが知られているからです。

[Lambda] VPC Lambdaの起動タイムラグを測定してみた|クラスメソッドブログ

この実験によるとVPC内に配置したLambdaは初回起動や実行環境の切替時に10秒前後遅延する、とのこと。「10秒くらい何か問題あるの?セキュリティの方が全然大事じゃないか」という方は迷わずVPCに入れてしまうことをお勧めします。問題は「10秒遅れるのはさすがにまずい。。。」というケースの場合です。例えば1秒に数回の頻度でデータを送り続けるようなIoTソリューションの場合、10秒Lambdaが動かない、ということは10秒分のデータがロストしてしまう可能性があります。そこでKinesis等を間にはさんでロストを防ぐのですが、リアルタイムでグラフ表示したい、というような要件が重なった場合、そもそも10秒遅延することが許されない事になります。

10秒以上遅延するのはまずい、でもRDSをPublicで晒すのもちょっと怖い。さあ、どうしましょう。

解決案

そこで考えたのが今回のソリューションです。要はLambdaで使用するIPからのアクセスのみにRDSをセキュリティグループで制限してしまおう、ということです。

iprestrict-2

これなら万が一接続情報が漏れた場合でもLambdaを立ち上げないとアクセス出来ないことになります。LambdaからアクセスをしたのであればAWSにてログが残りますので悪意ある攻撃があった場合にもAWSに問い合わせる等して攻撃者が特定できそうです。何となくセキュリティがあがりそうですね!

こちらの方法は「AWS全てのLambda / EC2のIPにアクセス範囲を限定する」方法となります。お使いのアカウント内のLambda / EC2のIPに絞るものではありませんのでご注意下さい。

でも「Lambdaで使用するIP」ってどうやって知るのでしょう。実はAWSはAWSで使用しているIPレンジを公開しています。

新しく更新されたAWSのIPレンジをAmazon SNSで通知してもらう|クラスメソッドブログ

Lambdaは"EC2"というカテゴリ内のIPを使用します。これを利用してIPが変更されたらSNSから新たにLambdaを叩いてセキュリティグループを更新する、という形をとることでセキュリティを保ちます。

iprestrict-3

それではやってみましょう。

やってみた

まずはVPC内にRDSを建てます。今回はmySqlで建ててみます。

iprestrict-4

サブネットはインターネットにも接続できるようにIGWがルーティングされているものを使用し、Publicly AccessをYesとします。

iprestrict-6

次にセキュリティグループを作ってRDSに設定します。まずはフルアクセス可能な感じで作ります。

iprestrict-5

クライアントからRDSにアクセスし、適当にテーブルを作ります。

iprestrict-7

iprestrict-8

次にこのテーブルにデータを入れるLambda Functionを作ります。mySqlにアクセスするのnpm-mySqlを使用するので手元でコードを書いてZip化してアップロードします。

var mysql = require('mysql');

var pool = mysql.createPool({
    host     : 'test.cdy2hmdijrau.ap-northeast-1.rds.amazonaws.com',
    user     : 'test',
    password : 'XXXXXXXXXXX',
    port     : '3306',
    database : 'test'
});

console.log('Loading function');

exports.handler = function(event, context){

    console.log('Begin Process');
    console.log('Connected');

    pool.getConnection(function(err, conn) {

        function finish_handler(err) {
          if(err) {
            conn.release();
            console.error(err);
            context.fail(err);
          } else {
            conn.release();
            context.succeed(`Successfully processed  records.`);
          }
        }

        conn.query('INSERT INTO test SET ?', {val1: 1, val2: 2}, function(err, result) {
                if (err) {
                  finish_handler(err);
                } else {
                  console.log(result.insertId);
                  finish_handler(err);
                }
            });
    });
};
npm install mysql
zip -r test.zip index.js node_modules/

iprestrict-9

ためしにテストしてみます。

iprestrict-10

iprestrict-11

成功したようです。クライアントツールから直接データを見てみます。

iprestrict-12

データが入っています。疎通が確認できました。

次にAWSのPublic IPレンジによる縛りをかけます。まずはSNSでAWSのPublic IPレンジを受け取ります。詳しくはこちらの記事を参考にして下さい。

iprestrict-13

マネージメントコンソールの仕様が若干変わっているようなのでまずは空のLambdaを作って、SNSのSubscriptionsからつなげます。SNSはus-east-1リージョンから行って下さい。

iprestrict-14

Lambda内にてPublic Rangeを取得してセキュリティグループを書き換えるFunctionを書きます。

var http = require('https');
var urlpath = 'https://ip-ranges.amazonaws.com/ip-ranges.json';

console.log('Loading function');

var AWS = require('aws-sdk');
var ec2 = new AWS.EC2();

var describeparam={
    GroupIds: [
    'sg-XXXXXXX'
  ]
}

var authorizeParam={
    GroupId: 'sg-XXXXXXX',
    IpPermissions: [{
                        IpProtocol: 'tcp',
                        FromPort: '3306',
                        ToPort: '3306',
                        IpRanges: []
                    }]
}

var revokeParam={
    GroupId: 'sg-XXXXXXX',
    IpPermissions: [{
                        IpProtocol: 'tcp',
                        FromPort: '3306',
                        ToPort: '3306',
                        IpRanges: []
                    }]
}

exports.handler = (event, context, callback) => {
    var val;
    http.get(urlpath, (res) => {
      var body = '';
      res.setEncoding('utf8');
    
      res.on('data', (chunk) => {
          body += chunk;
      });
    
      res.on('end', (res) => {
          res = JSON.parse(body);
          var authorizeIps=[];
          val = res.prefixes.filter(function(item, index){
              if (item.service == 'EC2' && item.region == 'ap-northeast-1') return true;
            });
            console.log("json length: " + val.length);
           for (var i in val) {
            authorizeIps[i]=val[i].ip_prefix;
          }
          
          var revokeIps=[];
          var d=0;
          ec2.describeSecurityGroups(describeparam, function(err, data) {
              if (err) {console.log(err, err.stack); // an error occurred
              } else  {   
                for (var i2 in data.SecurityGroups[0].IpPermissions[0].IpRanges) {
                    
                    d = authorizeIps.indexOf(data.SecurityGroups[0].IpPermissions[0].IpRanges[i2].CidrIp);
                    if (d < 0){
                        revokeIps[i2]=data.SecurityGroups[0].IpPermissions[0].IpRanges[i2].CidrIp
                    } else {
                        authorizeIps.splice(d, 1);
                        
                    }
                }
                
                if (authorizeIps.length > 0){
                    
                    for (var i3 in authorizeIps){
                        var ipPerm={};
                        ipPerm.CidrIp = authorizeIps[i3];
                        authorizeParam.IpPermissions[0].IpRanges.push(ipPerm);
                    }
                    console.log(JSON.stringify(authorizeParam));
                    ec2.authorizeSecurityGroupIngress(authorizeParam, function(err, data) {
                      if (err){
                          console.log(err, err.stack); // an error occurred
                      } else {
                          console.log('authorize success');           // successful response
                          if (revokeIps.length > 0){
                            revokesecuritygroups(revokeIps);
                          }
                      }
                    });
                }else {
                  if (revokeIps.length > 0){
                    revokesecuritygroups(revokeIps);
                  }
                }
              }
            });
          //console.log(val);
      });
    }).on('error', (e) => {
      console.log(e.message); //エラー時
    });
    
    function revokesecuritygroups(revokeIps){
       for (var i4 in revokeIps){
            var ipPerm2={};
            ipPerm2.CidrIp = revokeIps[i4];
            revokeParam.IpPermissions[0].IpRanges.push(ipPerm2);
        }
        ec2.revokeSecurityGroupIngress(revokeParam, function(err, data) {
          if (err) console.log(err, err.stack); // an error occurred
          else     console.log('revoke success');           // successful response
        });
    }
};

今回はRDSへのデータ送信に使っているLambdaが東京リージョンにあるので東京リージョン(ap-northeast-1)に縛りました。 元々あるセキュリティグループからIPを抜き、消込をしてから新たなIPは加え、無くなったIPは削除しています。
ではテストしてみます。

iprestrict-15

対象となるIPは22個でした。50個を超えるようであればセキュリティグループを分ける実装を追加した方がいいですね。ではセキュリティグループをチェックしてみます。

iprestrict-16

キチンと22個入っています。それでは本当にこれでセキュリティが保たれているのか確かめてみましょう。まずは先程のデータ送信のLambdaをもう一回叩いてみます。

iprestrict-17

成功しました。LambdaのIPからは許可されています。次に手元にあるクライアントツールから繋いでみます。

iprestrict-18

接続に失敗しました。Lambda(EC2)以外のIPからは接続が拒否されていますね。成功です。

まとめ

いかがでしたでしょうか。Public LambdaからRDSに繋ぐのにはこんな感じのやり方もあります。是非参考にしてみて下さい。