小規模利用(シングルアカウント)向けAWSアカウントを作ったら必須でやること

大規模事業者の場合、AWSアカウントを複数構成した「マルチアカウント構成」にして、AWS Organizationsを使ってガードレールを設定し、統制を効かせると思います。しかし、個人利用や小規模利用の場合、そこまでしっかりした統制をかけることはないでしょう。
ただ、AWSアカウントを作成したままの状態で使用するのは、やめた方が良く、セキュリティ的な観点や、うっかりミスでの影響を抑えるために、最低限やっておきたい設定をまとめておきます。
GUIで手動設定するのは面倒なので、AWS CLIを使ったスクリプトで半自動設定します。
なお、この記事に関する免責事項は、末尾に掲載されているリンク先に記載されていますので、ご確認ください。
まとめ
先にまとめておくと、最低限以下のことをやっておきましょう。
| # | やること | 目的 |
| 1 | IAMユーザのパスワードポリシー設定 | セキュリティ観点:アカウント乗っ取られ予防 |
| 2 | rootユーザの代わりの管理者IAMユーザを作成 | セキュリティ観点:アカウント乗っ取られ予防 |
| 3 | IAMユーザにMFA設定 | セキュリティ観点:アカウント乗っ取られ予防 |
| 4 | デフォルトVPCの削除 | セキュリティ観点:気づかない間に不正利用されることの予防 |
| 5 | Budgetアラートの設定 | コスト観点:自分の使い過ぎの検知/気づかない間に不正利用されたことの早めの検知 |
| 6 | CloudTrailのアラート設定 | セキュリティ観点:不正操作の早めの検知 |
| 7 | CloudWatch Queryの設定 | セキュリティ観点:CloudTrailアラート検知時の調査 |
スクリプトの実行方法
- このブログは、AWSアカウントを作成した直後で、何もない状態を前提としているので、この後で登場するスクリプトは、全て、rootユーザのCloudShellで実行する前提とします。

- スクリプトは、文字コードをUTF-8(BOMなし)、改行コードをLFにしてファイルに保存して、CloudShellにアップロードしてください。

- common.shと、awsenv.shは、他のスクリプトから読み込まれているので、必ずアップロードしておくようにしてください。
- スクリプトで作成されるAWSリソースには、自動的に以下のタグを付与します(environmentタグの値は、common.shで定義しているので書き換えてもOKです)。
- environment=develop
- group=trail
IAMユーザのパスワードポリシー設定
目的
パスワードの複雑さは、やはり、攻撃者の侵入を手間取らせる一番基本的な対策です。
パスワードポリシーで、複雑さや定期更新を強制しておけば、うっかり安易なパスワード設定が漏れて、攻撃を受ける可能性も減らせます。
スクリプト準備
setting_password_policy.shの---- Define ----のブロックに、パスワードポリシーの設定が定義されていますので、好みの内容に更新してください。
あまり緩い設定だと、ポリシー設定している意味がないので、厳しめで…
| # | 設定項目 | 設定できる値 | 説明 |
| 1 | mpl | 文字数(6~99) |
最小のパスワード文字数
|
| 2 | rn |
--require-numbers
|
最低1文字以上の数字を必須とする
|
|
--no-require-numbers
|
最低1文字以上の数字を必須としない
|
||
| 3 | rs |
--require-symbols
|
最低1文字以上の記号を必須とする
|
|
--no-require-symbols
|
最低1文字以上の記号を必須としない
|
||
| 4 | ruc |
--require-uppercase-characters
|
最低1文字以上の大文字英字を必須とする
|
|
--no-require-uppercase-characters
|
最低1文字以上の大文字英字を必須としない
|
||
| 5 | rlc |
--require-lowercase-characters
|
最低1文字以上の大文字英字を必須とする
|
|
--no-require-lowercase-characters
|
最低1文字以上の大文字英字を必須としない
|
||
| 6 | autcp |
--allow-users-to-change-password
|
ユーザ自身によるパスワード変更可
|
|
--no-allow-users-to-change-password
|
ユーザ自身によるパスワード変更不可
|
||
| 7 | he |
--no-hard-expiry
|
パスワード期限切れの際に、ユーザ自身がパスワードリセット可
|
|
--hard-expiry
|
パスワード期限切れの際に、ユーザ自身がパスワードリセット不可
|
||
| 8 | mpa | 日数(1~1,095) |
パスワードの有効期限の日数
|
| 9 | prp | 世代数(1~24) |
パスワードの再利用を禁止する過去世代数
|
設定
更新したスクリプトを、CloudShellに、アップロードしてください。
CloudShellで、スクリプトを実行してください。
bash ./setting_password_policy.sh設定内容を表示するので、確認の上、yを入力してください。

rootユーザの代わりの管理者IAMユーザを作成する
目的
小規模利用の場合、rootユーザの認証情報が漏洩すると、アカウントの完全な制御を失うため、重大な影響が生じます。なので、rootユーザは、普段なるべく使わずに、安全な所にしまっておいた方がいいです(認証情報を)。
そこで、普段使いのrootユーザの代わりになる管理者ユーザを作成しておきます。
万一、この普段使いの管理者ユーザが乗っ取られてしまっても、rootユーザを持ち出してくれば、管理者ユーザを無効化したり、パスワードを上書きして取り戻せます。
スクリプト準備
setting_base_iam.shの---- Define ----のブロックに、管理者グループ名と、管理者ユーザ名が定義されていますので、適宜書き換えてください。
パスワードだけはインタラクティブに聞くようにしています。
設定
更新したスクリプトを、CloudShellに、アップロードしてください。
CloudShellで、スクリプトを実行してください。
bash ./setting_base_iam.sh管理者ユーザのパスワードを聞くので、先ほど設定したパスワードポリシーに準じたパスワードを入力してください。
その後、設定内容を表示するので、確認の上、yを入力してください(パスワードは表示しません)。

最後に次のセクションのMFA設定に関する注意喚起が出ます。

IAMユーザにMFA設定
MFA設定は、CLIではできないので、AWSマネジメントコンソールから手動で行う必要があります。
AWSのマニュアルページを参照して、rootユーザと管理者IAMユーザに対してMFA設定してください。
https://docs.aws.amazon.com/ja_jp/singlesignon/latest/userguide/how-to-register-device.html
おすすめのMFAデバイスは、スマホアプリのGoogle AuthenticatorやMicrosoft Authenticatorですが、わざわざスマホを持ち出すのが面倒、と言う場合は、WebブラウザのプラグインのAuthenticatorを使っても良いと思います。
デフォルトVPCの削除
目的
AWSアカウントは初期状態で、様々なリージョンに、デフォルトVPCが用意されています。
最初から用意されているデフォルトVPCは、存在も設定も広く知られているため、そのまま残しておくと、侵入を許した場合に、そのままそこにEC2インスタンスなどを作成され、こっそりと不正使用される可能性があります(※デフォルトVPCがあるから侵入されるわけではありません。別の原因で侵入された後、悪用されて、気づかないリスクがあると言うことです)。
基本的に、VPCもサブネットも自分で設計したものを使用するでしょうし、デフォルトVPCをそのまま使う機会はないと言っていいでしょう。
なので、用意されている全リージョンのデフォルトVPCとその関連リソースを削除します。
スクリプト準備
この項目は、用意したスクリプトを更新する箇所はありません。
設定
CloudShellに、delete_default_vpcs.shをアップロードしてください。
CloudShellで、スクリプトを実行してください
bash ./delete_default_vpcs.sh削除するのは、デフォルトVPCと、その中に作成されているデフォルトインターネットゲートウェイと、デフォルトサブネットです。
先に削除するリソースが列挙されるので、確認してください。
このスクリプトは、全リージョンのデフォルトVPCとその関連リソースを削除します。対象のAWSアカウントが作成直後ではなく、既に使用し始めている場合、既にデフォルトVPCを使用していると、通信が停止する可能性があります。事前に必ず確認してください。

削除対象リソースの表示が終わると、一旦ポーズするので、何かキーを押して続行してください。

その先は、リージョンごとに、ひたすら削除対象リソースを消していきます。

Budgetアラートの設定
目的
小規模利用の場合、予算は潤沢じゃない場合が多いですよね。AWSアカウントを作成して最初の内は無料枠があるので、比較的動きやすいですが、勢い余って無料枠の外側を使いまくってしまったり、あるいは、セキュリティ的な問題で不正利用されて、多額の請求が来てびっくりするのは避けたいところです。
そこで、利用料が閾値を超えたら、メールアラートする設定をしておくと、自分の過ちにせよ、不正利用にせよ、早めに気付けて良いです。
スクリプト準備
create_budgets.shの---- Define ----ブロックに、Budgetアラートの設定が定義されていますので、お好みの内容に更新してください。
| # | 設定項目 | 設定できる値 | 説明 |
| 1 |
budgetName
|
任意の文字列 |
作成するバジェット名
|
| 2 |
currency
|
原則としてUSD固定 | 支払通貨 日本円(JPY)を設定する場合、別途、GUIで支払通貨をJPYに設定しておく必要があります |
| 3 |
limitAmount
|
任意の数値 |
アラートする利用料の閾値(支払通貨単位の金額) |
| 4 |
alertEmail
|
メールアドレス |
アラートメールを送る通知先のメールアドレス
|
設定
更新したスクリプトを、CloudShellに、アップロードしてください。
CloudShellで、スクリプトを実行してください。
bash ./create_budgets.sh設定内容を表示するので、確認の上、yを入力してください。

CloudTrailのアラート設定
目的
色々予防線を張り巡らせても、100%の安全はないので、万一、不正利用された場合に、なるべく早く検知し、対策に繋げたいところです。最近の攻撃者は、あまり派手に自己顕示せず、なるべく気づかれずに長期間不正利用を続けるのがトレンドだそうです。
このような不正利用をなる早で気づくには、AWSアカウントの利用を監視し、問題があったら通知する仕組みを作っておくと良いです。

AWSアカウント上での利用アクションは、CloudTrailに記録されます。しかし、そのままだと記録は消えていくので、以下2つの対応を行います。
- 保管のためにS3バケットに自動連携する
- 問題の検知と通知のために、Cloudwatchに自動連携する
Cloudwatchは、データを長期間蓄積するには、比較的単価が高いので、長期間のデータ保管はS3バケットにするのがセオリーです。また、一応、何かあった時のために貯めておくけど、定期的に使う用途はないデータは、更に安価なS3 Glacierに移すのもセオリーです。
Cloudwatchに連携したCloudTrailのアクションデータは、不正利用の可能性のある問題アクションの条件を、Cloudwatch Metrics Filterで定義やります。ここでは以下の2つを問題アクションとして定義します。
- rootユーザによるAWSコンソールへのログイン
- 普段利用しないリージョンでの更新系アクション
上記1は、前述の「rootユーザは普段使わない」の方針のもと、もし、自分は使ってないけど、rootユーザでログインがあったら、AWSアカウントが乗っ取られた可能性が高いです。
上記2は、普段使っているリージョンに、見知らぬリソースが作られていたら、気づきますが、普段使っていないリージョンは、わざわざ見に行かないので、気づきにくいため、長期間不正利用されてしまう可能性があります。これをなる早で気づくためのルールですが、例えば、普段東京リージョン(ap-northeast-1)だけ使っていても、グローバルサービス(IAMなど)は、バージニア(us-east-1)などで動作するアクションなので、誤検知が多くなりがちです。下記のスクリプトでは、なるべく誤検知が無いように、グローバルサービスのアクションを除外しています。
Cloudwatch Metrics Filterに定義した問題アクションを検知したら、Cloudwatch Alarmが、SNS Topicを呼び出し、SNS Topicに登録されているEmail Subscriptionのメールアドレスにメール通知します。
スクリプト準備
create_trail_s3.sh
このスクリプトは、CloudTrailから証跡ログデータを連携し、保管するS3バケットを作成します。
create_trail_s3.shの---- Define ----ブロックに、S3バケット設定が定義されていますので、お好みの内容に更新してください。
| # | 設定項目 | 設定できる値 | 説明 |
| 1 |
s3Bucket
|
${TRAIL_S3_KEY}$(altAwsIdStr 12)
|
CloudTrailデータを保管するS3バケット名(TRAIL_S3_KEYは、awsenv.shで定義されています)
|
| 2 |
s3Retention
|
日数 | 標準のS3バケットストレージにCloudTrailデータを保管する日数(この日数を経過してデータは、DeepArchiveに移行) |
| 3 |
daRetention
|
日数 |
DeepArchiveのS3バケットストレージにCloudTrailデータを保管する日数(この日数を経過したデータは削除) |
create_trail_cwlog.sh
このスクリプトは、CloudTrailから証跡ログデータを連携し、監視するためのCloudWatchロググループを作成します。
create_trail_cwlog.shの----Define----ブロックに、CloudWatchロググループ設定が定義されていますので、お好みの内容に更新してください。
| # | 設定項目 | 設定できる値 | 説明 |
| 1 |
TRAIL_LOG_GROUP
|
任意の文字列
|
CloudTrailデータを連携するCloudWatchロググループ名(TRAIL_LOG_GROUPは、awsenv.shで定義されています) |
| 2 |
logRetention
|
日数 | CloudWatchロググループにCloudTrailデータを保管する日数(この日数を経過してデータは削除) |
| 3 |
TRAIL_CW_ROLE
|
任意の文字列 |
CloudTrailに、CloudWatchロググループへの書き込み権限を与えるIAMロール名(TRAIL_CW_ROLEは、awsenv.shで定義されています) |
| 4 |
policyName
|
任意の文字列 |
TRAIL_CW_ROLEのインライン権限ポリシー名 |
create_cloudtrail.sh
このスクリプトは、CloudTrailを作成します。
create_cloudtrail.shの---- Define ----ブロックに、S3バケット設定が定義されていますので、お好みの内容に更新してください。
| # | 設定項目 | 設定できる値 | 説明 |
| 1 |
trailName
|
任意の文字列
|
CloudTrail名 |
| 2 |
s3Bucket
|
${TRAIL_S3_KEY}$(altAwsIdStr 12)
|
CloudTrailデータを保管するS3バケット名(TRAIL_S3_KEYは、awsenv.shで定義されています)
|
| 3 |
trailLogGroupARN
|
arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:${TRAIL_LOG_GROUP}
|
CloudTrailデータを連携するCloudWatchロググループのARN(TRAIL_LOG_GROUPは、awsenv.shで定義されています)
|
create_sns_topic.sh
このスクリプトは、CloudTrailのログに異常を検知した際の通知用SNSトピックを作成します。
create_sns_topic.shの---- Define ----ブロックに、S3バケット設定が定義されていますので、お好みの内容に更新してください。
| # | 設定項目 | 設定できる値 | 説明 |
| 1 |
SNS_TOPIC_NAME
|
任意の文字列
|
作成するSNSトピック名(SNS_TOPIC_NAMEは、awsenv.shで定義されています) |
| 2 |
alertEmail
|
Eメールアドレス
|
アラート通知先のEメールアドレス
|
setting_cw_alarm.sh
このスクリプトは、CloudWatchのログを連携したCloudWatchロググループに異常事態(rootユーザでのログインと、使用していないリージョンへのアクセス)のアラート定義を設定し、異常を検知した際に、通知用SNSトピックに連携する設定を行います。
| # | 設定項目 | 設定できる値 | 説明 |
| 1 |
topicARN
|
arn:aws:sns:ap-northeast-1:${ACCOUNT_ID}:${SNS_TOPIC_NAME}
|
連携先SNSトピック名(SNS_TOPIC_NAMEは、awsenv.shで定義されています) |
| 2 |
rootFilterName
|
任意の文字列
|
rootユーザログイン検知のCloudWatchログフィルタ名
|
| 3 |
rootMetricName
|
任意の文字列
|
rootユーザログイン検知のCloudWatchメトリクス名
|
| 4 |
alarmRootName
|
任意の文字列
|
rootユーザログイン検知のCloudWatchアラーム名
|
| 5 |
disallowedFilterName
|
任意の文字列
|
未使用リージョンアクセス検知のCloudWatchログフィルタ名
|
| 6 |
disallowedMetricName
|
任意の文字列
|
未使用リージョンアクセス検知のCloudWatchメトリクス名
|
| 7 |
alarmApiName
|
任意の文字列
|
未使用リージョンアクセス検知のCloudWatchアラーム名
|
| 8 |
allowedRegionsCSV
|
${REGION},${OTHER_REGION}
|
通常使用するリージョン(REGION、OTHER_REGIONは、awsenv.shで定義されています)
|
| 9 |
excludeGlobal
|
CloudWatchログフィルタの条件文
|
未使用リージョンアクセスの該当条件
|
設定
更新したスクリプトを、CloudShellに、アップロードしてください。
CloudShellで、スクリプトを順番に実行してください。
create_trail_s3.sh
bash ./create_trail_s3.sh設定内容を表示するので、確認の上、yを入力してください。

create_trail_cwlog.sh
bash ./create_trail_cwlog.sh設定内容を表示するので、確認の上、yを入力してください。

create_cloudtrail.sh
bash ./create_cloudtrail.sh設定内容を表示するので、確認の上、yを入力してください。

create_sns_topic.sh
bash ./create_sns_topic.sh設定内容を表示するので、確認の上、yを入力してください。

「AWS Notification - Subscription Confirmation」と言う件名のメールが、「no-reply@sns.amazonaws.com」から届くので、「Confirm Subscription」を押してメール通知を有効にします。
setting_cw_alarm.sh
bash ./setting_cw_alarm.sh設定内容を表示するので、確認の上、yを入力してください。

これで、不測の事態にメールが届くようになっています。
試しに、AWSコンソールに、rootユーザでログインしてみてください。
CloudWatch Queryの設定
目的
さて、AWSアカウントでの不測の事態にメールが届くようになりましたが、メール自体には大した情報は記載されていません。あくまで通知によるアテンションが目的だからです。
誤検知かもしれないし、本当に不正利用されているかもしれないので、調査手段を準備しておきます。

CloudWatch InsightsのQueryを登録しておき、メール通知があったら、このクエリを実行して、該当データを検索し、詳細を確認します。
スクリプト準備
create_loginsight_query.shの---- Define ----ブロックに、クエリの設定が定義されていますので、お好みの内容に更新してください。
| # | 設定項目 | 設定できる値 | 説明 |
| 1 |
queryDir
|
任意の文字列
|
クエリをまとめるフォルダ名 |
| 2 |
queryRootName
|
任意の文字列 | Rootユーザログイン確認用クエリ名 |
| 3 |
queryApiRegionName
|
任意の文字列 |
未使用リージョンアクセス確認用クエリ
|
| 4 |
allowedRegionsCSV
|
${REGION},${OTHER_REGION}
|
通常使用するリージョン(REGION、OTHER_REGIONは、awsenv.shで定義されています)
|
設定
更新したスクリプトを、CloudShellに、アップロードしてください。
CloudShellで、スクリプトを実行してください。
bash ./create_loginsight_query.sh設定内容を表示するので、確認の上、yを入力してください。

AWSコンソールのCloudWatch画面の、「ログのインサイト」の、右上のフォルダマークに、「QUERY-RootLoginSuccess」(rootユーザでログインを検索)、「QUERY-APICallOutsideAllowedRegions」(普段使用しないリージョンでの更新アクションを検索)が出来ています。

通知メールがあったら、このクエリを選択して、クエリ実行を押します。該当したアクションが1件以上あった時刻に、バーが表示され、その下に、該当データの一覧が表示されます。

結果レコードを開くと、詳細情報が見れます。

スクリプト
common.sh
コードを見る(クリック)
# tag
ENVTAG="develop"
# Prompter
CLR_RED="\033[31m"
CLR_GRN="\033[32m"
CLR_YEL="\033[33m"
CLR_BLE="\033[34m"
CLR_MGD="\033[35m"
CLR_CYN="\033[36m"
CLR_RST="\033[0m"
info () { echo -e "${CLR_CYN}[INFO]${CLR_RST} $*"; }
input () { echo -e "${CLR_GRN}[INPUT]${CLR_RST} $*"; }
imprt () { echo -e "${CLR_MGD}[IMPORTANT]${CLR_RST} $*"; }
warn () { echo -e "${CLR_YEL}[WARN]${CLR_RST} $*"; }
abort () { echo -e "${CLR_RED}[ABORT]${CLR_RST} $*"; }
# Command executer
function exec () {
printf '%q ' "$@"
printf '\n'
if [ ! -z "${DRYRUN}" ]; then
return 0
fi
"$@"
}
# Generate random string
function randstr () {
len=$1 # 生成する文字列の長さ
LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c ${len}
}awsenv.sh
コードを見る(クリック)
# ---- Get AWS Basic Infomation ----
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
# ---- Basic Keyword ----
# Region
REGION="ap-northeast-1" # メインリージョン
OTHER_REGION="ap-northeast-3" # メインリージョン以外で使用するリージョン(複数の場合はCSV形式で列挙)
# S3
TRAIL_S3_KEY="s3bucket-for-trail-" # CloudTrail向けS3バケット名Prefix
TRIL_S3_PREFIX="cloudtrail" # CloudTrail格納Prefix
RESOURCE_S3_KEY="s3bucket-resource-" # リソースファイル格納S3バケット名Prefix
CFN_S3_PREFIX="cfn" # CloudFormationテンプレートファイルファイル格納フォルダ
# CloudWatch
TRAIL_LOG_GROUP="/aws/cloudtrail/management" # CloudTrail向けCloudWatchロググループ名
TRAIL_CW_ROLE="CloudTrail_CloudWatchLogs_Role" # CloudTrail向けCloudWatchロググループアクセスロール名
# SNS
SNS_TOPIC_NAME="sns-Cloudtrail-alerts" # CloudTrailアラート用SNSトピック名
# ---- Function ----
# Generate altanative aws id string
function altAwsIdStr () {
len=$1 # 生成する文字列の長さ
start=$(echo "${ACCOUNT_ID:0:1}")
start=$((++start))
echo -n "${ACCOUNT_ID}" | sha1sum | cut -d' ' -f1 | cut -c${start}-${len}
}setting_password_policy.sh
コードを見る(クリック)
#!/bin/bash
mydir=$(dirname $0)
source "${mydir}"/common.sh
GRPTAG="trail"
# Accept parameter define
mpl=14 # 最小のパスワード文字数
rn="--require-numbers" # --require-numbers:最低1文字以上の数字を必須とする/--no-require-numbers:最低1文字以上の数字を必須としない
rs="--require-symbols" # --require-symbols:最低1文字以上の記号を必須とする/--no-require-symbols:最低1文字以上の記号を必須としない
ruc="--require-uppercase-characters" # --require-uppercase-characters:最低1文字以上の大文字英字を必須とする/--no-require-uppercase-characters:最低1文字以上の大文字英字を必須としない
rlc="--require-lowercase-characters" # --require-lowercase-characters:最低1文字以上の大文字英字を必須とする/--no-require-lowercase-characters:最低1文字以上の大文字英字を必須としない
autcp="--allow-users-to-change-password" # --allow-users-to-change-password:ユーザ自身によるパスワード変更可/--no-allow-users-to-change-password:ユーザ自身によるパスワード変更不可
he="--no-hard-expiry" # --no-hard-expiry:パスワード期限切れ時ユーザ自身がパスワードリセット可/--hard-expiry:パスワード期限切れ時ユーザ自身がパスワードリセット不可
mpa=90 # パスワードの有効期限の日数
prp=19 # パスワードの再利用を禁止する過去世代数
## Confirm
info "パスワードポリシー設定を確認してください"
info "--minimum-password-length ${mpl}"
info "${rn}"
info "${rs}"
info "${ruc}"
info "${rlc}"
info "${autcp}"
info "${he}"
info "--max-password-age ${mpa}"
info "--password-reuse-prevention ${prp}"
while true
do
input "パスワードポリシーの設定を続行しますか? (y/n): "
read input
if [ "${input}" == "y" ]; then
break
elif [ "${input}" == "n" ]; then
warn "Process cancelled."
exit 0
fi
done
## Update
exec aws iam update-account-password-policy \
--minimum-password-length ${mpl} \
"${rn}" \
"${rs}" \
"${ruc}" \
"${rlc}" \
"${autcp}" \
"${he}" \
--max-password-age ${mpa} \
--password-reuse-prevention ${prp}
iRet=$?
if [ ${iRet} -ne 0 ]; then
abort "Process aborted."
exit 1
fi
info "Process succeeded."
exit 0setting_base_iam.sh
コードを見る(クリック)
#!/bin/bash
mydir=$(dirname $0)
source "${mydir}"/common.sh
GRPTAG="trail"
# Accept parameter define
iamg="system-admin-iamg" # システム管理者グループ名
iamu="aws-admin-iamu" # システム管理者ユーザ名
# Parameter input
input "あなたの管理者ユーザのパスワードを入力してください : "
read -s pass
## Confirm
echo ""
info "## The parameters you entered ##"
info "IAM group name : ${iamg}"
info "IAM user name : ${iamu}"
while true
do
input "入力内容を確認してください。このまま続けますか? (y/n): "
read inyn
if [ "${inyn}" == "y" ]; then
break
elif [ "${inyn}" == "n" ]; then
warn "Process cancelled."
exit 0
fi
done
# Create Administrator IAM User & Group
## IAM Group
aws iam get-group --group-name "${iamg}" > /dev/null 2>&1
if [ $? -ne 0 ]; then
exec aws iam create-group --group-name "${iamg}"
exec aws iam attach-group-policy --group-name "${iamg}" --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
fi
aws iam get-group --group-name "${iamg}"
if [ $? -ne 0 ]; then
abort "Administrator IAM group ${iamg} is missing."
abort "Process aborted."
exit 1
fi
## IAM User
aws iam get-user --user-name "${iamu}" > /dev/null 2>&1
if [ $? -ne 0 ]; then
exec aws iam create-user --user-name "${iamu}"
exec aws iam add-user-to-group --user-name "${iamu}" --group-name "${iamg}"
exec aws iam create-login-profile --user-name "${iamu}" --password "${pass}"
else
warn "IAM User ${iamu} is aleady exsist."
fi
aws iam get-user --user-name "${iamu}"
if [ $? -ne 0 ]; then
abort "Administrator IAM user ${iamu} is missing."
abort "Process aborted."
exit 1
fi
## --Tagging--
exec aws iam tag-user --user-name "${iamu}" --tags Key=environment,Value="${ENVTAG}" Key=group,Value="${GRPTAG}"
echo ""
imprt "MFA(多要素認証)の設定を、すぐ実施してください。"
imprt "1)rootユーザのMFA設定"
imprt "2) 作成した管理者ユーザ(${iamu})"
imprt "MFAの設定方法は、マニュアルを参照ください。"
imprt "https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_mfa_enable_virtual.html"
echo ""
info "Process succeeded."
exit 0delete_default_vpcs.sh
コードを見る(クリック)
#!/bin/bash
mydir=$(dirname $0)
source "${mydir}"/common.sh
GRPTAG="trail"
regions=($(aws --output text ec2 describe-regions --query "Regions[].[RegionName]"))
# Display infomation
input "削除するVPC関連リソースを表示します"
for region in "${regions[@]}"
do
info "[${region}]"
## getting infomation
# VPC
vpcs=$(aws ec2 describe-vpcs --region ${region} --output text --query "Vpcs[?IsDefault].[VpcId]")
for vpc in "${vpcs[@]}"
do
info " vpc[${vpc}]"
# IGW
igws=($(aws ec2 describe-internet-gateways --region ${region} --output text --filters Name=attachment.vpc-id,Values=${vpc} --query "InternetGateways[].[InternetGatewayId]"))
for igw in "${igws[@]}"
do
info " igw[${igw}] in vpc[${vpc[@]}]"
done
# Subnet
subnets=($(aws ec2 describe-subnets --region ${region} --output text --filters --filters Name=vpc-id,Values=${vpc} --query "Subnets[].[SubnetId]"))
for subnet in "${subnets[@]}"
do
info " subnet[${subnet}] in vpc[${vpc}]"
done
done
done
# Confirm
echo ""
input "*** 続ける場合は何かキーを押してください ***"
read
echo ""
# Delete resources
for region in "${regions[@]}"
do
info "[${region}]"
# VPC
vpcs=$(aws ec2 describe-vpcs --region ${region} --output text --query "Vpcs[?IsDefault].[VpcId]")
for vpc in "${vpcs[@]}"
do
# IGW
igws=($(aws ec2 describe-internet-gateways --region ${region} --output text --filters Name=attachment.vpc-id,Values=${vpc} --query "InternetGateways[].[InternetGatewayId]"))
for igw in "${igws[@]}"
do
info "-->delete igw[${igw}] in vpc[${vpc[@]}]"
exec aws ec2 detach-internet-gateway --region ${region} --output text --internet-gateway-id ${igw} --vpc-id ${vpc}
exec aws ec2 delete-internet-gateway --region ${region} --output text --internet-gateway-id ${igw}
done
# Subnet
subnets=($(aws ec2 describe-subnets --region ${region} --output text --filters --filters Name=vpc-id,Values=${vpc} --query "Subnets[].[SubnetId]"))
for subnet in "${subnets[@]}"
do
info "-->delete subnet[${subnet}] in vpc[${vpc}]"
exec aws ec2 delete-subnet --region ${region} --output text --subnet-id ${subnet}
done
# VPC
info "---->delete vpc[${vpc}]"
exec aws ec2 delete-vpc --region ${region} --output text --vpc-id ${vpc}
done
done
info "Process succeeded."
exit 0
create_budgets.sh
コードを見る(クリック)
#!/bin/bash
mydir=$(dirname $0)
source "${mydir}"/common.sh
source "${mydir}"/awsenv.sh
GRPTAG="trail"
# Parameter define
BUDGET_NAME="Monthly-Cost-Budget" # 作成するバジェット名
CURRENCY="USD" # 支払通貨(原則USD)
LIMIT_AMOUNT=100 # アラート通知する閾値の金額(支払通貨単位)
ALERT_EMAIL="alert@example.com" # アラート通知先のメールアドレス
# Derived values
START_UTC=$(date -u +"%Y-%m-01T00:00:00Z")
BUDGET_ARN="arn:aws:budgets::${ACCOUNT_ID}:budget/${BUDGET_NAME}"
TAG_ENV="Key=environment,Value=${ENVTAG}"
TAG_GROUP="Key=group,Value=${GRPTAG}"
## Confirm
info "以下のBudgetアラート設定を行います"
info "対象アカウントID : ${ACCOUNT_ID}"
info "アラート設定名 : ${BUDGET_NAME}"
info "通貨単位 : ${CURRENCY}"
info "アラート閾値金額 : ${LIMIT_AMOUNT}"
info "アラート通知先 : ${ALERT_EMAIL}"
echo ""
while true
do
input "Budgetアラート設定を続行しますか? (y/n): "
read input
if [ "${input}" == "y" ]; then
break
elif [ "${input}" == "n" ]; then
warn "Process cancelled."
exit 0
fi
done
# create json file for budgets
cat > ""${TEMPFILE}.budget"" <<JSON
{
"BudgetName": "${BUDGET_NAME}",
"BudgetLimit": {
"Amount": "${LIMIT_AMOUNT}",
"Unit": "${CURRENCY}"
},
"TimeUnit": "MONTHLY",
"BudgetType": "COST",
"TimePeriod": {
"Start": "${START_UTC}"
}
}
JSON
# create json file for notification
cat > "${TEMPFILE}.notifications" <<JSON
[
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 80,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [
{
"SubscriptionType": "EMAIL",
"Address": "${ALERT_EMAIL}"
}
]
},
{
"Notification": {
"NotificationType": "FORECASTED",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 100,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [
{
"SubscriptionType": "EMAIL",
"Address": "${ALERT_EMAIL}"
}
]
}
]
JSON
# create budgets and notifications
if aws budgets describe-budget --account-id "${ACCOUNT_ID}" --budget-name "${BUDGET_NAME}" >/dev/null 2>&1; then
info "Budget ${BUDGET_NAME} は既に存在します。最新の定義で更新します。"
exec aws budgets update-budget \
--account-id "${ACCOUNT_ID}" \
--new-budget file://"${TEMPFILE}.budget"
else
info "Creating budget ${BUDGET_NAME} and associated notifications"
exec aws budgets create-budget \
--account-id "${ACCOUNT_ID}" \
--budget file://"${TEMPFILE}.budget"
notification_count=$(jq '. | length' "${TEMPFILE}.notifications")
for idx in $(seq 0 $((notification_count-1))); do
notif=$(jq ".[$idx].Notification" "${TEMPFILE}.notifications")
subs=$(jq ".[$idx].Subscribers" "${TEMPFILE}.notifications")
# Write temporary files for each notification and its subscribers
echo "${notif}" > "${TEMPFILE}.notif"
echo "${subs}" > "${TEMPFILE}.subs"
exec aws budgets create-notification \
--account-id "${ACCOUNT_ID}" \
--budget-name "${BUDGET_NAME}" \
--notification file://"${TEMPFILE}.notif" \
--subscribers file://"${TEMPFILE}.subs"
done
fi
# --Tagging--
exec aws budgets tag-resource \
--resource-arn "${BUDGET_ARN}" \
--resource-tags "${TAG_ENV}" "${TAG_GROUP}"
# --- Check ---
aws budgets describe-budget --account-id "${ACCOUNT_ID}" --budget-name "${BUDGET_NAME}" >& /dev/null
if [ $? -ne 0 ]; then
abort "budget ${BUDGET_NAME} is missing."
abort "Process aborted."
fi
rm -f "${TEMPFILE}*"
info "Process succeeded."
exit 0create_trail_s3.sh
コードを見る(クリック)
mydir=$(dirname $0)
source "${mydir}"/common.sh
source "${mydir}"/awsenv.sh
grptag="trail"
# ---- Define ----
s3Bucket="${TRAIL_S3_KEY}$(altAwsIdStr 12)" # S3バケット名
s3Retention=365 # S3保管期間
daRetention=1825 # DeepArchive保管期間
# Confirm
info "以下のCloudTrail保管先S3バケット設定を行います"
info "S3バケット名 : ${s3Bucket}"
info "リージョン : ${REGION}"
info "保管先ディレクトリ : ${TRIL_S3_Prefix}"
info "S3保管期間 : ${s3Retention}"
info "DeepArchive保管期間 : ${daRetention}"
while true
do
input "S3バケットの作成を続行しますか? (y/n): "
read input
if [ "${input}" == "y" ]; then
break
elif [ "${input}" == "n" ]; then
warn "Process cancelled."
exit 0
fi
done
# ---- S3 bucket for CloudTrail ----
# Check
if aws s3api head-bucket --region "${REGION}" --bucket "${s3Bucket}" 2>/dev/null; then
echo "S3 bucket exists: ${s3Bucket}"
else
echo "Creating S3 bucket: ${s3Bucket}"
# リージョンが us-east-1 以外なら LocationConstraint 必須
if [ "${REGION}" == "us-east-1" ]; then
exec aws s3api create-bucket --region "${REGION}" --bucket "${s3Bucket}" \
--object-lock-enabled-for-bucket
else
exec aws s3api create-bucket --region "${REGION}" --bucket "${s3Bucket}" \
--object-lock-enabled-for-bucket \
--create-bucket-configuration LocationConstraint="${REGION}"
fi
fi
# --Tagging--
exec aws s3api put-bucket-tagging --region "${REGION}" --bucket "${s3Bucket}" \
--tagging "TagSet=[{Key=environment,Value=${ENVTAG}},{Key=group,Value=${grptag}}]"
# Set Versining
exec aws s3api put-bucket-versioning --region "${REGION}" --bucket "${s3Bucket}" \
--versioning-configuration Status=Enabled
if [ $? -ne 0 ]; then
abort "${s3Bucket}のversioning設定に失敗しました"
abort "Process aborted."
exit 1
fi
# Public access block
exec aws s3api put-public-access-block \
--region "${REGION}" \
--bucket "${s3Bucket}"\
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
if [ $? -ne 0 ]; then
abort "${s3Bucket}のpublic access block設定に失敗しました"
abort "Process aborted."
exit 1
fi
# Serverside Encryption
exec aws s3api put-bucket-encryption \
--region "${REGION}" \
--bucket "${s3Bucket}"\
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
if [ $? -ne 0 ]; then
abort "${s3Bucket}の暗号化設定に失敗しました"
abort "Process aborted."
exit 1
fi
# Set Object lock
exec aws s3api put-object-lock-configuration --region "${REGION}" \
--bucket "${s3Bucket}" \
--object-lock-configuration "{
\"ObjectLockEnabled\": \"Enabled\",
\"Rule\": {
\"DefaultRetention\": {
\"Mode\": \"GOVERNANCE\",
\"Days\": ${s3Retention}
}
}
}"
if [ $? -ne 0 ]; then
abort "${s3Bucket}のObject Lock設定に失敗しました"
abort "Process aborted."
exit 1
fi
# Set Life Cycle
exec aws s3api put-bucket-lifecycle-configuration --region "${REGION}" \
--bucket "${s3Bucket}" \
--lifecycle-configuration "{
\"Rules\": [
{
\"ID\": \"MoveToDeepArchiveAfter1YearAndExpireAfter5Years\",
\"Status\": \"Enabled\",
\"Filter\": {
\"Prefix\": \"\"
},
\"Transitions\": [
{
\"Days\": ${s3Retention},
\"StorageClass\": \"DEEP_ARCHIVE\"
}
],
\"Expiration\": {
\"Days\": ${daRetention}
}
}
]
}"
if [ $? -ne 0 ]; then
abort "${s3Bucket}のLife Cycle設定に失敗しました"
abort "Process aborted."
exit 1
fi
# CloudTrailは s3:PutObject(x-amz-acl: bucket-owner-full-control 条件付き) と s3:GetBucketAcl を要求
ctPrefixPath="${TRIL_S3_Prefix:+${TRIL_S3_Prefix}/}AWSLogs/${ACCOUNT_ID}/*"
cat > "${TEMPFILE}.policy" <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AWSCloudTrailAclCheck",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::${s3Bucket}"
},
{
"Sid": "AWSCloudTrailWrite",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::${s3Bucket}/${ctPrefixPath}",
"Condition": { "StringEquals": { "s3:x-amz-acl": "bucket-owner-full-control" } }
},
{
"Sid": "DenyNonSSLRequests",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::${s3Bucket}",
"arn:aws:s3:::${s3Bucket}/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
POLICY
exec aws s3api put-bucket-policy --region "${REGION}" --bucket "${s3Bucket}" --policy file://"${TEMPFILE}.policy"
if [ $? -ne 0 ]; then
abort "${s3Bucket}のバケットポリシー設定に失敗しました"
abort "Process aborted."
exit 1
fi
rm -f "${TEMPFILE}*"
info "Process succeeded."
exit 0create_trail_cwlog.sh
コードを見る(クリック)
#!/bin/bash
mydir=$(dirname $0)
source "${mydir}"/common.sh
source "${mydir}"/awsenv.sh
grptag="trail"
# ---- Define ----
logRetention=30 # CloudWatchロググループ保管期間
policyName="CloudTrail_CloudWatchLogs_Policy" # CloudTrail向けCloudWatchロググループアクセスポリシー名
tagEnv="Key=environment,Value=${ENVTAG}"
tagGroup="Key=group,Value=${grptag}"
info "== Confirm =="
info "アカウント : ${ACCOUNT_ID}"
info "リージョン : ${REGION}"
info "ロググループ名 : ${TRAIL_LOG_GROUP}"
info " ログの保持日数 : ${logRetention}"
info " CloudTrail向けIAMロール名 : ${TRAIL_CW_ROLE}"
while true
do
input "CloudWatchロググループの作成を続行しますか? (y/n): "
read input
if [ "${input}" == "y" ]; then
break
elif [ "${input}" == "n" ]; then
warn "Process cancelled."
exit 0
fi
done
# ---- CloudWatch Logs group ----
aws logs describe-log-groups --region "${REGION}" --log-group-name-prefix "${TRAIL_LOG_GROUP}" --output text --query "logGroups[?logGroupName=='${TRAIL_LOG_GROUP}'].logGroupName" | grep -q "${TRAIL_LOG_GROUP}"
if [ $? -ne 0 ]; then
exec aws logs create-log-group --region "${REGION}" --log-group-name "${TRAIL_LOG_GROUP}"
exec aws logs put-retention-policy --region "${REGION}" --log-group-name "${TRAIL_LOG_GROUP}" --retention-in-days ${logRetention}
fi
## --Tagging--
LOG_GROUP_ARN="arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:${TRAIL_LOG_GROUP}"
exec aws logs tag-resource --region "${REGION}" --resource-arn "${LOG_GROUP_ARN}" --tags "environment=${ENVTAG},group=${GRPTAG}"
# ---- IAM role/policy for CloudTrail to put logs ----
aws iam get-role --role-name "${TRAIL_CW_ROLE}" >/dev/null 2>&1 || \
exec aws iam create-role --role-name "${TRAIL_CW_ROLE}" --assume-role-policy-document "{
\"Version\": \"2012-10-17\",
\"Statement\": [
{
\"Effect\": \"Allow\",
\"Principal\": {\"Service\": \"cloudtrail.amazonaws.com\"},
\"Action\": \"sts:AssumeRole\"
}
]
}"
aws iam put-role-policy --role-name "${TRAIL_CW_ROLE}" --policy-name "${policyName}" --policy-document "{
\"Version\": \"2012-10-17\",
\"Statement\": [
{
\"Effect\": \"Allow\",
\"Action\": [
\"logs:CreateLogStream\",
\"logs:PutLogEvents\"
],
\"Resource\": \"arn:aws:logs:*:${ACCOUNT_ID}:log-group:${TRAIL_LOG_GROUP}:*\"
}
]
}"
## --Tagging--
exec aws iam tag-role --role-name "${TRAIL_CW_ROLE}" --tags "${tagEnv}" "${tagGroup}"
# --- Check ---
iErr=0
aws logs describe-log-groups --region "${REGION}" --log-group-name-prefix "${TRAIL_LOG_GROUP}" --output text --query "logGroups[?TRAIL_LOG_GROUP=='${TRAIL_LOG_GROUP}'].TRAIL_LOG_GROUP" >& /dev/null
if [ $? -ne 0 ]; then
abort "CloudWatch Log Group ${TRAIL_LOG_GROUP} is missing."
((iErr++))
fi
aws iam get-role --role-name "${TRAIL_CW_ROLE}" >& /dev/null
if [ $? -ne 0 ]; then
abort "IAM role ${TRAIL_CW_ROLE} is missing."
((iErr++))
fi
if [ ${iErr} -gt 0 ]; then
abort "Process aborted."
fi
info "Process succeeded."
exit 0create_cloudtrail.sh
コードを見る(クリック)
#!/bin/bash
mydir=$(dirname $0)
source "${mydir}"/common.sh
source "${mydir}"/awsenv.sh
grptag="trail"
# ---- Define ----
trailName="Orgless-Management-Events" # CloudTrail名
s3Bucket="${TRAIL_S3_KEY}$(altAwsIdStr 12)" # S3バケット名
trailLogGroupARN="arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:${TRAIL_LOG_GROUP}" # LogGroup名
TAG_ENV="Key=environment,Value=${ENVTAG}"
TAG_GROUP="Key=group,Value=${grptag}"
info "== Confirm =="
info "アカウント : ${ACCOUNT_ID}"
info "CloudTrail証跡名 : ${trailName}"
info "連携先S3バケット : ${s3Bucket}"
info " 格納先フォルダ : ${TRIL_S3_PREFIX}"
info "連携先CloudWatchロググループ : ${TRAIL_LOG_GROUP}"
while true
do
input "CloudTrailの作成を続行しますか? (y/n): "
read input
if [ "${input}" == "y" ]; then
break
elif [ "${input}" == "n" ]; then
warn "Process cancelled."
exit 0
fi
done
# ---- CloudTrail (multi-region) ----
if aws cloudtrail get-trail --name "${trailName}" >& /dev/null; then
info "Trail exists: ${trailName}"
else
info "Creating trail: ${trailName}"
ctArgs=(
--region "${REGION}"
--name "${trailName}"
--s3-bucket-name "${s3Bucket}"
--is-multi-region-trail
--include-global-service-events
--enable-log-file-validation
--cloud-watch-logs-log-group-arn "${trailLogGroupARN}:*"
--cloud-watch-logs-role-arn "arn:aws:iam::${ACCOUNT_ID}:role/${TRAIL_CW_ROLE}"
--s3-key-prefix "${TRIL_S3_PREFIX}"
)
exec aws cloudtrail create-trail "${ctArgs[@]}"
fi
exec aws cloudtrail start-logging --region "${REGION}" --name "${trailName}"
## --Tagging--
trailARN="arn:aws:cloudtrail:${REGION}:${ACCOUNT_ID}:trail/${trailName}"
exec aws cloudtrail add-tags --region "${REGION}" --resource-id "${trailARN}" --tags-list "${TAG_ENV}" "${TAG_GROUP}"
# --- Check ---
iErr=0
aws cloudtrail get-trail --region "${REGION}" --name "${trailName}" >& /dev/null
if [ $? -ne 0 ]; then
abort "CloudTrail ${trailName} is missing."
((iErr++))
fi
if [ ${iErr} -gt 0 ]; then
abort "Process aborted."
fi
info "Process succeeded."
exit 0create_sns_topic.sh
コードを見る(クリック)
#!/bin/bash
mydir=$(dirname $0)
source "${mydir}"/common.sh
source "${mydir}"/awsenv.sh
GRPTAG="trail"
# ---- Define ----
alertEmail="alert@example.com" # 通知先Eメールアドレス
tagEnv="Key=environment,Value=${ENVTAG}"
tagGroup="Key=group,Value=${grptag}"
# ---- Inputs ----
info "リージョン : ${REGION}"
info "SNSトピック名 : ${SNS_TOPIC_NAME}"
info "通知先Eメールアドレス : ${alertEmail}"
while true
do
input "SNSトピックの作成を続行しますか? (y/n): "
read input
if [ "${input}" == "y" ]; then
break
elif [ "${input}" == "n" ]; then
warn "Process cancelled."
exit 0
fi
done
# ---- SNS topic & subscription ----
topicARN=$(aws sns create-topic --region "${REGION}" --name "${SNS_TOPIC_NAME}" --query TopicArn --output text)
subscExists=$(aws sns list-subscriptions-by-topic --region "${REGION}" --topic-arn "${topicARN}" --query "Subscriptions[?Endpoint=='${alertEmail}' && Protocol=='email'].SubscriptionArn" --output text)
if [[ -n "${subscExists}" && "${subscExists}" != "PendingConfirmation" ]]; then
info "${topicARN}に${alertEmail}向けのサブスクリプションが既に存在します"
else
exec aws sns subscribe --region "${REGION}" --topic-arn "${topicARN}" --protocol email --notification-endpoint "${alertEmail}"
imprt "サブスクリプションの確認メールが送信されたので、必ずメールのリンクを押して確定してください"
fi
## --Tagging--
exec aws sns tag-resource --region "${REGION}" --resource-arn "${topicARN}" --tags "${tagEnv}" "${tagGroup}"
# --- Check ---
iErr=0
topicARN=$(aws sns list-topics --region "${REGION}" --query 'Topics[].TopicArn' | jq -r ".[]" | grep "${SNS_TOPIC_NAME}")
if [ -z "${topicARN}" ];then
abort "SNS Topic ${SNS_TOPIC_NAME} is missing."
((iErr++))
fi
subscExists=$(aws sns list-subscriptions-by-topic --region "${REGION}" --topic-arn "${topicARN}" --query "Subscriptions[?Endpoint=='${alertEmail}' && Protocol=='email'].SubscriptionArn" --output text)
if [ -z "${subscExists}" ];then
abort "Subscription for ${alertEmail} in SNS Topic ${SNS_TOPIC_NAME} is missing."
((iErr++))
fi
if [ ${iErr} -gt 0 ]; then
abort "Process aborted."
fi
info "Process succeeded."
exit 0setting_cw_alarm
コードを見る(クリック)
#!/bin/bash
mydir=$(dirname $0)
source "${mydir}"/common.sh
source "${mydir}"/awsenv.sh
grptag="trail"
# ---- Define ----
topicARN="arn:aws:sns:ap-northeast-1:${ACCOUNT_ID}:${SNS_TOPIC_NAME}" # 通知先SNSトピックARN
rootFilterName="cw-RootLoginSuccessFilter" # rootユーザログインCloudWatchログフィルタ名
rootMetricName="cw-RootLoginSuccessCount" # rootユーザログインCloudWatchメトリクス名
alarmRootName="cw-ALARM-RootLoginSuccess" # rootユーザログインCloudWatchアラーム名
disallowedFilterName="cw-APICallOutsideAllowedRegions" # 未使用リージョンアクセスCloudWatchログフィルタ名
disallowedMetricName="cw-APICallOutsideAllowedRegionsCount" # 未使用リージョンアクセスCloudWatchメトリクス名
alarmApiName="cw-ALARM-APICallOutsideAllowedRegions" # 未使用リージョンアクセスCloudWatchアラーム名
excludeGlobal='
($.eventSource != "iam.amazonaws.com")
&& ($.eventSource != "cloudfront.amazonaws.com")
&& ($.eventSource != "route53.amazonaws.com")
&& ($.eventSource != "globalaccelerator.amazonaws.com")
&& ($.eventSource != "waf.amazonaws.com")
&& ($.eventSource != "wafv2.amazonaws.com")
&& ($.eventSource != "support.amazonaws.com")
&& ($.eventSource != "health.amazonaws.com")
&& ($.eventSource != "signin.amazonaws.com")
&& ($.eventSource != "sts.amazonaws.com")
&& ($.eventSource != "sso.amazonaws.com")
&& ($.eventSource != "sso-oidc.amazonaws.com")
&& ($.eventSource != "ce.amazonaws.com")
&& ($.userIdentity.invokedBy != "resource-explorer-2.amazonaws.com")
&& ($.readOnly = false)
' # CloudTrailのアラートから除外するグローバルサービス
allowedRegionsCSV="${REGION},${OTHER_REGION}"
tagEnv="Key=environment,Value=${ENVTAG}"
tagGroup="Key=group,Value=${grptag}"
info "== Confirm =="
info "プライマリリージョン : ${REGION}"
info "通常使用するリージョン : ${allowedRegionsCSV}"
info "CloudWatchフィルタ"
info " Rootユーザログイン : ${rootFilterName}"
info " 未使用リージョンアクセス: ${disallowedFilterName}"
info "CloudWatchアラーム"
info " Rootユーザログイン : ${alarmRootName}"
info " 未使用リージョンアクセス: ${disallowedFilterName}"
while true
do
input "CloudWatchアラームの設定を続行しますか? (y/n): "
read input
if [ "${input}" == "y" ]; then
break
elif [ "${input}" == "n" ]; then
warn "Process cancelled."
exit 0
fi
done
# ---- Metric filters & alarms ----
# 1) Root login success
exec aws logs put-metric-filter \
--region "${REGION}" \
--log-group-name "${TRAIL_LOG_GROUP}" \
--filter-name "${rootFilterName}" \
--metric-transformations "metricName=${rootMetricName},metricNamespace=CloudTrailSecurity,metricValue=1" \
--filter-pattern '{ ($.eventName = "ConsoleLogin") && ($.userIdentity.type = "Root") && ($.responseElements.ConsoleLogin = "Success") }'
exec aws cloudwatch put-metric-alarm \
--region "${REGION}" \
--alarm-name "${alarmRootName}" \
--namespace "CloudTrailSecurity" \
--metric-name "${rootMetricName}" \
--statistic Sum \
--period 60 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--alarm-actions "${topicARN}"
## --Tagging--
alarmRootARN="arn:aws:cloudwatch:${REGION}:${ACCOUNT_ID}:alarm:${alarmRootName}"
exec aws cloudwatch tag-resource --region "${REGION}" --resource-arn "${alarmRootARN}" --tags "${tagEnv}" "${tagGroup}"
# 2) API calls in NOT-allowed regions (exclude global services)
# Build region-not-in pattern: ($.awsRegion != "r1") && ($.awsRegion != "r2") ...
IFS=',' read -ra ALLOWED <<< "${allowedRegionsCSV}"
REG_CMP=""
for r in "${ALLOWED[@]}"; do
r_trim=$(echo "$r" | xargs)
[ -z "$regCmp" ] && REG_CMP="($.awsRegion != \"${r_trim}\")" || REG_CMP="${REG_CMP} && ($.awsRegion != \"${r_trim}\")"
done
filterPattern="{ ${REG_CMP} && ${excludeGlobal} }"
filterPattern=$(echo "$filterPattern" | tr -s ' ')
exec aws logs put-metric-filter \
--region "${REGION}" \
--log-group-name "${TRAIL_LOG_GROUP}" \
--filter-name "${disallowedFilterName}" \
--metric-transformations "metricName=${disallowedMetricName},metricNamespace=CloudTrailSecurity,metricValue=1" \
--filter-pattern "${filterPattern}"
exec aws cloudwatch put-metric-alarm \
--region "${REGION}" \
--alarm-name "${alarmApiName}" \
--namespace "CloudTrailSecurity" \
--metric-name "${disallowedMetricName}" \
--statistic Sum \
--period 300 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--alarm-actions "${topicARN}"
## --Tagging--
alarmApiARN="arn:aws:cloudwatch:${REGION}:${ACCOUNT_ID}:alarm:${alarmApiName}"
exec aws cloudwatch tag-resource --region "${REGION}" --resource-arn "${alarmApiARN}" --tags "${tagEnv}" "${tagGroup}"
# --- Check ---
iErr=0
flterExists=$(aws logs describe-metric-filters --region "${REGION}" --metric-name "${rootMetricName}" --metric-namespace "CloudTrailSecurity" --query 'metricFilters[].filterName' | jq -r ".[]")
if [ -z "${flterExists}" ];then
abort "Metrics filter ${rootFilterName} is missing."
((iErr++))
fi
alarmExists=$(aws cloudwatch describe-alarms --region "${REGION}" --alarm-names "${alarmRootName}" --query 'MetricAlarms[].AlarmName' | jq -r '.[]')
if [ -z "${alarmExists}" ];then
abort "Metrics alarm ${alarmRootName} is missing."
((iErr++))
fi
flterExists=$(aws logs describe-metric-filters --region "${REGION}" --metric-name "${disallowedMetricName}" --metric-namespace "CloudTrailSecurity" --query 'metricFilters[].filterName' | jq -r ".[]")
if [ -z "${flterExists}" ];then
abort "Metrics filter ${disallowedFilterName} is missing."
((iErr++))
fi
alarmExists=$(aws cloudwatch describe-alarms --region "${REGION}" --alarm-names "${alarmApiName}" --query 'MetricAlarms[].AlarmName' | jq -r '.[]')
if [ -z "${alarmExists}" ];then
abort "Metrics alarm ${alarmApiName} is missing."
((iErr++))
fi
if [ ${iErr} -gt 0 ]; then
abort "Process aborted."
fi
info "Process succeeded."
exit 0create_loginsight_query.sh
コードを見る(クリック)
#!/bin/bash
mydir=$(dirname $0)
source "${mydir}"/common.sh
source "${mydir}"/awsenv.sh
grptag="trail"
# ---- Define ----
queryDir="alarm/" # クエリをまとめるディレクトリ
queryRootName="${queryDir}QUERY-RootLoginSuccess" # rootユーザログイン確認用クエリ
queryApiRegionName="${queryDir}QUERY-APICallOutsideAllowedRegions" # 未使用リージョンアクセス確認用クエリ
allowedRegionsCSV="${REGION},${OTHER_REGION}"
info "== Confirm =="
info "CloudWatchロググループ名 : ${TRAIL_LOG_GROUP}"
info "rootユーザログイン確認用クエリ : ${queryRootName}"
info "未使用リージョンアクセス確認用クエリ : ${queryApiRegionName}"
info " 通常使用するリージョン : ${allowedRegionsCSV}"
while true
do
input "CloudWatchアラームの設定を続行しますか? (y/n): "
read input
if [ "${input}" == "y" ]; then
break
elif [ "${input}" == "n" ]; then
warn "Process cancelled."
exit 0
fi
done
# ---- Investigation Query for alarm ----
# 1) Root login success
queryString="$(cat <<'QL'
fields @timestamp, awsRegion, eventSource, eventName,
userIdentity.type as userType,
userIdentity.arn as userArn,
sourceIPAddress, userAgent, eventID
| filter eventName = "ConsoleLogin"
| filter userIdentity.type = "Root"
| filter responseElements.ConsoleLogin = "Success"
| sort @timestamp desc
| limit 200
QL
)"
queryExists="$(aws logs describe-query-definitions \
--region "${REGION}" \
--query-definition-name-prefix "${queryRootName}" \
--query 'queryDefinitions[?name==`'"${queryRootName}"'`].queryDefinitionId' \
--output text 2>/dev/null || true)"
if [[ -n "${queryExists:-}" && "${queryExists}" != "None" ]]; then
info "Updating existing query definition: ${queryRootName} ($queryExists)"
exec aws logs put-query-definition \
--region "${REGION}" \
--query-definition-id "${queryExists}" \
--name "${queryRootName}" \
--query-string "${queryString}" \
--log-group-names "${TRAIL_LOG_GROUP}"
else
info "Creating new query definition: ${queryRootName}"
exec aws logs put-query-definition \
--region "${REGION}" \
--name "${queryRootName}" \
--query-string "${queryString}" \
--log-group-names "${TRAIL_LOG_GROUP}"
fi
# 2) API calls in NOT-allowed regions (exclude global services)
allowedRegionsJSON=$(printf '"%s"' "${allowedRegionsCSV//,/\",\"}")
queryString="$(cat <<EOF
fields @timestamp, awsRegion, eventSource, eventName,
userIdentity.type as userType,
userIdentity.arn as userArn,
sourceIPAddress, userAgent, eventID
| filter ispresent(awsRegion)
| filter eventCategory = "Management"
| filter awsRegion not in [${allowedRegionsJSON}]
| filter eventSource not in [
"iam.amazonaws.com","cloudfront.amazonaws.com","route53.amazonaws.com",
"globalaccelerator.amazonaws.com","waf.amazonaws.com","wafv2.amazonaws.com",
"support.amazonaws.com","health.amazonaws.com",
"signin.amazonaws.com","sts.amazonaws.com","sso.amazonaws.com","sso-oidc.amazonaws.com",
"ce.amazonaws.com"
]
| filter userIdentity.invokedBy not in ["resource-explorer-2.amazonaws.com"]
| filter readOnly = false
| sort @timestamp desc
| limit 200
EOF
)"
queryExists="$(aws logs describe-query-definitions \
--region "${REGION}" \
--query-definition-name-prefix "${queryApiRegionName}" \
--query 'queryDefinitions[?name==`'"${queryApiRegionName}"'`].queryDefinitionId' \
--output text 2>/dev/null || true)"
if [[ -n "${queryExists:-}" && "${queryExists}" != "None" ]]; then
info "Updating existing query definition: ${queryApiRegionName} ($queryExists)"
exec aws logs put-query-definition \
--region "$REGION" \
--query-definition-id "${queryExists}" \
--name "${queryApiRegionName}" \
--query-string "${queryString}" \
--log-group-names "${TRAIL_LOG_GROUP}"
else
info "Creating new query definition: ${queryApiRegionName}"
exec aws logs put-query-definition \
--region "${REGION}" \
--name "${queryApiRegionName}" \
--query-string "${queryString}" \
--log-group-names "${TRAIL_LOG_GROUP}"
fi
# --- Check ---
iErr=0
queryExists=$(aws logs describe-query-definitions --region "${REGION}" --query-definition-name-prefix "${queryRootName}" --query 'queryDefinitions[].name' | jq -r '.[]')
if [ -z "${queryExists}" ];then
abort "Metrics alarm ${queryRootName} is missing."
((iErr++))
fi
queryExists=$(aws logs describe-query-definitions --region "${REGION}" --query-definition-name-prefix "${queryApiRegionName}" --query 'queryDefinitions[].name' | jq -r '.[]')
if [ -z "${queryExists}" ];then
abort "Metrics alarm ${queryApiRegionName} is missing."
((iErr++))
fi
if [ ${iErr} -gt 0 ]; then
abort "Process aborted."
fi
info "Process succeeded."
exit 0

