Skip to the content.

Adding Vault Configuration

Introduction

In addition to non-sensitive configuration that we can pass with Consul, we also need a way to pass sensitive values like passwords and secrets. In this guide will show how to bootstrap Vault integration and pass sensitive key value pairs through manifest.yml.

Goals

Prerequisites

Bootstrapping Vault

When a service is started inside MSX it is passed the required Vault configuration as environment variables:

SPRING_CLOUD_VAULT_HOST   = vault.service.consul
SPRING_CLOUD_VAULT_PORT   = 8200
SPRING_CLOUD_VAULT_SCHEME = https
SPRING_CLOUD_VAULT_TOKEN  = [a valid vault token]

As with Consul, we also need a convenient way to configure Vault when developing locally, and a common mechanism to surface those key value pairs to our service or application. We laid the groundwork for this in the previous guide on Consul, so for brevity we will document the required incremental changes.

GOTCHA

The Vault token passed as environment variable SPRING_CLOUD_VAULT_TOKEN expires after 30 days. Renewing the Vault token is left as an exercise for the reader.


go.mod

Update the module path in go.mod and create an alias for it as shown.

module github.com/CiscoDevNet/msx-examples/go-hello-world-service-4

go 1.13

require (
	github.com/gorilla/mux v1.7.3
	github.com/hashicorp/consul/api v1.8.1
	github.com/hashicorp/vault/api v1.0.4
	github.com/spf13/viper v1.7.1
)

replace github.com/CiscoDevNet/msx-examples/go-hello-world-service-4/go => ./go/


helloworld.yml

The file helloworld.yml is where we pass the values to bootstrap Vault, some of which will be overridden by environment variables at runtime when deployed in to MSX.

.
.
.
vault:
  scheme: "http"     # Bound to env var SPRING_CLOUD_VAULT_SCHEME at runtime.
  host: "127.0.0.1"  # Bound to env var SPRING_CLOUD_VAULT_HOST at runtime.
  port: "8200"       # Bound to env var SPRING_CLOUD_VAULT_PORT at runtime.
  token:             # Bound to env var SPRING_CLOUD_VAULT_TOKEN at runtime.
  cacert: "/etc/ssl/certs/ca-bundle.crt"
  insecure: false
.
.
.


manifest.yml

Update manifest.yml to set a secret in Vault that we can retrieve at runtime.

.
.
.
Secrets:
  - Name: "secret.squirrel.location"
    Value: "The acorns are buried under the big oak tree!"
.
.
.


internal/config/config.go

First add a struct to internal/config/config.go for the Vault configuration, and add it to service configuration.

.
.
.
// Config represents the complete helloworldservice config options.
type Config struct {
	Consul    Consul
	Vault     Vault
}

// Vault represents Vault config options.
type Vault struct {
	Scheme          string
	Host            string
	Port            string
	Token           string
	CACert          string
	Insecure        bool
	Prefix          string
}
.
.
.

Second update “ReadConfig” to bind the Vault environment variables to the Viper instance.

func ReadConfig() *Config {
    .
    .
    .
    	v.AllowEmptyEnv(true)
	// Bind config to environment based on expected injections.
	bindConfig(v,"Consul.Host", "SPRING_CLOUD_CONSUL_HOST")
	bindConfig(v,"Consul.Port", "SPRING_CLOUD_CONSUL_PORT")
	bindConfig(v,"Vault.Scheme", "SPRING_CLOUD_VAULT_SCHEME")
	bindConfig(v,"Vault.Host", "SPRING_CLOUD_VAULT_HOST")
	bindConfig(v,"Vault.Port", "SPRING_CLOUD_VAULT_PORT")
	bindConfig(v,"Vault.Token", "SPRING_CLOUD_VAULT_TOKEN")
    .
    .
    .
}


internal/consul/consul.go

We have to update internal/consul/consul.go to reference the new project path:

package consul

import (
	"github.com/CiscoDevNet/msx-examples/go-hello-world-service-4/internal/config"
	"fmt"
	"github.com/hashicorp/consul/api"
	"net/url"
)
.
.
.


internal/vault/vault.go

The module internal/vault/vault.go provides the code to connect to Vault and retrieve the value for a given key.

package vault

import (
	"errors"
	"github.com/CiscoDevNet/msx-examples/go-hello-world-service-4/internal/config"
	"github.com/hashicorp/vault/api"
)

type HelloWorldVault struct {
	Client *api.Client
	Config config.Vault
}

func (v *HelloWorldVault) Connect() error {
	c := api.DefaultConfig()
	c.Address = v.Config.Scheme + "://" + v.Config.Host + ":" + v.Config.Port

	if v.Config.Scheme == "https" {
		t := api.TLSConfig{
			CACert:   v.Config.CACert,
			Insecure: v.Config.Insecure,
		}
		err := c.ConfigureTLS(&t)
		if err != nil {
			return err
		}
	}
	client, err := api.NewClient(c)
	if err != nil {
		return err
	}

	// Ignore empty Vault tokens.
	if strings.TrimSpace(v.Config.Token) != "" {
		client.SetToken(v.Config.Token)
	}
	v.Client = client
	return nil
}

func (v *HelloWorldVault) GetSecret(s string) (*api.Secret, error) {
	result, err := v.Client.Logical().Read(s)
	return result, err
}

func (v *HelloWorldVault) getValue(secret string, key string) (interface{}, error) {
	s, err := v.GetSecret(secret)
	if err != nil {
		return nil, err
	}
	if s == nil {
		e := errors.New("Value " + secret + " not found.")
		return nil, e
	}
	for k, v := range s.Data {
		if k == key {
			return v, nil
		}
	}
	// It is possible that this is a versioned secret so look just in case.
	if s.Data["data"] != nil {
		for k, v := range s.Data["data"].(map[string]interface{}) {
			if k == key {
				return v, nil
			}
		}
	}
	e := errors.New("Value not found.")
	return nil, e
}

func (v *HelloWorldVault) GetString(secret string, key string, defaultValue string) (string, error) {
	value, error := v.getValue(secret, key)
	if error == nil {
		return value.(string), error
	}
	return defaultValue, error
}

func NewVault(c *config.Config) (HelloWorldVault, error) {
	pv := HelloWorldVault{
		Config: c.Vault,
	}
	err := pv.Connect()
	if err != nil {
		return pv, err
	}
	return pv, nil
}


main.go

We have to do a few things in “main.go”, for brevity we only include the changes.

package main

import (
	"github.com/CiscoDevNet/msx-examples/go-hello-world-service-4/internal/config"
	"github.com/CiscoDevNet/msx-examples/go-hello-world-service-4/internal/consul"
	"github.com/CiscoDevNet/msx-examples/go-hello-world-service-4/internal/vault"
	"log"
	"net/http"

	openapi "github.com/CiscoDevNet/msx-examples/go-hello-world-service-4/go"
)

func main() {
    .
    .
    .
	// Setup Consul.
	consul, err := consul.NewConsul(config)
	if err != nil {
		log.Printf("Could not initialize Consul: %s", err.Error())
	}
	config.Consul.Prefix = consul.FindPrefix()
	testConsul(config, &consul)
		
	// Setup Vault.
	vault, err := vault.NewVault(config)
	if err != nil {
		log.Printf("Could not initialize Vault: %s", err.Error())
	}
	config.Vault.Prefix = "secret/" + config.Consul.Prefix
	testVault(config, &vault)
    .
    .
    .
}
.
.
.
func testVault(config *config.Config, vault *vault.HelloWorldVault) {
	// Read a secret from Vault  and it to the console.
	// Do not leak secrets in production as it is a security violation.
	secretSquirrelLocation, _ := vault.GetString(config.Vault.Prefix + "/helloworldservice/", "secret.squirrel.location", "UNKNOWN")
	log.Printf("Where are the acorns buried?")
	log.Print(secretSquirrelLocation)
}

Pay attention to the key path in testVault as there are different patterns for different MSX versions and uses.

Pattern Description
{prefix}/helloworldservice/my.key for service specific secrets
{prefix}/defaultapplication/my.key for common system secrets


The prefix depends on the version of MSX you are running:

MSX Version Prefix
<= 4.0.0 secret/thirdpartyservices
>= 4.1.0 secret/thirdpartycomponents


Dockerfile

If you copied the Dockerfile from the previous guide, make sure you update “go-hello-world-service-3” to “go-hello-world-service-4”. Also make sure you have the line that pulls in the new Go files:

.
.
.
COPY internal/ /go/src/github.com/CiscoDevNet/msx-examples/go-hello-world-service-4/internal
.
.
.


Makefile

No changes are required in the Makefile from the Consul guide where we added helloworld.yml, which contains the configuration to bootstrap Consul and Vault.

Updating the Dependencies

The code we added above has dependencies on Vault, so we have to include references to them in go.mod. You can add them manually, but it is easier to use go mod tidy in a terminal window:

$ go mod tidy
go: finding module for package github.com/hashicorp/vault/api
go: found github.com/hashicorp/vault/api in github.com/hashicorp/vault/api v1.0.4

After you have run the command check that the “require” section in go.mod looks like this:

.
.
.
require (
	github.com/gorilla/mux v1.7.3
	github.com/hashicorp/consul/api v1.8.1
	github.com/hashicorp/vault/api v1.0.4
	github.com/spf13/viper v1.7.1
)
.
.
.

Building the Component

Like we did in earlier guides, build the component helloworldservice-1.0.0-component.tar.gz by calling make with component “NAME” and “VERSION” parameters. If you do not see helloworld.yml being added to the tarball you need to back and check the Makefile.

$ make NAME=helloworldservice VERSION=1.0.0 
.
.
.
Successfully built a4621de07764
Successfully tagged helloworldservice:1.0.0
docker save helloworldservice:1.0.0 | gzip > helloworldservice-1.0.0.tar.gz
tar -czvf helloworldservice-1.0.0-component.tar.gz manifest.yml helloworld.yml helloworldservice-1.0.0.tar.gz
a manifest.yml
a helloworld.yml
a helloworldservice-1.0.0.tar.gz
rm -f helloworldservice-1.0.0.tar.gz

Deploying the Component

Log in to your MSX environment and deploy helloworldservice-1.0.0-component.tar.gz using MSX UI->Settings->Components (help me). If the helloworldservice is already deployed, delete it before uploading it again.

Inspecting the Server Log

Leaking Vault configuration to the console is a security violation, but it is convenient for testing this example. Recall that we specified a Vault key “secret.squirrel.location” in manifest.yml, and retrieved and printed it in main.go. To prove that it worked we will use Kibana in the same we did for the Consul guide, but this time we will search for “acorns”.

The Missing Pieces

Our service can now accept sensitive and non-sensitive configuration, the missing pieces are:

References

Vault Container in Docker

MSX Component Manager Manifest Reference

Kibana Data Visualization Dashboard

PREVIOUS NEXT HOME