sorta kinda...

主にAWS関連ですが、これに限らずいろいろ勉強したことや思ったことを書いていきます。

SAP 回線オープンを見習って雑な AWS API オープンを作ってみた

日々の懸垂で姿勢がよくなってる気がします、那須です。

SAP の運用に携わっている方はご存知だと思いますが、SAP 社からリモートサポートを受ける際には回線オープンという手続きをしてからサポートを受けます。 流れとしては、

  1. SAP システムでなんか不具合が出た
  2. SAP 社からリモートサポートを受けるために回線オープンの手続きをする
  3. SAP 社のサポート担当が SAP システムに入ってくる

という感じですね。 SAP 社のサポートは受けたいけどサポート担当の方がいつでも入れる状態は良くないということで、必要な時のみユーザの方が回線オープンするようになっています。 回線オープンの仕組みですが、ネットワーク的にはつながっていて SAP 社のサポートポータルみたいな Web ページから回線オープンボタンを押せば SG で決められた時間だけ接続が許可される、とイメージしていただければ OK です。

これはこれでよくできていて参考になるんですよね。 AWS 環境をサポートする立場だといつでもお客様環境に入れてしまうのはどうなのか?みたいな話があったりなかったりするんですが、必要なければ別にいいけど必要ならそういう仕組みは作らないと無いので、雑ですが作ってみました。

直すべきポイントは山ほどあるんですが、思いついて 1 日で最低限の形にできたので個人ブログで書いてみます。 本当に最低限の形なので、絶対に本番運用では使わないでください!

もし 1 人でも需要があればちょっとだけ本気出して運用で使える感じで作ってみようと思います。

 

作ろうと思ったきっかけ

↓この記事を書いた時に、アクセスキーの管理さえいい感じにできればこれは運用で使えそうだなーと思いました。

nasrinjp1.hatenablog.com

IAM のベストプラクティスでは、不要なアクセスキーは作らない、使うなら定期的にローテーションする、とあってどうすればいいか考えた結果、SAP のリモートサポートの仕組みが案外使えるのでは?と思いついたのがきっかけですね。

 

実現したいこと

  • 平時は AWS 管理コンソールから Web ベースで運用したい
  • IAM ユーザにアクセスキーは作成したくない
  • SSM のセッションマネージャは使ってもいいけど、RDP はインターネット経由で入らせたくない
  • Windows OS 内の不具合で RDP 接続しないといけない場面では指定の期間のみ許可したい
  • RDP 接続する場合はセッションマネージャのポートフォワーディング機能を使ってほしい

 

考えた構成

f:id:nasrinjp1:20190905094956p:plain

↑図にするとこんな形です。
ユーザの方が行う API オープンするためのアクションは、単純な Web アクセスにしました。 これなら引き継ぎも簡単ですね。

API Gateway を通じて Lambda 関数が実行されて CloudFormation スタックが作成されます。 これで指定の IAM ユーザにアクセスキーが作成されます。 作成されたアクセスキーの連絡は、Lambda 関数から直接 Slack に投げることにしました。 ブログの見栄えのためだけの理由なので、メールでもなんでもいいです。 実際にアクセスキーを利用する保守担当(もしくは部門)にだけ届く形にしました。

保守作業が完了したら、また API Gateway を通じて Lambda 関数が実行されて保守用に作成されたアクセスキーが削除されます。 これなら、平時は有効なアクセスキーがない状態なのでインターネット等にキーが漏れる心配も少ないですし、アクセスキーが有効な期間も最短になるのでそこそこセキュアな運用にできるのではないでしょうか?

 

ざっくりとした構築手順

保守用 IAM ユーザのアクセスキーを作成する CloudFormation

アクセスキーを IAM ユーザにアタッチするだけなのでシンプルですね。 このテンプレートファイルを S3 にアップロードしておきます。

AWSTemplateFormatVersion: '2010-09-09'
Parameters: 
  Username: 
    Type: String
    Description: Enter username for maintenance.
Resources:
  AccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      Status: Active
      UserName: !Ref Username
Outputs:
  Username:
    Value: !Ref Username
  AccessKey:
    Value: !Ref AccessKey
  SecretKey:
    Value: !GetAtt AccessKey.SecretAccessKey

CloudFormation スタックで行われる操作は IAM ロールのポリシーで最低限にとどめて設定しておきましょう。

CloudFormation スタックを作成する Lambda 関数

いくつかパラメータがあるので決めて入力します。

import boto3
import json
import logging
import os

from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    cfn_client = boto3.client('cloudformation')

    cfn_stack_name = "CloudFormation スタック名"
    cfn_template_url = "CloudFormation テンプレートファイルの S3 URL"
    cfn_notification_arn = "AWS Chatbot で通知するための SNS トピック ARN"
    cfn_role_arn = "CloudFormation スタックに設定する IAM ロール ARN"
    maintenance_user = "testuser"

    cfn_stack_response = cfn_client.create_stack(
        StackName=cfn_stack_name,
        TemplateURL=cfn_template_url,
        Parameters=[
            {
                'ParameterKey': 'Username',
                'ParameterValue': maintenance_user
            }
        ],
        NotificationARNs=[
            cfn_notification_arn
        ],
        Capabilities=[
            'CAPABILITY_NAMED_IAM'
        ],
        RoleARN=cfn_role_arn
    )
    cfn_client.get_waiter('stack_create_complete').wait(
        StackName=cfn_stack_response['StackId'],
        WaiterConfig={
            'Delay': 3,
            'MaxAttempts': 180
        }
    )

    cfn_desc_response = cfn_client.describe_stacks(
        StackName=cfn_stack_response['StackId']
    )
    for output in cfn_desc_response['Stacks'][0]['Outputs']:
        if output['OutputKey'] == "Username":
            access_key = output['OutputValue']
        elif output['OutputKey'] == "AccessKey":
            access_key = output['OutputValue']
        elif output['OutputKey'] == "SecretKey":
            secret_key = output['OutputValue']
    notify_slack('作業用のアクセスキーです!', maintenance_user, access_key, secret_key)
    return "OK"


def decrypt_text_by_kms(encrypted_text):
    return boto3.client('kms').decrypt(CiphertextBlob=b64decode(encrypted_text))['Plaintext'].decode('utf-8')


def notify_slack(title, maintenance_user, access_key, secret_key):
    slack_url = decrypt_text_by_kms(os.getenv('SlackWebhookUrl'))
    attachments_json = [
        {
            "title": title,
            "text": f"ユーザ: {maintenance_user}\nアクセスキー: {access_key}\nシークレットキー: {secret_key}"
        }
    ]
    slack_message = {
        'attachments': attachments_json
    }
    req = Request(slack_url, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted")
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

Lambda 関数の IAM ロールのポリシーも最低限のものだけ設定しましょう。

ユーザの方にアクセス許可してもらうための API Gateway

~/open とアクセスすると API オープンします。
例:abcdefghij.execute-api.ap-northeast-1.amazonaws.com/rdp_control/open

~/close とアクセスすると API クローズします。
例:abcdefghij.execute-api.ap-northeast-1.amazonaws.com/rdp_control/close

上記の URL 以外の設定は特にしていません。Lambda 関数さえ実行できればよかったので。
URL の公開範囲は環境ごとに異なると思いますので、WAF 等で IP アドレス制限をかけるなりしてください。

 

使い方

ユーザの方が API Gateway の open 用 URL にブラウザでアクセスします。"OK" と表示されればアクセスキーが保守担当に届きます。 Slack には AWS chatbot で CloudFormation の通知と Lambda からのアクセスキーの通知が来ていますね。

f:id:nasrinjp1:20190905095933p:plain

保守担当者がアクセスキーを受け取ったら保守用 PC にアクセスキーを設定して、この記事の作ろうと思ったきっかけで紹介した記事を参考に RDP 接続して作業を行います。

保守作業が終了したらユーザの方が API Gateway の close 用 URL にブラウザでアクセスします。"OK" と表示されればアクセスキーが削除されます。 Slack には AWS chatbot で CloudFormation の通知が届きました。

f:id:nasrinjp1:20190905102520p:plain

ユーザの方にお願いする内容はめちゃ簡単ですよね? 保守担当にもすぐにアクセスキーが届くので初動時間が早くなります。

 

残念なところ

f:id:nasrinjp1:20190905092230p:plain

↑のように CloudFormation の Output でシークレットキーが丸見えになる点です…
IAM ポリシーで制限かければ済む話ですがこのためだけにポリシーをややこしくしたくないので、CloudFormation スタックで作っているアクセスキーは Lambda 関数の中で boto3 を使って作るようにした方がいいですね。

あとは、シークレットキーを初動時間を早めるためだけに通知するのがいいのかどうかは人によって意見がわかれそうですね。 シークレットキーは SSM パラメータストアか Secrets Manager に入れておいて、Slack では CloudFormation の通知だけ受け取って、それをトリガーに何らかの方法で保守担当者がシークレットキーを取りに行く運用が安全な気もしています。

 

さいごに

思いついて 1 日しか経ってないわりにはそれなりに形にできたので個人的には充実した 1 日でした!
あとはこの仕組みをもうちょっといい感じにして SAM でデプロイできるようにすれば楽しそうなので、時間作って挑戦してみます!(誰かが使ってくれそうであれば…