Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

ペンテスターはApple Silicon Macに夢を見るか?

この記事は、Akatsuki Advent Calendar 2021の24日目の記事です。

こんにちは、セキュリティエンジニアの小竹 泰一(aka tkmru)です。 アカツキでは、アプリケーションに対する脆弱性診断や社内ネットワークに対するペネトレーションテスト、ツール開発/検証を担当しています。

Apple Silicon MacをiOSアプリの脆弱性診断に使用する際にどのような利点、欠点があるのか調査した結果を書きたいと思います。2019年のAkatsuki Advent Calendarから続く、大好評企画(?)「ペンテスターは〇〇に夢を見るか」シリーズ第二弾です。 第一弾はこちら

Apple Silicon MacでiOSアプリが動作するように!

M1搭載MacBookが登場して、iOSアプリがエミュレータを使わずともmacOS上で動作するようになりました。

iPhoneおよびiPad向けAppが、Appleシリコン搭載のMac上のMac App Storeでも提供されるようになりました。AppをMac向けに修正する必要はなく、ユーザーとデベロッパの双方にとって新たな可能性が広がります。iPhoneおよびiPad向けAppで利用可能な既存の機能を使って、キーボード、ウインドウ、タッチ入力のジェスチャと連携して動作するようAppを最適化することができます。さらに既存の機能を確認し、必要に応じて機能を有効化/無効化することで、AppのMac上での動作を調整することも可能です。

developer.apple.com

JailbreakしていないiPhoneはAndroidと比べ、プラットフォーム側の制限が厳しく、かゆいところに手が届かないがちです。 そこでApple Silicon Macを使うことでiOSアプリの脆弱性診断が簡単になるのではと考え、検証しました。 この記事はその検証結果をまとめたものです。

iOSアプリのインストール方法

IPAファイルをダブルクリックすることでインストールできます。 Apple Silicon Mac上では、IPAファイルはインストーラとして振る舞います。 どんなIPAファイルでもインストールできるわけではなく、iOSデバイスと同等の制限があり、macOSのUUIDを実行許可している証明書でリザインされたものしかインストールできません。

f:id:TAKEmaru:20211223211535p:plain
インストール画面

tap1000000.appは検証用に作成したアプリの名前です。この記事内では度々tap1000000.appが登場します。

各種Tips

検証する過程で分かった各種Tipsを紹介します。 デスクトップアプリと同じようにiOSアプリを扱えました。

アクティビティモニタが便利

macOSにデフォルトでインストールされているアクティビティモニタを開き、実行しているプロセスを選択することで、プロセスの詳細な情報を取得できます。 PIDや使用しているファイルを確認できます。

f:id:TAKEmaru:20211220152731p:plain
アクティビティモニタの画面例

これはiOSアプリのためのTipsではなく、macOS上で動作する全てのアプリケーションで使えるテクニックです。

メモリマップの取得方法

vmmapコマンドでPIDで指定したプロセスのメモリマップを取得できます。

$ vmmap 10299
Process:         tap1000000 [10299]
Path:            /Volumes/VOLUME/*/tap1000000.app/tap1000000
Load Address:    0x1025e8000
Identifier:      jp.hoge.tap1000000
Version:         0.1 (0)
Code Type:       ARM64
Platform:        iOS
Parent Process:  ??? [1]
...

==== Non-writable regions for process 10299
REGION TYPE                    START - END         [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL
__TEXT                      1025e8000-1025f0000    [   32K    16K     0K     0K] r-x/r-x SM=COW          /var/folders/*/tap1000000.app/tap1000000
__LINKEDIT                  1025f4000-1025fc000    [   32K     0K     0K     0K] r--/r-- SM=COW          /var/folders/*/tap1000000.app/tap1000000
VM_ALLOCATE                 102604000-102608000    [   16K    16K    16K     0K] r--/rwx SM=PRV  
shared memory               102608000-10260c000    [   16K    16K    16K     0K] r--/r-- SM=SHM  
MALLOC metadata             10260c000-102610000    [   16K    16K    16K     0K] r--/rwx SM=COW          MallocHelperZone_0x10260c000 zone structure
MALLOC guard page           102614000-102618000    [   16K     0K     0K     0K] ---/rwx SM=ZER  
MALLOC guard page           102620000-102624000    [   16K     0K     0K     0K] ---/rwx SM=ZER  
MALLOC guard page           102624000-102628000    [   16K     0K     0K     0K] ---/rwx SM=NUL  
MALLOC guard page           102630000-102638000    [   32K     0K     0K     0K] ---/rwx SM=NUL  
...

これもiOSアプリのためのTipsではなく、macOS上で動作する全てのアプリケーションで使えるテクニックです。

中間者攻撃の方法

システム環境設定からネットワーク -> 詳細設定 を開き、使用しているネットワークを選択した後、 詳細 -> プロキシの順にメニューを開くことでプロキシの設定画面を開けます。 HTTP/HTTPS通信を取得するには構成するプロトコルにWebプロキシ(HTTP)保護されたWebプロキシ(HTTPS)を選択し、プロキシツールが動いているアドレス/ポートを指定してください。

f:id:TAKEmaru:20211220153633p:plain
プロキシ設定例

ただ、PCから出る全ての通信が指定したプロキシツールを通るようになるので、プロキシツール側でうまくフィルタしてあげる必要があります。 この辺りもiPhoneの場合と変わりませんが、PC上では診断対象のアプリと並行してブラウザなども動かすと思うのでフィルタリングがより大切になります。

各種パス

インストールされたアプリは次のパスに配置されます。

  • /Applications/tap1000000.app/Wrapper/tap1000000.app

また、用途は特定できていませんが、アプリの実行中は次のパスにもアプリが配置されます。

  • /private/var/folders/hc/XXXXXXXXnsfn1_c9n20jxw40000gq/X/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/d/Wrapper/tap1000000.app

アプリが使用するアプリケーションコンテナは次のパスに配置されます。

  • /Users/taichi.kotake/Library/Containers/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/Data/

このあたりのパスは前述のアクティビティモニタから確認できるので、興味がある方は見てみてください。

欠点

現状、Apple Silicon Mac上でWindows VMを動かせないのは欠点です。 ゲームアプリの脆弱性診断では、Unity製アプリのバイナリのシンボルを復元するIl2CppDumperというツールを動かすためにWindows VMをしばしば使います。

github.com

まず、VMware FusionがApple Silicon Mac上では動作しません。 VMware FusionのApple Silicon Mac対応は進んでいるようですが、Intel版のOSをサポートはしないようで、Apple Silicon Mac対応VMware Fusionがリリースされたとしても、Intel版Windowsは実行できない見込みです。 Microsoftは仮想環境向けにARM版Windowsを販売しないようで、当分はApple Silicon Mac上でWindows VMを動かせなさそうです。

news.mynavi.jp

内製メモリ改ざんツールをApple Silicon Macに対応させた話

アカツキのセキュリティチームでは脆弱性診断のためのツールをいくつか内製し、OSSとして公開しています。 その中の一つにipa-meditというiOSアプリ向けのメモリ改ざんツールがあります。 このツールをApple Silicon Mac上で動作するiOSアプリに対応させました。ここでは、その解説をしたいと思います。

github.com

Apple Silicon Macに対応する以前のバージョンのipa-meditは以前、Black Hat USA 2021 Arsenalで発表しました。 この記事はApple Silicon Macの解説のための記事なので、iPhone、iPad上で動作するiOSアプリに対してメモリ改ざんするロジックの解説はここではしません。興味がある方は次のスライドをご覧ください。

speakerdeck.com

メモリ改ざんとは

チートの方法の1つにUI上に表示されている値を端末のメモリ上から検索し、見つけた値を改ざんする、「メモリ改ざん」という手法があります。 これはゲームへのチート方法の中で最も簡単な方法で、脆弱性診断の際には実際にメモリ上のデータを改ざんをすることでチートできるかどうか確認しています。 対策としては、XOR等を使ってメモリ上ではエンコードされた状態で値を保持し、UI上に表示されている値を検索されても見つからないようにする方法があります。 ipa-meditでは次のようにメモリ改ざんを行えます。

LinuxとmacOSの違いに困惑...

要はmacOS上で動作するアプリケーションをデバッグすればいいだけなので、シュッと対応できるのではと開発時には考えていました。 しかし、慣れ親しんでいたLinux環境でのシステムプログラミングとは違い、macOS固有の知識が要求されるため、いくつか戸惑うことがありました。

Linuxとの違いに戸惑う....

apk-meditというAndroid向けのメモリ改ざんツールを以前作成したことがあったので、このツールと同じロジックで作れるのではないかと考えていましたが、かなり勝手が違いました。 LLDBやGolang製デバッガのDelveなどのmacOSでも動作するデバッガのコードが実装の参考になりました。

hackerslab.aktsk.jp

/proc がない....

Linux系OSの場合は/proc/$pid/mapsから、特定のプロセスのメモリマップを取得し、/proc/$pid/memよりメモリを読み書きできます。 しかし、macOSには/procがありません。 そのため、専用のAPIを用いて、メモリマップの取得とメモリの読み書きを行う必要があります。 ipa-meditではメモリマップの取得には前述のvmmapコマンドを使っていますが、 メモリの読み込みにはmach_vm_readを、メモリへの書き込みにはmach_vm_writeを使っています。

ptraceでメモリの読み書きができない....

ptraceは、デバッガを実装する際によく用いられるシステムコールで、第一引数にrequestという動作モードを表す定数を指定することで動作を切り替えられます。 Linux系OSの場合はptraceを使ってプロセスへアタッチし、プロセスを停止させたり、メモリを読み書きできます。

ptrace(int request, pid_t pid, caddr_t addr, int data);

しかし、macOSの場合はメモリの読み書きがサポートされておらず、第一引数にメモリを読むためのPTRACE_PEEKDATA、メモリへ書き込むためのPTRACE_POKEDATAを指定できません。そのため、前述のmach_vm_readmach_vm_writeを使ってメモリの読み書きを行っています。 macOSでもアタッチ/デタッチにはptraceを使えるので、ipa-meditではアタッチ/デタッチにのみptraceを使っています。

署名まわりで戸惑う....

macOSでは、署名されていないプログラムをデバッガとして使うことはできません。 デバッガとして使うプログラムには、com.apple.security.cs.debuggerという属性を有効にするようentitlements.plistで指定し、署名しておく必要があります。 ipa-meditではmake buildコマンドでビルドする際に署名の処理も行っています。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.debugger</key>
    <true/>
</dict>
</plist>

また、デバッグ対象のアプリではcom.apple.security.get-task-allow属性を有効にしておく必要があります。 この属性はデバッガによるアタッチを許可します。 Apple Silicon Macを使ってiOSアプリの脆弱性診断をする際には、リザインの際にこの属性を有効にしておかないとメモリ改ざんを行えません。 デバック対象のアプリでcom.apple.security.get-task-allow属性が有効になっているかどうかはcodesignコマンドで確認できます。

$ codesign -d --entitlements :- 47071
Executable=/private/var/folders/hc/XXXXXXXXnsfn1_c9n20jxw40000gq/X/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/d/Wrapper/tap1000000.app/tap1000000
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>application-identifier</key>
    <string>XXXXXXXXXX.jp.hoge.tap1000000</string>
    <key>com.apple.developer.team-identifier</key>
    <string>XXXXXXXXXX</string>
    <key>get-task-allow</key>
    <true/> <-- ここがtrueになっている必要がある
    <key>keychain-access-groups</key>
    <array>
        <string>XXXXXXXXXX.jp.hoge.tap1000000</string>
    </array>
</dict>
</plist>

まとめ

検証の結果、Apple Silicon Mac上で動作するiOSアプリに対しても問題なく脆弱性診断ができそうだと分かりました。 Apple Silicon Mac上のiOSアプリに対して脆弱性診断っぽいことを行うまとまったドキュメントは、インターネット上にまだなく(多分)、MITMする方法すら検索しても出てこないので、公開する意義のあるいいドキュメントになったかなと思います。

また、PC上で動作しているため、アプリケーションコンテナ内のファイルへの操作をXcodeを介さず行える点や、デバッガのアタッチをデスクトップアプリと同じ方法で行える点から、一部の診断は実機のiPhoneを使うよりやりやすくなる印象を受けました。 iPhoneをJailBreakしなくとも、ターミナルが動作している環境でiOSアプリを動かせるのは便利かなーと思います。 今後、Apple Silicon Mac環境特有の何かを用いたiOSアプリへのハック方法が出てくると面白いなと思います。

関連情報

Apple Silicon Macには、Intel Mac向けにビルドされたアプリケーションをApple Silicon Mac上で動作させるRosetta 2という仕組みが搭載されています。 この動作を明らかにするProject ChampollionというリサーチをFFRIが行っており面白いです。 iOSアプリとは関係ないですが、Apple Silicon Mac特有の仕組みに興味がある方はこちらもおすすめです。

github.com

Engineering Managerの役割を無くしてみた

こんにちは、ゆのん(id:yunon_phys)です。この記事は Akatsuki Advent Calendar 2021 の22日目の記事です。

アカツキでは数年前よりEngineering Managerの役割をおいているのですが、この度、Engineering Manager(EM)の役割を公式に無くしました。私の個人的な活動で、EMによるEMのためのPodcastをやっている*1、社内でもそういう活動を知っている人がいるし、世間からするとお前がそれを言うのか〜と言われそうなのでなかなか勇気のいる決断でした。ただ、今後の会社の方針を考えたときに、今見直すタイミングなんじゃないかと思い、思い切って無くしてみました。

本記事ではなぜ無くすことにしたのか、これからどうしていくのかを書いていきます。

EMのやることが消滅したわけではない

アカツキのEMはエンジニアのマネジメントと、プロジェクトマネジメントが主で、techのマネジメントも必要とあらば入っていました。

なぜこれだけいろいろなことをやる役割が必要なのかでいうと、プロダクトには、エンジニアだけでなく、プランナー、ディレクター、プロデューサー、デザイナー、QA、マーケターなど様々な職種が入っているからです。それらの様々な職種が混在するプロダクトチームが一体となって開発・運用を行うためには、狭い領域の専門性を磨く人も必要でしたし、広くプロダクトチームをカバーする人も必要で、EMは後者の役割でした。

ただ、EMといえどスーパーマンではないので、人事領域に強い人もいれば、プロジェクトマネジメント領域に強い人もいて、同じEMと言えども強みの発揮のやり方は違っていました。近年ではモバイルゲームがより成熟化してきていて、高度な専門性が求められるようになってきました。高度な専門性が求められれば求められるほど、同じEMと言いながら異なる強みを利用して価値を発揮しているようなEMに対して、EMは何をする人なのか、という質問が至るところで湧いてくるようになりました。さらに、Engineering Managerという言葉の響きからtechのリードとしての期待が暗黙のうちに持たれることもあり、その期待とのずれから、それぞれのEMの強みが否定されかねない状態になってきました。

私は、EMを社内にも社外にも広めた立場であるものの、EMという名称を使うことでこういったネガティブな状態になるのは本意ではありませんでした。一方で、EMのやっていたことがいらなくなったわけではないです。どの業務も必要だったからEMという役割が存在していたのは間違いありません。そこで、EMという名称を社内から無くしはするが、これまでEMが担っていた役割をどうすればより良い形で組織で実現出来るか、もう一度役割を整理し直しました。そのために改めて、エンジニア組織をどういう組織にしたいのか、ゼロベースで考えました。

f:id:yunon_phys:20211221232924p:plain

より技術力を高められる組織にしたい

まず、考えたのは、アカツキの技術力をもっと高めたいというものです。技術力がもっと高まれば、より豊かなゲームの表現が出来るようになり、プレイヤーにもっと満足していただけるゲームが作れると信じています。

そのためには、未来の技術を考え、技術を引っ張るリーダーの存在と、技術を正当に評価する体制が必要だと考えました。

技術リーダーを設置した

これまでのエンジニア組織はCTOとVPoEの体制になっていて、CTOが全社の技術を引っ張る役割、VPoEである私がエンジニア組織の組織マネジメントを行う役割として分かれていました。しかし、技術発展は著しいため、それぞれの技術の専門性をもった技術リーダーの存在が必要となってきました。

そこで、モバイルアプリケーション側を専門とするClient Engineering Leadと、サーバー・インフラ側を専門とするServer Engineering Leadの2人の技術リーダーをエンジニア組織に設置することにしました。この2人の技術リーダーが存在することで、これまでCTOに全ての技術の相談が行っていたところを、2人の技術リーダーに分散することができ、さらに、より深く、未来の技術に向けた話が出来るようになりました。

f:id:yunon_phys:20211221235015p:plain

評価体制を変更した

元々はエンジニアの納得感を優先して、評価されるエンジニアが評価者を選ぶという仕組みにしていました。EMがプロダクトチームのエンジニアのマネジメントを行う機会が多かったので、評価者としても選ばれやすかったです。これはこれでうまくいっていたところは多かったですが、技術的に組織全体を引き上げるということに関しては、目標設定・評価の段階で組織だって動けていませんでした。

今回2人の技術リーダーを設置したことで、より未来の技術の方向性がクリアになっていくため、評価もこの2人が最終意思決定出来るようにするのが良いと考えました。とはいえ、現在いるエンジニア全体を2人が評価するのはさすがに無理があるので、プロダクト単位で評価が行えるようにTeam Leadを各プロダクトに設置しました

f:id:yunon_phys:20211221234833p:plain

Engineering LeadはTeam Leadと、Team Leadはエンジニアメンバーと目標設定のすり合わせと評価のすり合わせをします。さらに、Engineering Leadはエンジニアメンバーの目標設定の内容も確認し、高い目標を掲げられているか、伸ばしたい技術の方向性として正しいかもチェックしていきます。

Team Leadがエンジニアメンバーの技術評価をする責任を持ったことで、今後プロダクトチームで技術的な課題が出たときは、EMではなくTeam Leadが担うことになります。これにより、EMの役割が一つ分解できました。

よりエンジニア組織の人事領域を強化したい

エンジニアが増えていく中で、採用ニーズが複雑化、エンジニアのキャリア形成が多様化などが課題となってきました。

これまで、VPoEである私を中心に、各プロダクトに配属されているEMたちと定期的にやりとりを行ってこのあたりの整理を行ってきました。しかし、プロダクト増にEMの採用が追いつかなかったり、1人分の仕事がそもそも無かったりと、エンジニアのマネジメントを満足に行えていない現状がありました。

Engineering Officeの立ち上げ

そこで、Engineering Officeの部署をエンジニア組織内に立ち上げ、エンジニアの採用ニーズやキャリア形成のための相談をサービス化することにしました。また、これまで全社の人事部門にあったエンジニアの採用部隊をこのEngineering Officeに統合することで、エンジニアの人事全般を一手に引き受けるようにしました。

f:id:yunon_phys:20211221235735p:plain

Engineering Officeには人事領域に強みのあるEMが入ることになりました。さらに、人事領域に元々興味のあったエンジニアメンバーも入ることにもなりました。Engineering Officeが設置されたことで、エンジニアの一つのキャリアパスに人事が加わったのは、組織的に一つ深みを増せたと思います。

ちなみに、Engineering Officeはメルカリさんの影響を大いに受けているので、この場を持って感謝の言葉をお伝えさせていただきます。ありがとうございます!!

よりプロジェクトマネジメントの領域を広く、深くしたい

EMはこれまでスクラムマスターとしての動きをしていたり、開発チームの課題発見・解決を行ってきました。しかし、いざゲームの運用となると、開発チームのマネジメントだけでなく、アセットを作ってデプロイする運用チームのマネジメントであったり、開発した機能をうまく運用にのせる必要がありました。これまではなんとなく、いろんなやり方でうまく回してきたのですが、モバイルゲームがより成熟化していくにつれ、プロダクトチーム全体の動きをうまく統合してマネジメントしていくやり方が求められるようになってきました

また、開発チームのプロセスをマネジメントしたり、運用チームのプロセスをマネジメントする人は、今後のプロダクトが増えていくときに無くてはならない存在です。一方、大規模ゲーム開発・運用経験 × プロジェクトマネジメントというのは、専門性も要求されるばかりか、そういった経験を積んでいる人はあまりにレアな存在であるため採用も難しいという課題がありました。

プロセスマネジメント職の立ち上げ

そこで、開発のプロセスや、運用アセットの製作プロセスをマネジメントするための専門職を立ち上げました*2。エンジニアだけでなく、プランナーやデザイナー、QAなどの様々なバックグラウンドを持つ人がこのプロセスマネジメント職*3に就くことで、それぞれの経験や人脈を活かしながら、プロダクトチームの課題を解決出来るようにしていきます。そして、このプロセスマネジメント職を社内で育成できる体制を整えることで、採用に左右されることなく、長期的な目線でプロダクトの増加に備えることになります。

f:id:yunon_phys:20211221235918p:plain

プロジェクトマネジメントに強みを持っていたEMがこの職に就くことで、よりプロセスマネジメントの専門性を深めながら、プロダクトチームに影響力を発揮していくことになります。

実はこの職が出来ることでもう一つ嬉しいことがあって、スクラムマスターを採用出来るようになりました。今まで、EMがスクラムマスターとしての役割も担っていたと書きましたが、逆にスクラムマスターに対してもEMとしての役割も期待していたが故に、より広範な知識や経験を要求してしまい、採用が難しかったという側面があります。これからはスクラムマスターにはスクラムマスターとしての業務のみを期待するようになり、よりスクラムマスターとしての専門性を評価出来るようになるので、良い方向に向かうと確信しています。

最後に

以上のように、EMという役割を無くすところから始まり、エンジニア組織を一から考え直してみました。結果として、今考えられる中で一番良い形になったのではないかと思います。

また数年後にはやっぱりEM復活しました、と言ってそうですが、組織はそのときに一番あった形に最適化していくのが当然やることだと思っているのでそのときはご容赦ください:)

*1:更新が滞っていてすみません

*2:正確にはこれからではあるのですが

*3:プロジェクトマネジメント職ではなくプロセスマネジメント職。プロジェクトマネジメントとすると、これまたいろんな業務を期待されてしまうことがわかった。いろいろ話を聞いていくと、プロジェクトマネジメントに期待しているのはほぼほぼプロセスマネジメントだとわかったので、あえてプロセスマネジメントとすることで業務の幅を想像しやすくなるようにした

【LT会】Akatsuki Geek Live 2021 開催レポート!Vol.2

みなさまこんにちは!アカツキ新卒採用担当の藤元です。

11/19(金)に、エンジニアを志す学生さま向けイベント「Akatsuki Geek Live2021 Vol.2」を開催しました!

7月に開催した「Akatsuki Geek Live2021 Vol.1」に負けないほどの大盛況で、なんと約80名の方にお越しいただきました。ありがとうございます!!

今回も、アカツキメンバーはもちろん、内定者や学生さんにもご登壇いただきました。

参加者のみなさまからは「予想以上にギークな話が聞けた!」や「アカツキの和気藹々とした雰囲気を感じれた」など満足度の高い評価をいただきました。

今回は登壇者がオフラインで集まったこともあり、メンバー同士の雰囲気もより伝わったのかな〜と思います。

今回は、イベント当日の様子をレポートします。

当日投影された資料も見られるので、ぜひ登壇者のギークネスを感じ取っていただけたらと思います!

「Akatsuki Geek Live」とは?

続きを読む

GCPのサービスアカウントキーなしでgcloudやbqコマンドをAWSから利用してみた

本記事は Akatsuki Advent Calendar 2021の9日目です。
前回は ぐんそう さんの 非エンジニアのためのGit/GitHub講座 でした。

概要

この記事では以下の内容について書きます。

  • GCPのWorkload Identity連携を利用して、AWSのEC2インスタンスからサービスアカウントキーを使わずにGCPを利用する方法
    • EC2インスタンスロールとGCPのサービスアカウントを紐付ける方法
  • gcloud CLIツールでWorkload Identity連携を利用する方法
  • gsutilコマンドやbqコマンドを使っていたのだけれどどうすれば良いの?

特にgcloud CLI周りについて、これまでAWS上でサービスアカウントキーを利用してシェルスクリプトによる自動化を行っていたが、サービスアカウントキーの使用をやめてセキュリティ向上を図りつつクレデンシャル管理の手間をなくしたいというニーズがあり、今回 Workload Identity 連携が利用できないか調べてみました。

最初にお断りしておきますが、Cloud Storageにアクセスするgsutilコマンドや、BigQueryにアクセスするbqコマンドついては現時点では正式な手順がなく、(パッチを当てたりして)無理やり動かしてみたという内容です。またその理由・将来的にどうなるかの予想についても書いてみます。

AWSとGCPのWorkload Identity連携のセットアップ

Workload Identity 連携は、GCP以外の認証情報(例えばAWSのIAM)をGCPのサービスアカウントに紐付けることで、永続するキーなしでGCPへのアクセスを認可するものです。技術的にはOpenID Connectを利用しています。
AWSであれば例えばEC2インスタンスにIAMロールをアタッチすることで、AWS・GCP共にキー管理を不要にできます。

最初にこの連携をセットアップしていきます。ここでは簡単のためにWebコンソールでセットアップする手順を書きます。(実際にはTerraform等のIaCを利用することが多いでしょう。)

まずは 「IAMと管理 > Workload Identity 連携」から「プールを追加」。

f:id:NeoCat:20211206161357p:plain
プールを追加

適当にプールの名前やID、説明を入力します。なおこのうちIDは後で変更できません。

プールの作成

次にプールにAWSをプロバイダとしてを追加します。AWSは他のOIDCプロバイダと違って専用の設定が用意されているので、アカウントIDとプロバイダ名(識別用の適当なID)を入力するだけでOKです。

プールにプロバイダを追加

AWSを選択した場合、次のステップでAWS用のマッピングが自動的に設定されます。

プロバイダの属性の設定

具体的には attribute.aws_role という属性でAWSのロールを使いやすく変換したものをマッピングするようになっています。長いCEL式でちょっと読みにくいですね。IAMロールをアタッチしたEC2インスタンス上で

aws sts get-caller-identity
を実行すると、 "Arn" として
"arn:aws:sts::...:assumed-role/<ロール名>/<EC2インスタンスID>"
という文字列が返ってくるはずです。先のCEL式は、 "assumed-role" が含まれる場合は、最後の "/<EC2インスタンスID>" 部分を削る、ということをしています。含まれない場合はArnそのままです。
これを利用して、例えば上記スクリーンショットの下部ようにCEL式を記述することで、EC2インスタンスに特定のIAMロール role-name がアタッチされている時のみWorkload Identity認証を許可するといった制御ができます。

最後に、このプールにサービスアカウントを紐付けて許可します。あらかじめ利用させたい権限*1を持つサービスアカウントを作成しておきます。

f:id:NeoCat:20211206163435p:plain
アクセスを許可
「アクセスを許可」で、そのサービスアカウントを設定します。また、ここでもロールとサービスアカウントの紐付けが可能です。こちらであれば、複数のロールとサービスアカウントの対応付けが可能です。
サービスアカウントとの紐付け

設定が終わると、構成ファイルと呼ばれるJSONをダウンロードすることができます。これをアプリケーションに渡すことで、Workload Identityによる認証が可能になります。サービスアカウントキーと違ってこれは機密情報を含まずセキュアに管理する必要がありませんので、気軽に取り扱うことができます。

構成JSONファイルのダウンロード

GOOGLE_APPLICATION_CREDENTIALS で利用する

Google Cloud SDKを利用して開発されたPythonやGo等のアプリケーションを利用する場合、標準的な方法で認証を行なっていれば、EC2インスタンス上にこのファイルを配置してGOOGLE_APPLICATION_CREDENTIALS 環境変数にこのファイルのパスを渡すだけでOKなはずです。

gcloud CLIで利用する

さて、gcloud コマンドでシェルスクリプトなどを書いている場合、どうすれば良いでしょうか?

gcloud コマンドは、gcloud auth login でWeb認証したり、 gcloud auth activate でサービスアカウントキーを登録し、それをデフォルトアカウントとして利用します。

Workload Identity を利用する場合、gcloud auth activate に構成ファイルを渡しても失敗します。正解は、以下のように auth login の --cred-file オプションに構成ファイルを渡すというものです。

gcloud auth login --cred-file=clientLibraryConfig.json

これで、以降はデフォルトでgcloudコマンドが Workload Identity を利用してくれます!

gsutilコマンドやbqコマンドでWorkload Identity認証するには…?

これがある意味でこの記事の本題かもしれません。

上記の方法で gcloud コマンドは利用可能になりました。が、それと同じデフォルトアカウントを利用するgsutilコマンドやbqコマンドも使いたいですよね。
しかし実際にやってみると、

$ gsutil ls
ServiceException: 401 Anonymous caller does not have storage.buckets.list access to the Google Cloud project.

$ bq ls
ERROR: (bq) Your current active account [...] does not have any valid credentials

と認証エラーと思しきメッセージが出て失敗してしまいます。何か回避策はないでしょうか? *2

gsutil コマンドの代替

まず gsutil ですが、alphaステージではありますが代替となるgcloud alpha storageコマンドが用意されていました。こちらであればきちんと認証が行えます。(gcloudコマンドの一部ですからね)*3
下記のようにcp, ls, rmといったサブコマンドがあります。

$ gcloud alpha storage
ERROR: (gcloud.alpha.storage) Command name argument expected.

Available groups for gcloud alpha storage:

Available commands for gcloud alpha storage:

      cp                      *(ALPHA)*  Upload, download, and copy Cloud
                              Storage objects.
      ls                      *(ALPHA)*  List Cloud Storage buckets and objects.
      rm                      *(ALPHA)*  Delete objects and buckets.

ところで、実は gsutil コマンドはアクセス先のプラグインに対応しており、デフォルトでS3などにもアクセスできます。
gcloud alpha storageもこれを継承しており、ちゃんとS3にもアクセスできます。

gcloud alpha storage ls s3://  # バケット一覧
gcloud alpha storage cp s3://bucket-name/file gs://bucket-name/

ということで、将来的に gsutil はこちらに移行していくことになると思われるので、これを待つか先行して試用することになります*4

bq コマンドは…?

では bq コマンドはどうでしょうか? gcloud alpha bq というコマンドはあり、データセットやテーブルのCRUD操作はできるようですが、 bq query に相当するクエリを実行するようなコマンドはなさそうでした。

そもそもなぜGoogle Cloud SDKの一部であるはずのbqコマンドはエラーを出してしまうのでしょう?
SDKはPythonで書かれていますので、直接ソースコードを読むことができます。調べてみましょう。

まず bq コマンドの実体はgoogle-cloud-sdk内の bin/bq ですが、これは適切なPythonを探してbin/bootstrapping/bq.py を実行します。
このbootstrappingは、まずデフォルトの認証情報を取得しています。

    store.Load()  # Checks if there are active credentials

その後、認証情報にはauth login、GKEやGCEのメタデータ、サービスアカウント、Application Default Credentialなど様々な認証方法がありますが、この種類ごとに分岐しています。

なんだか雲行きが怪しくなってきました。

分岐先では種類に応じたコマンドラインオプションを生成し、それを platform/bq/bq.pyrun_mainに渡しています。
その先でさらに渡されたオプションを元に認証を再度やり直しますが、そこで使われている認証ライブラリはthird_party/oauth2client_4_0 で、Google Cloud SDKの認証系とは別物*5であり、Workload Identity認証にも非対応です。

つまるところ、bqコマンドはSDKに入ってはいるものの、内部は完全に別の作りになっていて、起動部分で強引に認証方法の辻褄合わせをしているために、引き渡せないでいたわけです。起動と言ってもただのPythonの関数呼び出しなのでCLIオプション形式にする必要すらないはずなのですが、bqの中身は意地でも触りたくなかったのでしょうか…?

皮肉なことに、上記の

    store.Load()  # Checks if there are active credentials

を実行した時点で、実はWorkload Identityの認証は内部的に解決されており、返り値としてAPI呼び出しに必要なトークンはすでに取得できています。

ならばあとは引き渡してやれば良いのだろう? ということで、簡単なパッチを書いてみました(本記事の末尾に置いておきます)。
やっていることは、上記 store.Load() で取れていたトークンをオブションとしてそのまま bq.py に渡し、API呼び出し時に使うというだけです。
これで bq コマンドが Workload Identity 対応になってしまいました(笑)。

$ bq ls
       datasetId
 ---------------------
...

カスタムパッチを実運用に投入するのは微妙なので、これも将来的にgcloud bqに統合されていくのを待つことになるのでしょうが、こんなに簡単なら暫定で bq コマンドでも対応してくれないかな…という気持ちになりました。

まとめ

本記事では、Workload Identityによる認証を利用して、AWS上でgcloud CLIを使う方法を書きました。
AWS以外のOIDCプロバイダでも利用可能なはずです(GitHub Actions等の利用例は多く公開されていますね)。
現状、gsutilやbqについては残念な状況ですが、alphaコマンドを使ったり簡単なパッチ適用で半ば無理やり動かすことはできました。
簡単かつセキュアになるので、ぜひ早くもっと利用しやすくなればなと期待しています。

bqコマンドをWorkload Identity対応にするパッチ

diff -ur google-cloud-sdk/bin/bootstrapping/bq.py google-cloud-sdk-patched/bin/bootstrapping/bq.py
--- google-cloud-sdk/bin/bootstrapping/bq.py    1980-01-01 08:00:00.000000000 +0000
+++ google-cloud-sdk-patched/bin/bootstrapping/bq.py    2021-11-12 17:06:00.747650228 +0000
@@ -38,7 +38,7 @@
   args = []
   if cmd_args and cmd_args[0] not in ('version', 'help'):
     # Check for credentials only if they are needed.
-    store.Load()  # Checks if there are active credentials
+    cred = store.Load()  # Checks if there are active credentials

     project, account = bootstrapping.GetActiveProjectAndAccount()
     adc_path = config.Paths().LegacyCredentialsAdcPath(account)
@@ -57,8 +57,11 @@
                 '--service_account_credential_file', single_store_path,
                 '--service_account_private_key_file', p12_key_path]
       else:
-        # Don't have any credentials we can pass.
-        raise store.NoCredentialsForAccountException(account)
+        if len(cred.token) > 0:
+          args = ['--access_token', cred.token]
+        else:
+          # Don't have any credentials we can pass.
+          raise store.NoCredentialsForAccountException(account)

     _MaybeAddOption(args, 'project_id', project)

diff -ur google-cloud-sdk/platform/bq/bq_auth_flags.py google-cloud-sdk-patched/platform/bq/bq_auth_flags.py
--- google-cloud-sdk/platform/bq/bq_auth_flags.py       1980-01-01 08:00:00.000000000 +0000
+++ google-cloud-sdk-patched/platform/bq/bq_auth_flags.py       2021-11-12 16:26:43.453653046 +0000
@@ -44,3 +44,7 @@
     'service_account_credential_file', None,
     'Only for the gcloud wrapper use.'
 )
+flags.DEFINE_string(
+    'access_token', '',
+    'Only for the gcloud wrapper use.'
+)
diff -ur google-cloud-sdk/platform/bq/credential_loader.py google-cloud-sdk-patched/platform/bq/credential_loader.py
--- google-cloud-sdk/platform/bq/credential_loader.py   1980-01-01 08:00:00.000000000 +0000
+++ google-cloud-sdk-patched/platform/bq/credential_loader.py   2021-11-12 16:29:54.002440206 +0000
@@ -61,6 +61,16 @@
     raise NotImplementedError()


+class AccessTokenCredentialLoader(CredentialLoader):
+  """Use the specified access token."""
+
+  def __init__(self, access_token):
+    self._access_token = access_token
+
+  def _Load(self):
+    return oauth2client_4_0.client.AccessTokenCredentials(self._access_token, _CLIENT_USER_AGENT)
+
+
 class CachedCredentialLoader(CredentialLoader):
   """Base class to add cache capability to credential loader.

@@ -276,6 +286,10 @@
         credential_cache_file=FLAGS.credential_file,
         read_cache_first=True,
         credential_file=FLAGS.application_default_credential_file)
+
+  if FLAGS.access_token:
+    return AccessTokenCredentialLoader(FLAGS.access_token)
+
   raise app.UsageError(
       'bq.py should not be invoked. Use bq command instead.')

*1:セキュリティ向上のために最小限に絞りましょう。

*2:自明な解決策は、スクリプトをPython等で書き直してApplication Default Credentialを使うことです。規模が大きいと大変ですけど。。

*3:余談ですが、Python3がインストールされていないとエラーで動作しません。

*4:alphaですから、いきなりインターフェイスが変わったりするかもしれないので注意しましょう、念の為。

*5:SDK内部に持ってはいるんですが。。。

アカツキのサーバーサイドインターンに参加しました

初めまして、2021年の9/6 ~ 9/24の3週間、アカツキのサーバーサイドインターンに参加させていただきました、竹下といいます。この記事では、私が取りくんだこと、学んだことについて書かせていただきたいと思います。

自己紹介

慶應義塾大学 総合政策学部の学部3年生で、普段は個人や仲間内、アルバイトなどでWebサービス開発を行っています。使う言語などに強いこだわりがあるわけではないですが、Rustで開発をすることがひそかなマイブームです。 過去には趣味でオレオレWAFを実装したりしていました。

今回取り組んだこと

今回のインターンで私が配属させていただいたプロジェクトは、Ruby on Railsによるゲームのサーバーチームでした。その中で、アップデート後に処理が重くなってしまったエンドポイントについて、速度改善をするというタスクにチャレンジしました。

エンドポイントの役割としては、スマホゲームではおなじみの「ミッション機能(一定の条件をクリアすると報酬が貰える)」の一部です。デイリーミッションやコンプリート系のミッションなどといった、いつのまにか達成しているようなミッションを含めてミッション進捗を確認するために叩かれます。

続きを読む