Skip to content

Commit

Permalink
vault: add support for kubernetes authentication method
Browse files Browse the repository at this point in the history
This commit adds support for the Vault kubernetes authentication
method: https://www.vaultproject.io/docs/auth/kubernetes

Now, the KES configuration can contain the following K8S auth section:
```
kubernetes:
  engine: ""
  role:   ""
  jwt:    ""
  retry:  15s
```

The `engine` allows the customization of the auth path configured
at Vault. It defaults to the Vault default: `kubernetes`.

The `retry` is the delay KES waits before trying to re-authenticate
after connection loss.

The `role` and `jwt` are the credentials received from K8S.

Currently, KES does not support both auth methods (`approle` | `kubernetes`)
at the same time. So, a operator must not set AppRole and Kubernetes
credentials.
  • Loading branch information
Andreas Auernhammer authored and harshavardhana committed Dec 7, 2020
1 parent 63074f4 commit d85a727
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 35 deletions.
53 changes: 49 additions & 4 deletions cmd/kes/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"errors"
"fmt"
"io/ioutil"
stdlog "log"
"os"
"path/filepath"
Expand Down Expand Up @@ -75,6 +76,9 @@ func loadServerConfig(path string) (config serverConfig, err error) {
file.Close()
return config, err
}
if err = file.Close(); err != nil {
return config, err
}

// Replace identities that refer to env. variables with the
// corresponding env. variable values.
Expand All @@ -97,7 +101,26 @@ func loadServerConfig(path string) (config serverConfig, err error) {
}
}
}
return config, file.Close()

// We handle the Hashicorp Vault Kubernetes JWT specially
// since it can either be specified directly or be mounted
// as a file (K8S secret).
// Therefore, we check whether the JWT field is a file, and if so,
// read the JWT from there.
if config.Keys.Vault.Kubernetes.JWT != "" {
f, err := os.Open(config.Keys.Vault.Kubernetes.JWT)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return config, fmt.Errorf("failed to open Vault Kubernetes JWT: %v", err)
}
if err == nil {
jwt, err := ioutil.ReadAll(f)
if err != nil {
return config, fmt.Errorf("failed to read Vault Kubernetes JWT: %v", err)
}
config.Keys.Vault.Kubernetes.JWT = string(jwt)
}
}
return config, nil
}

// SetDefaults set default values for fields that may be empty b/c not specified by user.
Expand Down Expand Up @@ -158,6 +181,13 @@ type kmsServerConfig struct {
Retry time.Duration `yaml:"retry"`
} `yaml:"approle"`

Kubernetes struct {
EnginePath string `yaml:"engine"`
Role string `yaml:"role"`
JWT string `yaml:"jwt"` // Can be either a JWT or a path to a file containing a JWT
Retry time.Duration `yaml:"retry"`
} `yaml:"kubernetes"`

TLS struct {
KeyPath string `yaml:"key"`
CertPath string `yaml:"cert"`
Expand Down Expand Up @@ -219,7 +249,10 @@ func (config *kmsServerConfig) SetDefaults() {
config.Vault.EnginePath = "kv" // If not set, use the Vault default engine path.
}
if config.Vault.AppRole.EnginePath == "" {
config.Vault.AppRole.EnginePath = "approle" // If not set, use the Vault default auth path.
config.Vault.AppRole.EnginePath = "approle" // If not set, use the Vault default auth path for AppRole.
}
if config.Vault.Kubernetes.EnginePath == "" {
config.Vault.Kubernetes.EnginePath = "kubernetes" // If not set, use the Vault default auth path for Kubernetes.
}
if config.GCP.SecretManager.ProjectID != "" && config.GCP.SecretManager.Endpoint == "" {
config.GCP.SecretManager.Endpoint = "secretmanager.googleapis.com:443"
Expand Down Expand Up @@ -250,9 +283,15 @@ func (config *kmsServerConfig) Verify() error {
return errors.New("ambiguous configuration: AWS SecretsManager and GCP secret manager are specified at the same time")
case config.Gemalto.KeySecure.Endpoint != "" && config.GCP.SecretManager.ProjectID != "":
return errors.New("ambiguous configuration: Gemalto KeySecure endpoint and GCP secret manager are specified at the same time")
default:
return nil
}

if config.Vault.Endpoint != "" {
approle, k8s := config.Vault.AppRole, config.Vault.Kubernetes
if (approle.ID != "" || approle.Secret != "") && (k8s.Role != "" || k8s.JWT != "") {
return errors.New("invalid configuration: Vault AppRole and Kubernetes credentials are specified at the same time")
}
}
return nil
}

// Connect tries to establish a connection to the KMS specified in the kmsServerConfig.
Expand Down Expand Up @@ -295,6 +334,12 @@ func (config *kmsServerConfig) Connect(quiet quiet, errorLog *stdlog.Logger) (*s
Secret: config.Vault.AppRole.Secret,
Retry: config.Vault.AppRole.Retry,
},
K8S: vault.Kubernetes{
Engine: config.Vault.Kubernetes.EnginePath,
Role: config.Vault.Kubernetes.Role,
JWT: config.Vault.Kubernetes.JWT,
Retry: config.Vault.Kubernetes.Retry,
},
StatusPingAfter: config.Vault.Status.Ping,
ErrorLog: errorLog,
ClientKeyPath: config.Vault.TLS.KeyPath,
Expand Down
93 changes: 66 additions & 27 deletions internal/vault/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,35 +75,74 @@ func (c *client) CheckStatus(ctx context.Context, delay time.Duration) {
// from the vault server by using the login AppRole credentials.
//
// To renew the auth. token see: client.RenewToken(...).
func (c *client) Authenticate(login AppRole) (token string, ttl time.Duration, err error) {
location := path.Join("auth", login.Engine, "login") // /auth/<engine>/login
secret, err := c.Logical().Write(location, map[string]interface{}{
"role_id": login.ID,
"secret_id": login.Secret,
})
if err != nil || secret == nil {
// The Vault SDK eventually returns no error but also no
// secret. In this case have to return a (not very helpful)
// error to signal that the authentication failed - for some
// (unknown) reason.
if err == nil {
err = errors.New("vault: authentication failed: SDK returned no error but also no token")
func (c *client) AuthenticateWithAppRole(login AppRole) authFunc {
return func() (token string, ttl time.Duration, err error) {
location := path.Join("auth", login.Engine, "login") // /auth/<engine>/login
secret, err := c.Logical().Write(location, map[string]interface{}{
"role_id": login.ID,
"secret_id": login.Secret,
})
if err != nil || secret == nil {
// The Vault SDK eventually returns no error but also no
// secret. In this case have to return a (not very helpful)
// error to signal that the authentication failed - for some
// (unknown) reason.
if err == nil {
err = errors.New("vault: authentication failed: SDK returned no error but also no token")
}
return token, ttl, err
}
return token, ttl, err
}

token, err = secret.TokenID()
if err != nil {
return token, ttl, err
token, err = secret.TokenID()
if err != nil {
return token, ttl, err
}

ttl, err = secret.TokenTTL()
if err != nil {
return token, ttl, err
}
return token, ttl, nil
}
}

ttl, err = secret.TokenTTL()
if err != nil {
return token, ttl, err
func (c *client) AuthenticateWithK8S(login Kubernetes) authFunc {
return func() (token string, ttl time.Duration, err error) {
location := path.Join("auth", login.Engine, "login")
secret, err := c.Logical().Write(location, map[string]interface{}{
"role": login.Role,
"jwt": login.JWT,
})
if err != nil || secret == nil {
// The Vault SDK eventually returns no error but also no
// secret. In this case have to return a (not very helpful)
// error to signal that the authentication failed - for some
// (unknown) reason.
if err == nil {
err = errors.New("vault: authentication failed: SDK returned no error but also no token")
}
return token, ttl, err
}
token, err = secret.TokenID()
if err != nil {
return token, ttl, err
}

ttl, err = secret.TokenTTL()
if err != nil {
return token, ttl, err
}
return token, ttl, nil
}
return token, ttl, err
}

// authFunc implements a Vault authentication method.
//
// It returns a Vault authentication token and its
// time-to-live (TTL) or an error explaining why
// the authentication attempt failed.
type authFunc func() (token string, ttl time.Duration, err error)

// RenewToken tries to authenticate with the given AppRole
// credentials if the given ttl is 0. Further, it keeps
// trying to renew the the client auth. token before its
Expand All @@ -127,9 +166,9 @@ func (c *client) Authenticate(login AppRole) (token string, ttl time.Duration, e
// Since RenewToken starts a endless for-loop users should
// usually invoke CheckStatus in a separate go routine:
// go client.RenewToken(ctx, login, ttl)
func (c *client) RenewToken(ctx context.Context, login AppRole, ttl time.Duration) {
if login.Retry == 0 {
login.Retry = 5 * time.Second
func (c *client) RenewToken(ctx context.Context, authenticate authFunc, ttl, retry time.Duration) {
if retry == 0 {
retry = 5 * time.Second
}
for {
// If Vault is sealed we have to wait
Expand Down Expand Up @@ -157,10 +196,10 @@ func (c *client) RenewToken(ctx context.Context, login AppRole, ttl time.Duratio
token string
err error
)
token, ttl, err = c.Authenticate(login)
token, ttl, err = authenticate()
if err != nil {
ttl = 0 // On error, set the TTL again to 0 to re-auth. again.
timer := time.NewTimer(login.Retry)
timer := time.NewTimer(retry)
select {
case <-ctx.Done():
timer.Stop()
Expand Down
40 changes: 36 additions & 4 deletions internal/vault/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package vault

import (
"context"
"errors"
"fmt"
"io"
"log"
Expand All @@ -35,11 +36,18 @@ import (
// whenever it fails.
type AppRole struct {
Engine string // The AppRole engine path
ID string // The AppRole ID
ID string // The AppRole ID
Secret string // The Approle secret ID
Retry time.Duration
}

type Kubernetes struct {
Engine string // The Kubernetes auth engine path
Role string // The Kubernetes JWT role
JWT string // The Kubernetes JWT
Retry time.Duration
}

// Store is a key-value store that saves key-value
// pairs as entries on Vault's K/V secret backend.
type Store struct {
Expand All @@ -66,6 +74,10 @@ type Store struct {
// credentials.
AppRole AppRole

// K8S contains the Vault Kubernetes authentication
// credentials.
K8S Kubernetes

// StatusPingAfter is the duration after which
// the KeyStore will check the status of the Vault
// server. Particularly, this status information
Expand Down Expand Up @@ -143,14 +155,34 @@ func (s *Store) Authenticate(context context.Context) error {
// which is not what we want.
s.client.SetNamespace(s.Namespace)
}
go s.client.CheckStatus(context, s.StatusPingAfter)

token, ttl, err := s.client.Authenticate(s.AppRole)
var (
authenticate authFunc
retry time.Duration
)
switch {
case s.AppRole.ID != "" || s.AppRole.Secret != "":
if s.K8S.Role != "" || s.K8S.JWT != "" {
return errors.New("vault: ambigious authentication: AppRole and K8S credentials specified at the same time")
}
authenticate = s.client.AuthenticateWithAppRole(s.AppRole)
case s.K8S.Role != "" || s.K8S.JWT != "":
if s.AppRole.ID != "" || s.AppRole.Secret != "" {
return errors.New("vault: ambigious authentication: AppRole and K8S credentials specified at the same time")
}
authenticate = s.client.AuthenticateWithK8S(s.K8S)
default:
return errors.New("vault: no or empty authentication credentials specified")
}

token, ttl, err := authenticate()
if err != nil {
return err
}
s.client.SetToken(token)
go s.client.RenewToken(context, s.AppRole, ttl)

go s.client.CheckStatus(context, s.StatusPingAfter)
go s.client.RenewToken(context, authenticate, ttl, retry)
return nil
}

Expand Down
5 changes: 5 additions & 0 deletions server-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ keys:
id: "" # Your AppRole Role ID
secret: "" # Your AppRole Secret ID
retry: 15s # Duration until the server tries to re-authenticate after connection loss.
kubernetes: # Kubernetes credentials. See: https://www.vaultproject.io/docs/auth/kubernetes
engine: "" # The path of the Kubernetes engine e.g. authenticate. If empty, defaults to: kubernetes. (Vault default)
role: "" # The Kubernetes JWT role
jwt: "" # Either the JWT provided by K8S or a path to a K8S secret containing the JWT.
retry: 15s # Duration until the server tries to re-authenticate after connection loss.
tls: # The Vault client TLS configuration for mTLS authentication and certificate verification
key: "" # Path to the TLS client private key for mTLS authentication to Vault
cert: "" # Path to the TLS client certificate for mTLS authentication to Vault
Expand Down

0 comments on commit d85a727

Please sign in to comment.