セキュアなAPIの構築は、単に認証を追加するだけではありません。ユーザー入力の処理方法から サーバーを攻撃から保護する方法まで、アプリケーションのあらゆるレベルでセキュリティを 考える必要があります。このガイドでは、Goa APIに必要な重要なセキュリティプラクティスを、 今日すぐに実装できる実践的な例とともに説明します。
セキュリティは単一の強力な鍵を持つことではなく、複数の保護層を持つことです。 1つの層が破られても、他の層がアプリケーションを保護し続けます。以下は、Goaサービスで 複数のセキュリティ層を実装する方法です:
// 第1層:HTTPSを使用
var _ = Service("secure_service", func() {
Security(JWTAuth, func() { // 第2層:有効な認証を要求
Scope("api:write") // 第3層:特定の権限をチェック
})
// 第4層:すべての入力を検証
Method("secureEndpoint", func() {
Payload(func() {
Field(1, "data", String)
MaxLength("data", 1000) // 大きなペイロードを防止
})
})
})
このコードは、複数のセキュリティ制御を層状に重ねる方法を示しています。中世の城のように考えてください
レート制限については、Goa HTTPサーバーでミドルウェアを使用して実装する必要があります。 以下は、サービスにレート制限を追加する方法です:
package main
import (
"context"
"net/http"
"time"
"golang.org/x/time/rate"
goahttp "goa.design/goa/v3/http"
"goa.design/goa/v3/middleware"
)
// RateLimiterはリクエストレートを制限するミドルウェアを作成
func RateLimiter(limit rate.Limit, burst int) middleware.Middleware {
limiter := rate.NewLimiter(limit, burst)
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "リクエストが多すぎます", http.StatusTooManyRequests)
return
}
h.ServeHTTP(w, r)
})
}
}
func main() {
// ... ロガー、計装のセットアップ ...
// サービスとエンドポイントを作成
svc := NewService()
endpoints := gen.NewEndpoints(svc)
mux := goahttp.NewMuxer()
// サーバーを作成
server := gen.NewServer(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil)
// 生成されたハンドラーをマウント
gen.Mount(mux, server)
// サーバーハンドラーチェーンにミドルウェアを追加
var handler http.Handler = mux
handler = RateLimiter(rate.Every(time.Second/100), 10)(handler) // 100リクエスト/秒
handler = log.HTTP(ctx)(handler) // ログを追加
// HTTPサーバーを作成して起動
srv := &http.Server{
Addr: ":8080",
Handler: handler,
}
// ... グレースフルシャットダウンのコード ...
}
エンドポイントごとのレート制限については、特定のエンドポイントに直接レート制限を適用できます:
// RateLimitEndpointはエンドポイントをレート制限でラップ
func RateLimitEndpoint(limit rate.Limit, burst int) func(goa.Endpoint) goa.Endpoint {
limiter := rate.NewLimiter(limit, burst)
return func(endpoint goa.Endpoint) goa.Endpoint {
return func(ctx context.Context, req interface{}) (interface{}, error) {
if !limiter.Allow() {
return nil, fmt.Errorf("レート制限を超過しました")
}
return endpoint(ctx, req)
}
}
}
func main() {
// ... サービスのセットアップコード ...
// エンドポイントを作成
endpoints := &gen.Endpoints{
Forecast: RateLimitEndpoint(rate.Every(time.Second), 10)(
gen.NewForecastEndpoint(svc),
),
TestAll: gen.NewTestAllEndpoint(svc), // レート制限なし
TestSmoke: RateLimitEndpoint(rate.Every(time.Minute), 5)(
gen.NewTestSmokeEndpoint(svc),
),
}
// ... サーバーセットアップの残り ...
}
このアプローチは:
最も重要なセキュリティ原則の1つは、セキュアなデフォルト設定から始めることです。 すべてをロックダウンした状態から始めて選択的にアクセスを開放する方が、 オープンな状態から始めて後からロックダウンしようとするよりもはるかに安全です。 以下は、Goa APIでセキュアなデフォルト設定を行う方法です:
var _ = API("secure_api", func() {
// デフォルトで認証を要求
Security(JWTAuth)
})
これらの設定により、APIのすべてのエンドポイントがデフォルトで認証を必要とすることが 保証されます。トランスポートセキュリティ(HTTPS)については、実装でサーバーレベルで 設定します:
func main() {
// ... サービスとエンドポイントのセットアップ ...
// TLS設定を作成
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
// セキュアな設定でHTTPSサーバーを作成
srv := &http.Server{
Addr: ":443",
Handler: handler,
TLSConfig: tlsConfig,
// Slow-loris攻撃を防ぐためにタイムアウトを設定
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// TLSでサーバーを起動
log.Printf("HTTPSサーバーが%sでリッスンしています", srv.Addr)
if err := srv.ListenAndServeTLS("cert.pem", "key.pem"); err != nil {
log.Fatalf("HTTPSサーバーの起動に失敗しました: %v", err)
}
}
この実装は:
これをレート制限などの他のセキュリティミドルウェアと組み合わせることもできます。
権限に関しては、少ないほど良いです。すべてのユーザーとサービスは、仕事を行うために 必要な権限のみを持つべきです - それ以上でもそれ以下でもありません。これにより、 単一のアカウントが侵害された場合の潜在的な被害を制限します。以下は、APIで細かい 粒度の権限を実装する方法です:
var _ = Service("user_service", func() {
// 一般ユーザーは自分のプロファイルを読み取れる
Method("getProfile", func() {
Security(OAuth2Auth, func() {
Scope("profile:read")
})
// 実装により、ユーザーは自分のプロファイルのみを読み取れることを保証
Payload(func() {
UserID("id", String, "読み取るプロファイル")
})
})
// 書き込み権限を持つユーザーのみがプロファイルを更新可能
Method("updateProfile", func() {
Security(OAuth2Auth, func() {
Scope("profile:write")
})
})
// 管理操作には特別な権限が必要
Method("deleteUser", func() {
Security(OAuth2Auth, func() {
Scope("admin")
})
})
})
すべての入力は潜在的な攻撃ベクトルとして扱う必要があります。Goaは強力な入力検証機能を提供し、 これを使用してAPIを保護できます:
Method("createUser", func() {
Payload(func() {
Field(1, "username", String, "ユーザー名", func() {
Pattern(`^[a-zA-Z0-9]+$`) // 英数字のみ許可
MinLength(3) // 最小長
MaxLength(30) // 最大長
})
Field(2, "email", String, "メールアドレス", func() {
Format(FormatEmail) // メールアドレス形式を検証
MaxLength(100) // 最大長を制限
})
Field(3, "age", Int, "年齢", func() {
Minimum(0) // 負の値を防止
Maximum(150) // 合理的な上限
})
Required("username", "email")
})
})
機密データの処理には特別な注意が必要です:
// 機密データを含むペイロードを定義
var _ = Type("UserCredentials", func() {
Field(1, "password", String, "ユーザーパスワード", func() {
// パスワードの要件を強制
MinLength(8)
Pattern(`^.*[A-Z].*$`) // 大文字を要求
Pattern(`^.*[a-z].*$`) // 小文字を要求
Pattern(`^.*[0-9].*$`) // 数字を要求
Pattern(`^.*[^A-Za-z0-9].*$`) // 特殊文字を要求
// OpenAPIドキュメントでの機密性を示す
Meta("security:sensitive")
})
Field(2, "token", String, "セッショントークン", func() {
// トークンの形式を検証
Pattern(`^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$`)
})
})
// 機密データを処理するメソッド
Method("updatePassword", func() {
Payload(UserCredentials)
Result(String)
// セキュアなトランスポートを要求
Meta("security:transport", "https")
HTTP(func() {
POST("/password")
// レスポンスヘッダーでキャッシュを防止
Response(StatusOK, func() {
Header("Cache-Control", "no-store")
Header("Pragma", "no-cache")
})
})
})
セキュリティイベントの適切なロギングと、エラーの安全な処理は重要です:
// セキュリティイベントをログに記録するミドルウェア
func SecurityLogger(logger *log.Logger) middleware.Middleware {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// リクエストの詳細をログに記録(機密データは除外)
logger.Printf("セキュリティイベント: %s %s from %s",
r.Method,
r.URL.Path,
r.RemoteAddr,
)
// 認証の試行を追跡
if auth := r.Header.Get("Authorization"); auth != "" {
// トークンそのものは記録しない
logger.Printf("認証試行 from %s", r.RemoteAddr)
}
h.ServeHTTP(w, r)
})
}
}
// エラーハンドラー
func ErrorHandler(logger *log.Logger) middleware.ErrorHandler {
return func(ctx context.Context, err error) error {
// 内部エラーの詳細を記録
logger.Printf("エラー: %+v", err)
// クライアントに返す安全なエラー
switch err := err.(type) {
case *goa.ServiceError:
// Goaのサービスエラーはそのまま返す
return err
default:
// 内部エラーは一般的なメッセージに変換
return goa.Errorf(
"内部サーバーエラー",
"internal_error",
http.StatusInternalServerError,
)
}
}
}
適切なセキュリティヘッダーを設定することで、多くの一般的な攻撃を防ぐことができます:
// セキュリティヘッダーミドルウェア
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// クリックジャッキング防止
w.Header().Set("X-Frame-Options", "DENY")
// XSS保護を強制
w.Header().Set("X-XSS-Protection", "1; mode=block")
// MIMEタイプのスニッフィングを防止
w.Header().Set("X-Content-Type-Options", "nosniff")
// HTTPSを強制
w.Header().Set("Strict-Transport-Security",
"max-age=31536000; includeSubDomains")
// Content Security Policyを設定
w.Header().Set("Content-Security-Policy",
"default-src 'self'; frame-ancestors 'none';")
next.ServeHTTP(w, r)
})
}
func main() {
// ... その他のセットアップ ...
// セキュリティヘッダーを追加
handler = SecurityHeaders(handler)
srv := &http.Server{
Handler: handler,
// ... その他の設定 ...
}
}
APIを本番環境にデプロイする前に、以下のチェックリストを使用してください:
認証と認可
トランスポートセキュリティ
入力検証
エラー処理
レート制限とDDoS保護
監視とロギング
データ保護
セキュリティは継続的なプロセスです。新しい脆弱性は常に発見され、攻撃者は常に新しい 方法を見つけ出します。以下の原則に従うことで、APIのセキュリティを維持できます:
常に警戒を怠らない
深層防御を実践
最小権限の原則を遵守
監視とロギング