🔖

AWS セキュリティ強化スクリプト集:全リージョン対応の一括設定

に公開

AWS 環境でセキュリティや監査設定を一括で有効化するためのシェルスクリプトをまとめました。
CloudShell やローカル環境から実行可能で、基本的に idempotent(複数回実行しても安全) な形にしています。


1. Inspector 全リージョン有効化

for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do
  echo "Enabling Inspector in region: $region"
  aws inspector2 enable \
    --resource-types EC2 ECR LAMBDA LAMBDA_CODE CODE_REPOSITORY \
    --region $region
done

EC2 / ECR / Lambda / Code Repository を対象に Inspector v2 を一括有効化します。

2. デフォルト Security Group のルール削除

aws ec2 describe-regions --query "Regions[].[RegionName]" --output text \
| while read REGION; do
    echo "# deleting sg-rules in ${REGION}"
    aws ec2 describe-security-groups --filters "Name=group-name,Values=default" --region "$REGION" --query "SecurityGroups[].GroupId" --output text | xargs -n1 echo \
    | while read ID; do
        echo "# deleting default rule in ${ID}"
        SG=$(aws ec2 describe-security-groups --group-ids ${ID} --region "$REGION" --query "SecurityGroups[]" --output json)
        INGRESS=$(echo $SG | jq -r '.[].IpPermissions[]')
        EGRESS=$(echo $SG | jq -r '.[].IpPermissionsEgress[]')
        if [ -n "$INGRESS" ]; then
            aws ec2 revoke-security-group-ingress --group-id $ID --region "$REGION" --ip-permissions "$INGRESS"
        fi
        if [ -n "$EGRESS" ]; then
            aws ec2 revoke-security-group-egress --group-id $ID --region "$REGION" --ip-permissions "$EGRESS"
        fi
    done
done
  • デフォルト SG のルールを全リージョンで削除します
  • ⚠️ SG 自体は削除せず、ルールのみ削除します

3. GuardDuty 全リージョン有効化

REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)
for region in $REGIONS; do
  DETECTOR_ID=$(aws guardduty list-detectors --region "$region" --query 'DetectorIds[0]' --output text || true)
  [[ -z "$DETECTOR_ID" || "$DETECTOR_ID" == "None" ]] \
    && aws guardduty create-detector --region "$region" --enable 1>/dev/null
done

GuardDuty を有効化していないリージョンだけ新規作成します。

4. VPC Flow Logs(全リージョン・全 VPC → CloudWatch Logs)

set -euo pipefail

# ==== Settings (edit if needed) ====
ROLE_NAME="VPCFlowLogsToCW"
INLINE_POLICY_NAME="VPCFlowLogsToCWPolicy"
LOG_GROUP_PREFIX="/vpc/flow-logs"   # final log group name = /vpc/flow-logs/<vpc-id>
RETENTION_DAYS=30                   # e.g., 7 / 14 / 30 / 60 / 90 / 120 / 365 / 400 / 545 / 1827 / 3653
TRAFFIC_TYPE="ALL"                  # ACCEPT | REJECT | ALL
AGG_INTERVAL=60                     # 60 or 600
PROFILE_OPT=()                      # e.g., PROFILE_OPT=(--profile audit)
# ===================================

account_id=$(aws sts get-caller-identity "${PROFILE_OPT[@]}" --query Account --output text)
echo "[*] Target account: ${account_id}"

# 1) Ensure IAM role for VPC Flow Logs -> CloudWatch Logs
trust_policy=$(cat <<'JSON'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "vpc-flow-logs.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
JSON
)

# Allow put logs to any region under /vpc/flow-logs*
inline_policy=$(cat <<JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPutToVPCFlowLogGroups",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogGroups",
        "logs:DescribeLogStreams"
      ],
      "Resource": [
        "arn:aws:logs:*:${account_id}:log-group:${LOG_GROUP_PREFIX}*",
        "arn:aws:logs:*:${account_id}:log-group:${LOG_GROUP_PREFIX}*:log-stream:*"
      ]
    }
  ]
}
JSON
)

role_arn="arn:aws:iam::${account_id}:role/${ROLE_NAME}"
if aws iam get-role --role-name "${ROLE_NAME}" >/dev/null 2>&1; then
  echo "[✓] IAM role exists: ${role_arn}"
else
  echo "[+] Creating IAM role: ${ROLE_NAME}"
  aws iam create-role --role-name "${ROLE_NAME}" --assume-role-policy-document "${trust_policy}" >/dev/null
fi

echo "[+] Put/Update inline policy: ${INLINE_POLICY_NAME}"
aws iam put-role-policy \
  --role-name "${ROLE_NAME}" \
  --policy-name "${INLINE_POLICY_NAME}" \
  --policy-document "${inline_policy}" >/dev/null

# 2) Iterate all regions & VPCs
regions=$(aws ec2 describe-regions "${PROFILE_OPT[@]}" --query "Regions[].RegionName" --output text)
for region in $regions; do
  echo "=== Region: ${region} ==="
  vpcs=$(aws ec2 describe-vpcs "${PROFILE_OPT[@]}" --region "${region}" --query "Vpcs[].VpcId" --output text || true)
  [[ -z "${vpcs}" ]] && { echo "  (no VPC)"; continue; }

  for vpc in $vpcs; do
    # If ANY active flow log exists (S3 or CW), skip (requirementは「有効化」であり宛先は不問のため)
    active_any=$(aws ec2 describe-flow-logs "${PROFILE_OPT[@]}" --region "${region}" \
      --filter Name=resource-id,Values="${vpc}" \
      --query "length(FlowLogs[?FlowLogStatus=='ACTIVE'])" --output text || echo 0)

    if [[ "${active_any}" != "0" ]]; then
      echo "  [✓] ${vpc}: Flow Logs already enabled (skipped)"
      continue
    fi

    lg_name="${LOG_GROUP_PREFIX}/${vpc}"
    # Create log group if not exists; set retention
    aws logs create-log-group "${PROFILE_OPT[@]}" --region "${region}" --log-group-name "${lg_name}" 2>/dev/null || true
    aws logs put-retention-policy "${PROFILE_OPT[@]}" --region "${region}" --log-group-name "${lg_name}" --retention-in-days "${RETENTION_DAYS}" || true

    echo "  [+] Enabling Flow Logs to CloudWatch for ${vpc} -> ${lg_name}"
    aws ec2 create-flow-logs "${PROFILE_OPT[@]}" --region "${region}" \
      --resource-type VPC \
      --resource-ids "${vpc}" \
      --traffic-type "${TRAFFIC_TYPE}" \
      --max-aggregation-interval "${AGG_INTERVAL}" \
      --log-destination-type cloud-watch-logs \
      --log-group-name "${lg_name}" \
      --deliver-logs-permission-arn "${role_arn}" >/dev/null

    echo "      -> created"
  done
done

echo "[DONE] Checked all regions. New Flow Logs (if any) are delivered to CloudWatch Logs under ${LOG_GROUP_PREFIX}/<vpc-id> with retention ${RETENTION_DAYS} days."
  • IAM Role を自動作成し、各 VPC の Flow Logs を CloudWatch Logs へ転送
  • 既に有効化済みの VPC はスキップ

5. Route 53 Public Hosted Zone の Query Logging 有効化

set -euo pipefail

# ===== Settings (edit if needed) =====
LOG_GROUP_PREFIX="/aws/route53/public"     # 例: /aws/route53/public/<hosted-zone-name>
RETENTION_DAYS=30                          # CloudWatch Logs 保持日数
POLICY_NAME="Route53PublicQueryLogsPolicy" # CloudWatch Logs リソースポリシー名
CW_REGION="us-east-1"                      # 必ず us-east-1 を使用
# =====================================

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo "[*] Account: ${ACCOUNT_ID}"

# 1) CloudWatch Logs リソースポリシー(us-east-1)
#   Route 53 が LOG_GROUP_PREFIX 配下へ書き込めるように許可
#   混同代理問題対策として SourceArn / SourceAccount 条件を付与(ホストゾーンごとに評価される)
POLICY_DOC=$(cat <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Route53PublicQueryLogsToCW",
      "Effect": "Allow",
      "Principal": { "Service": "route53.amazonaws.com" },
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:us-east-1:*:log-group:__REPLACE_LOG_GROUP_PREFIX__*",
        "arn:aws:logs:us-east-1:*:log-group:__REPLACE_LOG_GROUP_PREFIX__*:log-stream:*"
      ],
      "Condition": {
        "StringEquals": {
          "aws:SourceAccount": "__REPLACE_ACCOUNT_ID__"
        },
        "ArnLike": {
          "aws:SourceArn": "arn:aws:route53:::hostedzone/*"
        }
      }
    }
  ]
}
EOF
)
POLICY_DOC=${POLICY_DOC/__REPLACE_ACCOUNT_ID__/${ACCOUNT_ID}}
POLICY_DOC=${POLICY_DOC/__REPLACE_LOG_GROUP_PREFIX__/${LOG_GROUP_PREFIX//\//\\/}}

# 既存置換のため put-resource-policy を常に実行
echo "[+] Put/Update CloudWatch Logs resource policy: ${POLICY_NAME} (region: ${CW_REGION})"
aws logs put-resource-policy \
  --region "${CW_REGION}" \
  --policy-name "${POLICY_NAME}" \
  --policy-document "${POLICY_DOC}" >/dev/null

# 2) すべての Public Hosted Zone を取得
#    list-hosted-zones はグローバルAPI(ページング対応)
NEXT_TOKEN=""
ZONES=()
while :; do
  if [[ -z "${NEXT_TOKEN}" ]]; then
    RESP=$(aws route53 list-hosted-zones --max-items 100)
  else
    RESP=$(aws route53 list-hosted-zones --max-items 100 --starting-token "${NEXT_TOKEN}")
  fi

  IDS=$(echo "$RESP" | jq -r '.HostedZones[] | select(.Config.PrivateZone==false) | .Id')
  while read -r id; do [[ -n "$id" ]] && ZONES+=("$id"); done <<< "$IDS"

  NEXT_TOKEN=$(echo "$RESP" | jq -r '.NextToken // empty')
  [[ -z "$NEXT_TOKEN" ]] && break
done

if [[ ${#ZONES[@]} -eq 0 ]]; then
  echo "[!] No public hosted zones found."
  exit 0
fi

echo "[*] Public hosted zones: ${#ZONES[@]}"

# 3) 各ゾーンに対して設定
for ZID_PATH in "${ZONES[@]}"; do
  # ZID は形式 "/hostedzone/XXXXXXXXXXXXX" なのでプレフィクスを剥がす
  ZID="${ZID_PATH#/hostedzone/}"

  # 既にクエリロギングが設定済みかチェック
  HAS=$(aws route53 list-query-logging-configs --hosted-zone-id "${ZID}" \
        --query 'length(QueryLoggingConfigs)' --output text)
  if [[ "${HAS}" != "0" ]]; then
    echo "  [✓] ${ZID}: query logging already enabled (skip)"
    continue
  fi

  # ゾーン名(ロググループ名に使う、末尾ドットを取り除く)
  ZONE_NAME=$(aws route53 get-hosted-zone --id "${ZID}" --query 'HostedZone.Name' --output text)
  ZONE_NAME="${ZONE_NAME%.}"
  LG_NAME="${LOG_GROUP_PREFIX}/${ZONE_NAME}"

  # ロググループ作成(us-east-1)。既存ならOK。保持期間設定。
  aws logs create-log-group --region "${CW_REGION}" --log-group-name "${LG_NAME}" 2>/dev/null || true
  aws logs put-retention-policy --region "${CW_REGION}" --log-group-name "${LG_NAME}" --retention-in-days "${RETENTION_DAYS}" || true

  LG_ARN="arn:aws:logs:${CW_REGION}:${ACCOUNT_ID}:log-group:${LG_NAME}"
  echo "  [+] Enabling query logging: ${ZID} -> ${LG_NAME}"
  aws route53 create-query-logging-config \
    --hosted-zone-id "${ZID}" \
    --cloud-watch-logs-log-group-arn "${LG_ARN}" >/dev/null

  echo "      -> created"
done

echo "[DONE] All public hosted zones checked. New query logging (if any) delivered to ${LOG_GROUP_PREFIX}/<zone-name> in ${CW_REGION}, retention ${RETENTION_DAYS} days."
  • us-east-1 の CloudWatch Logs にログを集約
  • 既に有効化されているゾーンはスキップ

6. Athena Query 結果保存先を共通 S3 バケットへ

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
COMMON_BUCKET="athena-query-logs-${ACCOUNT_ID}"
REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)

aws s3api create-bucket \
  --bucket "$COMMON_BUCKET" \
  --region ap-northeast-1 \
  --create-bucket-configuration LocationConstraint=ap-northeast-1 \
  2>/dev/null || echo "[✓] bucket already exists: $COMMON_BUCKET"

for region in $REGIONS; do
  echo "=== Updating Athena workgroup in $region ==="
  aws athena update-work-group \
    --region "$region" \
    --work-group primary \
    --configuration-updates ResultConfigurationUpdates="{OutputLocation=s3://${COMMON_BUCKET}/primary/}" \
    --state ENABLED
done

全リージョンの Athena Primary Workgroup に共通バケットを設定します。

7. EBS 暗号化 / Snapshot Public Access Block / IMDSv2 有効化

for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do
  echo "=== $region enable-ebs-encryption-by-default ==="
  aws ec2 enable-ebs-encryption-by-default --region "$region"

  echo "=== $region enable-snapshot-block-public-access ==="
  aws ec2 enable-snapshot-block-public-access --region "$region" --state block-all-sharing

  echo "=== $region modify-instance-metadata-defaults ==="
  aws ec2 modify-instance-metadata-defaults \
    --region "$region" \
    --http-tokens optional \
    --instance-metadata-tags enabled \
    --http-endpoint enabled
done

実行方法

  • CloudShell または AWS CLI 認証済みの環境で実行可能
  • 一部 jq が必要なスクリプトあり
  • ⚠️ 本番環境で実行前に必ず検証環境でのテストを推奨

まとめ

ここに掲載したスクリプトは、全リージョンでのセキュリティ標準化・監査強化 を自動化することを目的としています。
一括で有効化しておくことで、漏れなくガードレールを敷くことができます。

Discussion