はじめに
インフラGの鈴木です。先日高知競馬で負けた後、朝5時に起き、エクストリーム出勤してこの記事を書いています。
ところで、前回AWS Inspectorから脆弱性の検出結果をGitHubのIssueに自動起票するワークフローを作りました。その結果、大量にissueがつくられてすごく面倒になりました。
made.livesense.co.jp
上記の記事の通り推奨コマンドがAPIから取れるので、これをSystems ManagerのRun Commandを使って実行すればいいのではと思い作ってみました。
その結果、まあまあしんどかったので供養がてら記事にします。
イメージ
実行
フローチャート
しんどいポイント
VS インタラクティブな操作
アップデート作業は基本的にインタラクティブな操作(
y/n
で選ぶetc...)が求められるので、その瞬間にコケます- インストールするか否かのy/nに関しては
-y
をつければ解決しますが… - 設定方式が変わった(confからyamlなど)ときに現設定を残すか、キーボードで選ぶようなやつが出た瞬間にコケます
- これに関してはどうしようもないので手で実行するようにしました
- インストールするか否かのy/nに関しては
他にも実行に問題はありませんが、こんなメッセージが標準エラー出力にでます
- Dockerfileによく書く
DEBIAN_FRONTEND=noninteractive
を設定することで、インタラクティブ操作を無視してインストールできるようです - その場合デフォルト値が採用される
- Dockerfileによく書く
----------ERROR------- debconf: unable to initialize frontend: Dialog debconf: (TERM is not set, so the dialog frontend is not usable.) debconf: falling back to frontend: Readline debconf: unable to initialize frontend: Readline debconf: (This frontend requires a controlling tty.) debconf: falling back to frontend: Teletype dpkg-preconfigure: unable to re-open stdin:
APIからstdoutが取れるが、途中で切れる
- マネジメントコンソールからはフルログがダウンロードできるが、APIからは切れてしまいます
- paginateされているのかと思ったら、boto3のドキュメントに何もありませんでした
- 仕方ないので、コンソールへのリンクを作ってコメントで貼ることにしました
https://ap-northeast-1.console.aws.amazon.com/systems-manager/run-command/${コマンドID}/${インスタンスID}?region=ap-northeast-1
に誘導することにした
sudoでコマンド叩こうとするとttyがなくてエラーになったが…
- エラー内容は以下でした、Run Commandで
sudo apt ほにゃらら -y
をしようとしました。
----------ERROR------- sudo: sorry, you must have a tty to run sudo failed to run commands: exit status 1
- 結論から言うとRun Commandで使用されるユーザーを
ssm-user
だと思い込んでいたのですが、実際に使われているユーザーはroot
でした- ssmのドキュメント
AWS-RunShellScript
を使ってwhoami
を実行したところ判明しました - マネジメントコンソールからログインするときは
ssm-user
なので勘違いでハマってしまいました… ドキュメントの目立つところに書いておいて欲しい…
- ssmのドキュメント
- 仮にssm-userでRun Commandされていた場合どうしたらいいのでしょうか?
- これの解決方法は
/etc/sudoers
にDefaults:ssm-user !requiretty
を追加すればいいです。- Defaults requiretty はsudoにtty(端末)からの実行を必要とさせるオプションです。この設定が有効だと、cronやスクリプトからのsudo実行をブロックします
弊社にはまあまあな数のEC2がありますが、仮に全インスタンスの/etc/sudoers
を機械的に置き換えるのは流石に勇気がいりますね…
- これの解決方法は
実装
Issueへのコメントを実行トリガーにする
- issue_commentを使えばissueにコメントされた場合にワークフローを実行することができます
- さらにissueのタイトルやコメントの内容をもとにifで制御しています
- ドキュメントを読むと色々なことができそうでワクワクしますね。
on: issue_comment: types: - created jobs: vulnerability-issue-comment-execute: if: contains(github.event.issue.title, 'CVE') && contains(github.event.comment.body, '/execute')
実行トリガーのコメントにリアクションでいいねをつける
- GitHub ActionはREST APIでさまざまなことができます
- これもドキュメントが面白いです
- name: Create a reaction uses: actions/github-script@v7 with: script: | await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: context.payload.comment.id, content: "+1", })
Issue本文からコマンドと対象インスタンスを取得する
サンプルIssue本文
- これをシェルでどうにかします。
### 説明 Alon Zahavi は、Linux カーネル内の NVMe-oF/TCP サブシステムを発見しました。 特定のキューの初期化エラーを適切に処理しませんでした 状況により、解放後使用の脆弱性が発生します。リモート攻撃者 これを利用してサービス妨害 (システムクラッシュ) を引き起こしたり、場合によっては 任意のコードを実行します。 ### 原文 Alon Zahavi discovered that the NVMe-oF/TCP subsystem in the Linux kernel did not properly handle queue initialization failures in certain situations, leading to a use-after-free vulnerability. A remote attacker could use this to cause a denial of service (system crash) or possibly execute arbitrary code. ### 詳細 https://people.canonical.com/~ubuntu-security/cve/2023/CVE-2023-5178.html ### インスタンス名 hoge-instance(i-hogehoge) ### 対処方法 \`\`\` apt-get update && apt-get upgrade \`\`\`
コマンドの取得
- 整形とsudo権限の付加を行なっています
- ちなみに何故かラインフィードとキャリッジリターンが混在しており、1日ハマりました
tee -a "$GITHUB_OUTPUT"
はdebug用です
- name: Get issue body id: get_command run: | commands=$(echo $ISSUE_BODY|sed 's/.*対処方法//'|sed 's/```//g'|sed '/^$/d') if [[ $commands == *" && "* ]]; then commands="${commands// && / -y && }" fi commands="${commands} -y" echo "commands=${commands}" | tee -a "$GITHUB_OUTPUT" env: ISSUE_BODY: ${{ github.event.issue.body }}```
インスタンスIDの取得
- シンプルに正規表現で抜き出しました
- name: Get Instance ID id: get_instance_id run: | instance_id=$(echo $ISSUE_BODY|grep -o 'i-[a-zA-Z0-9]*') echo "instance_id=${instance_id}" | tee -a "$GITHUB_OUTPUT" env: ISSUE_BODY: ${{ github.event.issue.body }}
OIDCでAWSへの操作権限を安全に取得する
【参考】IAMロールの権限
OIDCで権限の取得
- name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::XXXXXXXXXX:role/hoge-ssm-run-command # 各自のiamロールに置き換える aws-region: ap-northeast-1
コマンドの実行
- サブシェル内の
sed -e "s/[\r\n]\+//g"|tr -s ' '
は前述のLFとCRの除去です、このタイミングで行わないと正しく除去できなかった(なぜ?) - 先ほど取得したコマンドとインスタンスIDをここで使用します
- name: send command id: send_command run: | commands=$(echo "${{ steps.get_command.outputs.commands }}"|sed -e "s/[\r\n]\+//g"|tr -s ' ') echo $commands # debug command_id=$(aws ssm send-command \ --document-name "AWS-RunShellScript" \ --instance-ids "${{ steps.get_instance_id.outputs.instance_id }}" \ --parameters commands="\"$commands\"" \ --output json|jq -r '.Command.CommandId') && echo "command_id=${command_id}" | tee -a "$GITHUB_OUTPUT"
実行結果の取得
- 複数行の出力を取得するためにEOFを使うについてはこの記事が詳しいです、ちなみに弊社VPoEの記事です
- name: get command status id: get_command_status run: | command_id="${{ steps.send_command.outputs.command_id }}" if [[ -z "$command_id" ]]; then echo "Error: command_id is empty" exit 1 fi status="" while [[ "$status" != "Success" && "$status" != "Failed" ]] do output=$(aws ssm list-command-invocations --command-id "$command_id" --details --output json) status=$(echo $output | jq -r ".CommandInvocations[0].Status") sleep 10 # sleep for 10 seconds done if [[ "$status" == "Failed" ]] ; then echo "Command failed. Exiting." exit 1 fi echo "Final command status: $status" echo "Full command output: $output" #debug # 複数行の出力を取得するためにEOFを使う EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) echo "report<<$EOF" | tee -a "$GITHUB_OUTPUT" jq -r ".CommandInvocations[0].CommandPlugins[0].Output" <<< "$output" | tee -a "$GITHUB_OUTPUT" echo "$EOF" | tee -a "$GITHUB_OUTPUT"
実行結果をissueにコメントで貼り付ける
- name: Post result to issue uses: actions/github-script@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | const output = `${{steps.get_command_status.outputs.report}}`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `Here is the result of the command execution:\n\`\`\`${output}\`\`\`` });
debugに役にたつ結果をissueにコメントで貼り付ける
- name: Comment Result if: always() uses: actions/github-script@v7 env: GHA_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} AWS_URL: https://ap-northeast-1.console.aws.amazon.com/systems-manager/run-command/${{ steps.send_command.outputs.command_id }}/${{ steps.get_instance_id.outputs.instance_id }}?region=ap-northeast-1 with: script: | const output = `## ${ context.workflow } #### Command Result 📖\`${{ steps.get_command_status.outcome }}\` *↑successじゃなかったら何かがおかしいよ↑、GHA Result Detailsで確認してね* *なんかダメだったら手動でコマンド叩き入れてください* *GHA Result Details: [${ process.env.GHA_URL }](${ process.env.GHA_URL })*; *AWS Result Details: [${ process.env.AWS_URL }](${ process.env.AWS_URL })*`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: output });
作ってみて
- よく考えたら推奨コマンドによく出てくる
apt update && apt upgrade
を何も考えずに叩いたら、依存関係無視してインストールするので最悪ぶっ壊れるのでは?- Inspector v2 + EventBridgeでAWSが脆弱性検知したらRun Commandで自動実行も考えましたが、人の承認がないコマンド実行は怖いのでこうしました
- モノ自体は何人から良いと言われたので、運用しつつ、改善したいと思います
- 結論:こういうのを考えなくていいマネージドサービスは神