ブログ

Cloud IAP で作る手軽でセキュアな社内サービス

こんにちは、ソフトウェアエンジニアの yuku です。好きなおでんの種は大根です。
仕事をしていると社内向けに何かしらのサービスを作りたくなることがありますよね。この時面倒なのが認証と認可です。外部に公開されていない社内ネットワークにだけ公開すれば安全ではありますが、外からアクセスするためにいちいち VPN 接続せねばならず面倒です。インターネットからアクセス可能にする場合、画像などコンテンツもセキュアな場所に確実に配置する必要があります。
こんな時 G Suite を使っている会社であれば Cloud IAP を使ってどこからでもアクセス可能な社員向けサービスを簡単に作ることができます。今回の記事では Cloud IAP と、フライウィールで実際に Cloud IAP を使って実装されている社内向けドキュメント共有サービスを紹介します。

Cloud IAP とは

Cloud IAP (Identity-Aware Proxy) は GCP のサービスの一つです。
簡単に言うと、 Cloud IAP を有効にするとその背後にある Web アプリケーションには Cloud IAP で認証・認可されたリクエストだけが届くようになります。当然どういう人を認可するかは制御可能で、特定 G Suite のアカウントに所属する人だったら認可する、というルールにすれば Google が責任を持って社外の人のアクセスを遮断してくれるわけです。

Cloud IAP + GAE

現時点で Cloud IAP でプロキシできるのは GAE のスタンダード環境とフレキシブル環境、 GKE そして GCE インスタンスです。
もちろんどれを使っても構わないのですが、 GAE スタンダード環境と組み合わせるメリットがいくつかあります:

  • Cloud IAP + GAE の構成の場合、 GAE に届くリクエストが必ず Cloud IAP によって認証・認可されていることが保証されます。 GKE と GCE の場合アプリケーションのポートに直接アクセスできるユーザや同一 VM 内での接続が Cloud IAP をバイパスできてしまいます。
  • 社内向けサービスは一般的にアクセスが少ないので、インスタンスを常時起動しておくのは無駄が多いです。社内サービスに潤沢予算があることはあまりないと思いますが、 GAE スタンダード環境であれば無料枠に収まるかも知れません。
  • GAE スタンダード環境は細かいところで融通が利かない代わりに、 Google がほとんどの管理業務を行ってくれます。たかが社内向けサービスのために運用に人を割り当てる余裕はどこの会社にもないでしょう。

GAE スタンダード環境というと、特に昔の GAE を知っている人だと、ロックインが気になるかも知れません。たしかにかつてはそうだったのですが、最近ではだいぶその傾向が弱くなってきています。GAE の詳細は今回の記事の範囲外なので詳細は各言語毎のドキュメントを参照していただきたいのですが、例えば Nodejs 環境であれば `PORT` 環境変数を listen する express サーバがほぼそのまま動きます。 Go 言語も 1.11 以降では同じような感じです。なので、凝ったことをしないなら使い慣れた環境でそのままアプリを作り始めることができます。

ユーザの認証

Cloud IAP を通ったリクエストにはいくつかのヘッダーが追加されています。その中の x-goog-iap-jwt-assertion にある署名付きヘッダーを使うことでユーザを認証することができます。詳しくはドキュメントを参照してください。 Nodejs であればたとえば次のようなコードでアクセスしているユーザのメールアドレスを取得することができます(単純化のためにエラー処理を省略しています):

const request = require("request") 
const jwt = require("jsonwebtoken") 
const getPublicKey = (token: string) => { 
  return new Promise(resolve => { 
    request({ 
      url: "https://www.gstatic.com/iap/verify/public_key", 
      method: "GET", 
      json: true 
    }, (_, res) => { 
      resolve(res.body[jwt.decode(token, { complete: true }).header.kid]) 
    }) 
  }) 
} 
app.get("/", async (req, res) => { 
  const token = req.headers["x-goog-iap-jwt-assertion"] 
  const publicKey = await getPublicKey(token) 
  jwt.verify(token, publicKey, (_, decoded) => { 
    // decoded.email にユーザのメールアドレスが入っている 
  }) 
})

こうして取得したメールアドレスから Firebase のアクセストークンを発行してクライアントに渡せば、 Firebase アプリを書くこともできます。こちらも詳細は割愛しますが、 How to Develop Real-Time Apps with Firebase and Firestore Secured with Auth0 などが参考になります。

例: 社内向けドキュメント共有サービス

それでは実際に Cloud IAP と GAE を使った社内サービスとして、フライウィール社内向けに開発したドキュメント共有サービスである Mouseion を紹介します。コードは https://github.com/flywheel-jp/mouseion で公開されているので、興味がある方はそちらも読んでみてください。

背景

パブリックリポジトリに格納したドキュメントは GitHub pages で世界に向けて公開することができます。 GitHub pages はとても便利なサービスですが、アクセス制御の機能が無いのでプライベートリポジトリでは使うことができません。GitHub Enterprise を使っているなら話は別なのでしょうが、フライウィールでは SaaS 版の GitHub を使っているので、これまで Git リポジトリにドキュメントを入れても共有するのが難しく、代わりに Google ドキュメントが使われていました。たしかに Google ドキュメントは便利ですが、コード修正時に一緒には変更しないので、どうしても更新し忘れて乖離が発生してしまいます。やはりコードのドキュメントは同じリポジトリに格納するべきでしょう。
そこで社員だけがアクセスできる GitHub pages として作られたのが Mouseion です。

実装

Mouseion のアーキテクチャ

flywheel-jp/mouseion リポジトリに含まれているコードのほとんどは GAE のコードです。Cloud IAP の背後にあるので、フライウィール社員がアクセスしてきているものとして実装することができます。そのため GAE は GCS をプロキシする機能だけでよく、かなり単純な作りになっています。

利用

各リポジトリは所定の GCS バケットにファイルをアップロードすることでファイルを公開できます。 flywheel-jp/mouseion にはこのアップロード手続きを簡単に行うための GitHub Actions が含まれています。 GCP プロジェクトの管理者(つまりフライウィールの場合は僕)に依頼するとリポジトリに `GCLOUD_AUTH` という名前でアップロード用の service accont の鍵が置かれるので、あとはそのリポジトリに GitHub Actions を書くだけです:

on:
  push:
    branches: [master]
jobs:
  upload:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - uses: flywheel-jp/mouseion/upload@master
        with:
          service-account-key: ${{ secrets.GCLOUD_AUTH }}
# bucket: your-bucket-name

これで master ブランチが push されるたびに自動的に docs ディレクトリが Mouseion にアップロードされるようになります。

おわりに

社内向けのサービスを作ろうと思い立つけれど「これって本当に安全なのだろうか?」と考え始めて手が止まってしまうことありませんか?そういうときは Cloud IAP の後ろにとりあえず全てを配置すれば、細かいことを気にしなくてもよくなります。 Cloud IAP を使って、埋もれてしまった会社を良くするアイデアを形にしてみましょう。
フライウィールでは社内向け便利サービスを作って業務効率を改善するのが好きなソフトウェアエンジニアを募集しています