Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10966] ファイルの概要

このコミットは、Go言語のダッシュボードアプリケーションにおいて、認証に使用される秘密鍵(secret key)をソースコードに直接記述するのではなく、Google App EngineのDatastoreに保存するように変更することで、セキュリティを強化し、誤って秘密鍵がリポジトリにコミットされるリスクを排除することを目的としています。

コミット

commit 550856c59d153b8a92a5e26b6a5db1e06ff848ba
Author: Russ Cox <rsc@golang.org>
Date:   Thu Dec 22 10:21:59 2011 -0500

    dashboard: do not require key in source code

    Or else eventually someone will check it in.

    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5504071

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/550856c59d153b8a92a5e26b6a5db1e06ff848ba

元コミット内容

このコミットの元の内容は、「ダッシュボード: ソースコードにキーを要求しない」というものです。これは、認証キーをソースコード内に直接記述することをやめるという意図を示しています。その理由として、「さもないと、最終的には誰かがそれをチェックインしてしまうだろう」と述べられており、秘密情報が誤ってバージョン管理システムにコミットされることへの懸念が背景にあります。

変更の背景

この変更の主な背景は、セキュリティの向上と、開発プロセスにおける潜在的なリスクの排除です。

  • 秘密鍵の誤コミット防止: 認証や暗号化に使用される秘密鍵は、機密性の高い情報であり、公開リポジトリに誤ってコミットされると、システム全体のセキュリティが危険に晒されます。以前の実装では、secretKeyがソースコード内にプレースホルダーとして存在し、デプロイ前に手動で変更する必要がありました。これは、開発者が誤って変更前のコードをコミットしてしまうリスクを伴いました。
  • デプロイメントの安全性と利便性: 秘密鍵をソースコードから分離し、Google App EngineのDatastoreのような安全な場所に保存することで、デプロイメントプロセスがより安全になります。開発者はコードをデプロイする際に、手動で秘密鍵を挿入する手間が省け、デプロイ後の設定変更も容易になります。
  • Google App Engineのベストプラクティスへの準拠: Google App Engineのようなクラウドプラットフォームでは、機密情報をDatastoreやSecret Managerなどの専用サービスに保存することが推奨されています。この変更は、このようなクラウドネイティブな環境におけるベストプラクティスに沿ったものです。

前提知識の解説

このコミットを理解するためには、以下の技術的知識が必要です。

  • Google App Engine (GAE): Googleが提供するPaaS(Platform as a Service)。ウェブアプリケーションやモバイルバックエンドを構築・ホストするためのプラットフォームで、スケーラビリティや運用管理が容易なのが特徴です。
  • Google App Engine Datastore: GAEが提供するNoSQLデータベースサービス。高可用性とスケーラビリティを備え、アプリケーションのデータを永続的に保存するために使用されます。キーと値のペアでデータを管理し、エンティティと呼ばれる構造でデータを格納します。
  • appengine.Context: App Engineアプリケーションのリクエストコンテキストを表すインターフェース。Datastoreへのアクセスなど、App EngineのAPIを呼び出す際に必要となります。
  • datastore.NewKey: Datastoreのエンティティを一意に識別するためのキーを生成する関数。
  • datastore.Get: Datastoreからエンティティを取得する関数。
  • datastore.Put: Datastoreにエンティティを保存(または更新)する関数。
  • datastore.ErrNoSuchEntity: datastore.Getが指定されたキーに対応するエンティティを見つけられなかった場合に返されるエラー。
  • appengine.IsDevAppServer(): 現在のアプリケーションが開発サーバー(ローカル環境)で実行されているかどうかを判定する関数。これにより、開発環境と本番環境で異なる挙動をさせることができます。
  • HMAC-MD5: Hash-based Message Authentication Code (HMAC) の一種で、MD5ハッシュ関数を使用します。メッセージの認証と完全性検証のために使用される暗号学的ハッシュ関数です。このコミットでは、builderKeyの生成に利用されています。
  • http.HandlerFunc: Go言語のnet/httpパッケージにおけるHTTPリクエストを処理するための関数型。
  • sync.RWMutex: Go言語のsyncパッケージが提供する読み書きロック(Reader-Writer Mutex)。複数のゴルーチンが同時に読み取りアクセスできるが、書き込みアクセスは排他的に行われるように制御します。これにより、共有データへの安全なアクセスを保証しつつ、読み取りの並行性を高めることができます。
  • panic: Go言語における回復不可能なエラーを示すメカニズム。プログラムの実行を即座に停止させます。通常、予期せぬ重大なエラーが発生した場合に使用されます。
  • secretKey: 認証に使用される秘密の文字列。
  • builderKey: secretKeybuilder名から生成されるキー。おそらく、特定のビルドプロセスやビルドサーバーを識別・認証するために使用されます。
  • AuthHandler: 認証ロジックをカプセル化するHTTPハンドラ。
  • validKey: 提供されたキーが有効であるかを検証する関数。

技術的詳細

このコミットの核心は、misc/dashboard/app/build/key.go ファイルにおける secretKey の管理方法の変更と、それに伴う misc/dashboard/app/build/handler.go ファイルの修正です。

misc/dashboard/app/build/key.go の変更点:

  1. secretKey のDatastoreへの移行:

    • 以前は const secretKey = "" のようにソースコードに直接記述されていた secretKey が削除されました。
    • 代わりに、secretKey(c appengine.Context) string という関数が導入されました。この関数は、appengine.Context を引数に取り、Datastoreから秘密鍵を取得します。
    • 秘密鍵は BuilderKey という構造体(Secret string フィールドを持つ)としてDatastoreに保存されます。この構造体には、Datastoreのキーを生成するための Key(c appengine.Context) *datastore.Key メソッドも追加されました。
    • theKey というグローバル変数(sync.RWMutexBuilderKey を含む)が導入され、秘密鍵のキャッシュとスレッドセーフなアクセスを可能にしています。
    • secretKey 関数は、まず theKey のキャッシュを読み取りロック (RLock) で確認します。キャッシュに存在すればそれを返します。
    • キャッシュに存在しない場合、書き込みロック (Lock) を取得し、再度キャッシュを確認します(二重チェックロックパターン)。
    • それでもキャッシュに存在しない場合、Datastoreから BuilderKey エンティティを取得しようとします。
    • もしDatastoreにエンティティが存在しない場合 (datastore.ErrNoSuchEntity)、開発サーバーであればデフォルトのキー "gophers rule" を設定し、Datastoreに保存します。本番環境でこのエラーが発生した場合は panic します。これは、本番環境では秘密鍵がDatastoreに存在することが前提となるためです。
    • Datastoreからの取得に成功した場合、その秘密鍵をキャッシュに保存し、返します。
  2. 初期化関数の削除:

    • 以前存在した init() 関数(開発サーバー以外でのデプロイ時に panic を発生させることで、秘密鍵の変更を強制していた)が削除されました。これは、秘密鍵がDatastoreから動的に取得されるようになったため、もはや不要になったためです。

misc/dashboard/app/build/handler.go の変更点:

  1. appengine.Context の伝播:
    • validKey 関数と builderKey 関数が、新たに appengine.Context を引数として受け取るように変更されました。これは、これらの関数内で secretKey(c) を呼び出すために必要です。
    • AuthHandler 内で validKey を呼び出す際、appengine.NewContext(r) で取得したコンテキストを渡すように変更されました。
    • keyHandler 内で builderKey を呼び出す際も同様に、コンテキストを渡すように変更されました。

これらの変更により、秘密鍵はソースコードから完全に分離され、Google App EngineのDatastoreによって管理されるようになりました。これにより、秘密鍵の誤コミットのリスクが排除され、アプリケーションのセキュリティとデプロイメントの柔軟性が向上しました。

コアとなるコードの変更箇所

misc/dashboard/app/build/handler.go

--- a/misc/dashboard/app/build/handler.go
+++ b/misc/dashboard/app/build/handler.go
@@ -322,7 +322,7 @@ func AuthHandler(h dashHandler) http.HandlerFunc {
 		// Validate key query parameter for POST requests only.
 		key := r.FormValue("key")
 		builder := r.FormValue("builder")
-		if r.Method == "POST" && !validKey(key, builder) {
+		if r.Method == "POST" && !validKey(c, key, builder) {
 			err = os.NewError("invalid key: " + key)
 		}
 
@@ -368,7 +368,8 @@ func keyHandler(w http.ResponseWriter, r *http.Request) {
 		logErr(w, r, os.NewError("must supply builder in query string"))
 		return
 	}
-	fmt.Fprint(w, builderKey(builder))
+	c := appengine.NewContext(r)
+	fmt.Fprint(w, builderKey(c, builder))
 }
 
 func init() {
@@ -392,18 +393,18 @@ func validHash(hash string) bool {
 	return hash != ""
 }
 
-func validKey(key, builder string) bool {
+func validKey(c appengine.Context, key, builder string) bool {
 	if appengine.IsDevAppServer() {
 		return true
 	}
-	if key == secretKey {
+	if key == secretKey(c) {
 		return true
 	}
-	return key == builderKey(builder)
+	return key == builderKey(c, builder)
 }
 
-func builderKey(builder string) string {
-	h := hmac.NewMD5([]byte(secretKey))
+func builderKey(c appengine.Context, builder string) string {
+	h := hmac.NewMD5([]byte(secretKey(c)))
 	h.Write([]byte(builder))
 	return fmt.Sprintf("%x", h.Sum())
 }

misc/dashboard/app/build/key.go

--- a/misc/dashboard/app/build/key.go
+++ b/misc/dashboard/app/build/key.go
@@ -4,13 +4,59 @@
 
 package build
 
-import "appengine"
+import (
+	"sync"
 
-// Delete this init function before deploying to production.
-func init() {
-	if !appengine.IsDevAppServer() {
-		panic("please read misc/dashboard/app/build/key.go")
-	}\n+\t"appengine"\n+\t"appengine/datastore"\n+)\n+\n+var theKey struct {\n+\tsync.RWMutex\n+\tBuilderKey\n+}\n+\n+type BuilderKey struct {\n+\tSecret string\n+}\n \n-const secretKey = "" // Important! Put a secret here before deploying!\n+func (k *BuilderKey) Key(c appengine.Context) *datastore.Key {\n+\treturn datastore.NewKey(c, "BuilderKey", "root", 0, nil)\n+}\n+\n+func secretKey(c appengine.Context) string {\n+\t// check with rlock\n+\ttheKey.RLock()\n+\tk := theKey.Secret\n+\ttheKey.RUnlock()\n+\tif k != "" {\n+\t\treturn k\n+\t}\n+\n+\t// prepare to fill; check with lock and keep lock\n+\ttheKey.Lock()\n+\tdefer theKey.Unlock()\n+\tif theKey.Secret != "" {\n+\t\treturn theKey.Secret\n+\t}\n+\n+\t// fill\n+\tif err := datastore.Get(c, theKey.Key(c), &theKey.BuilderKey); err != nil {\n+\t\tif err == datastore.ErrNoSuchEntity {\n+\t\t\t// If the key is not stored in datastore, write it.\n+\t\t\t// This only happens at the beginning of a new deployment.\n+\t\t\t// The code is left here for SDK use and in case a fresh\n+\t\t\t// deployment is ever needed.  "gophers rule" is not the\n+\t\t\t// real key.\n+\t\t\tif !appengine.IsDevAppServer() {\n+\t\t\t\tpanic("lost key from datastore")\n+\t\t\t}\n+\t\t\ttheKey.Secret = "gophers rule"\n+\t\t\tdatastore.Put(c, theKey.Key(c), &theKey.BuilderKey)\n+\t\t\treturn theKey.Secret\n+\t\t}\n+\t\tpanic("cannot load builder key: " + err.String())\n+\t}\n+\n+\treturn theKey.Secret\n+}\n```

## コアとなるコードの解説

### `misc/dashboard/app/build/key.go`

*   **`var theKey struct { sync.RWMutex; BuilderKey }`**:
    *   `theKey` は、秘密鍵とそのキャッシュ、そしてそれらを保護するための読み書きロック (`sync.RWMutex`) を保持するグローバル変数です。これにより、複数のゴルーチンからの安全なアクセスと、読み取り操作の並行性を確保します。
*   **`type BuilderKey struct { Secret string }`**:
    *   Datastoreに保存される秘密鍵の構造を定義します。`Secret` フィールドに実際の秘密鍵の文字列が格納されます。
*   **`func (k *BuilderKey) Key(c appengine.Context) *datastore.Key`**:
    *   `BuilderKey` エンティティをDatastoreで一意に識別するためのキーを生成するメソッドです。`"BuilderKey"` という種類名と `"root"` というIDを持つキーを使用しています。
*   **`func secretKey(c appengine.Context) string`**:
    *   この関数が、アプリケーションが秘密鍵を取得するための主要なインターフェースです。
    *   **キャッシュの利用**: まず `theKey.RLock()` で読み取りロックを取得し、`theKey.Secret` に値がキャッシュされているかを確認します。キャッシュヒットすれば、すぐにその値を返します。これにより、Datastoreへの不要なアクセスを減らし、パフォーマンスを向上させます。
    *   **Datastoreからの取得**: キャッシュミスの場合、`theKey.Lock()` で書き込みロックを取得し、Datastoreから `BuilderKey` エンティティを取得します。
    *   **初回デプロイ時の処理**: `datastore.ErrNoSuchEntity` が返された場合(Datastoreにまだ秘密鍵が保存されていない場合)、開発サーバーであれば `"gophers rule"` というデフォルトの秘密鍵を設定し、Datastoreに保存します。これは、新しいデプロイメントの初期設定やSDKでの使用を想定しています。本番環境でこのエラーが発生した場合は `panic` し、秘密鍵がDatastoreから失われたことを開発者に知らせます。
    *   **エラーハンドリング**: Datastoreからの取得中に他のエラーが発生した場合も `panic` します。
    *   取得した秘密鍵は `theKey.Secret` にキャッシュされ、次回の呼び出しで利用されます。

### `misc/dashboard/app/build/handler.go`

*   **`func AuthHandler(h dashHandler) http.HandlerFunc` 内の変更**:
    *   `validKey` 関数の呼び出しに `appengine.Context` (`c`) が追加されました。これにより、`validKey` 関数内で新しい `secretKey(c)` 関数を呼び出すことが可能になります。
*   **`func keyHandler(w http.ResponseWriter, r *http.Request)` 内の変更**:
    *   `builderKey` 関数の呼び出しに `appengine.Context` (`c`) が追加されました。同様に、`builderKey` 関数内で `secretKey(c)` を呼び出すために必要です。
*   **`func validKey(c appengine.Context, key, builder string) bool` の変更**:
    *   引数に `appengine.Context` (`c`) が追加されました。
    *   `key == secretKey` の比較が `key == secretKey(c)` に変更され、秘密鍵がDatastoreから動的に取得されるようになりました。
    *   `key == builderKey(builder)` の比較が `key == builderKey(c, builder)` に変更され、`builderKey` 関数もコンテキストを受け取るようになりました。
*   **`func builderKey(c appengine.Context, builder string) string` の変更**:
    *   引数に `appengine.Context` (`c`) が追加されました。
    *   `hmac.NewMD5([]byte(secretKey))` の部分が `hmac.NewMD5([]byte(secretKey(c)))` に変更され、HMACの生成に使用される秘密鍵もDatastoreから取得されるようになりました。

これらの変更により、アプリケーションは実行時にDatastoreから秘密鍵を安全に取得し、それを使用して認証やキー生成を行うようになりました。これにより、ソースコードに秘密鍵をハードコードする必要がなくなり、セキュリティと運用性が大幅に向上しました。

## 関連リンク

*   [Google App Engine Documentation](https://cloud.google.com/appengine/docs)
*   [Google Cloud Datastore Documentation](https://cloud.google.com/datastore/docs)
*   [Go言語 `net/http` パッケージ](https://pkg.go.dev/net/http)
*   [Go言語 `crypto/hmac` パッケージ](https://pkg.go.dev/crypto/hmac)
*   [Go言語 `sync` パッケージ](https://pkg.go.dev/sync)

## 参考にした情報源リンク

*   コミット情報: `./commit_data/10966.txt`
*   GitHubコミットページ: [https://github.com/golang/go/commit/550856c59d153b8a92a5e26b6a5db1e06ff848ba](https://github.com/golang/go/commit/550856c59d153b8a92a5e26b6a5db1e06ff848ba)
*   Go言語の公式ドキュメント (pkg.go.dev)
*   Google Cloudの公式ドキュメント (cloud.google.com)
*   一般的なソフトウェア開発におけるセキュリティベストプラクティスに関する知識