やりたいこと
Google Cloud の Security Command Center を有効化したところ、
複数の "Open SSH Port" や "Open RDP Port" 検出結果があるが、
コンソール上で対象リソースを調べて回るのが面倒なので、コマンドで簡単に抽出したい。
また、攻撃リスクが高い環境を優先して対応すべく、
ファイアウォールルールが Open かつ、Public IP address のあるインスタンスがある環境の一覧が欲しい。
前提条件
- Google Cloud 以外のサードパーティ製セキュリティツールは利用しない。
- Connectivity Test や nmap などによる実際のアクセス確認までは行わない。
- Firewall Logging は有効になっておらず、Firewall Insights は利用できない。
- これが使えれば、不要なファイアウォールルールの特定がより楽になりますが、今回は使用しません。
コマンド
Cloud Shell で、1 Project 毎の実行を想定しています。
パフォーマンスは考慮していません。
(あと、やたら長いコマンドになりましたが、もっと効率の良い書き方があるかも...)
1. Project ID 設定
export PROJECT_ID=プロジェクトID
gcloud config set project $PROJECT_ID
2. パブリックIP をもつインスタンスと VPC の一覧を出力
検出カテゴリ名 PUBLIC_IP_ADDRESS のインスタンスと、VPC 名をファイル出力します。
(各コマンドで何やってるかは後述)
gcloud scc findings list projects/$PROJECT_ID \
--filter="state=\"ACTIVE\" AND category=\"PUBLIC_IP_ADDRESS\"" \
--format="value[separator=\" \"](resource.displayName,resource.name)" |
sed -r 's#^([a-z0-9-]+) //compute\.googleapis\.com/projects/[a-z0-9-]+/zones/([a-z0-9-]+)/instances/[0-9]+$#\1 \2#' |
xargs -r echo |
xargs -r -n 2 bash -c 'gcloud compute instances describe ${0} --zone=${1} \
--format="value[separator=\",\"](name,networkInterfaces.network)"' |
sed -r 's#^([a-z0-9-]+),https://www\.googleapis\.com/compute/v1/projects/[a-z0-9-]+/global/networks/([a-z0-9-]+)$#\1,\2#' \
> public-ip.txt
出力されるファイルはこんな感じです。(インスタンス名,VPC名)
instance-1,vpc-1
instance-2,vpc-2
instance-3,vpc-3
3. OPEN なファイアウォールルールと VPC の一覧を出力
"OPEN" を含む検出カテゴリで検出されたファイアウォールと、VPC を抽出します。
(同じく、各コマンドで何やってるかは後述)
gcloud scc findings list projects/$PROJECT_ID \
--filter="state=\"ACTIVE\" AND category:\"OPEN\" AND resource.type=\"google.compute.Firewall\"" \
--format="value[separator=\" \"](finding.category,resource.displayName)" |
xargs -r echo |
xargs -r -n 2 bash -c 'echo "${0},$(gcloud compute firewall-rules list \
--filter="name=${1}" \
--format="value[separator=\",\"](name,network)")"' \
> open-firewall.txt
出力されるファイルはこんな感じです。(検出カテゴリ名,ファイアウォールルール名,VPC名)
OPEN_SSH_PORT,vpc-2-allow-ssh,vpc-2
OPEN_RDP_PORT,vpc-1-allow-rdp,vpc-1
OPEN_SSH_PORT,vpc-1-allow-ssh,vpc-1
4. 2と3の結果を JOIN
join -a 1 -t, -1 2 -2 3 -o auto -e "NULL" \
<(sort -t, -k2 public-ip.txt) <(sort -t, -k3 open-firewall.txt)
出力結果はこんな感じです。
vpc-1,instance-1,OPEN_RDP_PORT,vpc-1-allow-rdp
vpc-1,instance-1,OPEN_SSH_PORT,vpc-1-allow-ssh
vpc-2,instance-2,OPEN_RDP_PORT,vpc-2-allow-ssh
vpc-3,instance-3,NULL,NULL
VPC、Public IP をもつインスタンス、同じ VPC に存在する OPEN なファイアウォールルールの組み合わせを抽出することができました。
PUBLIC IP はあるけれどもファイアウォールが OPEN なのは検出されていませんよ、という場合は NULL 表示になります。
あとは、これを元にファイアウォールやアクセス方法の見直しをすれば良さそうです。
(アクセス元IPの絞り込みや、SSH 接続だけならIAP 利用への切り替えなど。)
TODO: ファイアウォールルールは OPEN だが、Public IP をもつインスタンスは無い という情報も欲しいので、FULL JOIN に直す
備考
上記で出力されたからといって、必ずしもアクセス可能とは限らず、
他のファイアウォールルールで Deny されている可能性も、
ネットワークタグ指定によりアクセス制限されている可能性も、
インスタンス側でポートが空いていないという可能性もあります。
また、そもそも Public IP が付与される前に防止したい!というのであれば、
Organization Policy (constraints/compute.vmExternalIpAccess) を設定しておく手があります。
各コマンドの詳細
各行の詳細をメモしておきます。
参考:
2 のコマンド詳細
まずは、検出カテゴリ PUBLIC_IP_ADDRESS のインスタンスを抽出する。
--format="value" はデフォルトではタブ区切りで各項目を出力するが、xargs でdelimiter 指定が要らないようにスペース区切りに変更している。
$ gcloud scc findings list projects/$PROJECT_ID \
--filter="state=\"ACTIVE\" AND category=\"PUBLIC_IP_ADDRESS\"" \
--format="value[separator=\" \"](resource.displayName,resource.name)"
instance-1 //compute.googleapis.com/projects/PROJECT_ID/zones/us-central1-a/instances/1234567890123456789
instance-2 //compute.googleapis.com/projects/PROJECT_ID/zones/us-west1-b/instances/1234567890123456789
続いて gcloud compute コマンドに渡して VPC 情報を取得したい...のだが、インスタンス名とゾーン名が必要なので、ゾーン名を前コマンドの resource.name からで抽出する
-r オプションで拡張正規表現使用可能にしておく
$ echo "instance-1 //compute.googleapis.com/projects/PROJECT_ID/zones/us-central1-a/instances/1234567890123456789" |
sed -r 's#^([a-z0-9-]+) //compute\.googleapis\.com/projects/[a-z0-9-]+/zones/([a-z0-9-]+)/instances/[0-9]+$#\1 \2#'
instance-1 us-central1-a
※上記のコマンドそのまま実行しても、PROJECT_ID 部分がマッチしないため抽出されません
続いて gcloud compute instance describe コマンドを各インスタンスについて実行...する前に、xargs で扱いやすいようにで改行を除去する
-r オプションで、入力が空の場合はコマンド実行しない
$ echo -e "instance-1 us-central1-a\ninstance-2 us-west1-b" |
xargs -r echo
instance-1 us-central1-a instance-2 us-west1-b
1行になったので、先頭から2つずつ取り出して gcloud compute instances describe コマンドに渡す
結果はカンマ区切りで、インスタンス名とVPC情報のみ取得する
$ echo "instance-1 us-central1-a instance-2 us-west1-b" |
xargs -r -n 2 bash -c 'gcloud compute instances describe ${0} --zone=${1} \
--format="value[separator=\",\"](name,networkInterfaces.network)"'
instance-1,https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/vpc-1
instance-2,https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/vpc-2
めんどくさいことに VPC は URL で返ってくるので、名前だけ抽出する
$ echo "instance-1,https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/vpc-1" |
sed -r 's#^([a-z0-9-]+),https://www\.googleapis\.com/compute/v1/projects/[a-z0-9-]+/global/networks/([a-z0-9-]+)$#\1,\2#'
instance-1,vpc-1
※上記のコマンドそのまま実行しても、PROJECT_ID 部分がマッチしないため抽出されません
3 のコマンド詳細
まずは、SCC から "OPEN ~" として検出されているファイアウォールルールを抽出する
category:OPEN だけだと、OPEN_GROUP_IAM_MEMBER なども引っかかる可能性があるので、resource.type も指定している
uniq は複数の検出結果に表れるファイアウォールルールがあるかもしれないので指定している
$ gcloud scc findings list projects/$PROJECT_ID \
--filter="state=\"ACTIVE\" AND category:\"OPEN\" AND resource.type=\"google.compute.Firewall\"" \
--format="value[separator=\" \"](finding.category,resource.displayName)" | uniq
OPEN_SSH_PORT,vpc-2-allow-ssh
OPEN_RDP_PORT,vpc-1-allow-rdp
OPEN_SSH_PORT,vpc-1-allow-ssh
改行除去する部分は 2 と同様なので割愛
検出カテゴリ名は そのまま echo コマンドへ、
ファイアウォールルール名は gcloud compute firewall-rules list コマンドに渡してから、その結果を echo コマンドへ
$ echo "OPEN_SSH_PORT vpc-2-allow-ssh OPEN_RDP_PORT vpc-1-allow-rdp OPEN_SSH_PORT vpc-1-allow-ssh" |
xargs -r -n 2 bash -c 'echo "${0},$(gcloud compute firewall-rules list \
--filter="name=${1}" \
--format="value[separator=\",\"](name,network)")"'
OPEN_SSH_PORT,vpc-2-allow-ssh,vpc-2
OPEN_RDP_PORT,vpc-1-allow-rdp,vpc-1
OPEN_SSH_PORT,vpc-1-allow-ssh,vpc-1
4 のコマンド詳細
join -a 1 -t, -1 2 -2 3 -o auto -e "NULL" <(sort -t, -k2 public-ip.txt) <(sort -t, -k3 open-firewall.txt)
-a 1 で LEFT JOIN
-t, で区切り文字をカンマに指定
-1 で一番目のファイルのどの列を JOIN に使うかを指定
-2 で二番目のファイルのどの列を JOIN に使うかを指定
-o auto で出力項目を自動に指定 (-e 利用時には -o が必要)
-e "NULL" で、マッチする行がなかった時に表示する文字列を指定
また、事前にソートされていないと join コマンドでエラーになるため、それぞれのファイルを sort している
-t, で区切り文字指定
-k3 でソートに使う列を指定
手順2,3 の時点でソートしておいても良かったが、コマンドがさらに長くなるのでこちらでソートをかけている