こんにちは。katoです。
今回はEC2インスタンスの起動にパスワードを要求する起動管理の仕組みをご紹介させていただきます。
EC2インスタンスの不正利用を防止できるため、セキュリティの向上につながります。
概要
今回ご紹介する仕組みはSlackのSlash Commandsと連携したものとなります。
Slackでコマンドを実行するとランダムパスワードが発行され、そのパスワードを利用してインスタンスを起動することが可能となります。 インスタンスの起動前に、認証用のタグに発行されたパスワードを登録し、インスタンスを起動するという流れになります。 なお、パスワードが一致しない場合には、即座に停止処理が実行されます。
全体の構成としては下図のようになります。
Slackでコマンドが実行されると、ランダムパスワードの発行と同時にパスワードチェック用のLambdaに環境変数として同じパスワードを設定しています。 認証用タグに設定された値と、lambdaの環境変数に設定されたパスワードを比較し、起動の許可を判定しています。 また、パスワード利用は一度のみ可能であり、起動判定が行われたのちに環境変数はリセットされます。
手順
それでは実際に設定を行っていきましょう。
Slash CommandsとAPI Gatewayの設定手順に関しましては、以下の記事にてご紹介させていただきましたので今回は省略させていただきます。
Slack(Slash Commands) + Lambdaで工数管理
Slash Commandsの設定が完了したら、Lambda関数を作成します。
まず、パスワードチェックを行うLambda関数を作成します。
import boto3 import json import os def lambda_handler(event, context): trigger = event['detail'] instanceid = trigger['instance-id'] ec2 = boto3.client('ec2') target = ec2.describe_tags( Filters=[ { 'Name': 'resource-id', 'Values': [instanceid], }]) tags = target['Tags'] count = 0 for num in range(len(tags)): if tags[num]['Key'] == "Auth" and tags[num]['Value'] == os.environ['password'] and tags[num]['Value'] != "": count += 1 else: pass if count == 0: response = ec2.stop_instances( InstanceIds=[ instanceid, ] ) client = boto3.client('lambda') response = client.update_function_configuration( FunctionName='password-check-lambda', Environment={ 'Variables': { 'password': "" } } )
このLambda関数には環境変数として「password」というキーを設定します。値は設定しなくて大丈夫です。
次にパスワードの発行を行うLambda関数を作成します。
import boto3 import json import logging import os import random import string from base64 import b64decode from urlparse import parse_qs ENCRYPTED_EXPECTED_TOKEN = os.environ['Token'] logger = logging.getLogger() logger.setLevel(logging.INFO) def respond(params, err, res=None): n = 16 random_str = ''.join([random.choice(string.ascii_letters + string.digits) for i in range(n)]) client = boto3.client('lambda') response = client.update_function_configuration( FunctionName='password-check-lambda', Environment={ 'Variables': { 'password': random_str } } ) return { 'text': random_str } def lambda_handler(event, context): params = parse_qs(event['body']) token = params['token'][0] if token != ENCRYPTED_EXPECTED_TOKEN: logger.error("Request token (%s) does not match expected", token) return respond(Exception('Invalid request token')) return respond(params, None)
このLambda関数はSlackから呼び出される関数となるので、Slash Commandsの設定時に発行されたトークンの値を環境変数として設定します。 キーを「Token」として値に発行されたトークンの値を設定します。
環境変数の設定が完了したら、トリガーとしてAPI Gatewayの設定を行います。 API Gatewayの設定およびSlash Commandsへの登録手順は前述の記事をご参照ください。
Lambda関数の作成が完了したら、最後にCloudWatch Eventsの設定を行います。
イベントパターンのルールにてEC2インスタンスのステータスが「pending」となったタイミングでLambda関数を呼び出すよう設定します。
イベントパターンとして「pending」と「running」の2つを登録した場合には、認証が連続で走りインスタンスを起動できなくなるので、必ずどちらか一方をイベントパターンとして登録してください。
動作確認
それでは実際に動作を見ていきましょう。
Slash Commandsに登録したコマンドをSlack上で実行すると以下の様にランダムパスワードが返ってきます。
コマンドを実行後にパスワードチェック用のLambda関数を確認すると、同じパスワードが環境変数として登録されていることがわかります。
EC2サービスページから起動するインスタンスを選択し、先程のパスワードを値として認証用の「Auth」キーを追加します。 なお、パスワードはインスタンスごとに割り当てられるわけではないので、発行されたパスワードで任意のインスタンスを起動できます。
認証用タグの列表示を有効化しておくとパスワードの入力が簡単になります。
パスワードをタグに設定したらインスタンスを起動し、問題なく起動できることを確認します。 また、異なるパスワードを設定したり、パスワードを入力せずにインスタンスを起動した場合は、インスタンスがすぐに停止することを確認します。
インスタンスの起動後にパスワードチェック用のLambda関数を確認すると、先程設定されていた環境変数が削除されていることがわかります。 なお、環境変数のリセットは認証に失敗した場合にも行われるので、起動に失敗した場合には、再度パスワードを発行し、タグの登録を実施し直す必要があります。
また、パスワードの発行を複数回行った場合、Lambda関数の環境変数は都度上書きされるため、最後に発行されたパスワードのみ利用可能となります。 複数ユーザで利用する場合などには、パスワード取得のタイミングが被らないよう注意する必要があります。
まとめ
今回はSlackを利用したEC2インスタンスの起動管理の仕組みをご紹介させていただきました。
「アカウントの流出によるEC2インスタンスの不正利用」といった記事は少なからず目にすることがあると思います。 アカウントやアクセスキーなどが流出しないようにするのは、当たり前なのですが、もしもといった時のために対策を用意しておくことは必要かと思います。
今回のインスタンスの起動管理では、EC2インスタンスの不正利用を防止することができます。 認証失敗時にSNSでpublishするような仕組みを追加すれば、不正利用の早期発見にもつながります。
インスタンスを起動するのにひと手間かかりますが、セキュリティリスクの低下に繋がりますので、今回のような起動認証の仕組みの導入をご検討されてみてはいかがでしょうか。