[インデックス 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
:secretKey
とbuilder
名から生成されるキー。おそらく、特定のビルドプロセスやビルドサーバーを識別・認証するために使用されます。AuthHandler
: 認証ロジックをカプセル化するHTTPハンドラ。validKey
: 提供されたキーが有効であるかを検証する関数。
技術的詳細
このコミットの核心は、misc/dashboard/app/build/key.go
ファイルにおける secretKey
の管理方法の変更と、それに伴う misc/dashboard/app/build/handler.go
ファイルの修正です。
misc/dashboard/app/build/key.go
の変更点:
-
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.RWMutex
とBuilderKey
を含む)が導入され、秘密鍵のキャッシュとスレッドセーフなアクセスを可能にしています。secretKey
関数は、まずtheKey
のキャッシュを読み取りロック (RLock
) で確認します。キャッシュに存在すればそれを返します。- キャッシュに存在しない場合、書き込みロック (
Lock
) を取得し、再度キャッシュを確認します(二重チェックロックパターン)。 - それでもキャッシュに存在しない場合、Datastoreから
BuilderKey
エンティティを取得しようとします。 - もしDatastoreにエンティティが存在しない場合 (
datastore.ErrNoSuchEntity
)、開発サーバーであればデフォルトのキー"gophers rule"
を設定し、Datastoreに保存します。本番環境でこのエラーが発生した場合はpanic
します。これは、本番環境では秘密鍵がDatastoreに存在することが前提となるためです。 - Datastoreからの取得に成功した場合、その秘密鍵をキャッシュに保存し、返します。
- 以前は
-
初期化関数の削除:
- 以前存在した
init()
関数(開発サーバー以外でのデプロイ時にpanic
を発生させることで、秘密鍵の変更を強制していた)が削除されました。これは、秘密鍵がDatastoreから動的に取得されるようになったため、もはや不要になったためです。
- 以前存在した
misc/dashboard/app/build/handler.go
の変更点:
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)
* 一般的なソフトウェア開発におけるセキュリティベストプラクティスに関する知識