Skip to content

Commit

Permalink
feat(vault): add operations and development mode support (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
wsalles committed Feb 7, 2022
1 parent a0c34d7 commit b508db9
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 71 deletions.
195 changes: 124 additions & 71 deletions pkg/common/clients/vault_client.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package clients

import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"

"github.com/hashicorp/vault/api"
"github.com/ydataai/go-core/pkg/common/config"
Expand All @@ -15,96 +13,47 @@ import (
// VaultClient defines the Vault client struct, holding all the required dependencies
type VaultClient struct {
configuration VaultClientConfiguration
path string
role string
logger logging.Logger
client *api.Client
secret *api.Secret
}

// NewVaultClient returns an initialized struct with the required dependencies injected
func NewVaultClient(path, role string, configuration VaultClientConfiguration, logger logging.Logger) (*VaultClient, error) {
func NewVaultClient(logger logging.Logger, configuration VaultClientConfiguration, role string,
authenticator Authenticator) (*VaultClient, error) {

config := &api.Config{Address: configuration.VaultURL}

if authenticator == nil {
return nil, errors.New("missing authenticator")
}

client, err := api.NewClient(config)
if err != nil {
return nil, err
}

vc := &VaultClient{
configuration: configuration,
path: path,
role: role,
logger: logger,
client: client,
}

if err = vc.login(); err != nil {
if err = authenticator.Authenticate(vc); err != nil {
return nil, err
}

go vc.renew()

return vc, nil
}

// login the k8s service account
func (vc *VaultClient) login() error {
vc.logger.Info("performing vault k8s login.")
// reads jwt from service account
jwt, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return fmt.Errorf("unable to read file containing service account token: %v 😱", err)
}
params := map[string]interface{}{
"jwt": string(jwt),
"role": vc.role, // the name of the role in Vault that was created with this app's Kubernetes service account bound to it
}
// perform login
secret, err := vc.client.Logical().Write("auth/kubernetes/login", params)
if err != nil {
return fmt.Errorf("unable to log in with Kubernetes auth: %v 😱", err)
}
if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" {
return errors.New("login response did not return client token 😱")
}
// client update with the access token
vc.logger.Info("login: client logged in successfully 🔑")
token := strings.TrimSuffix(secret.Auth.ClientToken, "\n")
vc.client.SetToken(token)
// stores login response secret
vc.secret = secret
return nil
}

// renew the token according to secret.Auth.LeaseDuration automatically
func (vc *VaultClient) renew() {
vc.logger.Info("stating vault token auto renew ...")
// schedule the token renew operation
for range time.Tick(time.Second * time.Duration(vc.secret.Auth.LeaseDuration-(vc.secret.Auth.LeaseDuration/10))) {
// perform renew
resp, err := vc.client.Auth().Token().Renew(vc.secret.Auth.ClientToken, vc.secret.Auth.LeaseDuration)
if err != nil {
vc.logger.Errorf("unable to renew the access token %v 😱", err)
}
// client update with the renewed token
if resp != nil && resp.Auth != nil && resp.Auth.ClientToken != "" {
token := strings.TrimSuffix(resp.Auth.ClientToken, "\n")
vc.logger.Info("renew: client token renewed successfully 🔑")
vc.client.SetToken(token)
} else {
// new login to deal with system token expiration
vc.login()
}
}
}

// StoreCredentials receives the name and the respective map of credentials and attempts to store them
// StoreCredentials receives the path and the respective map of credentials and attempts to store them
// on the Vault server.
func (vc *VaultClient) StoreCredentials(name string, credentials map[string]string) error {
func (vc *VaultClient) StoreCredentials(path string, credentials map[string]string) error {
vc.logger.Info("Sending credentials to Vault ☄️")

_, err := vc.client.Logical().Write(fmt.Sprintf("%s/data/%s", vc.path, name), map[string]interface{}{
_, err := vc.client.Logical().Write(path, map[string]interface{}{
"data": credentials,
})
if err != nil {
Expand All @@ -116,12 +65,12 @@ func (vc *VaultClient) StoreCredentials(name string, credentials map[string]stri
return nil
}

// GetCredentials receives the name and attemps to retrieve the map of credentials present
// GetCredentials receives the path and attemps to retrieve the map of credentials present
// on the Vault server.
func (vc *VaultClient) GetCredentials(name string) (*config.Credentials, error) {
func (vc *VaultClient) GetCredentials(path string) (*config.Credentials, error) {
vc.logger.Info("Fetching credentials from Vault ☄️")

secret, err := vc.client.Logical().Read(fmt.Sprintf("%s/data/%s", vc.path, name))
secret, err := vc.client.Logical().Read(path)
if err != nil {
vc.logger.Errorf("Unable to fetch credentials from Vault 😱. Err: %v", err)
return nil, err
Expand All @@ -148,12 +97,12 @@ func (vc *VaultClient) GetCredentials(name string) (*config.Credentials, error)
return &credentials, nil
}

// DeleteCredentials receives the name and attempts to delete the existing credentials on Vault.
// DeleteCredentials receives the path and attempts to delete the existing credentials on Vault.
// Is performs a soft delete, per docs > https://www.vaultproject.io/docs/commands/kv/delete
func (vc *VaultClient) DeleteCredentials(name string) error {
func (vc *VaultClient) DeleteCredentials(path string) error {
vc.logger.Info("Deleting credentials from Vault ☄️")

_, err := vc.client.Logical().Delete(fmt.Sprintf("%s/data/%s", vc.path, name))
_, err := vc.client.Logical().Delete(path)
if err != nil {
vc.logger.Errorf("Unable to delete credentials from Vault 😱. Err: %v", err)
return err
Expand All @@ -165,10 +114,10 @@ func (vc *VaultClient) DeleteCredentials(name string) error {

// CheckIfEngineExists attempts to call the /tune API endpoint on the Secrets Engine. Should it fail, it might be an
// indication that the Secrets Engine is not created, which it's useful to know whether or not to call CreateEngine
func (vc *VaultClient) CheckIfEngineExists() bool {
func (vc *VaultClient) CheckIfEngineExists(path string) bool {
vc.logger.Info("Checking if vault engine exists☄️")

epath := fmt.Sprintf("sys/mounts/%s/tune", vc.path)
epath := fmt.Sprintf("sys/mounts/%s/tune", path)

if _, err := vc.client.Logical().Read(epath); err != nil {
switch err.(type) {
Expand All @@ -182,3 +131,107 @@ func (vc *VaultClient) CheckIfEngineExists() bool {
}
return true
}

// List ...
func (vc *VaultClient) List(path string) (interface{}, error) {
vc.logger.Infof("[Vault] Listing the path: '%s' ☄️", path)

data, err := vc.client.Logical().List(path)
if err != nil {
vc.logger.Errorf("[Vault] Unable to list the path: '%s' 😱. Err: %v", path, err)
return nil, err
}

if data == nil {
vc.logger.Infof("[Vault] ❌ No data found in path: '%s'", path)
return nil, nil
}

vc.logger.Infof("[Vault] Listed the path: '%s' ☄️", path)
return data.Data, nil
}

// Get ...
func (vc *VaultClient) Get(path string) (map[string]interface{}, error) {
vc.logger.Infof("[Vault] Getting the '%s' ☄️", path)

secret, err := vc.client.Logical().Read(path)
if err != nil {
vc.logger.Errorf("[Vault] Unable to get '%s' 😱. Err: %v", path, err)
return nil, err
}

if secret == nil {
vc.logger.Infof("[Vault] ❌ No data found: %s", path)
return nil, nil
}

vc.logger.Infof("[Vault] Got the '%s' ☄️", path)
return secret.Data, nil
}

// Delete ...
func (vc *VaultClient) Delete(path string) error {
vc.logger.Infof("[Vault] Deleting the path: '%s'", path)

secret, err := vc.client.Logical().Read(path)
if err != nil {
return fmt.Errorf("[Vault] Unable to delete the path: '%s' 😱. Err: %v", path, err)
}

if secret == nil {
vc.logger.Infof("[Vault] ❌ No data found in path: '%s'", path)
return nil
}

_, err = vc.client.Logical().Delete(path)
if err != nil {
return fmt.Errorf("[Vault] Unable to delete the path: '%s' 😱. Err: %v", path, err)
}

vc.logger.Infof("[Vault] Deleted the path: '%s' ☄️", path)
return nil
}

// Put ...
func (vc *VaultClient) Put(path string, data map[string]interface{}) error {
vc.logger.Infof("[Vault] Creating the '%s' ☄️", path)

_, err := vc.client.Logical().Write(path, data)
if err != nil {
return fmt.Errorf("[Vault] Unable to create '%s' 😱. Err: %v", path, err)
}

vc.logger.Infof("[Vault] Created the '%s' ☄️", path)
return nil
}

// Patch ...
func (vc *VaultClient) Patch(path string, data map[string]interface{}) error {
vc.logger.Infof("[Vault] Patch the '%s' ☄️", path)
// try to patch the path
_, err := vc.client.Logical().JSONMergePatch(context.Background(), path, data)
if err == nil {
return nil
}
// If it's a 405, that probably means the server is running a pre-1.9
// Vault version that doesn't support the HTTP PATCH method.
if re, ok := err.(*api.ResponseError); ok && re.StatusCode != 405 {
return fmt.Errorf("[Vault] Unable to add the path: '%s' 😱. Err: %v", path, err)
}
// get data to update it in memory
existingData, err := vc.Get(path)
if err != nil {
return fmt.Errorf("[Vault] Unable to get the path: '%s' 😱. Err: %v", path, err)
}
// if exists data, then update
if existingData != nil {
// if it exists, then update
for key, value := range data {
existingData[key] = value
}
return vc.Put(path, existingData)
}
// it doesn't exists, create
return vc.Put(path, data)
}
6 changes: 6 additions & 0 deletions pkg/common/clients/vault_client_authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package clients

// Authenticator is an interface to identify which way to authenticate
type Authenticator interface {
Authenticate(vc *VaultClient) error
}
80 changes: 80 additions & 0 deletions pkg/common/clients/vault_client_authenticator_k8s.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package clients

import (
"errors"
"fmt"
"os"
"strings"
"time"
)

// K8sAuthenticator defines a struct for authenticating with Kubernetes.
type K8sAuthenticator struct{}

// NewK8sAuthenticator defines a new K8sAuthenticator struct.
func NewK8sAuthenticator() Authenticator {
return &K8sAuthenticator{}
}

// Authenticate is used to authenticate using Kubernetes.
func (a *K8sAuthenticator) Authenticate(vc *VaultClient) error {
if err := a.login(vc); err != nil {
return err
}

// do the token renewal cycle
go a.renew(vc)

return nil
}

func (a *K8sAuthenticator) login(vc *VaultClient) error {
vc.logger.Info("performing vault k8s login.")
// reads jwt from service account
jwt, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return fmt.Errorf("unable to read file containing service account token: %v 😱", err)
}
params := map[string]interface{}{
"jwt": string(jwt),
"role": vc.role, // the name of the role in Vault that was created with this app's Kubernetes service account bound to it
}
// perform login
secret, err := vc.client.Logical().Write("auth/kubernetes/login", params)
if err != nil {
return fmt.Errorf("unable to log in with Kubernetes auth: %v 😱", err)
}
if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" {
return errors.New("login response did not return client token 😱")
}
// client update with the access token
vc.logger.Info("login: client logged in successfully 🔑")
token := strings.TrimSuffix(secret.Auth.ClientToken, "\n")
vc.client.SetToken(token)
// stores login response secret
vc.secret = secret

return nil
}

// renew the token according to secret.Auth.LeaseDuration automatically
func (a *K8sAuthenticator) renew(vc *VaultClient) {
vc.logger.Info("stating vault token auto renew ...")
// schedule the token renew operation
for range time.Tick(time.Second * time.Duration(vc.secret.Auth.LeaseDuration-(vc.secret.Auth.LeaseDuration/10))) {
// perform renew
resp, err := vc.client.Auth().Token().Renew(vc.secret.Auth.ClientToken, vc.secret.Auth.LeaseDuration)
if err != nil {
vc.logger.Errorf("unable to renew the access token %v 😱", err)
}
// client update with the renewed token
if resp != nil && resp.Auth != nil && resp.Auth.ClientToken != "" {
token := strings.TrimSuffix(resp.Auth.ClientToken, "\n")
vc.logger.Info("renew: client token renewed successfully 🔑")
vc.client.SetToken(token)
} else {
// new login to deal with system token expiration
a.login(vc)
}
}
}
17 changes: 17 additions & 0 deletions pkg/common/clients/vault_client_authenticator_local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package clients

// LocalAuthenticator is used to configure the development mode
type LocalAuthenticator struct {
token string
}

// NewLocalAuthenticator defines a new LocalAuthenticator
func NewLocalAuthenticator(token string) Authenticator {
return &LocalAuthenticator{token: token}
}

// Authenticate is used to authenticate local (development mode).
func (a *LocalAuthenticator) Authenticate(vc *VaultClient) error {
vc.client.SetToken(a.token)
return nil
}

0 comments on commit b508db9

Please sign in to comment.