カンムにおけるGitHub Projects Beta活用方法

マニアックなSQLに続き2回目の登場、COOの achiku です。

これは

カンムでは GitHub Projects (Beta) を利用してプロダクト改善を推進している。Private Betaの時点から使い始めてから約4ヶ月、今の運用に落ち着いてから約2ヶ月程度経過したため、導入の目的、目的を鑑みた運用方法、現時点での状態をまとめる。誰かの参考になれば嬉しい。

※以降断りのない場合はGitHub ProjectsもしくはProjectsはGitHub Projects (Beta)を指す ※同様に以降断りのない場合はprはGitHub上のPull Requestを指す

前提(2022/03時点)

まずは前提の共有から。ぱっと見ても分かるように、小さくはないがとんでもないサイズでもない、という状況のチームの話であるという前提がある。

  • 作っているもの
  • 2016年ローンチ当時からGitHubを利用して開発している
  • アプリと通信するVandle API、決済処理を担うProcessor、ネイティブアプリのリポジトリは別れている
  • プロダクトのサイズ感が分かりそうな情報(as of 2022/03)
    • Vandel API
      • テーブル数: 310
      • APIエンドポイント数: 121
    • Processor
      • テーブル数: 47
      • APIエンドポイント数: 6
  • バンドルカードチーム構成: 約15名
    • ソフトウェアエンジニア(バックエンド/インフラ): 4
    • ソフトウェアエンジニア(モバイル): 1.5
    • データサイエンティスト/アナリスト: 2
    • デザイナー: 2.5
    • マーケター(広告運用含む): 3
    • PM: 1
    • 何でもやる人(achiku): 1
    • (0.5=poolという新プロダクト兼務)

2021/06に Introducing new GitHub issue が公開された。その3ヶ月後の2021/09のThe new GitHub Issues – September 29th updateにて、Workflowsが導入された事が確認できた。これで複数リポジトリを跨いでソフトウェアエンジニアは通常通りissue/prを中心に仕事をしていれば進捗がProjects上に反映される基礎が整った。

導入の目的

Projects、なんとなく良さそうとはいえ目的を明確にしなければ情報の設計、運用の設計、チームの納得感の醸成を行うことは出来ない。サービスありきで導入したが結局使われないというのは悲しい。よってまずは以下2点に絞って導入の目的を明確化した。

1. 進捗の把握/報告という行動を撲滅する

  • 普通に仕事をしていたら勝手に進捗が記録されて欲しい(or 最小限の手数で記録されて欲しい)
    • 進捗を報告するのも確認するのも極力緊急事態発生時のみにし、普段は誰が見てもサクッと分かるようにすることでより生産的な仕事に時間を使いたい
  • 誰がどの程度のボリュームの仕事に取り組んでおり、どのタイミングで次の大きめのタスクに取り組めそうか/今ちょっとお願い毎出来るのかを"全員"が確認する術を持ちたい
    • 一人が優先順位とチームの稼働状況をリアルタイムに把握して差配する形式だと、チームメンバーが5人を超えたあたりから非効率の方が大きいと感じている

2. チームが自律的に改善に取り組めるような情報の通り道を作る

  • 優先順位が明確になっており且つチームがその優先順位に納得感を持つことで自律的に動きを取りやすくする
    • あくまでも情報の通り道なのでGitHub Projectsを導入するだけで自律的に改善が出来るわけではないが、"最新情報が常にメンテされ続けている場所"として活用可能と考える
  • 優先順位によって事業インパクトが出る確率を上げる工夫、その優先順位が現時点で最善であるという思考プロセスの伝達の工夫は別途必要
    • プロダクトが提供する価値の言語化、事業計画とプロダクトが提供する価値の中間表現、プロダクト会議体の設計、優先順位の明示とその背景ストーリー、学習した事とその共有、全てのあわせ技

優先度は1の方を高く設定した。仕組みで解決可能っぽいが人数が増えれば増えるほど全体としての無駄が大きくなる性質を持っているからだ。1と比較して2はかなり複雑で、あわせ技と継続的努力で改善していく類のもの。ただし、GitHub Projectsの中でもissue/prに優先順位を分かりやすく付ける事ができる為一旦目標の中に入れた。あわせ技である「プロダクトが提供する価値の言語化」「事業計画とプロダクトが提供する価値の中間表現」「事業計画の四半期計画策定プロセス改善」等も同時に進めていたので、それらの情報の通り道としての役割を担ってもらえないかなぁという期待があった。

導入前の実験

まずは優先度を高く設定した「進捗の把握/報告という行動を撲滅する」という目的を、GitHub Projects導入で解決できそうなのかを小さく実験することにした。vandleというProjectsを作成し、既存のissue/prをachikuが勝手にProjectsに登録しViewを作りながら動作確認していくという流れ。今各チームが沿っているフローの変更は最小限にして本当に目的が達成できるのかを見たかった。

余談だがProjectsをissue/prに紐付けるというのはよく出来た設計だなと思った。issue/prが適切に運用できているチームであればという前提はつくが、それぞれに対してメタデータを付与し一覧化/グループ化を試しながら小さく実践投入出来る。そしてそこまで大きなプロダクトでなければ1名GitHubに慣れた人が本気出せばいける(と思う)。ここで小さく価値を感じてもらえれば、まずは開発チーム内部での運用、ひいてはプロダクトチーム全体での運用につなげていける。そしてそれは開発者のみが参加するGitHubのシート数という市場を拡張することにつながる。実験が軽いのは本当に正義だし、市場をズラして拡大するのは大正義だなと思う。

この実験の中でやったことは以下。

Workflowsを設定する

いつもの仕事してたら勝手に進捗が記録されるようにする為には必要な設定。カンムではissue/prを中心として開発になっているのでこれらがclose/mergeされたら自動的にProjects側にも反映される。もちろん、issue/prに現れない仕事や大きなissue/prになると正確に記録し続ける事は難しい。限界はあるし、詳細にやろうとしすぎると効用が逓減していく類のものと認識しているので最初はおおらかな気持ちで良いんじゃないかと考えている。2022/03現在Default workflowsは "Code review approved" 以外全てEnabledにしている。

f:id:kanmu-tech:20220301190755p:plain
Workflows

StatusとViewを作る

カンムではBoard/Priority/Milestone/Archived/NoPriorityの5つのViewを作っている。Status属性はToDo/Planning/In Progress/In Review/Done/Archivedの6つに増やした。ToDo/In Progress/Done/Archivedだけでスタートしたが、機能の検討も入れたいよね(=Planningの追加)、レビュー依頼まで終わってるものは分かりたいよね(=In Review)を追加している。が、最初はシンプルに始めるのが良いと思う。

BoardはStatus属性でGroup Byしているだけなので飛ばして、Priority/NoPriority/Milestone/ArchiveというViewの役割とどうやってフィルタしているかを以下で解説する。

Priority/NoPriority

まず以下のような形でザクッとPriorityを定義した。あまり言い回しにこだわらず、この段階ではなんとなくこんなもんかなくらいで良いと思う。後で精緻化すれば良い。

  • Priorityの定義
    • P0🔥
      • ユーザー影響が出ている障害/影響が出かねない事案/セキュリティ関連の緊急対応はP0とする
    • P1💨
      • 四半期で定めた注力事項、事業計画上の必達事項、パートナーとの依存関係がありリリースの期限が存在はP1とする
    • P2😗
      • ユーザー/チーム/会社に取ってやった方が良い事は分かりきっているがP1ではないものはP2とする
    • P3🌴
      • 出来たらやりたいがそこまでインパクトなさそうなものはP3とする

Priority ViewはTable View、Priority属性でGroup ByしPriority属性昇順でソートし -status:Done -status:Archived とフィルタをかけてDone/Archivedなものを除外する。こうすることで今Q抱えている注力項目(P1)は何か、P1がブロックされているのであれば手をつけれるP2は何か、という事を常時更新し続けられているリストを見ながら考える事が出来る。

f:id:kanmu-tech:20220301201600p:plain
Priority View (1)

f:id:kanmu-tech:20220301201623p:plain
Priority View (2)

NoPriority ViewはTable View、 no:priority とフィルタをかけてPriorityがついていないものを表示する。Group Byやソートは設定していない。暫定運用時はこのViewを定期的に確認し、登録されたissue/prの優先度を一旦achikuが判断することにしていた。このViewがあることで「一応Priorityの定義はあるが最初は難しい事を考えずにProjectsに放り込んでほしい!」というお願いが可能になる。

Milestone

以前はIssueのMilestoneを利用して複数リポジトリを跨いだリリース時期定義をしていたが、現在はProjects内で定義しリポジトリ跨いで付与できる属性であるIterationという概念を利用している。(The new GitHub Issues – October 14th update Milestoneでgroup byするのはまぁまぁ面倒だったのでこのリリースノートを読んで小躍りして喜んだ事を覚えている。)

現在のMilestone ViewはTable View、Iteration属性でGroup ByしPriority属性でソートし -status:Archived というフィルタをかけてArchivedなものを除外する。自分はこのViewを最も頻繁に見ている。既存の流れを踏襲して1 Iteration = 1 weekとして運用しており、金曜の段階で全てがクリアされていると爽快な気分になる。

f:id:kanmu-tech:20220301201930p:plain
Milestone View

Archive

Archive ViewはTable View、Iteration属性でGroup ByしIteration属性を降順でソートし status:Archived -no:iteration とフィルタを掛けてStatus属性がArchivedでIteration属性が付与されているものだけ表示する。もちろん登録しDoneまで遷移したissue/prをProjectsから外すこともできるが、過去の振り返りも行いたい為このViewを作っている。前週あるいは前Q単位で振り返るのにもこのArchive Viewは便利だと思う。

f:id:kanmu-tech:20220301202159p:plain
Archive View

実験中のコミュニケーション

なるべくチームに負担を掛けないように、ただし「何やってんのか分からんな」とならない程度にやっている事を共有しつつフィードバックをもらい、「それ便利じゃん」となってもらえるように微調整していった。この辺はあまり言語化できることはなく、自分は今までの信頼貯金的な部分に助けられたと思う。各位自分のポジションを鑑みていい塩梅でよろしくやっていって欲しい。

実験中の学び

実験中は結構学び多かった。あまり客観的数値と共に示せるものがなく心苦しいが重要だなと思った学びをいくつか挙げる。各位割引ながら読んでほしい。

モメンタムが視えるッ!

毎週少しずつプロダクトが提供する価値が上がっていく、実験を通して分からない事が減っていく、というのを実感できる。また、チーム全体のスループットは大体これくらいなんだなというのが確認できる。それはかなり基礎的な事では?という指摘はそうなんだけど、一部の開発メンバーだけでなくプロダクトチーム全体が上記を認識できるようになったのは良かったと思う(Projectsを利用したプロダクト定例のあわせ技的な側面もあるが)。

P0/P1が分かるからP2/P3がやりやすい

実験していた時期は自分もバックエンドの開発に参加しており、優先順位が明確になっていると細かい改善もやりやすいと感じた。「このP1は一旦フロントエンド側待ちなのでこっちのサクッと片付きそうなP2/P3やるかな」や「休憩の仕事としてのP2/P3」みたいな動きがやりやすくなっている(※休憩の仕事は自分の造語なのでチーム内で特に使われているわけではないが雰囲気伝わるかな...)。

同時にArchive Viewを振り返りながら「今回のIterationがP1だけになってしまっているのでP2/P3もう少し入れれないか」「このIterationのP1/P2/P3割合はいい塩梅」「このIterationはP0結構入ってしまったので他あまり出来なかったな...」等、どういう優先度の割合でタスクに取り組んでいるのかを見返せるのは良いなと思う。

もちろん、P1をしっかりとプロダクトが提供する価値の増大に紐付ける為の事前調査や実験設計はとても重要。だが往々にしてP1仕事は不確実性が高く複雑で、1週間やったら即時結果が出る類のものではない。そういう難しい問題をチームで解くためにもフォーカスを保つことは、事前調査や実験設計と同様に重要。ただ、これは個人的な話ではあるが、少しでも、しかし確実に良くなる改善があるのであれば、時間を見つけてサッとやる、しかも品質も高い、それが腕前だろうという思いがある。「"要はバランスおじさん"をしない為に腕磨いとるんじゃこちとらよォォ」と、心の野山に住んでいる山賊が言っているのだ。今回実施した優先順位付けのProjects登録はこの心の野山に住まう山賊の性に合っており自分は気に入っている。

システム的な改善を四半期注力事項にどうやって入れていくか

これは実験の振り返りをしていく中でソフトウェアエンジニア側から上った指摘。実験時に策定したP1は、事業計画を四半期に割った際に目指すべき事業の結果とプロダクトがユーザーに提供する価値向上を合わせて決めていく方式を取った。この際、システム的な安定性/開発容易性は事業計画上に現れにくい為、あまり考慮出来ていなかった。が、しっかりと商売していく為にはBS/PL双方が重要であるように、プロダクトが提供し続ける価値とプロダクト内部のシステム的/オペレーション的品質にも目を配り、適切にメンテナンスし続けていく事は重要だ。この部分はまだ決定打と言えるような解決策は練れていないが、以前から四半期単位で運用しているOKR運用に載せ、そこでプロダクトマネージャー(achiku)とソフトウェアエンジニアチームの議論を経て決めた四半期注力項目をソフトウェアエンジニアチームのP1とする、という形を取っている(OKR、2019年から運用しているんだけどその話は別途)。

今のところObjectiveとKey Resultの形に落とす事でリファクタをするにもパフォーマンスチューニングをするにも監視強化するにも地に足のついた議論が出来るので、結構良い気はしている。

Qの最後に2週間ほど開発は手を止めて細かい改善を行うのは良いリズムを作るかもしれない

以前 Shape Up を読んだ際にソフトウェアエンジニアは6週間で作りその後の2週間で作る際に荒れてしまった部分、作っていたら見つけた改善可能なポイント、リファクタをやるという話があった。自分はShape Upの「アイディア/機能を考える人」と「作る人」を明確に分けるスタンスにはそこまで共感できていないのだけど、このサイクル自体は良いなと思っていた。最初から内部的な品質には固定割合の労力を割く事を決めておき、対応が後手になるのをルールでカバーするというか。また、deeeet氏と話した際に彼のチームでも6週間+2週間のサイクルで回していて調子良いと言っていたので、まずフロントエンドチームで実験。2週間で細かいライブラリの更新、やろうと思って積み残していたリファクタ、CI/リリースプロセスの改善、等を経てみて話を聞いたが好評だった。

もちろんプロダクトの内部品質で重要な部分はOKRに載せてP1として対応するんだけど、どうしてもそこに載せるまでもない細かい改善ポイントは残ってしまう。よって、こういう時間を固定で定義して早めに倒しておくのは費用対効果としても良いのかもしれないと今は考えている。この辺みんなどうやって運用しているのか知りたい。

まとめ

再度目的をまとめる。

  • 進捗の把握/報告という行動を撲滅する
  • チームが自律的に改善に取り組めるような情報の通り道を作る

「進捗の把握/報告という行動を撲滅する」はProjects導入し、チームに便利さを体感してもらい、運用方法を周知し、振り返りを行い、今後もこのフローを改善しながらやっていこうという流れは作れている。もちろんこれで完成というわけではなく、プロダクトチームがより一体となって提供する価値を向上させる為に改善していきたい。

「チームが自律的に改善に取り組めるような情報の通り道を作る」に関してはPriorityで進む方向の大枠を示し、Iterationでリズム良く改善をしていける形は整った。再度になるが、この目的の中でProjectsが貢献できる部分は比較的小さな一部であり、プロダクトが提供する価値をより大きくしていく為にはより包括的な活動が必要になる。その部分に関しての工夫や学びもいつか共有できるようにしたいと考えている。

カンムにおけるGitHub Projects (Beta)の導入方法、運用方法、1周回してみての学びを書いた。プロダクトやチームのサイズによるが、GitHubを利用してさえいれば上記のViewを作りとりあえず始められる。すでに利用しているチームがあれば是非どうやっているのか教えてほしいし議論したいと思っているので achiku まで気軽に話しかけて欲しい。Meety もあるので是非!

TFLintを使ったterraformのチェックとカスタムルールの設定

インフラエンジニアの菅原です。

カンムはサービスの運用にAWSを使用し、そのリソースの管理にterraformを使用しています。 リソースの定義はGitHub上でコードとして管理されているので、何かリソースを追加する場合はプルリクエストを作成してレビューを受けることになるので、運用のポリシーに反するようなリソースの作成はある程度防ぐことができます。

しかしレビューはあくまで人の目によるものなので、チェックが漏れてしまうこともあります。 また「RDSは必ず暗号化すること」などのルールはCIで機械的にチェックして欲しいところです。

そこでカンムではtflintを導入してチェックの自動化を行うようにしました。

TFLintの導入

github.com

TFLintはterraform用のlinterで、非推奨な書式に警告を出してくれたり、ベストプラクティスを強制することができたりします。 メジャーなプロバイダー(AWS/Azure/GCP)のルールセットはすでに存在しており、カンムではtflint-ruleset-awsを利用しています。

tflintを導入するにはまず対象のtfファイルが置かれているフォルダに .tflint.hcl を作成し tflint --init を実行します。

plugin "aws" {
  enabled = true
  version = "0.12.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}
$ tflint --init
Installing `aws` plugin...
Installed `aws` (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.12.0)

tflintを実行するとルールに違反した箇所を表示してくれます。

$ tflint
3 issue(s) found:

Warning: resource `aws_acm_certificate` needs to contain `create_before_destroy = true` in `lifecycle` block (aws_acm_certificate_lifecycle)

  on route53.tf line 25:
  25: resource "aws_acm_certificate" "stg_example_com" {

Reference: https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.12.0/docs/rules/aws_acm_certificate_lifecycle.md

Notice: "default.redis6.x" is default parameter group. You cannot edit it. (aws_elasticache_replication_group_default_parameter_group)

  on redis.tf line 123:
 123:   parameter_group_name          = "default.redis6.x"

Reference: https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.12.0/docs/rules/aws_elasticache_replication_group_default_parameter_group.md

Error: "t1.2xlarge" is an invalid value as instance_type (aws_instance_invalid_type)

  on ec2.tf line 40:
  40:   instance_type           = "t1.2xlarge"

AWSのルールセットの場合、デフォルトで有効になっているルールはここにあるとおりです。

.tflint.hcl で個々のルールの有効・無効を指定することもできます。

rule  "aws_elasticache_replication_group_default_parameter_group" {
  enabled = false
}

また、tflint-ignore というコメントをつけることで特定の箇所のチェックを無視することもできます。

resource "aws_instance" "my-instance" {
  # tflint-ignore: aws_instance_invalid_type
  instance_type           = "t1.2xlarge"

GitHub Actionsへの組み込み

terraform-lintersからsetup-tflintアクションが提供されているので、簡単にGitHub Actionsに組み込むことができます。

      - uses: actions/checkout@v2
      - run: tflint --init
        env:
          GITHUB_TOKEN: ${{ secret.GITHUB_TOKEN }}
      - run: tflint

reviewdogというツールを使ったアノテーションを入れてくれるアクション reviewdog/action-tflintもありますが、そちらを試したことはないです。

カスタムルールの作成

カンムではRDSの暗号化など必須にしたいポリシーがいくつかあるため、カスタムルールを作成しています。

カスタムルールを作成する場合、まず tflint-ruleset-template をテンプレートとしてGitHubリポジトリを作成します。

たとえば aws_rds_cluster リソースの storage_encrypted をチェックするルールを作成する場合、チェックを書いたGoのソースコードを作成します。

  • rules/aws_rds_cluster_must_be_encrypted.go
package rules

import (
    "fmt"

    hcl "github.com/hashicorp/hcl/v2"
    "github.com/terraform-linters/tflint-plugin-sdk/terraform/configs"
    "github.com/terraform-linters/tflint-plugin-sdk/tflint"
)

type AwsRdsClusterMustBeEncryptedRule struct{}

func NewAwsRdsClusterMustBeEncryptedRule() *AwsRdsClusterMustBeEncryptedRule {
    return &AwsRdsClusterMustBeEncryptedRule{}
}

func (r *AwsRdsClusterMustBeEncryptedRule) Name() string {
    // ルール名
    return "aws_rds_cluster_must_be_encrypted"
}

func (r *AwsRdsClusterMustBeEncryptedRule) Enabled() bool {
    return true
}

func (r *AwsRdsClusterMustBeEncryptedRule) Severity() string {
    return tflint.ERROR
}

func (r *AwsRdsClusterMustBeEncryptedRule) Link() string {
    // ルールのドキュメントのリンク先
    return "https://rule-document.example.com/posts/12345"
}

func (r *AwsRdsClusterMustBeEncryptedRule) Check(runner tflint.Runner) error {
    err := runner.WalkResources("aws_rds_cluster", func(resource *configs.Resource) error {
        content, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
            Attributes: []hcl.AttributeSchema{
                {Name: "storage_encrypted"},
            },
        })

        if diags.HasErrors() {
            return diags
        }

        // storage_encryptedが存在しない場合は違反
        if _, exists := content.Attributes["storage_encrypted"]; !exists {
            return runner.EmitIssue(r, "`storage_encrypted` attribute not found", resource.DeclRange)
        }

        return nil
    })

    if err != nil {
        return err
    }

    return runner.WalkResourceAttributes("aws_rds_cluster", "storage_encrypted", func(attribute *hcl.Attribute) error {
        var storageRncrypted string
        err := runner.EvaluateExpr(attribute.Expr, &storageRncrypted, nil)

        if err != nil {
            return err
        }

        if storageRncrypted == "true" {
            return nil
        }

        // storage_encryptedがtrueでない場合は違反
        return runner.EmitIssueOnExpr(
            r,
            fmt.Sprintf("`storage_encrypted` is %s", storageRncrypted),
            attribute.Expr,
        )
    })
}

そして main.go にルールを追加します。

  • main.go
func main() {
    plugin.Serve(&plugin.ServeOpts{
        RuleSet: &tflint.BuiltinRuleSet{
            Name:    "kanmu",
            Version: "0.2.0",
            Rules: []tflint.Rule{
                rules.NewAwsRdsClusterMustBeEncryptedRule(),
            },
        },
    })
}

これで aws_rds_cluster リソースが storage_encrypted = true でない場合はエラーになります。

Error: `storage_encrypted` is false (aws_rds_cluster_must_be_encrypted)

  on rds.tf line 70:
  70:   storage_encrypted                   = false

Reference: https://rule-document.example.com/posts/12345

カスタムルールセットの配布

カスタムルールをプラグインとして tflint --init でインストールする場合、terraform providerと同様にgpgでの署名が必要になります。

基本的にはterraformのドキュメントに従ってgpgの鍵を作成し、terraform-provider-scaffoldingのGoReleaserの設定を編集して使えば、ルールセットのCIで自動的にインストール可能なアーカイブファイルが作成できます。

また.tflint.hcl にはカスタムルールセットの設定を追加しておきます。

plugin "kanmu" {
  enabled = true
  version = "0.1.0"
  source  = "github.com/xxx/tflint-ruleset-kanmu"

  signing_key = <<-KEY
  -----BEGIN PGP PUBLIC KEY BLOCK-----
  ...
  -----END PGP PUBLIC KEY BLOCK-----
  KEY
}

まとめ

tflintを使うことで、単純なミスをチェックしたりベストプラクティスを強制することができるようになります。 また、独自のルールセットを作成することで、RDSの暗号化のような「作成後に変更できない」設定をCIでチェックできるようになりました。

カンムではインフラの自動化に興味のあるインフラエンジニアを絶賛募集中です。

open.talentio.com

ECSとDatadogを使ったネットワーク機器のモニタリング

インフラエンジニアの菅原です。

カンムはバンドルカードというVisaプリペイドカードのサービスを提供していますが、Visaと決済情報をやりとりするためにオンプレミスのサーバと通信しています。

カンムのサービスはAWS上で構築されており、AWSとオンプレミスのサーバの通信はAWS Direct Connectを経由してます。

また、ネットワーク制御のためスイッチとしてCisco CatalystとJuniper SRXを使用しています。

f:id:winebarrel:20220130132559j:plain

ネットワーク機器は通常のサーバと同様になにかしらの問題が発生することがあるため、SNMPによるメトリクスの収集やSNMPトラップでのイベントの検知が必要になります。

また、Syslogはネットワーク機器内にファイルとして保存されていますが、外部にログを転送・保存しておくことで何か問題が発生したときに分析がやりやすくなります。

以前まではEC2インスタンスSNMPの収集・SNMPトラップの受信・ネットワーク機器用のSyslogサーバ、NTPサーバの運用を行っていました。

f:id:winebarrel:20220130134323j:plain

ECSとDatadogによる監視の構築

EC2インスタンスはサーバーの自体の管理やOS・ミドルウェアのアップデートなどでそれなりに運用の手間がかかります。

そこでECSとDatadogを使って、コンテナとSaaSでネットワーク機器の監視をできるようにしました。

f:id:winebarrel:20220130135659j:plain

Datadog AgentによるSNMPの収集とSNMPトラップの受信

ネットワーク機器のSNMPの収集とSNMPトラップの受信にはDatadog Agentのコンテナを使用しています。*1

SNMPトラップはUDPで送信されるためNetwork Load Balancer(NLB)を使ったECSサービスを作成しました。

Dockerfile

FROM datadog/agent:7

# (中略)

COPY datadog.yaml.tmpl /etc/datadog-agent/
COPY snmp.d_conf.yaml.tmpl /etc/datadog-agent/conf.d/snmp.d/conf.yaml.tmpl
COPY ping.d_conf.yaml.tmpl /etc/datadog-agent/conf.d/ping.d/conf.yaml.tmpl

# https://github.com/progrium/entrykit を使っています
ENTRYPOINT [ \
  "render",\
  "/etc/datadog-agent/datadog.yaml", \
  "--", \
  "render",\
  "/etc/datadog-agent/conf.d/snmp.d/conf.yaml", \
  "--", \
  "/bin/entrypoint.sh" \
  ]

datadog.yml

# cf. https://github.com/DataDog/datadog-agent/blob/main/pkg/config/config_template.yaml
snmp_traps_enabled: true
snmp_traps_config:
  bind_host: "0.0.0.0"
  community_strings:
    - '{{ var "COMMUNITY_STRING" }}'

# NOTE: Needed for SNMP trap
logs_enabled: true

snmp.d/conf.yml

# cf. https://docs.datadoghq.com/ja/network_performance_monitoring/devices/setup/?tab=snmpv2
init_config:
  loader: core
  use_device_id_as_hostname: true
instances:
{{ range $_, $ip_address := var "IP_ADDRESSES" | split "," }}
- ip_address: "{{ $ip_address }}"
  community_string: "{{ var "COMMUNITY_STRING" }}"
{{ end }}

Datadog Agentで収集された情報はDatadogに送られます。

f:id:winebarrel:20220130141331p:plain

SNMPトラップはDatadog Logsに送られます。

f:id:winebarrel:20220130141837p:plain

RsyslogによるSyslogの受信とDatadogへの送信

ネットワーク機器からのSyslogの受信にはRsyslogのコンテナを使用しています。

Datadog Agentと同じNLBを使ったECSサービス上でコンテナを起動し、受信したSyslogはDadadog Logsへと送られます。*2

Dockefile

FROM    ubuntu:20.04

# (中略)

COPY rsyslog.conf.tmpl /etc/
COPY datadog.conf.tmpl /etc/rsyslog.d/

ENTRYPOINT [ \
  "render",\
  "/etc/rsyslog.d/datadog.conf", \
  "--", \
  "render",\
  "/etc/rsyslog.conf", \
  "--", \
  "rsyslogd", "-n" \
  ]

rsyslog.conf

module(load="imuxsock")
module(load="immark" interval="20")

module(load="imudp")
input(type="imudp" port="{{ var "RSYSLOG_PORT" | default 514 }}")

module(load="imtcp")
input(type="imtcp" port="514")

module(load="omstdout")

$WorkDirectory /var/spool/rsyslog
$IncludeConfig /etc/rsyslog.d/*.conf
$AbortOnUncleanConfig on
$DefaultNetstreamDriverCAFile /etc/ssl/certs/ca-certificates.crt

action(type="omstdout")

datadog.conf

## Set the Datadog Format to send the logs
$template DatadogFormat,"{{ var "DD_API_KEY" }} <%pri%>%protocol-version% %timestamp:::date-rfc3339% %HOSTNAME% %app-name% - - [metas ddsource=\"rsyslog\" ddtags=\"env:prd\"] %msg%\n"

action(
  type="omfwd"
  protocol="tcp"
  target="intake.logs.datadoghq.com"
  port="10516" template="DatadogFormat"
  StreamDriver="gtls"
  StreamDriverMode="1"
  StreamDriverAuthMode="x509/name"
  StreamDriverPermittedPeers="*.logs.datadoghq.com"
  action.resumeRetryCount="10"
  action.reportSuspension="on"
)

Datadog Logsに送られたSyslogはコンソールから見ることができます。

f:id:winebarrel:20220130143356p:plain

Amazon Time Sync ServiceによるNTPサービス

ネットワーク機器はNTPサーバとしてAWS上のサーバを参照しています。

ntpdのコンテナを作成してもよかったのですがAWSAmazon Time Sync Serviceを提供しているので、stoneを使ってNTPをAmazon Time Sync Serviceに中継するようにしました。

Dockerfile

FROM debian:bullseye AS build

ARG STONE_VERSION=2.4

RUN apt-get update && \
  apt-get install -y curl build-essential

RUN curl -sSfLO https://www.gcd.org/sengoku/stone/stone-${STONE_VERSION}.tar.gz && \
  tar xf stone-${STONE_VERSION}.tar.gz --strip-components 1 && \
  make linux

FROM debian:bullseye-slim

COPY --from=build stone /usr/local/bin/

ENTRYPOINT ["/usr/local/bin/stone"]

タスク定義の一部

      command: [
        // NTP
        // cf. https://aws.amazon.com/jp/blogs/news/keeping-time-with-amazon-time-sync-service/
        '169.254.169.123:123/udp',
        '123/udp',
        '--',
        // HTTP (for healthcheck)
        '169.254.169.253:53',
        '80',
      ],

苦労した点

NLBのヘルスチェック

NLBでUDPを使っている場合でもヘルスチェックはTCPで行われます。 そのため、各コンテナでTCPのヘルスチェックができるように工夫しました。

  • Datadog Agentはいくつかデーモンが立ち上がっているので、そのうちの一つでLISTENされているポートをヘルスチェックに利用しています
  • RsyslogはTCPでのSyslogの受信は行わないのですが、ヘルスチェックのためにinput(type="imtcp" port="514")の設定を追加しました
  • stoneはそれ自体がヘルスチェック機能を持っていないので、VPCDNSサーバにTCPパケットを中継してヘルスチェックとしています

DatadogとRsyslogの接続が切れる

ドキュメントにも書いてあるのですが、DatadogとRsyslogの接続は非アクティブな状態が続くと切断されて再接続ができません。

このためmodule(load="immark" interval="20")して定期的にログを流すようにしています。 また、切断されても気づけるようにログが一定時間流れなかったらアラートをあげるようにもしています。

ECSのデプロイ後にSyslogが受信できなくなる

ECSをデプロイすると一部のネットワーク機器からSyslogが受信できなくなるという問題もありました。 ネットワーク機器はNLBのIPアドレスを参照しているのですが、デプロイ完了後も古いコンテナのほうにSyslogが流れて続けてしまい、新しいコンテナにはSyslogが流れなくなるという状態になっていました。

詳しい原因を調査できていないのですが、NLBのTarget GroupでConnection termination on deregistrationを有効にすることで、この問題を解決できました。

f:id:winebarrel:20220130150057p:plain

まとめ

いままでは監視用のEC2インスタンスの運用が地味に手間だったのですが、コンテナ化によってだいぶ手間を軽減することができました。

また、Datadogを利用することでメトリクスやログの閲覧がとても楽になりました。

カンムでは引き続きEC2からECSの移行が進行中です。興味のあるインフラエンジニアを絶賛募集しております。

open.talentio.com

ECSで作業用のタスクをサクッと作るためのツールを作成した

インフラエンジニアの菅原です。

最近、バイクに念願のグリップヒーターをつけました。 これでツーリング時の手の寒さが多少楽になりそうで喜んでいます。

とはいってもなかなか出かけられないのですが…


現在私はAWS Fargateを使ったサービスをECS上に構築を進めており、日々コンテナと戯れています。

基本的にストレージ以外のコンポーネントはほとんどECSで動いているのですが、VPCのネットワーク内でちょっとした作業(たとえばネットワークの疎通確認など)をしたい場合、都度新しいタスクを起動して作業しています。 また、DBにテストデータを入れたかったり、どうしてもDBを直接操作したいことがある場合、stoneを新しいタスクを起動した上で、そのタスクを踏み台としてaws ssm start-sessionでポートフォワーディングを行い、手元から直接DBにアクセスできるようにしたりしています。

しかしそのような一時的なタスクの起動は

  1. タスク定義のディレクトリに移動する*1
  2. タスクを起動
  3. 起動したコンテナにECS Execでログイン、あるいはポートフォワーディングをする場合はaws ssm start-sessionを実行
  4. 作業後はタスクを停止

…と、なかなか手間がかかります。

imageを実行時にオーバーライドできないので、都度都度タスク定義をエディタで書き換えるのも面倒です。

アプリケーションを動かしているコンテナにECS Execでログインすることもできるのですが、稼働しているコンテナにログインしたくはないですし、そのコンテナにパッケージをインストールするのもさすがに憚られます。

かといって作業用・ポートフォワーディング用にEC2インスタンスを動かすのは、管理コストなどからやりたくはないです。

もっと簡単に作業したい、kubectl run/exec/port-forwardのようにサクッと作業用のコンテナを作りたい…

と考えてそれらをなんとかするツール、demitasを作成しました。

demitas

github.com

demitasはecspressoのラッパーで、ECSタスクの起動・ECS Exec・ポートフォワーディングを簡単にするツールです。

  • タスク定義を~/.demitas配下で一括管理する
  • 単一のコンテナの定義だけ書ける
  • すべての定義を実行時にオーバーライドできる

などの特徴があります。

使い方

まず~/.demitas配下に設定ファイルを作成します。

~/.demitas
├── ecs-container-def.jsonnet
├── ecs-service-def.jsonnet
├── ecs-task-def.jsonnet
└── ecspresso.yml

基本的にecspressoの設定と同じですがecs-task-def.jsonnetにcontainerDefinitionsがないです。 そのかわりecs-container-def.jsonnetに単一のコンテナの定義を書いています。

// ecs-service-def.jsonnet
{
  name: 'oneshot',
  cpu: 0,
  image: 'ubuntu',
  essential: true,
  logConfiguration: {
    logDriver: 'awslogs',
    options: {
      'awslogs-group': '/ecs/oneshot',
      'awslogs-region': 'ap-northeast-1',
      'awslogs-stream-prefix': 'ecs',
    },
  },
}

設定ファイルを作成したら、どのディレクトリにいてもdemitasコマンドでタスクを起動できます。

$ demitas -c '{image: "public.ecr.aws/runecast/busybox:1.33.1", command: [echo, test]}'
2021/10/30 16:52:45 hello/hello Running task
2021/10/30 16:52:45 hello/hello Registering a new task definition...
2021/10/30 16:52:46 hello/hello Task definition is registered busybox:46
2021/10/30 16:52:46 hello/hello Running task
2021/10/30 16:52:47 hello/hello Task ARN: arn:aws:ecs:ap-northeast-1:822997939312:task/hello/d51ca2de190548e2a2b8e8c9644cfab1
2021/10/30 16:52:47 hello/hello Waiting for run task...(it may take a while)
2021/10/30 16:52:47 hello/hello Watching container: busybox
2021/10/30 16:52:47 hello/hello logGroup: /ecs/busybox
2021/10/30 16:52:47 hello/hello logStream: ecs/busybox/d51ca2de190548e2a2b8e8c9644cfab1
..

すべての定義はdemitasのオプションでオーバーライド可能です。上記の例ではコンテナのイメージとコマンドをオーバーライドしています。

また、設定ファイルのディレクトリをさらに~/.demitas/db-subnet ~/.demitas/app-subnetなどと分けることで、実行時にプロファイルを指定してタスクを起動できます。

~% demitas -p db-subnet -c '{command: [psql, -c, "SELECT * FROM foo"]}'

demitas-exec・demitas-pf

demitasではさらに起動したコンテナにログインしたり、ポートフォワーディングを簡易化するためのラッパーを作成しました。

demitas-execはタスクの起動・ECS Exec・タスクの停止を行います。

$ demitas-exec -e bash
Start ECS task...
ECS task is running: 8fba2576ba3841e7aff88f4ecfb7b32b

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-05d903fcaef0393ed
root@ip-10-0-0-18:/# echo test
test
root@ip-10-0-0-18:/# exit
exit


Exiting session with sessionId: ecs-execute-command-05d903fcaef0393ed.

Stopping ECS task... (Please wait for a while): 8fba2576ba3841e7aff88f4ecfb7b32b
done

demitas-pfはタスクの起動・ポートフォワーディング・タスクの停止を行います。

$ demitas-pf -h www.yahoo.com -r 80 -l 10080
Start ECS task for port forwarding...
ECS task is running: c16548617cab480e8fb37f195a8be708
Start port forwarding...

Starting session with SessionId: root-084c5c16763c0f36f
Port 10080 opened for sessionId root-084c5c16763c0f36f.
Waiting for connections...

Connection accepted for session [root-084c5c16763c0f36f]
^CTerminate signal received, exiting.


Exiting session with sessionId: root-084c5c16763c0f36f.

Stopping ECS task... (Please wait for a while): c16548617cab480e8fb37f195a8be708
done
$ curl -s localhost:10080 | grep title
    <title>Yahoo</title>

まとめ

kubectl run、欲しいなぁ、欲しいなぁ」とずーっと思っていたのですが、demitasでだいぶ解消されました。 いまはdocker runぐらいの感覚で作業用タスクの起動しています。

*1:ecspressoを使っています

Go Conference 2021 Autumn CTF: Go 1.16.4 に含まれる脆弱性を突いてリバースプロキシを突破する

エンジニアの佐野です。Go Conference 2021 Autumn にて Kanmu はスポンサー枠をいただき、オフィスアワーの催しで Go x セキュリティというコンセプトの CTF のような問題を用意させていただきました。

問題はこちら "Go" beyond your proxy になります。

github.com

f:id:kanmu-tech:20211112223834p:plain The Go gopher was designed by Renee French.

当日解けなかった人やこのブログを読んで興味が沸いた人もチャレンジしてみてください。

問題を簡単に説明すると、 Go 1.16.4 で書かれたリバースプロキシの背後の HTTP サーバに flag.txt というファイルが置かれています。このファイルには簡単なアクセス制限が施されているのですが、それを突破してそのファイルの中身を参照して解答してください、というものになります。 Go 1.16.4 には CVE-2021-33197脆弱性が含まれていて Go の issue にも上がっています(次のバージョンである 1.16.5 で Fix されています)。この脆弱性を利用してアクセス制限を突破するというのが想定解法になります。

※ 蛇足になりますがこのようにアプリケーションレイヤーにて送信元IPでアクセスコントロールすることはおすすめしません

本記事では出題の意図、問題の解説と解き方、問題を作るときに考えていたことを書きます。

  • 出題の意図
  • 問題の解説と想定解法
  • 問題を作るときに考えていたこと
  • 小ネタ
  • おわりに

1. 出題の意図

出題の意図としては Go に潜むセキュリティ issue を知ってもらい、実際にその脆弱性を突いたハックを体験してもらうことになります。

この問題を通して、

  • Go 本体にセキュリティ issue が潜んでいることを知る
  • Go の標準ライブラリを読む

という経験を積んでもらえていたら幸いです。

1.1 Go 本体に潜むセキュリティ issue

Go 本体にもセキュリティ関連の issue は報告されていて随時その修正がされています。試しに Go のコミットログから "CVE" という文字列を検索してみます。

git log -i --grep 'cve' --oneline
61536ec030 debug/macho: fail on invalid dynamic symbol table command
77f2750f43 misc/wasm, cmd/link: do not let command line args overwrite global data
5abfd2379b [dev.fuzz] all: merge master (65f0d24) into dev.fuzz
bacbc33439 archive/zip: prevent preallocation check from overflowing
b7a85e0003 net/http/httputil: close incoming ReverseProxy request body
a98589711d crypto/tls: test key type when casting
aa4da4f189 [dev.cmdgo] all: merge master (912f075) into dev.cmdgo
ad7e5b219e [dev.typeparams] all: merge master (4711bf3) into dev.typeparams
f9d50953b9 net: fix failure of TestCVE202133195
0e39cdc0e9 [dev.typeparams] all: merge master (8212707) into dev.typeparams
106851ad73 [dev.fuzz] all: merge master (dd7ba3b) into dev.fuzz
dd7ba3ba2c net: don't rely on system hosts in TestCVE202133195
cdcd02842d net: verify results from Lookup* are valid domain names
950fa11c4c net/http/httputil: always remove hop-by-hop headers
74242baa41 archive/zip: only preallocate File slice if reasonably sized
c89f1224a5 net: verify results from Lookup* are valid domain names
a9cfd55e2b encoding/xml: replace comments inside directives with a space
4d014e7231 encoding/xml: handle leading, trailing, or double colons in names
d0b79e3513 encoding/xml: prevent infinite loop while decoding
cd3b4ca9f2 archive/zip: fix panic in Reader.Open
953d1feca9 all: introduce and use internal/execabs
46e2e2e9d9 cmd/go: pass resolved CC, GCCGO to cgo
d95ca91380 crypto/elliptic: fix P-224 field reduction
dea6d94a44 math/big: add test for recursive division panic
062e0e5ce6 cmd/go, cmd/cgo: don't let bogus symbol set cgo_ldflag
1e1fa5903b math/big: fix shift for recursive division
64fb6ae95f runtime: stop preemption during syscall.Exec on Darwin
4f5cd0c033 net/http/cgi,net/http/fcgi: add Content-Type detection
027d7241ce encoding/binary: read at most MaxVarintLen64 bytes in ReadUvarint
fa98f46741 net/http: synchronize "100 Continue" write and Handler writes
82175e699a crypto/x509: respect VerifyOptions.KeyUsages on Windows
b13ce14c4a src/go.mod: import x/crypto/cryptobyte security fix for 32-bit archs
953bc8f391 crypto/x509: mitigate CVE-2020-0601 verification bypass on Windows
552987fdbf crypto/dsa: prevent bad public keys from causing panic
41b1f88efa net/textproto: don't normalize headers with spaces before the colon
145e193131 net/http: update bundled golang.org/x/net/http2 to import security fix
61bb56ad63 net/url: make Hostname and Port predictable for invalid Host values
12279faa72 os: pass correct environment when creating Windows processes
9b6e9f0c8c runtime: safely load DLLs
193c16a364 crypto/elliptic: reduce subtraction term to prevent long busy loop
1102616c77 cmd/go: fix command injection in VCS path
1dcb5836ad cmd/go: accept only limited compiler and linker flags in #cgo directives
2d1bd1fe9d syscall: fix Exec on solaris
91139b87f7 runtime, syscall: workaround for bug in Linux's execve
8d1d9292ff syscall: document that Exec wraps execve(2)
cad4e97af8 [release-branch.go1.7] net/http, net/http/cgi: fix for CGI + HTTP_PROXY security issue
b97df54c31 net/http, net/http/cgi: fix for CGI + HTTP_PROXY security issue
84cfba17c2 runtime: don't always unblock all signals
eeb8d00c86 syscall: work around FreeBSD execve kernel bug

本問題の修正コミットである 950fa11c4c net/http/httputil: always remove hop-by-hop headers に加えて他の修正も多くヒットします。

ちなみにちょうど先日 Go 1.17.3, 1.16.10 がリリースされましたがこれらにもセキュリティ Fix が含まれていました。

セキュリティ関連のバグというと言語そのものよりもその言語を利用して実装されたウェブサーバやデータベースなどのミドルウェア、OS、その他ソフトウェアなどに注目しがちですが、言語自体にもセキュリティの問題は潜んでいることがあります。

私が書いた問題のコード自体にもバグはなかった(はず)です。しかしバグは Go の標準ライブラリの方に含まれている、というのがこの問題を解くポイントです。

1.2 Go の標準ライブラリを読む

issue とその修正 PR を見てみます。本問題の肝となっている issue とそれに対応する PR が下記なのですが、これらが問題を解くための最大のヒントになります。

github.com

github.com

詳しい解説は次の「問題の解説と想定解法」でしますが、この issue と修正コミットおよびそのテストコードから、

  • そもそも Connection ヘッダの仕様として、任意のヘッダ名を値に入れるとそのヘッダを消すことができる
  • 同じく Connection ヘッダの仕様として、Connection ヘッダは Proxy を経由する際にはそれ自体が削除される
  • しかし Go 1.16.4 では空の Connection ヘッダをリバースプロキシに送りつけるとそのまま背後のサーバに到達させることができる
  • Abusing HTTP hop-by-hop request headers という攻撃手法がある
  • 脆弱性を利用すると X-Forwarded-For を消すことができそうだ

といったことを読み取ることができます。本問題を解くためには Go の net/http/httputil パッケージとそのテストコードを少し読む必要があります。

2. 問題の解説と想定解法

私が想定した解答へのルートは次の流れの通りです。

  • サーバにリクエストを投げつつ問題のソースを読んでみる
  • Go のバージョンを特定する(気づく)
  • Go のバージョンに潜む脆弱性を確認して issue にたどり着く
  • issue の修正 PR や issue に貼られた Abusing hop by hop header を調べる
  • 空の Connection ヘッダとともに Connection: X-Forwarded-For を送りつける

「3. 問題を作るときに考えていたこと」で書きますが、挑戦者にいかにバージョンに目を付けてもらいどのように issue に誘導するか?というのが作問中の悩みでした。ヒントでいきなり issue を教えると簡単すぎる、しかし issue にたどり着けないと明後日の方向のアプローチをしてしまう。最初のヒントで Go 1.16.4 を強調したのはこのためです。

2.1 サーバにリクエストを投げつつ問題のソースを読んでみる

docker run でサーバを起動したら単純に 8000 番ポートにリクエストを投げてみます。以下のようなトレースが出力されます。

========================================================
Welcome to Kanmu Office hour @ Go Conference 2021 Autumn
"Go" beyond your proxy! Go version: go1.16.4
---------------- Front Proxy ----------------
GET /flag.txt HTTP/1.1
Host: localhost:8000
Accept-Encoding: gzip
User-Agent: Go-http-client/1.1

--- Front Proxy が受信した RemoteAddr ---
RemoteAddr: 172.17.0.1:58588

---------------- Middle Proxy ----------------
GET /flag.txt HTTP/1.1
Host: localhost:8000
Accept-Encoding: gzip
User-Agent: Go-http-client/1.1
X-Forwarded-For: 172.17.0.1

--- Middle Proxy が受信した RemoteAddr ---
RemoteAddr: 127.0.0.1:38448

---------------- Backend Server ------------------
GET /flag.txt HTTP/1.1
Host: localhost:8000
Accept-Encoding: gzip
User-Agent: Go-http-client/1.1
X-Forwarded-For: 172.17.0.1, 127.0.0.1

--- Backend Server が受信した RemoteAddr ---
RemoteAddr: 127.0.0.1:51188

--- 最終的な X-Forwarded-For と送信元IP ---
X-Forwarded-For: 172.17.0.1, 127.0.0.1
Source IP: 172.17.0.1

残念!送信元IP が 127.0.0.1 になるようにリクエストを送ってください!(172.17.0.1 != 127.0.0.1)
==========================

トレースを見つつ、問題のソースを読んでみると backend に到達したときの Source IP が 127.0.0.1 であればフラグが取れるということがわかります。コードの解説と模式図はヒントに書いた通りです。

人によっては送信元IPを変更してリクエストを送信してみたり、X-Forwarded-For を送りつけてみたりしたかもしれませんが、問題のコード自体にはおそらくバグはないはずです。

2.2 Go のバージョンを特定する(気づく)

1.16.4 を使っていることに気づいてもらいます。docker run 実行時、ヒント、go.mod の中身などバージョンを知ることができる箇所はいくつかあります。

2.3 Go のバージョンに潜む脆弱性を確認して issue にたどり着く

こちらについては検索します。Go 1.16.4 について Google 検索, issue の探索, Goのリリースノートを調べるなどなんでも良いです。「検索かよ...」と思う人もいるかもしれませんが、使われているソフトウェアのバージョンを調べてそれに関する既知の問題を調べるのはオフェンシブセキュリティという文脈では正攻法の一つになります。

2.4 issue の修正 PR や issue に貼られた Abusing hop by hop header を調べる

Go 1.16.4 やリバースプロキシについて調べていると issue にたどり着くことができます。たどり着けずにヒントが出たことで辿れた人もいるかもしれません。たどり着いたら issue を読みます。

github.com

issue を見るとまず Connection ヘッダの説明が書かれています。プロキシによって Connection ヘッダは削除されること。また Connection ヘッダに値としてセットされているヘッダも同様に削除されること。そしてこの issue によると X-Forwarded-For のようなヘッダをドロップできるかもしれないと示唆するとともに、Abusing HTTP hop-by-hop request headers というタイトルの記事のリンクが貼られています。

そしてこの issue から辿れる PR にはこの issue の修正コミットとそのテストコードが追加されています。

github.com

テストコードを読んでみると frontend と backend 2つの HTTP サーバを起動しています。frontend は NewSingleHostReverseProxy を使ってバックエンドにリクエストを転送するような構成になっています。本問題では front, middle, backend の多段構成ですが、これと非常に似ています。

テストコードは何を確認しているでしょうか?テストするにあたり、空の値を持つ Connection ヘッダを送信していること、Connection ヘッダの値として X-Some-Conn-Header をセットして送信していることがわかります。そして Connection ヘッダとその値は backend では削除されることを確認しています。

そして当の標準ライブラリの修正はどうでしょうか?以下のあたりが消されていますね。

   for _, h := range hopHeaders {
        hv := outreq.Header.Get(h)
        if hv == "" {
            continue
        }
        if h == "Te" && hv == "trailers" {
            // Issue 21096: tell backend applications that
            // care about trailer support that we support
            // trailers. (We do, but we don't go out of
            // our way to advertise that unless the
            // incoming client request thought it was
            // worth mentioning)
            continue
        }
        outreq.Header.Del(h)
    }

これは hopHeaders (Connectionヘッダなど hop-by-hop header 一式が定義されている)の値を取得して削除するが、その値が空だったら削除しない、という処理をしています。つまり本来であれば Connection ヘッダは転送する際には削除するべきなのですが、空の値が含まれていたらそれは削除せずにそのまま転送するという処理になっていたようです。

2.5 空の Connection ヘッダとともに Connection: X-Forwarded-For を送りつける

ここまでで、

  • [仕様] Connection ヘッダはその値に含まれているヘッダを消す
  • [バグ] 普通は裏側には Connection ヘッダ自体を転送しないが、空の値を持たせると削除されずに転送してしまう

ということがわかりました。ということで以下のようなコードで空の Connection ヘッダと、 Connection: X-Forwarded-For を送ってみましょう。

package main

import (
        "bytes"
        "fmt"
        "io"
        "log"
        "net/http"
)

func main() {
        req, err := http.NewRequest("GET", "http://localhost:8000/flag.txt", nil)
        if err != nil {
                log.Fatal(err)
        }
        req.Header.Add("Connection", "")
        req.Header.Add("Connection", "X-Forwarded-For")

        client := &http.Client{}
        resp, err := client.Do(req)
        if err != nil {
                log.Fatal(err)
        }
        defer resp.Body.Close()

        bb := &bytes.Buffer{}
        io.Copy(bb, resp.Body)
        fmt.Println(bb.String())
}

あっさりフラグが取れました。Connection ヘッダが裏側まで届き、Connection: X-Forwarded-For が設定されていることで X-Forwarded-For が削除されました。

フラグはこのURLですね。本記事の冒頭の Gopher くんと正解のメッセージが現れます。

https://gocon2021autumn-ctf-flag.net/

3. 問題を作るときに考えていたこと

ここから先は裏話になります。このような問題を考えることは初めてだったのでいろいろ考えをする必要がありました。その際に考えていたことを書いてみます。要約すると次のようなことを考えながら問題および当日のヒントを考えました。

  • CTF といっても Go を中心にした問題にすること
  • 前回よりも少し難易度を高めること
  • 最終的に全員が解ける問題にすること
  • いかに Go 1.16.4 の issue に誘導するか

3.1 CTFといっても Go を中心にした問題にすること

Go Conference ということもあり基本的には Go の問題であるべきです。しかしながらそこにセキュリティを絡めた CTF のような問題を作れるだろうか?と悩みました。

先ほど、ソフトウェアのバージョンから既知の脆弱性を調べるのはセキュリティの攻撃の世界では正攻法であると書きました。なんか良いバグないかな?と Go のコミットログを眺めていたら割と最近でしかも扱いやすいリバースプロキシの修正コミットがありました。

この問題を作ることができたのはちょうど良い issue があったということに尽きます。他にもいくつか問題を考えたのですが、どうしても Go というよりは Linux のテクニックの問題になってしまったりしてちょうどいい問題を作るのにいくらか難儀しました。

3.2 前回よりも少し難易度を高めること

カンムは前回の Go Conference 2021 Spring でも CTF 様式の問題を出題しております。以下が前回の問題の解説記事なのですが、

tech.kanmu.co.jp

次回機会があれば strings だけでは倒せない歯ごたえがある問題も用意しようと思うので、今回興味を持ってもらえた方はぜひ CTF に入門して倒せるように鍛えてきてください!

と書き残していました。ということで今回は少し骨のある問題を用意しようと思って問題を作成しました。 Go のソースは読める+HTTPの知識がある程度ある人というレベル感にしましたが、HTTP と題材であるリバースプロキシの知識については知らない人であっても挑戦しやすいようにヒントに説明を書きました。あの説明で伝わっていれば幸いです。

3.3 最終的に全員が解ける問題にすること

本丸は登壇者のトークなのでそれを邪魔しない程度の分量、難易度にすべきと考えました。「ほーら、解けねーだろ(笑)」という問題ではなく、こちらがヒントを出して解答に誘導しつつも最終的には全員が解ける問題になるように作問しました。 重要なのは本問題を通して Go のセキュリティに関心を持ってもらうことと、Go の標準ライブラリのコードや issue を読むという体験をしてもらうことです。

3.4 いかに Go 1.16.4 の issue に誘導するか

作問中、そして本番での運営では、挑戦者にいかにバージョンに目を付けてもらいどのように issue に誘導するか?に頭を悩ませました。プロトタイプを CTO に解いてもらってのですが、最初にもらったフィードバックは、

  • 脆弱性を調べるという発想がないと明後日の方向のアプローチをしてしまいそう
  • ふつうの人は脆弱性を自主的に探しにいかなそう -とりあえず Go のソース読むかということで自分は進めたけど、まず初心者の人にはそこでとまってしまいそう
  • そもそも HTTP ヘッダーの仕組みを理解してるか、知っていたとして細工した HTTP リクエスト投げれるか
  • Issue とか全部英語なのでそこにもハードルがありそう

などでした。

当日の運営では開始直後にいきなりヒントを出し、そこでバージョンと脆弱性の存在を匂わせる形にしました。そこからタイムラインなどの様子を見て次のヒントを考えて誘導していこう、と話をしていました。

4. 小ネタ

4.1 コミットID

3319700 になっているのですがこれは CVE-2021-33197 に合わせました。

f:id:kanmu-tech:20211113131822p:plain

こちらのツールを使うことでコミットIDを好きなものに変更できます。

github.com

4.2 正解のページの Gopher くんは何?

わたしの手書きです。わたしがハッカーっぽい Gopher くんを書いて CTO がそれに魂を吹き込んでくれて本記事の冒頭にも載っている闇の底から覗く Gopher くんができあがりました。

f:id:kanmu-tech:20211113132835p:plain The Go gopher was designed by Renee French.

5. おわりに

Twitter などを見ると今回も多くの方に解いていただけました。作問担当としては感無量です!ありがとうございます!

そしてお約束の宣伝をさせてください。カンムでは Go やセキュリティに関心のあるエンジニア、もちろんそれ以外の職種も募集しております! kanmu.co.jp

GoCon の運営の皆様、参加してくださった皆様ありがとうございました!

おわり

カンムは Go Conference 2021 Autumn にスポンサーとして参加します #GoConference #gocon

こんにちは!カンムで採用担当をやっている @ayapoyo と申します(テックブログ初参戦です!)

11月13日は Go Conference 2021 Autumn の開催日ですね! 今回もたくさんの gopher とお話できることを楽しみにしております。

gocon.jp

シルバースポンサーやります ʕ◔ϖ◔ʔ

4月に開催された Go Conference 2021 Spring に引き続き、今回もスポンサーとして Go Conference に参加させていただきます!

カンムが展開する "バンドルカード" そして新規事業の "Pool" のバックエンドは Go メインで開発しており、このイベントを通して Go コミュニティの発展に寄与できればと思っています 💳

kanmu.co.jp

COO がセッションに登壇します ʕ◔ϖ◔ʔ

今回の GoCon では、カンム COO の @_achiku が登壇します! 13:50 から Track A に登場しますので、みなさんぜひご覧ください!

gocon.jp

オフィスアワーで CTF を開催します ʕ◔ϖ◔ʔ

今回の GoCon も完全リモート開催!Remo で開催される オフィスアワー にてブースを出展させていただきます!

そして前回の GoCon で好評いただいた CTF 、今回も実施します!

Goで書かれたHTTPサーバのバグを見つけて秘密のファイルにアクセスしよう!

Goを読めてHTTPはちょっとわかるぞという人に向けて。問題は当日公開予定、ブースやTwitterで随時ヒントの発表や解説などを行います!

前回の問題と解説は こちら に掲載しています!GoCon 当日までに復習しておくのはいかがでしょうか?

このほかにも

  • カンムやバンドルカードについて聞いてみたい!
  • 登壇していたメンバーと話してみたい!
  • Go についてわいわい話したい!

などなども大歓迎です!Remo のブースに気軽に遊びに来てみてくださいね!

参加登録は connpass から ʕ◔ϖ◔ʔ

gocon.connpass.com

gopher のみなさんとお会いできることを楽しみにしております👋

カンムを支える技術 ~モバイルアプリ編~

カンムのCTOの伊藤です。

カンムではバンドルカード、そしてこれからリリース予定の pool https://pool-card.jp/ においても React Native を採用しています。 実際にどういった環境で開発運用をしているかについて簡単にご紹介します。

React Native の採用理由や経緯については React Native Matsuri 2021 で話したこちらの記事も参考にしてみてください。

speakerdeck.com

基本的な構成技術

  • React Native
  • Redux
  • Flow
  • Storybook
  • Firebase

開発の進め方

普段の開発は GitHub でタスク管理をし、PR を出してレビューするという一般的なスタイルです。 GitHub Projects を利用して進捗を管理していたり、他のチームに今後のリリースに含められる予定のものを共有する目的でアプリのリリースごとにマイルストーンを作成しています。

言語

言語はバンドルカードに関しては JavaScript + Flow 、pool は TypeScript を採用しています。 エコシステムの充実度や新しく入ってくるメンバーの経験などの理由からバンドルカードも TypeScript へ移行したいと考えています。

ビルド・リリース周り

ビルドやリリースに関しては基本的に fastlane で完結させ、それを Bitrise にて実行しています。二段階認証を使用しているApple Developerアカウントと連携することができるのでBitriseを選択しています。

App Store Connect 2FA solved on Bitrise | Bitrise

開発版の社内への配布は DeployGate を利用しています。

CI に関しては社内の既存のリソースとの兼ね合いで、テストや lint などは CircleCI を利用しています。

f:id:kanmu-tech:20211002143621p:plain

アプリのリリース自体はおおよそ週1のサイクルで行っており、コードフリーズをして必要なQAを行ってというサイクルで進めています。

API連携

バンドルカードにおいては、JSON Hyper-Schema から API クライアントの実装を自動生成しそれを利用しています。 バックエンドのAPIサーバーも同じスキーマからリクエスト/レスポンスの構造体やバリデーション実装を自動生成しています。 pool では OpenAPI を採用していますが、基本的にはバンドルカードと同様にクライアント・サーバーどちらもそのスキーマから自動生成しています。

テスト

ロジック部分のテストには Jest を利用しています。すべてを網羅できているわけではありませんが基本的に新規作成する際はテストを追加していくようにしています。
コンポーネントのテストに関しては Snapshot testing を行い、意図しない差分が出ていないかを確認しています。 E2E テストに関してはアプリのフローに SMS 認証が挟まる都合上実現できておらずどのように解決するかを模索中です。

Storybook

いわゆるUIコンポーネントカタログのような使い方をしています。デザイナーも Xcode を使用してアプリをビルドすることで Storybook を参照できるようにしており、いつでも実際の実装を確認することができます。Storybook を起動してもらう際にアップデートなどで様々なトラブルが発生しますが所持していない端末で確認できるようになったので結果的には良かったと思っています。
ライトに確認できるように将来的には社内用に独立したアプリとしてビルドしておきたいと考えてはいます。
使い回さないページなどのコンポーネントも全て Storybook 化しており、これに対して Snapshot testing を行っています。

エラートラッカー/クラッシュレポート

エラートラッキングには Sentry を利用しています。定期的に発生しているエラーをトリアージして issue に転記しながら優先順位をつけて随時対応していっています。 合わせて Firebase Crashlytics も利用しており、Firebase で記録しているイベントに紐付けられて便利なので Sentry の補助的な役割として使っています。

Firebase の他の機能

アプリの行動ログは Firebase Analytics で収集しています。react-navigation の遷移イベントをフックにページ遷移のトラッキングなどもしています。 他には Remote Config を A/B テストや、その他任意のタイミングで文言を切り替えたい場合などに利用しています。

その他

Expo は使っていないか?

バンドルカードを開発した当初 Expo はなかったためそのまま使用せず開発しており、必要に応じてモジュール単位で使用しています。

自前の Native Module はどのくらいあるか?

バンドルカードにおいて、スクリーンショットの制御周りと、ローカル認証の制御のためにすごく小さな Native Module 2 つ管理しています。 ネイティブの機能をゴリゴリ使いたいような類のアプリケーションではないので Native Module を書く機会はかなり少ないです。

おわりに

こう振り返ってみるとかなり素朴な構成でここまでやってきております。 長年運用してきている中での課題も色々見えて来ているというのが現状で、やりたいことは無限にあります。

カンムでは React Native で開発するエンジニアもそうでないエンジニアも絶賛募集しております

kanmu.co.jp