WebアプリケーションでのSSHで不具合を出さないためにknown_hostsを理解する

PHPSSHを使ってファイルを転送する際にうまく転送できず,原因がknown_hosts周りだったので改めて調べてみた.うまくいかなかった事象・原因とその解決方法もまとめておく.なお今回はPHPを使用しているが,Rubyシェルスクリプトでも同様の問題は起きうる.

known_hostsとは

一般にクライアントが公開鍵認証のSSHを使ってサーバへログインする際,通信を暗号化するためにサーバが公開しているホスト鍵を使って通信を始める.ホスト鍵とは,SSHで公開鍵認証や暗号化通信を使用するためにサーバが用意している秘密鍵と公開鍵のペアである.この記事では公開鍵のことをホスト鍵と呼ぶことにする.
known_hostsにはSSHで繋いだサーバのホスト鍵もしくはホスト鍵のフィンガープリント」が登録されている.デフォルトの設定だと初めてサーバにSSHでアクセスする際以下のように聞かれると思う.これにyesと答えるとknown_hostsに接続先のサーバのホスト鍵もしくはフィンガープリントが登録される.

$ ssh user@192.168.100.200
The authenticity of host '192.168.100.200 (192.168.100.200)' can't be established.
ECDSA key fingerprint is SHA256:qKZyNM7JGgEGH7QWLANKBpNlFwM1XT7i45Z5oB7V61Y.
Are you sure you want to continue connecting (yes/no)? yes

二度目のアクセスからは「known_hostsに記録されているサーバのホスト鍵もしくはフィンガープリントと実際のホストが返すサーバのホスト鍵もしくはフィンガープリントが一致した場合にそのホストは認証済みである」と判断され確認メッセージは表示されない.ここでは割愛するがホスト公開鍵に対するホスト秘密鍵をサーバが所有していることを確認するには公開鍵暗号技術のチャレンジ&レスポンス方式を用いる.

known_hostsには「なりすまし」による第三者の介入を防ぐという重要な役割がある.二度目以降のSSHによるアクセスにおいてホスト鍵を検証することでホストを認証するので,誤って第三者が用意しているサーバに繋ぐことを防ぐ.
また,ワーム1のようなマルウェアの感染に対する緩和策としても有効である.要するに,初回のSSHに確認が求められるので,被害を受けたサーバから自動で違う新しいサーバへ接続できなくする効果があるというわけだ.

まとめるとknown_hostsとは「公開鍵認証のSSHでログインしたサーバのホスト鍵を記録し,次回からのホスト認証で利用されるファイル」である.

構成

今回はPHPのcmd関数を使って,SSHを用いたrsyncコマンドを実行してサーバAからサーバBへファイルを転送することを想定する.このプログラムはブラウザからApache経由でアクセスされ,プログラムの実行ユーザはwww-dataとする.
サーバAからサーバBへのSSHでは公開鍵認証を使用するため,サーバA上でsu -u www-data ssh-keygenのようにwww-dataとしてパスフレーズ無しの秘密鍵・公開鍵のペアを生成し,公開鍵をサーバBへ設定した.

f:id:a-mochan:20191220000318p:plain
構成図

起きた事象と原因

ブラウザでアクセスしてWebアプリケーションが動いてrsyncコマンドが実行されると,以下のようなメッセージがApacheのエラーログに出力された.

Host key verification failed.
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(235) [sender=3.1.2]

ホスト鍵の検証に失敗している.冒頭で述べたように,「サーバにSSHで初めてアクセスする場合,クライアントのknown_hostsにホスト鍵を登録しなければならない」ためである.

解決策1

1つめの解決策は事前にサーバBSSHでアクセスしておき,known_hostsにホスト鍵を登録しておく方法である.この方法ではサーバBのホスト鍵が何らかの理由で変わると,known_hostsで保存されているホスト鍵と一致しなくなりホスト鍵の検証に失敗するので注意する.また接続するサーバが増えた時も事前にSSHでアクセスしknown_hostsにホスト鍵を追加しておく必要があることに注意する.

解決策2

2つめの解決策は,クライアントのSSHの挙動を設定するファイルである/etc/ssh/ssh_configStrictHostKeyCheckingnoとする方法である.これでサーバBへの初回アクセス時に対話的に聞かれることなく自動でknown_hostsにホスト鍵が追加される.StrictHostKeyCheckingのパラメータは以下のようになっていてデフォルトはaskである.2

StrictHostKeyChecking 意味
ask 初回のSSHでサーバにアクセスする際にknown_hostsへサーバのホスト鍵を登録するか対話的に確認される
yes known_hostsへの新規追加はできず,既にknown_hostsで定義されているサーバへしかアクセスできない
no 初回のSSHでサーバにアクセスする際に自動でknown_hostsにホスト鍵を追加

StrictHostKeyCheckingnoにする時は,利便性とセキュリティ面を考慮して設定しよう.

まとめ

  • 公開鍵認証のSSHを使ったファイル転送を実現するのにknown_hosts周りを調べた
  • /etc/ssh/ssh_configはマニュアルページにまだまだたくさんの設定項目がまとめられているので困ったら参考にしようと思う
  • known_hostsに追加されているデータが何なのか,追加・削除にどんな意味があるのか等の概要を理解できた

EC2 Image Builderの「ロール」と「ビルド環境」は適切に設定しよう

先日のre:Inventで発表されたEC2 Image Builderを試してみた.EC2 Image Builderで簡単なAMIを作成して,つまづいた箇所があったのでメモとして残しておこうと思う.

EC2 Image Builder

EC2 Image Builderとは,OSイメージのレシピとイメージ作成後のテストをパイプラインでつないでAMIの作成を自動化してくれるサービスである.従来,AMIの作成を自動化するにはAWS Systems Manager(以下,SSM)やHashiCorp製品のPackerなどを組み合わせる利用が目立ったが,EC2 Image BuilderはAMI作成に関する一連の作業を一気に引き受けて自動化してくれる.仕組みとしてはEC2 Image Builderが裏でSSMを動かしてAMI作成作業を抽象化している.現状ではAmazon Linux 2Windows Server 2012R2, 2016, 2019をOSとしてサポートしており,すべてのリージョンで使用可能である.

今回試してみること

以下のブログを参考にEC2 Image BuilderでAMIを作成する.今回はPHP7.3がインストールされたAMIを作成し,そのAMIが正常にEC2で起動するかテストする.その中でつまづいたポイントを紹介していく.

aws.amazon.com

EC2 Image Builderを使用してAMIを作成

説明に入る前につまづいたポイントを列挙しておく.

  1. EC2 Image Builderで使うロールのポリシーにEC2InstanceProfileForImageBuilderAmazonSSMManagedInstanceCoreが必要
  2. AMIをビルドする環境がインターネットに出ることができないとダメ

それではEC2 Image BuilderでAMIを使っていく.まずはEC2 Image Builderのサービスページで「Create image pipeline」を押下する.すると全部で3ステップあるパイプラインの作成画面が出てくる.1ステップ目でレシピを設定し,2ステップ目でパイプラインを設定し,3ステップ目でオプションを設定する.それぞれ設定していく.

1ステップ目では,AMIのOSAMIに入れるソフトウェアテスト方法を設定する.
今回使うOSはAmazon Linux 2とし,新規でAMIを作成することとする.ちなみに既存のAMIを指定することも可能である.
次に,componentsを選択する.EC2 Image BuilderではAMIにインストールするパッケージやミドルウェアcomponentsとして管理しており,componentsを選択することでAMIに必要なソフトウェア群を入れることができる.componentsはカスタムして作成することもできるが,ここではAWS管理のPHP7.3のパッケージを指定する.設定は以下のようになる.

f:id:a-mochan:20191213224408p:plain
AMIにインストールされるOS

f:id:a-mochan:20191214000955p:plain
AMIにインストールするソフトウェア群とテスト方法

2ステップ目では,EC2 Image Builderを実行するロールビルドのスケジュールビルドする環境を設定する.
ロールの設定では1つめのつまづきポイントに気をつける必要がある.AWS管理のcomponentsのビルドとテスト実行に必要十分な権限を持つEC2InstanceProfileForImageBuilderポリシーとSSMを動かすためのAmazonSSMManagedInstanceCoreポリシーをロールに付与しなければ,ほとんどの場合EC2 Image Builderが正常に動かない.自分はAmazonSSMManagedInstanceCoreポリシーをつけ忘れたのでエラーが出た.エラーの詳細はSSMの「オートメーション」ページで確認することができる.このポリシーをつけ忘れると下図のようなタイムアウトが起こるので,もしこのエラーが出たらAmazonSSMManagedInstanceCoreポリシーの付与忘れを疑うとよいかもしれない.

f:id:a-mochan:20191213211441p:plain
SSMでのエラー

ビルドのスケジュールは手動で行うように設定しておき,ビルドする環境の設定に移る.ここでは2つめのつまづきポイントに気をつける.AMIを作成する際多くの場合インターネットを通して通信すると思うので,ビルドする環境はインターネットへ通信可能なところに置いておく必要がある.自分はビルド環境を特に何も指定していなかったので,インターネットに出ることができないデフォルトの環境でビルドしてエラーが出てしまった.このエラー画面はいくつかあるので割愛させていただくが,1つめのつまづきポイントを解消してなお失敗する場合はビルド環境を疑うとよいかもしれない.設定は以下のようになる.

f:id:a-mochan:20191213233737p:plain
ロールとビルドスケジュールの設定

f:id:a-mochan:20191213234355p:plain
ビルド環境の設定

3ステップ目は,ソフトウェアライセンスとAMIの関連付けAMIの名前・タグAMIの配布先指定の設定を行う.
こちらはオプションなので必要であれば設定する.

これでパイプラインの設定は終わりである.あとは作成したパイプラインを実行すれば,EC2上での起動テスト済みかつPHP7.3がインストールされたAMIの出来上がり.

まとめ

  • EC2 Image Builderを使って簡単なAMIを作成した
  • AMI作成に自動化をサクッと作れるのはいい
  • エラー内容から直接的な原因が分かりづらかった
  • AWS管理されていないソフトウェアをcomponentsに書いて管理するより,既存で管理しているGitHubやCodeCommitから適用したいができない
  • サポートしているOSが増えて欲しいなという気持ち
  • まだまだ出たばかりなので今後に期待

PHPerの学びの場「PHPカンファレンス2019」に参加した

自身としては今年で3回目の参加となるPHPカンファレンス2019に参加してきた.年に一度開催されるPHPカンファレンスは今年も盛況でとても学びのある面白いセッションが多かった!自分の参加したセッションのメモと感想を残そうと思う.

fortee.jp

MVCにおける「モデル」とはなにか

MVCにおけるモデルって何だっけ?という技術よりもむしろ思想や概念に近いお話をされた.

  • ドメインモデル
    • ドメインモデル=ドメイン(目的)を情報処理システム(手段)によってモデル化したもの
    • ドメインモデルは社会的なシステムとして個人を制約する
  • メンタルモデル

    • メンタルモデルとは頭の中にある「ああなったらこうなる」といった行動のイメージを表現したもの
    • 道具から適切なメンタルモデルを構築するにはフィードバックも重要.フィードバックに即時性がないとメンタルモデルを作り上げられない.
    • コンピュータのメンタルモデルを作るのは難しい
    • ユーザイリュージョンとは,ユーザに錯覚を起こさせることで本来なら理解し難い対象をあたかも理解したかのように思わせること.
    • スプレッドシートはユーザイリュージョンが働いているいい例
    • コンピュータのメンタルモデルにおいて,人は作業そのものに取り組んでいる状態がいい
  • パーソナルコンピュータ

    • システムと個人の関係を変える
    • 人がシステムをコントロールする.そのために,コンピュータの言語を理解する必要がある.
  • MVCとは何か

    • ユーザがドメインの情報(ドメインモデル)を直接みて操作する時,ユーザのメンタルモデルとドメインモデルには距離があるのでユーザイリュージョンが必要.
    • ユーザイリュージョンを起こすためにツールとモデリング言語が必要.これがMVCでいうVCの部分.

感想

MVCについてこんなに考えたことはなかったし,思想や概念の話がメインですごく頭を使った.理解が追いつかない部分もあったが,自分の中にあるふわっとした考えを深めて言語化できそうな情報源や言葉を知ることができたのでまた後で見直したい.

徳丸先生による徳丸試験例題解説とPHP7初級書籍贈呈キャンペーンと市場動向

ランチセッションは2019/12から始まるWebセキュリティ試験(通称徳丸試験)の例題を徳丸先生自ら解説された. www.phpexam.jp

大学院生の頃に「体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践」(通称徳丸本)を読みふけっていたので出題された3問の例題に正解できた🎉出題範囲は徳丸本(第二版)からだそう(ただし7章 脆弱性診断入門は範囲外)なので受けみようかという気になってきた.

思想と理想の果てに -- クリーンアーキテクチャのWebフレームワークを作ろう

クリーンアーキテクチャフレームワークを作っているお話をされた.

質疑応答
Q ユースケースからのエンティティは直接操作してよい?
A ユースケースからのエンティティはインタフェースを介さないので直接操作してOK

感想

前半部分のクリーンアーキテクチャの解説がとても分かりやすく,それだけでもとても勉強になった.フレームワークを作成する話では試行錯誤の様子をたくさん話されていた.大事なポイントはアウトプットすると情報が集まってくるだと思う.新しいことにチャレンジするには,まずやってみてアウトプットして周りからの反応やアドバイスをもらいさらに改良していくことの重要さを改めて感じることができた!

Webサービスのトラブルの現場 ~ Webサービスの今と昔 ~

昔に経験した現場のトラブルを振り返り,今はどのようなことに気をつければよいかをお話された.

Webサーバが死ぬ原因

  • アプリケーションのバグ
    • テストを書こう
  • ハードウェア障害
  • アクセス過多
    • モニタリングしよう
    • trdsqlがめちゃくちゃ便利
  • おや?共有サーバの様子が...
    • 一緒にホスティングされている他のサービスがおかしいと巻き込まれることもある

バッチの突き抜け

サイトにアクセスできない

  • どこのレイヤーがダメなのか把握するためにしっかり切り分けする.

失敗から学ぶクラウドの正しい歩き方

  • データストアが死ぬと対応が難しい
  • 制御できるところできないところを知る
  • 自分たちの急所を知る

まとめ

  • 便利になるということは抽象化されている.トラブル時は抽象化されているものの仕組みを知っている必要がある.

質疑応答
Q 障害など何か問題が起きた時,自分の知らないところが原因だった場合はどうやって調べる?
A 知らないことはTwitterで調べたりして,知らない人から聞くことも1つの方法.

感想

全体としてインフラっぽい内容だった.身の周りで起きた障害を紹介しつつ,n+1構成やシステムの切り分けの話など基本的な部分の大事さをお話された.システムを作るにあたって基本は大事だしおろそかにしてはいけないと感じた.さらに,trdsqlといった便利なツールや可視化の方法などは実務で使えそうでとても有意義な時間だった.

オニギリペイのセキュリティ事故に学ぶ安全なサービスの構築法

オニギリペイという架空の決済システムのロールプレイを通してサービスの構築を学ぶ.

オニギリペイの8つの試練

  1. キャンペーンを実施したら,某筋からお叱りを受ける
  2. ログインIDを発番したのに不正ログインが多発
  3. 二段階認証を強制したのに不正ログインが止まらない
  4. ヘルプデスクが狙われる
  5. スマホアプリの脆弱性を指摘される
  6. スマホアプリのアップデートを広報したらアプリがリジェクトされる
  7. 「あの有名な脆弱性」で大変なことに
  8. WAFを導入したらかえって脆弱になる

オニギリペイから学ぶ今後のセキュリティ

  • 開発時のセキュリティも重要だが,上流からのセキュリティの検討も大事
  • リスクアセスメントの方法
    • ベースラインアプローチ:既存の標準や基準をもとにベースライン(自組織の対策基準)を策定し,チェックしていく方法.徳丸本を活用するのもよい.
    • 非形式的アプローチ:徳丸さんのような専門家に聞く.
    • 詳細リスク分析:情報資産に対し資産価値脅威脆弱性セキュリティ要件を識別し,リスクを評価していく.
    • 組み合わせアプローチ:よく用いられるのがベースラインアプローチ詳細リスク分析のいいとこ取り.
  • 業務フローでリスク分析
  • 発注がセキュリティを左右する.発注者が提案書の内容に関与できるのは提案依頼書を出すタイミングだけ.
  • 脅威分析はIPAが公開している「IoT開発におけるセキュリティ設計の手引き」がよくまとまっているので参考にすると良い

感想

業務で開発現場を離れインフラをメインでやるようになってからセキュリティについて考えることが増えてきた.そんな状況の中,今起こりうるシチュエーションでセキュリティのお話を聞けたのはすごくよかった.開発現場レベルのセキュリティももちろん大事だが,脅威分析やセキュリティアセスメント等の上流工程でのセキュリティも重要だということをしみじみ感じた良いセッションだった.

www.slideshare.net

LT

PHPカンファレンスはメインのセッションの後にLTが行われる.弊社からも3人の新卒が発表したので紹介させていただきたい.

1人目は新卒2年目の@yu12co_mmさん.1つのリポジトリで管理されていた2つのサービスを分割する際に使用したビジュアルリグレッションテストの話をされた.この言葉を聞いたことのない方はお読みいただければと思う.スライドのストーリーも面白い!

2人目もまた新卒2年目の@shimamizさん.PHP力を高めるためにPHP競技プログラミングをやった話をされた.PHPtという標準入力で便利なツールがあるらしい.

2年目エンジニアがスキルアップのためにPHPで競プロやってみた by 瑞 | トーク | PHP Conference Japan 2019 #phpcon - fortee.jp

3人目は新卒6年目の@_ohshigeさん.今まで実績のないクリーンアーキテクチャを採用してプロダクト作った時の話をされた.クリーンアーキテクチャの導入を検討しているそこのあなたにおすすめ.

全体の感想

毎度のことながら参加したすべてのセッションがとても面白かった.登壇者や関係者のみなさまにお礼を申し上げたい!
PHPにあまり関係のないセッションもあったりするが,それがとても勉強になったりして,いい意味で裏切られる感じが好きだ.
一応PHPカンファレンスなのでPHPの話もしておくと,次のPHP8ではJust-In-Time Compilerにより処理が早くなることいくつかの構文のシンプル化型の強化が見所だ.PHP7が出たと思ったのがすごく最近な気がするのにあっというまにPHP8がくる.開発が早く活発なのでこれからも楽しみだ.
あと,先輩や後輩が発表してたのを見て刺激をもらったので自分もどこかで発表したいなと思った.また来年も行くぞ!

GASを使ってALBのIPアドレス変更を検知しSlackへ通知する

世の中の数多くのサービスがAWSやAzure,GCPなどのクラウドにホストされることが普通になり,オンプレの時に比べてサーバや各種リソースのIPアドレスが動的に変わることが増えてきた.そんななか,業務でどうしてもクラウドで動作中のリソースに紐づいているIPアドレスの変更を検知して通知する仕組みが必要だったので,今日はその方法を紹介しようと思う.

前提

今回はAWSのロードバランサの1つであるApplication Load Balancer(以降ALB)を検知対象とする.ALBを立てると多くの場合,1つのDNS名に対して複数のIPアドレスを持つ状態となる,ここでは2つIPアドレスを持つ場合を考える.

やりたいこと

ALBが持つ2つのIPアドレスのうち,片方を事前に記録しておく.ALBのIPアドレスの変更により,その記録したIPアドレスがALBのIPアドレスと異なる状態になったとき通知を行う.

検知と通知方法

スプレッドシートGoogle Apps Script(以降GAS)を使い,Slackへ通知する方法で実現する. まずはシートに初期値として現状のALBのIPアドレスをメモしておく.また検知した日新しいIPアドレスを記録するように列を用意しておく.

f:id:a-mochan:20191128234922p:plain
ALBの初期IPアドレスをメモ

GASで実装する内容は主に以下の2つ.

  1. DNS over HTTPSを使ってクラウド上のALBのIPアドレスを取得
  2. もし今記録しているIPアドレスと違ったら変更履歴をシートに記録しSlackへ通知

まずはDNS over HTTPSを使って名前解決する.DNS over HTTPSとは,文字通りHTTPSを用いてDNSの通信を行う技術である.今回はGoogleが提供してくれているGoogle Public DNS over HTTPSという全世界の人が無料で使えるフルリゾルバを使用する.どんなものかというのは,以下のURLを叩くと雰囲気がわかると思う.

https://dns.google.com/resolve?type=A&name=example.com

次に,Slack通知を行う部分を書く.以下のブログが参考になった.

qiita.com

Incoming WebhookをSlackのチャンネルに設定して,hookするURLさえ分かれば通知できる.完成版のGASは以下のようになる.

function resolveName() {
  var ss = SpreadsheetApp.getActiveSheet();
  var apiUrl = 'https://dns.google.com/resolve'; // Google Pubic DNS API URL
  var type = 'A'; // Aレコードを指定
  var name = 'hogehoge.com'; // 検知するドメイン名

  // 名前解決
  var requestUrl = apiUrl + '?name=' + name + '&type=' + type;
  var response = UrlFetchApp.fetch(requestUrl);
  var responseText = response.getContentText();
  
  // レスポンスをパース
  var json = JSON.parse(responseText);
  var newIpList = json.Answer.map(function(ans) {
      return ans.data
    });
  
  // 登録IPアドレス取得
  var oldIp = ss.getRange(2,1).getValue();

  // IPが変更したか確認
  if (newIpList.indexOf(oldIp) !== -1) {
    return;
  }
  
  // 文字列としてパース
  newIpList.sort();
  var ipAddr = newIpList.join('\n');

  // 変更日時とIPアドレスを記録
  var date = new Date();
  Utilities.formatDate( date, 'Asia/Tokyo', 'yyyyMMdd: hhmm');
  var lastRow = ss.getLastRow();
  ss.getRange(lastRow + 1, 3).setValue(date);
  ss.getRange(lastRow + 1, 4).setValue(ipAddr);

  // Slackへ通知
  notifySlack(name,ipAddr);
}

function notifySlack(name,ip_addr) {
  var postUrl = 'https://hooks.slack.com/services/hoge/fuga/moge';
  var username = 'IP報告する男';
  var message = name + 'のIPアドレスが変わりました\n' + ip_addr;

  var jsonData =
  {
     "username" : username,
     "text" : message
  };
  var payload = JSON.stringify(jsonData);

  var options =
  {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : payload
  };

  UrlFetchApp.fetch(postUrl, options);
}

テストとしてGAS上のタブから「実行」を選んでresolveName関数を実行する.IPアドレスが変わっていればスプレッドシート検知した日新しいIPアドレスが記録され,Slackへ通知が届く.

f:id:a-mochan:20191128234446p:plain
変更履歴が追加されたシート

f:id:a-mochan:20191128233942p:plain
Slack通知画面

あとは,GAS上のタブから「編集」->「現在のプロジェクトのトリガー」をクリックし,このスクリプトを時間ベースで定期実行するようにしておけば,IPアドレスが変わったタイミングでSlackに通知が届く.Slackに気づいたら,シートの初期値として設定したIPアドレス通知された新しいIPアドレスに手動で書き換える運用をしている.もちろんGASで自動的に書き換えてもらってもよい.

f:id:a-mochan:20191129100746p:plain
定期実行設定画面

まとめ

  • GASを使ってALBのIPアドレス変更を検知してSlackへ通知した
  • GASからSlack通知も便利だが,Google Public DNS over HTTPSも便利だった
  • 今回の対象はALBだったが,他の対象にも使えそうな手段だと思う

証明書入れ替えでAkamaiにまつわるトラブルに遭遇した話

Webサービスに関わるインフラエンジニアをやっていると証明書を入れ替える時がしばしばあると思う.今日は業務中の証明書入れ替えで起こったトラブルについて書いていく.CDNサービスの1つであるAkamaiと関係のあるトラブルなので,Akamaiを使っている方は参考になるかもしれない.

構成

弊社はメディアサービスを運用しており,ページの表示速度向上のために,画像等の静的なファイルについてはAkamaiを利用してキャッシュを効かせている.まずは必要な部分のシステムと画像が表示されるまでのフローを図示する.

f:id:a-mochan:20191121234104p:plain
画像が表示されるまでのフロー

弊社では画像を置くためのサーバ(以降オリジンサーバ)を用意しており,それらの画像はAkamaiが管理するサーバ(以降エッジサーバ)にキャッシュとして存在している.画像が表示されるまでのフローは以下の通り.

  1. ユーザがWebサーバからコンテンツを取得
  2. そのコンテンツで表示される画像はエッジサーバから取得
  3. エッジサーバは画像がキャッシュに存在するならそれをユーザへ返し,存在しなければエッジサーバがオリジンサーバから画像を取得しユーザへ画像を返す

なお,社内のDNS上でimage.example.jpexample.jp.edgesuite.netへCNAMEとして向けられており,画像へのアクセスはhttps://image.example.jp/path/to/image.jpgのような形でアクセスすると思っていただけたらよい.

起きた現象

今回,証明書の入れ替えを行うのはWebサーバとオリジンサーバである.いつものように証明書を入れ替えて,入れ替え対象となるドメインを確認した.ざっと確認してOKだったので,今日も無事に完了かと思ったその時,現場の開発エンジニアから以下のようなスクショが飛んできた.

f:id:a-mochan:20191121220647p:plain
ブラウザのエラー画面

エッジサーバからan error occurred while processing your requestが返されるらしい.詳しく聞くとどうやらエッジサーバにキャッシュとして存在するhttps://image.example.jp/path/to/image.jpgのようなアクセスだと画像が表示されるが,エッジサーバにキャッシュとして存在しないhttps://image.example.jp/path/to/image.jpg?hogehogeのようなアクセスだと画像が表示されないらしい.オリジンサーバのアクセスログを見てもそれらしいアクセスはない.おそらくエッジサーバからオリジンサーバへのアクセスが怪しい.報告してくれたエンジニアの話を聞くとタイミング的に証明書を変えた時間からおかしくなったようなので,試しに元の証明書に戻したところ無事画像が表示された.

原因調査

すぐにAkamaiのサポートに問い合わせた.するとサポートから,「エッジサーバがオリジンサーバとSSL通信を実施する際に,Origin SSL Certificate Verificationの設定と一致しない証明書が使用されたため,エラーが発生した可能性がある」と返答がきた.これはAkamaiが信頼しているルート認証局がチェーンに入っていない証明書をオリジンサーバで使うとエラーになるということを表している.どうやらAkamaiの管理コンソールで,Akamaiが信頼するルート認証局を設定する項目があるらしく探してみるとそれらしき項目を発見.

f:id:a-mochan:20191121222159p:plain
Akamaiが信頼するルート認証局を設定する画面

信頼するルート認証局として1つ登録してある.ここで我々インフラは原因に気づいた.今回発行した証明書のルート認証局が変わっていたのである.つまり変更した新しいルート認証局Akamaiの信頼するルート認証局一覧になかったのでエッジサーバからオリジンサーバへのアクセスができなくなったというわけだ.

対応

原因がわかったので,Akamaiの信頼するルート認証局一覧に新しい認証局を追加しようかと考えた.しかし,頻繁に起こるわけではないにしてもルート認証局が変わるたびにAkamaiの設定をいじるのはあまりしたくない.そう思いながら設定項目を見ていると,我々が設定していた方法は認証局をカスタムで設定する方法であって,Akamaiが管理するルート認証局セットを使う方法もあることに気が付いた.仮にまた証明書のルート認証局が変わった場合,Akamaiの信頼するルート認証局に新しいルート認証局が登録されているかを確認しなければならないのはどちらの設定方法でも言えることだ.しかし,Akamaiが管理するルート認証局セットを使う方法では以下で図示しているように数多くのルート認証局が登録してあり,今後の運用で今回のような実害が発生しにくいのはこの方法だと思ったのでこちらを使うことにした.ただここで1つ罠なのが,以下の図で示すように,我々が新たに使うルート認証局の共通名がブランクになっていてわかりにくい点だ.この場合はフィンガープリントの方で判断するしかないので注意する.

f:id:a-mochan:20191121222342p:plain
Akamaiが管理するルート認証局一覧

Akamaiの信頼するルート認証局の設定を変更し,既存のWebサービスに影響が出ていないことを確かめ,改めて証明書を入れ替えると,エッジサーバにない画像はオリジンサーバから返却され正常に表示された.めでたしめでたし.

まとめ

  • 証明書入れ替えにより,Akamaiのエッジサーバからオリジンサーバへのアクセスがエラーになった問題を解決した
  • 原因はAkamaiが信頼しているルート認証局がチェーンに入っていない証明書をオリジンサーバで使ったこと
  • 今回のように証明書の更新でルート認証局が変わる可能性を考慮すると,Akamaiでの信頼するルート認証局の設定はAkamaiが管理するルート認証局セットを使う方法がベターかなと思う

HashiCorpの日本語Vaultハンズオンを実施した!

業務でHashiCorpのVaultを使うかもしれないのでハンズオンをやってみた.GitHubに日本語のハンズオン資料が公開されているのでそれをもとに進めた.

github.com

ハンズオンアジェンダ

GitHubで公開されている日本語のハンズオンアジェンダを以下に示す.この中にはハンズオンとしての内容がまだ公開されていないものも含まれている.

  • 初めてのVault
  • Secret Engine 1: Key Value
  • Secret Engine 2: Databases
  • 認証とポリシー
  • Auth Method 1: LDAP
  • Auth Method 2: AppRole
  • Auth Method 3: OIDC
  • Auth Method 4: GitHub
  • Response Rapping
  • Secret Engine 3: Public Cloud (AWS, Azure, GCP)
  • Secret Engine 4: PKI Engine
  • Secret Engine 5: Transit (Encryption as a Service)
  • Secret Engine 6: SSH
  • 運用系機能色々
  • CIツール連携(Concourse, Jenkins)
  • Kubernetes連携
  • Cloud Foundry連携
  • Enterprise機能の紹介

今回は初めてVaultに触るので初めてのVaultSecret Engine 1: Key ValueSecret Engine 2: Databases認証とポリシーの4つをやってみる.具体的な手順はハンズオンに任せるとして,この記事ではそれぞれのハンズオンで学んだことと,自分が思う重要な概念や押さえるべきポイントについてまとめてみようと思う.

1. 初めてのVault

ここではVaultをインストールしてVaultを立ち上げ,あとのハンズオンの準備をしていく.

このハンズオンで学んだこと

  • Vaultのインストール
  • Vaultサーバの立ち上げ(開発モード)
  • シークレットエンジン
    • シークレットの保存・取得
  • Vaultサーバの立ち上げ(本番モード)
    • Vaultの初期化処理

シークレットエンジン

シークレットエンジンとはkey=valueでデータを格納・生成・暗号化するVaultのコンポーネントである. AWSのIAMのキーやDatabaseへアクセスするシークレットなどさまざまな用途で使えるように,Vaultで専用のコンポーネントが用意されている.例えばkvシークレットエンジンを新規で使う時はvault secrets enable -path=kv kvのようにシークレットエンジンを有効にする.さらに,パスを指定してエンドポイントにみたてて,以後そのエンドポイントに操作を実行するのが特徴である.用途にあったシークレットエンジンを使うことでシークレットを適切に扱うことが可能となる.

Vaultの初期化処理

本番モードのVaultではセキュアな設計がされているので,起動直後はsealedという状態になっておりVaultへログインできない.ログインするにはinitunsealという初期化処理が必要になる.まずinit処理をする.

$ vault operator init
Unseal Key 1: JLYUBHrdwWu2dxwjCazqsCQ4OJPJtiMFsIZeO1osyJ1t
Unseal Key 2: nEjGt+rYSOomqmyTsuF7PnKPS+NE3yPEfuo6WDXm/QDR
Unseal Key 3: d5nTCzEGKIBPFGCC3ANzKf8gGgwoV8APr6V9KdDcNjOW
Unseal Key 4: sd7vzV1FQk/96xQmIuKRKhydy9tGEmORbFyozAKxFc4n
Unseal Key 5: JqLGxcvA3gLrwQHIliWvl1ytkMbuDGu/6p2KzGpvnCa9

Initial Root Token: s.Wi5WjPfPHqbFAcXjwsDCHQLd
~~~

initの処理をすると,VaultをunsealするためのUnseal KeyInitial Root Tokenが生成される.試しにこの状態でRoot Tokenを使ってログインしてみる.

$ vault login
Token (will be hidden):
Error authenticating: error looking up token: Error making API request.

URL: GET http://127.0.0.1:8200/v1/auth/token/lookup-self
Code: 503. Errors:

* error performing token check: Vault is sealed

エラーになる.Vaultではsealed状態になっているといかに強力な権限のあるトークンを使ったとしても操作は受け付けない.unsealの処理はUnseal Keyを使う.デフォルトだと5つのキーが生成され,そのうち3つのキーが集まるとunsealされる.このアルゴリズムシャミアの秘密鍵分散法と呼ばれる.3つのUnseal Keyを入れてみる.

$ vault operator unseal
Unseal Key (will be hidden):
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    1/3
Unseal Nonce       32d20912-88dd-16e2-28d3-87344abec5fc
Version            1.2.3
HA Enabled         false
$ vault operator unseal
Unseal Key (will be hidden):
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    2/3
Unseal Nonce       32d20912-88dd-16e2-28d3-87344abec5fc
Version            1.2.3
HA Enabled         false
$ vault operator unseal
Unseal Key (will be hidden):
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3
Version         1.2.3
Cluster Name    vault-cluster-7815e4aa
Cluster ID      12747d60-78d3-8fad-6082-102052ac8c74
HA Enabled      false

これでログインができる.

$ vault login
s.Wi5WjPfPHqbFAcXjwsDCHQLdToken (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                s.Wi5WjPfPHqbFAcXjwsDCHQLd
token_accessor       NX8h0laChNLO5oObu2nDtovT
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

やや面倒なこの仕組みの何が嬉しいのかということについては以下のブログが参考になった.

christina04.hatenablog.com

2. Secret Engine 1: Key Value

ここではシンプルなKeyValueStore型のシークレットエンジンを使ってデータのCRUDをするハンズオンを行う.

このハンズオンで学んだこと

  • Key Value Store型のシークレットエンジン
    • データのCRUD
    • データのバージョニング管理
    • 2つのデータ更新パターン

3. Secret Engine2: Databases

ここではMySQLとVaultを使い,Vaultで発行したシークレットを用いてMySQLにアクセスするハンズオンを行う.

このハンズオンで学んだこと

  • Databaseのシークレットエンジン
  • Vaultが対応しているDatabaseシークレットエンジン一覧
  • VaultがMySQLの操作を制限するようなロールを作成し,そのロールの内容に基づいてVaultがアプリケーションへシークレットを生成する流れ
  • 動的シークレットの破棄
  • Rootユーザのパスワードローテーション

MySQLへのアクセスの流れ

以下の図のような流れでアプリケーションはMySQLへのアクセスを実現する.

f:id:a-mochan:20191117084705p:plain
MySQLへのアクセスの流れ

それぞれのステップをみていく.

① Vaultに特権ユーザのクレデンシャルとデータベースの接続先を登録する.MySQLrootユーザ(特権ユーザ)を指定してコネクションの設定をする.②で作成するロールもここで指定しておく必要がある.また,ロールは複数指定できる.

$ vault write database/config/mysql-handson-db \
  plugin_name=mysql-legacy-database-plugin \
  connection_url="{{username}}:{{password}}@tcp(127.0.0.1:3306)/" \
  allowed_roles="role-handson" \
  username="root" \
  password="rooooot"

MySQLへの権限やシークレットのTTLを記述したロールを定義する

$ vault write database/roles/role-handson \
  db_name=mysql-handson-db \
  creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT SELECT ON *.* TO '{{name}}'@'%';" \
  default_ttl="1h" \
  max_ttl="24h"

③ クライアントからVaultに対してシークレットの発行を依頼する

$ vault read database/creds/role-handson
Key                Value
---                -----
lease_id           database/creds/role-handson/G3tJu3gN8nGoL8Z8MlVkvD2g
lease_duration     1h
lease_renewable    true
password           A1a-qIpUomJUoXOiHlQh
username           v-role-PaKXo1BO1

④ 取得したシークレットでMySQLにアクセス

$ mysql -u v-role-PaKXo1BO1 -p -h 127.0.0.1
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 5.7.22 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

$ mysql>

⑤ ロールによりSelectはできるがInsertはできないことを確認する.

$ mysql> select * from products;
+------+-------------+-------+
| id   | name        | price |
+------+-------------+-------+
|    1 | Nice hoodie | 1580  |
+------+-------------+-------+
1 row in set (0.00 sec)

$ mysql> insert into products (id, name, price) values (2, "hoge", 1600);
ERROR 1142 (42000): INSERT command denied to user 'v-role-PaKXo1BO1'@'127.0.0.1' for table 'products'

動的シークレットの破棄

ロールにシークレットのTTLを設定できる.TTLを過ぎれば当該シークレットではMySQLにログインできなくなる.またrevoke処理で発行したシークレットを明示的に無効にできる.再びMySQLにログインしたい場合は,再度シークレットを発行するか,シークレットのTTLが切れる前にrenew処理と呼ばれるTTLを延長する処理のどちらかを実行すればよい.

Rootユーザのパスワードローテーション

VaultにMySQLの特権を持たせているためそのパスワードの扱いが非常にセンシティブである.Vaultにはコンフィグレーションとして登録したデータベースのパスワードをローテーションさせるAPIがある.これを使ってこまめにRootのパスワードをリフレッシュできる.ただし,MySQLのRootユーザがVaultに登録してあるもの1つだけだと,Rootユーザのパスワードのローテーションを行った後はRootのパスワードはVaultしか扱うことができない.そのため通常別の特権ユーザをMySQLに準備してから行う

4. 認証とポリシー

ここではエンドポイントごとにポリシーを設定してトークンを発行し,アクセスコントロールを試してみるハンズオンを行う.

このハンズオンで学んだこと.

  • ポリシーの利用方法

ロールとポリシーの違い

ポリシーの作成方法や使い方はハンズオンで十分学ぶことができる.個人的に思うここでの重要なポイントは1つ前のSecret Engine2: Databasesで学んだロールと今回のポリシーの違いだと思う.利用するアプリケーションのアクセスをコントロールをしているという点においては同じだが,ロールとポリシーではコントロールする対象領域が異なる.1つ前のハンズオンを例にとると,ロールの場合コントロールする対象領域はMySQLだった.つまり,MySQLへアクセスするシークレットが発行された上で,そのシークレットでMySQLにログインするアプリケーションにどんな制限を持たせるかというのを定義するのがロールである.一方,ポリシーはVaultのエンドポイントを対象領域としている.例えば,「kvタイプのとあるエンドポイントにはread,writeを持たせるけど,databaseタイプのエンドポイントにはアクセスさせない」というようなポリシーを定義をする.もしこの例が適用されたトークンが発行されると,たとえロールの設定にすべてのテーブルへのアクセス許可を書いていたとしても,そもそもdatabaseタイプのエンドポイントにアクセスができないのでシークレットが発行されず,MySQLにログインすることすらできない.まとめると,ポリシーはエンドポイント単位でのアクセスコントロールを制御し,ロールはエンドポイントに対応しているSecret Engineの中でもっと細かいアクセスコントロールを制御するものだと思う.

まとめ

  • HashiCorpのVaultハンズオンを一部試してみた
  • Vaultの日本語ハンズオンはとても分かりやすかった.Vaultが何か分からない方におすすめ!
  • 認証を絡めたハンズオンもいくつかあるのでやっていこうと思う.また,この記事を書いている間にもハンズオンが変更されたり増えていたりしたのでまた試してみようと思う.

Configで検知した異常をAWS ChatbotでSlack通知できるようになったってよ

以前自分が書いた記事の最後に,AWS Config(以降Config)で検知した異常をAWS Chatbot(以降Chatbot)を使ってSlackへ通知できるようになったことについて触れたので実際に試してみた.

a-mochan.hatenablog.com

前提

今回も以前の記事と同様にAmazon Simple Notification Service(以降SNS)とConfigを使うが,それらについては設定済みとする.また,Configで検知するルールは,インバウンドのsshを設定しているセキュリティグループでIPをフルオープンにしていないかをチェックするrestricted-sshを使う.SNSやConfigの設定については上記の記事に書いてあるので参考までにご覧いただけたらと思う.

今回使用するChatbotについて公式を引用して説明しておく.公式によるとまだBeta版だそう.

AWS Chatbot は、Slack チャンネルや Amazon Chime チャットルームで AWS のリソースを簡単にモニタリングおよび操作できるようにしてくれるインタラクティブエージェントです。 https://aws.amazon.com/jp/chatbot/

構成

今回の登場人物はConfigとSNS,Chatbot,Slack,Amazon CloudWatch(以降CloudWatch)である. 流れとしてはまずChatbotでSNSとSlackを紐づける.そしてCloudWatchで「Configで検知した異常」をトリガーとして「SNSへ通知する」ルールを設定.以上の設定によりConfigで検知した異常をSlackまで届けることができる.また,今回の構成は以下のように1つのアカウント内で完結する.なお構成図のChatbotがAWS提供のアイコンではないのは,ChatbotがBeta版だからかアイコンを公式からダウンロードできなかったからである...ご了承願いたい.

f:id:a-mochan:20191109192025p:plain
Configの検知をSlackへ通知するまでの流れ

ChatbotによるSNSとSlackの紐づけ

さっそくChatbotのページで,Slackを選んでchat clientを作成.Slackへのログインが求められるのでログインする.

f:id:a-mochan:20191109150437p:plain
Chatbot作成画面

ここからchat clientの設定に入る.chat clientでは主にSNSに届いたリクエストをどのSlackチャンネルに転送するかを定義する.まずは,最終的に通知するSlackチャンネルを選択する.

f:id:a-mochan:20191109153452p:plain
通知するSlackを選択

次にchat clientに割り当てるロールを設定する.特にこだわらなければデフォルトで設定されている新規作成でOK.

f:id:a-mochan:20191109153903p:plain
Chatbotに割り当てるロールを選択

最後に,Slackへ転送されるSNSトピックを選択する.

f:id:a-mochan:20191109155244p:plain
Slackと紐づけるSNSを選択

chat clientを作成が成功した時点でSNSサブスクリプションにもchat clientが追加されていると思うので,これでSlackとSNS間の設定は終了.

CloudWatchの設定

次に,Configの異常をSNSへ通知するためにCloudWatchを設定する.Configの異常イベントをトリガーにSNSへ通知するCloudWatchのルールを作成する.画面左側では,Configで設定済みのルールに違反したイベントを監視するよう設定している.画面右側では,通知先SNSトピックを設定している.

f:id:a-mochan:20191109182721p:plain
CloudWatch設定画面

テスト

ここまでで設定は終わったので実際にテストしてみる.試しにセキュリティグループを任意の場所からsshできるよう設定してみると,少し経ってからSlackへ通知が届く.

f:id:a-mochan:20191109200450p:plain
届いたSlackの内容

まとめ

  • ChatbotとSNS,CloudWatchを使ってConfigで検知した異常をSlackへ通知してみた
  • Lambdaを使ってもできるがChatbotならコードを書かなくてすむ
  • Chatbotは無料だしLambdaを使わない分料金が安くなるかと思ったが,ChatbotだとCloudWatchを使わなければならないので料金はあまり変わらないかもしれない
  • Slackへの通知をカスタマイズしたいならLambdaを使った方法,とにかく通知がまずしたいならChatbotを使う方法がいいかもしれない