From 810a44e951c11148428bf01ec03f8863a97f2480 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Wed, 4 Sep 2019 22:19:44 +0200 Subject: [PATCH] KMS Admin-API: add route and handler for KMS key info (#7955) This commit adds an admin API route and handler for requesting status information about a KMS key. Therefore, the client specifies the KMS key ID (when empty / not set the server takes the currently configured default key-ID) and the server tries to perform a dummy encryption, re-wrap and decryption operation. If all three succeed we know that the server can access the KMS and has permissions to generate, re-wrap and decrypt data keys (policy is set correctly). --- cmd/admin-handlers.go | 84 +++++++++++++++++++++++++++++++ cmd/admin-router.go | 5 ++ pkg/madmin/README.md | 42 +++++++++++++--- pkg/madmin/examples/kms-status.go | 60 ++++++++++++++++++++++ pkg/madmin/kms-commands.go | 62 +++++++++++++++++++++++ 5 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 pkg/madmin/examples/kms-status.go create mode 100644 pkg/madmin/kms-commands.go diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index afde2d4fa..7107cdf67 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -19,6 +19,7 @@ package cmd import ( "bytes" "context" + "crypto/subtle" "encoding/base64" "encoding/json" "errors" @@ -40,6 +41,7 @@ import ( "github.com/tidwall/gjson" "github.com/tidwall/sjson" + "github.com/minio/minio/cmd/crypto" xhttp "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/cpu" @@ -1882,3 +1884,85 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque } } } + +// KMSKeyStatusHandler - GET /minio/admin/v1/kms/key/status?key-id= +func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "KMSKeyStatusHandler") + + objectAPI := validateAdminReq(ctx, w, r) + if objectAPI == nil { + return + } + + if GlobalKMS == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL) + return + } + + keyID := r.URL.Query().Get("key-id") + if keyID == "" { + keyID = globalKMSKeyID + } + var response = madmin.KMSKeyStatus{ + KeyID: keyID, + } + + kmsContext := crypto.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation + // 1. Generate a new key using the KMS. + key, sealedKey, err := GlobalKMS.GenerateKey(keyID, kmsContext) + if err != nil { + response.EncryptionErr = err.Error() + resp, err := json.Marshal(response) + if err != nil { + writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL) + return + } + writeSuccessResponseJSON(w, resp) + return + } + + // 2. Check whether we can update / re-wrap the sealed key. + sealedKey, err = GlobalKMS.UpdateKey(keyID, sealedKey, kmsContext) + if err != nil { + response.UpdateErr = err.Error() + resp, err := json.Marshal(response) + if err != nil { + writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL) + return + } + writeSuccessResponseJSON(w, resp) + return + } + + // 3. Verify that we can indeed decrypt the (encrypted) key + decryptedKey, err := GlobalKMS.UnsealKey(keyID, sealedKey, kmsContext) + if err != nil { + response.DecryptionErr = err.Error() + resp, err := json.Marshal(response) + if err != nil { + writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL) + return + } + writeSuccessResponseJSON(w, resp) + return + } + + // 4. Compare generated key with decrypted key + if subtle.ConstantTimeCompare(key[:], decryptedKey[:]) != 1 { + response.DecryptionErr = "The generated and the decrypted data key do not match" + resp, err := json.Marshal(response) + if err != nil { + writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL) + return + } + writeSuccessResponseJSON(w, resp) + return + } + + resp, err := json.Marshal(response) + if err != nil { + writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL) + return + } + writeSuccessResponseJSON(w, resp) +} diff --git a/cmd/admin-router.go b/cmd/admin-router.go index 71b9b6f01..3f04f52c4 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -135,9 +135,14 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) // HTTP Trace adminV1Router.Methods(http.MethodGet).Path("/trace").HandlerFunc(adminAPI.TraceHandler) + // Console Logs adminV1Router.Methods(http.MethodGet).Path("/log").HandlerFunc(httpTraceAll(adminAPI.ConsoleLogHandler)) + // -- KMS APIs -- + // + adminV1Router.Methods(http.MethodGet).Path("/kms/key/status").HandlerFunc(httpTraceAll(adminAPI.KMSKeyStatusHandler)) + // If none of the routes match, return error. adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandlerJSON)) } diff --git a/pkg/madmin/README.md b/pkg/madmin/README.md index 79879efa3..d1a38e7c3 100644 --- a/pkg/madmin/README.md +++ b/pkg/madmin/README.md @@ -42,14 +42,13 @@ func main() { } ``` - -| Service operations | Info operations | Healing operations | Config operations | Top operations | IAM operations | Misc | -|:------------------------------------|:------------------------------------------------|:-------------------|:----------------------------------|:------------------------|:--------------------------------------|:--------------------------------------------------| -| [`ServiceRestart`](#ServiceRestart) | [`ServerInfo`](#ServerInfo) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) | [`TopLocks`](#TopLocks) | [`AddUser`](#AddUser) | | -| [`ServiceStop`](#ServiceStop) | [`ServerCPULoadInfo`](#ServerCPULoadInfo) | | [`SetConfig`](#SetConfig) | | [`SetUserPolicy`](#SetUserPolicy) | [`StartProfiling`](#StartProfiling) | -| | [`ServerMemUsageInfo`](#ServerMemUsageInfo) | | [`GetConfigKeys`](#GetConfigKeys) | | [`ListUsers`](#ListUsers) | [`DownloadProfilingData`](#DownloadProfilingData) | -| [`ServiceTrace`](#ServiceTrace) | [`ServerDrivesPerfInfo`](#ServerDrivesPerfInfo) | | [`SetConfigKeys`](#SetConfigKeys) | | [`AddCannedPolicy`](#AddCannedPolicy) | [`ServerUpdate`](#ServerUpdate) | -| | [`NetPerfInfo`](#NetPerfInfo) | | | | | | +| Service operations | Info operations | Healing operations | Config operations | Top operations | IAM operations | Misc | KMS | +|:------------------------------------|:------------------------------------------------|:-------------------|:----------------------------------|:------------------------|:--------------------------------------|:--------------------------------------------------|:----------------------------------| +| [`ServiceRestart`](#ServiceRestart) | [`ServerInfo`](#ServerInfo) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) | [`TopLocks`](#TopLocks) | [`AddUser`](#AddUser) | | [`GetKeyStatus`](#GetKeyStatus) | +| [`ServiceStop`](#ServiceStop) | [`ServerCPULoadInfo`](#ServerCPULoadInfo) | | [`SetConfig`](#SetConfig) | | [`SetUserPolicy`](#SetUserPolicy) | [`StartProfiling`](#StartProfiling) | | +| | [`ServerMemUsageInfo`](#ServerMemUsageInfo) | | [`GetConfigKeys`](#GetConfigKeys) | | [`ListUsers`](#ListUsers) | [`DownloadProfilingData`](#DownloadProfilingData) | | +| [`ServiceTrace`](#ServiceTrace) | [`ServerDrivesPerfInfo`](#ServerDrivesPerfInfo) | | [`SetConfigKeys`](#SetConfigKeys) | | [`AddCannedPolicy`](#AddCannedPolicy) | [`ServerUpdate`](#ServerUpdate) | | +| | [`NetPerfInfo`](#NetPerfInfo) | | | | | | | ## 1. Constructor @@ -580,3 +579,30 @@ __Example__ log.Println("Profiling data successfully downloaded.") ``` + +## 11. KMS + + +### GetKeyStatus(keyID string) (*KMSKeyStatus, error) +Requests status information about one particular KMS master key +from a MinIO server. The keyID is optional and the server will +use the default master key (configured via `MINIO_SSE_VAULT_KEY_NAME` +or `MINIO_SSE_MASTER_KEY`) if the keyID is empty. + +__Example__ + +``` go + keyInfo, err := madmClnt.GetKeyStatus("my-minio-key") + if err != nil { + log.Fatalln(err) + } + if keyInfo.EncryptionErr != "" { + log.Fatalf("Failed to perform encryption operation using '%s': %v\n", keyInfo.KeyID, keyInfo.EncryptionErr) + } + if keyInfo.UpdateErr != "" { + log.Fatalf("Failed to perform key re-wrap operation using '%s': %v\n", keyInfo.KeyID, keyInfo.UpdateErr) + } + if keyInfo.DecryptionErr != "" { + log.Fatalf("Failed to perform decryption operation using '%s': %v\n", keyInfo.KeyID, keyInfo.DecryptionErr) + } +``` diff --git a/pkg/madmin/examples/kms-status.go b/pkg/madmin/examples/kms-status.go new file mode 100644 index 000000000..136558fe6 --- /dev/null +++ b/pkg/madmin/examples/kms-status.go @@ -0,0 +1,60 @@ +// +build ignore + +/* + * MinIO Cloud Storage, (C) 2019 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "log" + + "github.com/minio/minio/pkg/madmin" +) + +func main() { + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are + // dummy values, please replace them with original values. + + // API requests are secure (HTTPS) if secure=true and insecure (HTTP) otherwise. + // New returns an MinIO Admin client object. + madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) + if err != nil { + log.Fatalln(err) + } + + status, err := madmClnt.GetKeyStatus("") // empty string refers to the default master key + if err != nil { + log.Fatalln(err) + } + + log.Printf("Key: %s\n", status.KeyID) + if status.EncryptionErr == "" { + log.Println("\t • Encryption ✔") + } else { + log.Printf("\t • Encryption failed: %s\n", status.EncryptionErr) + } + if status.UpdateErr == "" { + log.Println("\t • Re-wrap ✔") + } else { + log.Printf("\t • Re-wrap failed: %s\n", status.UpdateErr) + } + if status.DecryptionErr == "" { + log.Println("\t • Decryption ✔") + } else { + log.Printf("\t • Decryption failed: %s\n", status.DecryptionErr) + } +} diff --git a/pkg/madmin/kms-commands.go b/pkg/madmin/kms-commands.go new file mode 100644 index 000000000..e4a6766c4 --- /dev/null +++ b/pkg/madmin/kms-commands.go @@ -0,0 +1,62 @@ +/* + * MinIO Cloud Storage, (C) 2019 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package madmin + +import ( + "encoding/json" + "net/http" + "net/url" +) + +// GetKeyStatus requests status information about the key referenced by keyID +// from the KMS connected to a MinIO by performing a Admin-API request. +// It basically hits the `/minio/admin/v1/kms/key/status` API endpoint. +func (adm *AdminClient) GetKeyStatus(keyID string) (*KMSKeyStatus, error) { + // GET /minio/admin/v1/kms/key/status?key-id= + qv := url.Values{} + qv.Set("key-id", keyID) + reqData := requestData{ + relPath: "/v1/kms/key/status", + queryValues: qv, + } + + resp, err := adm.executeMethod("GET", reqData) + if err != nil { + return nil, err + } + defer closeResponse(resp) + if resp.StatusCode != http.StatusOK { + return nil, httpRespToErrorResponse(resp) + } + var keyInfo KMSKeyStatus + if err = json.NewDecoder(resp.Body).Decode(&keyInfo); err != nil { + return nil, err + } + return &keyInfo, nil +} + +// KMSKeyStatus contains some status information about a KMS master key. +// The MinIO server tries to access the KMS and perform encryption and +// decryption operations. If the MinIO server can access the KMS and +// all master key operations succeed it returns a status containing only +// the master key ID but no error. +type KMSKeyStatus struct { + KeyID string `json:"key-id"` + EncryptionErr string `json:"encryption-error,omitempty"` // An empty error == success + UpdateErr string `json:"update-error,omitempty"` // An empty error == success + DecryptionErr string `json:"decryption-error,omitempty"` // An empty error == success +}