サーバサイドでJWTの即時無効化機能を持っていないサービスは脆弱なのか?

きっかけ

昨年(2021年9月ごろ)に徳丸さんのこのツイートを見て、「2022年にはJWTを用いたセッション管理に代表される、ステートレスなセッション管理は世の中に受け入れられなくなっていくのだろうか?」と思っていました。

結論

冒頭だけ読んで勘違いが広まるとよくないので、議論の結果最新版では方針が変わったということだけ先に書いておきます。詳細が気になる方は最後まで読んでみてください🙇‍♂️

現状(2022年3月)の日本語版の記述

OWASP Top10 2022の「A01:2021 – アクセス制御の不備」の日本語版では、2022年3月3日時点では以下のように書かれています。

JWTトークンはログアウト後にはサーバー上で無効とされるべきである。

github.com

この記述からは、ログアウト時にクライアントサイドでセッション(JWT)を捨てるだけでは不十分で、クライアント側がJWTを再利用すればサーバへのアクセスが成功してしまうのが問題であるということが読み取れます。 サーバ側で無効なJWTを管理しなければならないということは、JWTを利用したセッション管理に代表されるようなステートレスなセッション管理は認証の実装方法として不十分なのでしょうか?

議論

こちらのIssueで議論がされていました。(徳丸さんのツイートよりも後に行われた議論のようです)

github.com

議論の詳細はここでは書きませんが、ここの議論からわかることをまとめます。

まず、JWT仕様を見る限り、OAuthをベースとしたOpenID Connectで使用することが想定された設計であることは明らかです。

datatracker.ietf.org

OAuthには2つのトークンがあり、短命なアクセストークンと長命なリフレッシュトークンから構成されています。 それぞれのトークンがどんな形式であるべきかは指定されていませんが、アクセストークンにはJWTが使われている実装が多いようです。

アクセストークンの生存期間を短命にすることで、アクセストークンが不正に利用されるリスクを低減しています(リスクを低減しているのであって、完全に防いでいるわけではない)。この短命なトークンの実装としてJWTを使うことで、ステートレスな認証を実装することができ、データベースの負荷低減に寄与します。

その代わりにリフレッシュトークンはサーバサイドでの管理を必須とし、ユーザがログアウトするなどの操作を行うとリフレッシュトークンが即時無効化され、アクセストークンの更新はできなくなります。

この前提があるため、少なくとも長命なアクセストークンを生成することは許容されなさそうです。どれくらいの時間が短命なのかどうかはサービスごとのリスク許容度によって判断する形になりそうですね。

(追記ここから 2022/04/18) 徳丸さんから反応をいただいたのですが、

たしかに誤解が広がるとよくないので、こちらでも強調しておきます。 JWTは本来的にはセッション管理のためのものではなく認証情報の受け渡しのためのものですが、その便利な特性からセッション管理にも転用されることもあるという背景があります。

こちらも参考になったので紹介しておきます 🙇‍♂️

(追記ここまで)

「数分であろうと即時無効化されているべきであるセッションが有効なものとして使えるのはリスクは低減されているが問題であることに変わりはないのでは?」という意見ももちろんありますが、絶対に許容されないという温度感ではないようです。

その代わりに、この特性が一般にはまだ広く理解が広まっていないのでは?という懸念への対応として、以下のリンクを紹介することでメリットとデメリットを正しく理解してもらおう、ということで着地したようです。 www.oauth.com

変更

この議論の結果、2021年9月24日にはA01の記述を修正するプルリクエストが出おり、即日取り込まれていたのでした。

github.com

Stateful session identifiers should be invalidated on the server after logout. Stateless JWT tokens should rather be short-lived so that the window of opportunity for an attacker is minimized. For longer lived JWTs it's highy recommended to follow the OAUTH standards to revoke access.

ということで、

「ステートフルなセッション識別子は、ログアウト後にはサーバー上で無効とされるべきである。ステートレスなJWTトークンは、攻撃者の機会を最小にするために、むしろ短命であるべきである。寿命の長いJWTの場合は、アクセスを取り消すためにOAuth標準に従うことが強く推奨される。」

という内容に記述が変わりました。

日本語版が追いついていない😱

日本語版の記述はどうなっているかというと、この議論の結果が反映されておらず、英語版と日本語版で乖離が出ていることがわかりました。 日本語版だけを読んで勘違いが広がってしまうのもよくないので、英語版の最新に追従するプルリクエストを出してみました。問題がなければ取り込んでもらえるかな?と思っています。

github.com

JWTの取り扱いに関する記述が意味的な差分としては一番大きそうですが、他の差分に関しては軽微な修正だと思われます。

「Amazon S3のストレージ料金を無料にする裏技」改

こちらの記事を読んで「なるほど、確かに面白いな」と思いました。

kusano-k.hatenablog.com

特に気になったのはこの部分。まだまだいけるんじゃないかと。

ファイルの取り出しのLISTは1000件まとめて取得できるので、無視できる。 PUTリクエストは0.0047USD/1,000回。 1回、1バイトあたり6.3172×10-9USD。 ストレージ料金の「GB」が10003なのか10243なのか分からない。 10243とすると、6.783USD/GB。 ということで、3,392月=283年以上保存するならば、S3 Glacier Deep Archiveに保存するよりも、ファイル名にエンコードしたほうがお得💰💰💰

Amazon S3のストレージ料金を無料にする裏技 - kusano_k’s blog

追記(2022/01/28 11:00)

職場の方から指摘をもらって、AWSの公式FAQに

1 か月に請求されるストレージの量は、月全体を通じて使用される平均ストレージです。これには、お客様の AWS アカウントで、お客様が作成したバケット内に格納されるすべてのオブジェクトデータやメタデータが含まれます。

Amazon S3 のよくある質問

という記述があることを知りました。メタデータもストレージ使用量の課金対象のようなので、この手法は残念ながら(?)無料ではなさそうということが発覚しました。
ということでブレイクスルーは起きませんでした。残念🥺🥺🥺

ブレイクスルーを起こしたい💰💰💰

3,392月=283年以上保存するならば、S3 Glacier Deep Archiveに保存するよりも、ファイル名にエンコードしたほうがお得💰💰💰

Amazon S3のストレージ料金を無料にする裏技 - kusano_k’s blog

これを超える方法が何かないか考えてみたところ、思いついたのでご紹介します。

docs.aws.amazon.com

Amazon S3にはメタデータという自由記述欄があります。メタデータには2種類あり、「System-defined object metadata」と「User-defined object metadata」があります。

System-defined object metadata

System-definedなメタデータは使えるキーの名前が決まっていて、自由に追加はできません。試しに適当にオブジェクトをアップロードし、メタデータを確認しました。

{
    "AcceptRanges": "bytes",
    "LastModified": "2022-01-27T13:14:34+00:00",
    "ContentLength": 0,
    "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
    "ContentType": "text/plain",
    "Metadata": {}
}

余計なキーを増やすと容量を消費してしまうので追加しないとして、この中で唯一任意の文字列が設定できるのは ContentType 一択のようです。

AcceptRanges, ETag はドキュメントにはメタデータとして記載されていないので、容量は消費しなさそうです。keyとvalueの合計なので、 LastModified で37byte消費、ContentLength で14byte消費といったところでしょうか。 ContentType のキー名は11byteです。

System-defined object metadataは2KBまで保存できるようなので、 2048 - 37 - 14 - 11 = 1986byte 使えそうです。引用元記事同様Base64エンコードするとすれば 1938 / 4 * 3 = 1489byteですね。

User-defined object metadata

User-definedなメタデータは使えるキーの名前は x-amz-meta- ではじまればなんでもいいようです。ここで文字数を消費したくないので x-amz-meta-0 とでもしておきましょう。 x-amz-meta-0 のキー名は12byteです。

User-defined object metadataは2KBまで保存できるようなので、 2048 - 12 = 2036byte 使えそうです。引用元記事同様Base64エンコードするとすれば 2036 / 4 * 3 = 1527byteですね。

$ aws  s3api head-object --bucket bucket --key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.txt
{
    "AcceptRanges": "bytes",
    "LastModified": "2022-01-27T13:36:57+00:00",
    "ContentLength": 0,
    "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
    "ContentType": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "Metadata": {
        "0": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    }
}

AWSコンソール上の見た目もよくわからない感じになっている。

f:id:kuro_m88:20220127224241p:plain
aws console上の見た目

気になるお値段

list-objects-v2 APIではメタデータが取得できないので、head-object APIでオブジェクトごとにリクエストしないといけない。この操作にかかる料金は1回あたり0.0000004USDで、誤差ということで考えないことにする。

1個のオブジェクトのキー名に、(1024-32)/(4/3)で744バイト分のデータを詰め込める。

Amazon S3のストレージ料金を無料にする裏技 - kusano_k’s blog

ということだったので、 オブジェクトのキー: 744 byte, システムのメタデータ: 1489byte, ユーザのメタデータ: 1527byte, 744 + 1480 + 1527 = 3751byteまで保存できそう。3751 / 744 = 5.04倍保存できるので、

3,392月=283年以上保存するならば、S3 Glacier Deep Archiveに保存するよりも、ファイル名にエンコードしたほうがお得💰💰💰

Amazon S3のストレージ料金を無料にする裏技 - kusano_k’s blog

3392ヶ月 / 5.04 = 673ヶ月 = 56年以上保存するならば、S3 Glacier Deep Archiveに保存するよりも、ファイル名にエンコードしたほうがお得!!💰💰💰

M1 MacでLima + Dockerの環境構築

Docker Desktopが一定条件で有償化*1されるので、脱Docker Desktopしてみた。 意外とそんなにハマることもなく環境構築に成功して、Docker Desktopを使っていた時代とほぼ変わらない開発体験が得られました。

Limaを選んだ理由

  • lima コマンドを打つだけでデフォルトのVM(Ubuntu)のシェルに入れる(もしくはlimaの後ろに付加した文字列がそのままコマンドになる)
  • 標準設定でホストとネットワークを共有する(dockerでportをexportしたらlocalhost:1234でアクセスできる)
    • --net=host が使える
  • 標準設定でMacのホームディレクトリがVMにマウントされてる(嫌だったら設定変えられる, sshfsでマウントされてるだけ)

環境

  • M1 Mac Book Pro
    • Intelでもいけるはず

Lima側の構築

  • これがDocker Daemonを実行するVM(Ubuntu)
  • コマンドは全てMacのターミナルで入力
$ uname -a
Darwin my-m1-mac 21.2.0 Darwin Kernel Version 21.2.0: Sun Nov 28 20:28:41 PST 2021; root:xnu-8019.61.5~1/RELEASE_ARM64_T6000 arm64

$ brew install lima

$ limactl start
# ディスク容量やCPUなど、設定をカスタマイズしたい場合は好みで変える。特に何も変えなくてもOK

# 起動確認
# limaコマンドだけ打つとシェルログインできます
$ lima uname -a
Linux lima-default 5.13.0-22-generic #22-Ubuntu SMP Fri Nov 5 13:22:27 UTC 2021 aarch64 aarch64 aarch64 GNU/Linux

$ lima sh -c 'curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg'
$ lima sh -c 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null'

$ lima sudo apt-get update
$ lima sudo apt-get install docker-ce docker-ce-cli containerd.io

# 動作確認
$ lima sudo docker run hello-world

# Macと通信できるように設定
$ lima sudo sh -c 'echo "{\"hosts\": [\"tcp://127.0.0.1:2375\", \"unix:///var/run/docker.sock\"]}" > /etc/docker/daemon.json'
$ lima sudo mkdir -p /etc/systemd/system/docker.service.d/
$ lima sudo sh -c 'echo "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd" > /etc/systemd/system/docker.service.d/override.conf'

$ lima sudo systemctl daemon-reload
$ lima sudo systemctl restart docker.service

Mac側の構築

  • dockerのクライアントをインストールする
  • コマンドは全てMacのターミナルで入力
$ cd ~
$ mkdir bin

# Intel Macの場合
$ curl -o docker.tgz https://download.docker.com/mac/static/stable/x86_64/docker-20.10.9.tgz

# M1 Macの場合
$ curl -o docker.tgz https://download.docker.com/mac/static/stable/aarch64/docker-20.10.9.tgz

$ tar xvf docker.tgz
$ mv docker/docker ~/bin/docker
$ rm docker.tgz
$ rm -rf docker/

# .bashrcや.zshrcのPATHに~/binを通す

# .bashrcや.zshrcで
# export DOCKER_HOST='tcp://127.0.0.1:2375'
# を追記する

$ docker version
Client:
 Version:           20.10.8
 API version:       1.41
 Go version:        go1.16.6
 Git commit:        3967b7d
 Built:             Fri Jul 30 19:55:20 2021
 OS/Arch:           darwin/arm64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.12
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.12
  Git commit:       459d0df
  Built:            Mon Dec 13 11:43:40 2021
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          v1.5.8
  GitCommit:        1e5ef943eb76627a6d3b6de8cd1ef6537f393a71
 runc:
  Version:          1.0.3
  GitCommit:        v1.0.3-0-gf46b6ba2
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

# 動作確認
$ docker run hello-world

docker-composeも入れておく

# Intel Macの場合
$ curl -Lo docker-compose https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-darwin-x86_64

# M1 Macの場合
$ curl -Lo docker-compose https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-darwin-aarch64

$ chmod +x docker-compose
$ mv docker-compose ~/bin/

普段の使い方

VMを開始する時

$ limactl start

VMを停止する時

$ limactl stop

もっと使いこなすには

Cloudflareのアカウントを勝手に開設され、Webサイトやドメインが乗っ取られうる状態になった話

タイトルのとおり、自分のメールアドレスを使ってCloudflareのアカウントを第三者に勝手に開設されました。何が起きたのかとその後の対応についてまとめてみます。

※ Cloudflareのアカウントを第三者に勝手に開設されるだけでは即座に何かしらのサイトやドメインが乗っ取られるわけではありません。

目次

時系列

何が起きて、何をしたのか、簡単に時系列でまとめました。

時刻(JST) 事象
9/26 22:54 何者かにCloudflareのアカウントを勝手に作成される
9/26 22:54 Cloudflareから"Please verify your email address"というメールが届く
9/26 22:57 何者かにCloudflareのGlobal API Keyを勝手に生成される
9/26 22:57 何者かにCloudflareのGlobal API Keyを勝手に閲覧される
9/26 22:57 何者かにCloudflareのGlobal API Keyを勝手に閲覧される
9/26 22:57 何者かにCloudflareのGlobal API Keyを勝手にローテートされる
9/26 23:?? Cloudflareからの身に覚えのないメールに気づき、フィッシングを疑う
9/26 23:?? Cloudflareのアカウントの存在を確かめる
9/27 00:03 自分でCloudflareのパスワードをリセット(全ログインセッションが無効化される)
9/27 00:04 自分でCloudflareにログインしなおす
9/27 00:?? 自分でCloudflareのAudit Logを閲覧し事態を把握する
9/27 00:34 自分でCloudflareのGlobal API KeyとOrigin CA Keyをローテートする

何者かにCloudflareのアカウントを勝手に作成された

f:id:kuro_m88:20210927004738p:plain
突然の身に覚えがないemail verification通知

こんなメールが突然届きました。CloudFlareの操作はおろかブラウジングもしていなかった時間帯だったので、明らかに怪しいメールです。 このリンクをクリックすると何が起きるのかもよくわからなかったので、警戒しつつ次になにをしようか考えます。( 身に覚えがないメールのリンクは絶対にクリックするのはやめましょう )

このメールを受け取ったメールアドレスは(当たり前ですが)自分のメールアドレスで、この記事では仮に admin@example.com とします。

Cloudflareのアカウントが実在するのか確認する

admin@example.com でCloudflareのアカウントを開設した記憶はなかったため、実在するのか確認しました。 アカウントの存在確認は簡単で、 admin@example.com のメールアドレスでアカウントを新規作成しようとすればよいわけです。

f:id:kuro_m88:20210927010135p:plain
アカウントがすでに存在するらしい

開設した覚えのないアカウントがすでに存在しているので、本当に何者かにアカウントを勝手に作成された可能性が高いことがわかりました。

パスワードリセット

すでにアカウントは存在していますが、メールアドレスは自分のものなので、パスワードリセットは可能です。

パスワードを忘れた時の手順でリセットをかけ、アカウントをひとまず自分の管理下に置くことにします。

Audit Logを見る

f:id:kuro_m88:20210927010652p:plain
CloudflareのAudit Log

CloudflareにはAudit Log(監査ログ)機能があり、Cloudflare上で行われた操作のログがのこっています。やはり身に覚えがない時間帯にアカウントが作成されたようです。

アカウントの作成だけでなく、 APIキーの作成と閲覧 もしているようですね。

f:id:kuro_m88:20210927010908p:plain
犯人のIPアドレス

犯人のIPアドレスもわかりました。実際にはただのproxyかもしれませんが。

whois情報の照会ができるwebサイトで見てみると、アルゼンチンのISPに割り当てられているIPアドレスのようです。明らかにおかしい。

f:id:kuro_m88:20210927011307p:plain
アルゼンチンのIPアドレス

APIキーをローテートする

Audit Logより、APIキーが作成され、閲覧された形跡があることがわかりました。 そのあと再度ローテートされているのは何がしたかったのかはよくわかりませんね…。

ローテート後、新しいAPIキーを閲覧した形跡はないためたぶん犯人は最新のAPIキーを知らないのでは?という気はするものの、ローテートすると同時に新しいAPIキーが閲覧できる仕組みだったら怖いので、再度自分でAPIキーをローテートしました。

Audit LogがないサービスだったらAPIキーを作成し閲覧されたことに気づくのは難しいので、 何をされたのかわからない場合はアカウントを削除する のがこれ以上の被害を防ぐ上で一番いいと思います。

今回は興味本位で調査に使いましたが、使わないアカウントを保持し続けても仕方がないのでどのみち削除するのがよさそうです。

攻撃者は何がしたかったのか(推測)

ここからは自分の推測でしかないですが、犯人は何がしたかったのか考えてみました。

どこからメールアドレスを収集してきたのか

まずどこからこの実在するメールアドレスを収集してきたのかですが、たぶんGithubのコミットログなどから拾ってきたのかなぁと思います。

Cloudflareのアカウントを乗っ取ってなにがしたかったのか

メールアドレスの情報源がGithubだったと仮定すると、そのメールアドレスの持ち主はソフトウェアエンジニアである確率が高そうです。 となるとCloudflareを知っている確率は高いので、Cloudflareからemail verificationのリンクが来ても不審に思わなかったり、ついうっかりクリックしてしまう可能性はあります。

忘れるくらい時間が経った頃に自分がCloudflareを使おうとしたとします。正規のユーザ(自分のこと)はログインできないことに気づき、パスワードリセットをかけてCloudflareを利用しますが、APIキーを悪意のある第三者に盗まれていたことに気づくことは難しいでしょう。

そのCloudflareのアカウントを使ってCDNの設定やDNSの管理をしはじめると、 悪意のある第三者はそのCDNDNSを操作し放題 になるでしょう。 これが犯人の目的かな?と想像しています。

教訓

  • 身に覚えのないemail verificationが届いても、絶対にクリックしない
  • email verificationが行われていないアカウントであっても、何をされるかわからないので、アカウントを勝手に作成された場合は正規の方法で取り返す
  • 乗っ取られたアカウントは取り返した上で削除するか、audit logを見て何をされたのか把握する

たまたま不審に思ったので対処することができましたが、怖いですね。