Skip to content

Commit

Permalink
gemalto: add support for Gemalto KeySecure
Browse files Browse the repository at this point in the history
This commit adds support for Gemalto KeySecure
as KMS.

KeySecure is a KMS implementation developed by Gemalto
(now Thales) and provides a KMIP as well as a REST API.
This commit adds a REST client that uses the KeySecure
secrets API (`/v1/vault/secrets`) to fetch and store
cryptographic keys.

***

KeySecure has multi-user support (username / password).
However, KeySecure also has the concept of refresh and
authentication tokens for programmatic access.

A refresh token is an opaque string, e.g.
`CEvk5cdHLG7si05LReIeDbXE3PKD082YdUFAnxX75md3jzV0BnyHyAmPPJiA0Kgn`,
that can be generated by a user to give an application programmatic
access. With the refresh token an application can request a short-lived
(5 min by default) authentication token. The authentication token
can then be used to perform operations - like creating a new secret.
Therefore, it has to be sent as part of the `Authorization` header.

With the refresh token the application can renew the authentication
token before it expires. KES supports only refresh tokens, not
username/password, since a refresh token is the recommended approach
to grant programmatic access. A refresh token can be easily revoked.

***

Apart from authentication, KeySecure provides a secret store that is
quite similar to e.g. Hashicorp Vault's K/V engine. Therefore, KES
uses the secrets API to store cryptographic keys.

***

This commit also adds a wrapper for an HTTP client that implements an
automatic retry mechanism. This is needed since KeySecure does not
provide a Go SDK and KES should be robust and tolerate temp. network
errors.
  • Loading branch information
Andreas Auernhammer authored and harshavardhana committed Jul 5, 2020
1 parent b0826ba commit 1b64284
Show file tree
Hide file tree
Showing 6 changed files with 842 additions and 3 deletions.
16 changes: 16 additions & 0 deletions cmd/kes/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,22 @@ type serverConfig struct {
} `yaml:"credentials"`
} `yaml:"secretsmanager"`
} `yaml:"aws"`

Gemalto struct {
KeySecure struct {
Endpoint string `yaml:"endpoint"`

Login struct {
Token string `yaml:"token"`
Domain string `yaml:"domain"`
Retry time.Duration `yaml:"retry"`
} `yaml:"credentials"`

TLS struct {
CAPath string `yaml:"ca"`
} `yaml:"tls"`
} `yaml:"keysecure"`
} `yaml:"gemalto"`
} `yaml:"keys"`
}

Expand Down
28 changes: 25 additions & 3 deletions cmd/kes/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/minio/kes/internal/auth"
"github.com/minio/kes/internal/aws"
"github.com/minio/kes/internal/fs"
"github.com/minio/kes/internal/gemalto"
xhttp "github.com/minio/kes/internal/http"
xlog "github.com/minio/kes/internal/log"
"github.com/minio/kes/internal/mem"
Expand Down Expand Up @@ -118,11 +119,17 @@ func server(args []string) error {

switch {
case config.Keys.Fs.Path != "" && config.Keys.Vault.Endpoint != "":
return errors.New("Ambiguous configuration: FS and Vault key store are specified at the same time")
return errors.New("Ambiguous configuration: FS and Hashicorp Vault endpoint specified at the same time")
case config.Keys.Fs.Path != "" && config.Keys.Aws.SecretsManager.Endpoint != "":
return errors.New("Ambiguous configuration: FS and AWS Secrets Manager key store are specified at the same time")
return errors.New("Ambiguous configuration: FS and AWS Secrets Manager endpoint are specified at the same time")
case config.Keys.Fs.Path != "" && config.Keys.Gemalto.KeySecure.Endpoint != "":
return errors.New("Ambiguous configuration: FS and Gemalto KeySecure endpoint are specified at the same time")
case config.Keys.Vault.Endpoint != "" && config.Keys.Aws.SecretsManager.Endpoint != "":
return errors.New("Ambiguous configuration: Vault and AWS SecretsManager key store are specified at the same time")
return errors.New("Ambiguous configuration: Hashicorp Vault and AWS SecretsManager endpoint are specified at the same time")
case config.Keys.Vault.Endpoint != "" && config.Keys.Gemalto.KeySecure.Endpoint != "":
return errors.New("Ambiguous configuration: Hashicorp Vault and Gemalto KeySecure endpoint are specified at the same time")
case config.Keys.Aws.SecretsManager.Endpoint != "" && config.Keys.Gemalto.KeySecure.Endpoint != "":
return errors.New("Ambiguous configuration: AWS SecretsManager and Gemalto KeySecure endpoint are specified at the same time")
}

if mlock {
Expand Down Expand Up @@ -215,6 +222,21 @@ func server(args []string) error {
return fmt.Errorf("Failed to connect to AWS Secrets Manager: %v", err)
}
store.Remote = awsStore
case config.Keys.Gemalto.KeySecure.Endpoint != "":
gemaltoStore := &gemalto.KeySecure{
Endpoint: config.Keys.Gemalto.KeySecure.Endpoint,
CAPath: config.Keys.Gemalto.KeySecure.TLS.CAPath,
ErrorLog: errorLog.Log(),
Login: gemalto.Credentials{
Token: config.Keys.Gemalto.KeySecure.Login.Token,
Domain: config.Keys.Gemalto.KeySecure.Login.Domain,
Retry: config.Keys.Gemalto.KeySecure.Login.Retry,
},
}
if err := gemaltoStore.Authenticate(); err != nil {
return fmt.Errorf("Failed to connect to Gemalto KeySecure: %v", err)
}
store.Remote = gemaltoStore
default:
store.Remote = &mem.Store{}
}
Expand Down
173 changes: 173 additions & 0 deletions internal/gemalto/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright 2020 - MinIO, Inc. All rights reserved.
// Use of this source code is governed by the AGPLv3
// license that can be found in the LICENSE file.

package gemalto

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"sync"
"time"

xhttp "github.com/minio/kes/internal/http"
)

// authToken is a KeySecure authentication token.
// It can be used to authenticate API requests.
type authToken struct {
Type string
Value string
Expiry time.Duration
}

// String returns the string representation of
// the authentication token.
func (t *authToken) String() string { return fmt.Sprintf("%s %s", t.Type, t.Value) }

// client is a KeySecure REST API client
// responsible for fetching and renewing
// authentication tokens.
type client struct {
xhttp.Retry
ErrorLog *log.Logger

lock sync.Mutex
token authToken
}

// Authenticate tries to obtain a new authentication token
// from the given KeySecure endpoint via the given refresh
// token.
//
// Athenticate should be called to obtain the first authentication
// token. This token can then be renewed via RenewAuthToken.
func (c *client) Authenticate(endpoint string, login Credentials) error {
type Request struct {
Type string `json:"grant_type"`
Token string `json:"refresh_token"`
Domain string `json:"domain"`
}
type Response struct {
Type string `json:"token_type"`
Token string `json:"jwt"`
Expiry uint64 `json:"duration"` // KeySecure returns expiry in seconds
}

body, err := json.Marshal(Request{
Type: "refresh_token",
Token: login.Token,
Domain: login.Domain,
})
if err != nil {
return err
}

url := fmt.Sprintf("%s/api/v1/auth/tokens", endpoint)
req, err := http.NewRequest(http.MethodPost, url, xhttp.RetryReader(bytes.NewReader(body)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")

resp, err := c.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
response, err := parseServerError(resp)
if err != nil {
return fmt.Errorf("%s: %v", resp.Status, err)
}
return fmt.Errorf("%s: %s (%d)", resp.Status, response.Message, response.Code)
}

const MaxSize = 1 << 20 // An auth. token response should not exceed 1 MiB
var response Response
if err = json.NewDecoder(io.LimitReader(resp.Body, MaxSize)).Decode(&response); err != nil {
return err
}
if response.Token == "" {
return errors.New("server response does not contain an auth token")
}
if response.Type != "Bearer" {
return fmt.Errorf("unexpected auth token type '%s'", response.Type)
}
if response.Expiry <= 0 {
return fmt.Errorf("invalid auth token expiry '%d'", response.Expiry)
}

c.lock.Lock()
c.token = authToken{
Type: response.Type,
Value: response.Token,
Expiry: time.Duration(response.Expiry) * time.Second,
}
c.lock.Unlock()
return nil
}

// RenewAuthToken tries to renew the client's authentication
// token before it expires. It blocks until <-ctx.Done() completes.
//
// Before calling RenewAuthToken the client should already have a
// authentication token. Therefore, RenewAuthToken should be called
// only after a Authenticate.
//
// RenewAuthToken tries get a new authentication token from the given
// KeySecure endpoint by presenting the given refresh token.
// It continuesly tries to renew the authentication before it expires.
//
// If RenewAuthToken fails to request or renew the client's authentication
// token then it keeps retrying and waits for the given login.Retry delay
// between each retry attempt.
//
// If login.Retry is 0 then RenewAuthToken uses a reasonable default retry delay.
func (c *client) RenewAuthToken(ctx context.Context, endpoint string, login Credentials) {
if login.Retry == 0 {
login.Retry = 5 * time.Second
}
var (
timer *time.Timer
err error
)
for {
if err != nil {
logf(c.ErrorLog, "gemalto: failed to renew auth token: %v", err)
timer = time.NewTimer(login.Retry)
} else {
c.lock.Lock()
timer = time.NewTimer(c.token.Expiry / 2)
c.lock.Unlock()
}

select {
case <-ctx.Done():
timer.Stop()
return
case <-timer.C:
err = c.Authenticate(endpoint, login)
timer.Stop()
}
}
}

// AuthToken returns an authentication token that can be
// used to authenticate API requests to a KeySecure instance.
//
// Typically, it is a JWT token and should be used as HTTP
// Authorization header value.
func (c *client) AuthToken() string {
c.lock.Lock()
defer c.lock.Unlock()

return c.token.String()
}

0 comments on commit 1b64284

Please sign in to comment.