mirror of
https://github.com/minio/minio.git
synced 2024-12-24 22:25:54 -05:00
kes: try to auto. create master key if not present (#9790)
This commit changes the data key generation such that if a MinIO server/nodes tries to generate a new DEK but the particular master key does not exist - then MinIO asks KES to create a new master key and then requests the DEK again. From now on, a SSE-S3 master key must not be created explicitly via: `kes key create <key-name>`. Instead, it is sufficient to just set the env. var. ``` export MINIO_KMS_KES_KEY_NAME=<key-name> ``` However, the MinIO identity (mTLS client certificate) must have the permission to access the `/v1/key/create/` API. Therefore, KES policy for MinIO must look similar to: ``` [ /v1/key/create/<key-name-pattern> /v1/key/generate/<key-name-pattern> /v1/key/decrypt/<key-name-pattern> ] ``` However, in our guides we already suggest that. See e.g.: https://github.com/minio/kes/wiki/MinIO-Object-Storage#kes-server-setup *** The ability to create master keys on request may also be necessary / useful in case of SSE-KMS.
This commit is contained in:
parent
62b1da3e2c
commit
b1845c6c83
@ -34,6 +34,10 @@ import (
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
// ErrKESKeyNotFound is the error returned a KES server
|
||||
// when a master key does not exist.
|
||||
var ErrKESKeyNotFound = NewKESError(http.StatusNotFound, "key does not exist")
|
||||
|
||||
// KesConfig contains the configuration required
|
||||
// to initialize and connect to a kes server.
|
||||
type KesConfig struct {
|
||||
@ -154,6 +158,12 @@ func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sea
|
||||
|
||||
var plainKey []byte
|
||||
plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context.Bytes())
|
||||
if err == ErrKESKeyNotFound { // Try to create the key if it does not exist.
|
||||
if err = kes.client.CreateKey(keyID); err != nil {
|
||||
return key, nil, err
|
||||
}
|
||||
plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context.Bytes())
|
||||
}
|
||||
if err != nil {
|
||||
return key, nil, err
|
||||
}
|
||||
@ -214,16 +224,19 @@ type kesClient struct {
|
||||
httpClient http.Client
|
||||
}
|
||||
|
||||
// Response KES response struct
|
||||
type response struct {
|
||||
Plaintext []byte `json:"plaintext"`
|
||||
Ciphertext []byte `json:"ciphertext,omitempty"`
|
||||
}
|
||||
|
||||
// Request KES request struct
|
||||
type request struct {
|
||||
Ciphertext []byte `json:"ciphertext,omitempty"`
|
||||
Context []byte `json:"context"`
|
||||
// CreateKey tries to create a new cryptographic key with
|
||||
// the specified name.
|
||||
//
|
||||
// The key will be generated by the server. The client
|
||||
// application does not have the cryptographic key at
|
||||
// any point in time.
|
||||
func (c *kesClient) CreateKey(name string) error {
|
||||
url := fmt.Sprintf("%s/v1/key/create/%s", c.addr, url.PathEscape(name))
|
||||
_, err := c.postRetry(url, nil, 0) // No request body and no response expected
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateDataKey requests a new data key from the KES server.
|
||||
@ -235,48 +248,155 @@ type request struct {
|
||||
// such that you have to provide the same context when decrypting
|
||||
// the data key.
|
||||
func (c *kesClient) GenerateDataKey(name string, context []byte) ([]byte, []byte, error) {
|
||||
body, err := json.Marshal(request{
|
||||
type Request struct {
|
||||
Context []byte `json:"context"`
|
||||
}
|
||||
type Response struct {
|
||||
Plaintext []byte `json:"plaintext"`
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
}
|
||||
|
||||
body, err := json.Marshal(Request{
|
||||
Context: context,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/key/generate/%s", c.addr, url.PathEscape(name))
|
||||
|
||||
const limit = 1 << 20 // A plaintext/ciphertext key pair will never be larger than 1 MB
|
||||
url := fmt.Sprintf("%s/v1/key/generate/%s", c.addr, url.PathEscape(name))
|
||||
resp, err := c.postRetry(url, bytes.NewReader(body), limit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return resp.Plaintext, resp.Ciphertext, nil
|
||||
var response Response
|
||||
if err = json.NewDecoder(resp).Decode(&response); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return response.Plaintext, response.Ciphertext, nil
|
||||
}
|
||||
|
||||
func (c *kesClient) post(url string, body io.Reader, limit int64) (*response, error) {
|
||||
resp, err := c.httpClient.Post(url, "application/json", body)
|
||||
// GenerateDataKey decrypts an encrypted data key with the key
|
||||
// specified by name by talking to the KES server.
|
||||
// On success, the KES server will respond with the plaintext key.
|
||||
//
|
||||
// The optional context must match the value you provided when
|
||||
// generating the data key.
|
||||
func (c *kesClient) DecryptDataKey(name string, ciphertext, context []byte) ([]byte, error) {
|
||||
type Request struct {
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
Context []byte `json:"context,omitempty"`
|
||||
}
|
||||
type Response struct {
|
||||
Plaintext []byte `json:"plaintext"`
|
||||
}
|
||||
|
||||
body, err := json.Marshal(Request{
|
||||
Ciphertext: ciphertext,
|
||||
Context: context,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const limit = 1 << 20 // A data key will never be larger than 1 MiB
|
||||
url := fmt.Sprintf("%s/v1/key/decrypt/%s", c.addr, url.PathEscape(name))
|
||||
resp, err := c.postRetry(url, bytes.NewReader(body), limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response Response
|
||||
if err = json.NewDecoder(resp).Decode(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response.Plaintext, nil
|
||||
}
|
||||
|
||||
// NewKESError returns a new KES API error with the given
|
||||
// HTTP status code and error message.
|
||||
//
|
||||
// Two errors with the same status code and
|
||||
// error message are equal:
|
||||
// e1 == e2 // true.
|
||||
func NewKESError(code int, text string) error {
|
||||
return kesError{
|
||||
code: code,
|
||||
message: text,
|
||||
}
|
||||
}
|
||||
|
||||
type kesError struct {
|
||||
code int
|
||||
message string
|
||||
}
|
||||
|
||||
// Status returns the HTTP status code of the error.
|
||||
func (e kesError) Status() int { return e.code }
|
||||
|
||||
// Status returns the error message of the error.
|
||||
func (e kesError) Error() string { return e.message }
|
||||
|
||||
func parseErrorResponse(resp *http.Response) error {
|
||||
if resp == nil || resp.StatusCode < 400 {
|
||||
return nil
|
||||
}
|
||||
if resp.Body == nil {
|
||||
return NewKESError(resp.StatusCode, "")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
const MaxBodySize = 1 << 20
|
||||
var size = resp.ContentLength
|
||||
if size < 0 || size > MaxBodySize {
|
||||
size = MaxBodySize
|
||||
}
|
||||
|
||||
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
var response Response
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, size)).Decode(&response); err != nil {
|
||||
return err
|
||||
}
|
||||
return NewKESError(resp.StatusCode, response.Message)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
if _, err := io.Copy(&sb, io.LimitReader(resp.Body, size)); err != nil {
|
||||
return err
|
||||
}
|
||||
return NewKESError(resp.StatusCode, sb.String())
|
||||
}
|
||||
|
||||
func (c *kesClient) post(url string, body io.Reader, limit int64) (io.Reader, error) {
|
||||
resp, err := c.httpClient.Post(url, "application/json", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Drain the entire body to make sure we have re-use connections
|
||||
defer xhttp.DrainBody(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, c.parseErrorResponse(resp)
|
||||
return nil, parseErrorResponse(resp)
|
||||
}
|
||||
|
||||
response := &response{}
|
||||
if err = json.NewDecoder(io.LimitReader(resp.Body, limit)).Decode(response); err != nil {
|
||||
// We have to copy the response body due to draining.
|
||||
var respBody bytes.Buffer
|
||||
if _, err = io.Copy(&respBody, io.LimitReader(resp.Body, limit)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
return &respBody, nil
|
||||
}
|
||||
|
||||
func (c *kesClient) postRetry(url string, body io.ReadSeeker, limit int64) (*response, error) {
|
||||
func (c *kesClient) postRetry(url string, body io.ReadSeeker, limit int64) (io.Reader, error) {
|
||||
for i := 0; ; i++ {
|
||||
body.Seek(0, io.SeekStart) // seek to the beginning of the body.
|
||||
|
||||
if body != nil {
|
||||
body.Seek(0, io.SeekStart) // seek to the beginning of the body.
|
||||
}
|
||||
response, err := c.post(url, body, limit)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
@ -296,45 +416,6 @@ func (c *kesClient) postRetry(url string, body io.ReadSeeker, limit int64) (*res
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateDataKey decrypts an encrypted data key with the key
|
||||
// specified by name by talking to the KES server.
|
||||
// On success, the KES server will respond with the plaintext key.
|
||||
//
|
||||
// The optional context must match the value you provided when
|
||||
// generating the data key.
|
||||
func (c *kesClient) DecryptDataKey(name string, ciphertext, context []byte) ([]byte, error) {
|
||||
body, err := json.Marshal(request{
|
||||
Ciphertext: ciphertext,
|
||||
Context: context,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/key/decrypt/%s", c.addr, url.PathEscape(name))
|
||||
|
||||
const limit = 32 * 1024 // A data key will never be larger than 32 KB
|
||||
resp, err := c.postRetry(url, bytes.NewReader(body), limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Plaintext, nil
|
||||
}
|
||||
|
||||
func (c *kesClient) parseErrorResponse(resp *http.Response) error {
|
||||
if resp.Body == nil {
|
||||
return Errorf("%s: no body", http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
const limit = 32 * 1024 // A (valid) error response will not be greater than 32 KB
|
||||
var errMsg strings.Builder
|
||||
if _, err := io.Copy(&errMsg, io.LimitReader(resp.Body, limit)); err != nil {
|
||||
return Errorf("%s: %s", http.StatusText(resp.StatusCode), err)
|
||||
}
|
||||
|
||||
return Errorf("%s: %s", http.StatusText(resp.StatusCode), errMsg.String())
|
||||
}
|
||||
|
||||
// loadCACertificates returns a new CertPool
|
||||
// that contains all system root CA certificates
|
||||
// and any PEM-encoded certificate(s) found at
|
||||
|
Loading…
Reference in New Issue
Block a user