Compare commits

..

1 commit

Author SHA1 Message Date
Joseph Irving 7c7bfac372 added vault user instead of root 2016-11-11 15:29:43 +00:00
35 changed files with 285 additions and 883 deletions

1
.gitignore vendored
View file

@ -5,7 +5,6 @@
.idea/
bin/
release/
secrets/
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o

View file

@ -1,5 +1,3 @@
services:
- docker
env:
global:
- secure: Vq4593WUn2Hng93OynvydesiWu7dAnhIFhX6KtmVwqyuWuk60D8bJAd9qiJYax6+jqsAVnouaD+0TtfLb+SFJ9+vFMqykP+iSVJyC6p3DCRqMmKN+KN20hmq5WSvoE9Rs1elNV770plZdDbAIJ36iFsSpJnrp6dA9vFs0G4E2ceLw0KTJDW7C/eFO1LFiWdgsIrq4DpSCGqjcAzrRKsAkajBmsLNO//MFlsUnnJX4YBQKzibSAyoHeTHPitkvZ6Qf11N7pFMqeX78Stqeat7Xks/C0RzCOZipIGX/yU5qTGg8yGS9FG1qDkNf2Vh4osZySRHy4QJfukTlrLBB1skPHfQpU6n3bNVkF2dhQv0M4TzkrFXB+DF5WyrvKhM7i/w6WvTmg6Zc66ut0qXbTUOSJeZpuHo7c195ly51GxmbNIDw9F2RrJbjRqJrrJ9EXGCBC1wbz4wIYXt5sGh2bl1HdZqGEYkzFfXsIrFc6e8mal/AEMwos3hfezoU9kyriM5sA4M4+yK4ERQYL3Kc4UsFg4oyZrbna6k4Pas/mdFYeM3jhQxOKWxCaSSoeKE/FrbKcMb2/4btQe12Ik9zUIca58VJ04KawEBoyF/+YTSCpQOapnoFmWJoCrXJZlIU56zdn4LXNjDL+pENnjDXhutQck3ZqErcewQQzSepIUtFQo=
@ -7,29 +5,30 @@ env:
- AUTHOR_EMAIL=gambol99@gmail.com
- REGISTRY_USERNAME=ukhomeofficedigital+vault_sidekick
- REGISTRY=quay.io
services:
- docker
language: go
go: 1.8.1
install: true
script:
go:
- 1.7.1
install:
- make test
- if ([[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]) || [[ -n ${TRAVIS_TAG} ]]; then
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X main.gitsha=${TRAVIS_TAG:-git+${TRAVIS_COMMIT}}" -o bin/vault-sidekick_linux_amd64;
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X main.gitsha=${TRAVIS_TAG:-git+${TRAVIS_COMMIT}}" -o bin/vault-sidekick_darwin_amd64;
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X main.gitsha=${TRAVIS_TAG:-git+${TRAVIS_COMMIT}}" -o bin/vault-sidekick_windows_amd64.exe;
- if ([[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} != "true" ]]) || [[ -n ${TRAVIS_TAG} ]]; then
docker login -u ${REGISTRY_USERNAME} -p ${REGISTRY_TOKEN} -e ${AUTHOR_EMAIL} ${REGISTRY};
VERSION=${TRAVIS_TAG:-latest} make docker-release;
VERSION=latest make docker-release;
fi
before_deploy:
- NAME=GOOS=linux GOARCH=amd64 godep go build -o bin/vault-sidekick-linux-amd64
after_deploy:
- docker login -u ${REGISTRY_USERNAME} -p ${REGISTRY_TOKEN} -e ${AUTHOR_EMAIL} ${REGISTRY}
- VERSION=$TRAVIS_TAG make docker-release
deploy:
provider: releases
skip_cleanup: true
on:
go: 1.7.1
repo: UKHomeOffice/vault-sidekick
tags: true
api_key:
secure: "${GITHUB_TOKEN}"
file:
- bin/vault-sidekick_linux_amd64
- bin/vault-sidekick_darwin_amd64
- bin/vault-sidekick_windows_amd64.exe
- bin/vault-sidekick-linux-amd64

View file

@ -1,50 +1,19 @@
#### **Version v0.3.4**
##### FEATURES
* Adding a jitter option to the resources
#### **Version v0.3.3**
##### FEATURES
* Loading vault url from kubernetes vault auth file, exit if vault url is not set
#### **Version v0.3.2**
##### FEATURES
* Added kubernetes-vault support
* Added onetime only mode via the one-shot option
* Added the 'retries' parameter to resources to allow optional maxRetries
#### **Version v0.3.1**
##### FEATURES
* Added a mode option to the resource specification enabling secrets to set the file permissions
* Fixed a bug in the renewal time, when a resource does not have a custom update and the lease time is 0s
* Cleaned up some of the vetting issues
* Change the travis build to use golang v1.8.1
* Added a version flag -version and passing the gitsha in the version
* Updated the kubernete deployment files
#### **Version v0.1.0**
##### FEATURES
BUGS
* Fixed the bundle format to produce four file, a bundle with cert+ca, and the FILENAME-ca.pem, FILENAME-key.pem,
* Fixed the bundle format to produce four file, a bundle with cert+ca, and the FILENAME-ca.pem, FILENAME-key.pem,
and the FILENAME.pem certificate
#### **Version v0.0.9-1**
##### FEATURES
* Adding the ability to perform environment variable substituted of the resource path i.e.
-resource=secret:/secrets/%ENV%/myset : %ENV% will substituted
* Adding the ability to perform environment variable substituted of the resource path i.e.
-resource=secret:/secrets/%ENV%/myset : %ENV% will substituted
#### **Version v0.0.9**
##### FEATURES
@ -55,7 +24,7 @@ BUGS
##### FEATURES
* Adding an exec option to the control set, the command is called whenever a change is made on the resource with a
* Adding an exec option to the control set, the command is called whenever a change is made on the resource with a
condfigurable timeout (default to 60s)
-cn=secret:platform/secrets/se2:fmt=yaml,exec=tests/runme.sh,update=1s
@ -72,11 +41,11 @@ BUGS
* Fixed up a number of niggling issues
* Added the bundle format to pki paths can write a bundle private and certificate file and a separate ca file
* Added the env format which will create a environment variables file
* Adding comma separated list as resource arguments comes in the form <ARG>|<ARG> i.e.
* Adding comma separated list as resource arguments comes in the form <ARG>|<ARG> i.e.
-cn=pki:platform/pki/issue/example-dot-com:common_name=blah.example.com,alt_names='me.example.com|ted.example.com'
##### BREAKING CHANGES:
* Note, because all params excluding the control options are passed as arguments to vault the arguments must be the
* Note, because all params excluding the control options are passed as arguments to vault the arguments must be the
same as those for vault, i.e. for pki cn -> common_name
##### BUGS:

View file

@ -1,13 +1,15 @@
FROM alpine:3.5
FROM alpine:3.4
MAINTAINER Rohith <gambol99@gmail.com>
RUN apk update && \
apk add ca-certificates bash
RUN adduser -D vault
ADD bin/vault-sidekick /vault-sidekick
USER vault
RUN adduser -D vault && \
chown -R vault:vault /vault-sidekick && \
mkdir /etc/secrets && \
chown -R vault:vault /etc/secrets
ENTRYPOINT [ "/vault-sidekick" ]
USER vault

View file

@ -2,11 +2,9 @@
NAME=vault-sidekick
AUTHOR ?= ukhomeofficedigital
REGISTRY ?= quay.io
GOVERSION ?= 1.8.1
GOVERSION ?= 1.7.1
HARDWARE=$(shell uname -m)
VERSION ?= $(shell awk '/release =/ { print $$3 }' main.go | sed 's/"//g')
GIT_SHA=$(shell git --no-pager describe --always --dirty)
LFLAGS ?= -X main.gitsha=${GIT_SHA}
VERSION ?= $(shell awk '/Version =/ { print $$3 }' main.go | sed 's/"//g')
VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr
.PHONY: test authors changelog build docker static release
@ -16,12 +14,12 @@ default: build
build: deps
@echo "--> Compiling the project"
mkdir -p bin
godep go build -ldflags '-w ${LFLAGS}' -o bin/${NAME}
godep go build -o bin/${NAME}
static: deps
@echo "--> Compiling the static binary"
mkdir -p bin
CGO_ENABLED=0 GOOS=linux godep go build -a -tags netgo -ldflags '-w ${LFLAGS}' -o bin/${NAME}
CGO_ENABLED=0 GOOS=linux godep go build -a -tags netgo -ldflags '-w' -o bin/${NAME}
docker-build:
@echo "--> Compiling the project"

View file

@ -12,53 +12,30 @@ Vault Sidekick is a add-on container which can be used as a generic entry-point
**Usage:**
```shell
$ sudo docker run --rm quay.io/ukhomeofficedigital/vault-sidekick:v0.3.3 -help
Usage of /vault-sidekick:
-alsologtostderr
log to standard error as well as files
-auth string
a configuration file in json or yaml containing authentication arguments
-ca-cert string
the path to the file container the CA used to verify the vault service
-cn value
a resource to retrieve and monitor from vault
-dryrun
perform a dry run, printing the content to screen
-exec-timeout duration
the timeout applied to commands on the exec option (default 1m0s)
-format string
the auth file format (default "default")
-log_backtrace_at value
when logging hits line file:N, emit a stack trace
-log_dir string
If non-empty, write log files in this directory
-logtostderr
log to standard error instead of files
-one-shot
retrieve resources from vault once and then exit
-output string
the full path to write resources or VAULT_OUTPUT (default "/etc/secrets")
-stats duration
the interval to produce statistics on the accessed resources (default 1h0m0s)
-stderrthreshold value
logs at or above this threshold go to stderr
-tls-skip-verify
whether to check and verify the vault service certificate
-v value
log level for V logs
-vault string
url the vault service or VAULT_ADDR (default "https://127.0.0.1:8200")
-version
show the vault-sidekick version
-vmodule value
comma-separated list of pattern=N settings for file-filtered logging
[jest@starfury vault-sidekick]$ bin/vault-sidekick --help
Usage of bin/vault-sidekick:
-alsologtostderr=false: log to standard error as well as files
-auth="": a configuration file in a json or yaml containing authentication arguments
-cn=: a resource to retrieve and monitor from vault (e.g. pki:name:cert.name, secret:db_password, aws:s3_backup)
-ca-cert="": a CA certificate to use in order to validate the vault service certificate
-delete-token=false: once the we have connected to vault, delete the token file from disk
-dryrun=false: perform a dry run, printing the content to screen
-log_backtrace_at=:0: when logging hits line file:N, emit a stack trace
-log_dir="": If non-empty, write log files in this directory
-logtostderr=false: log to standard error instead of files
-output="/etc/secrets": the full path to write the protected resources (VAULT_OUTPUT if available)
-stats=5m0s: the interval to produce statistics on the accessed resources
-stderrthreshold=0: logs at or above this threshold go to stderr
-tls-skip-verify=false: skip verifying the vault certificate
-token="": the token used to authenticate to teh vault service (VAULT_TOKEN if available)
-v=0: log level for V logs
-vault="https://127.0.0.1:8200": the url the vault service is running behind (VAULT_ADDR if available)
-vmodule=: comma-separated list of pattern=N settings for file-filtered logging
```
**Building**
There is a Makefile in the base repository, so assuming you have make and go:
`$ make`
There is a Makefile in the base repository, so assuming you have make and go: # make
**Example Usage**
@ -68,19 +45,19 @@ The below is taken from a [Kubernetes](https://github.com/kubernetes/kubernetes)
spec:
containers:
- name: vault-side-kick
image: quay.io/ukhomeofficedigital/vault-sidekick:v0.3.3
image: gambol99/vault-sidekick:latest
args:
- -output=/etc/secrets
- -cn=pki:project1/certs/example.com:common_name=commons.example.com,revoke=true,update=2h
- -cn=secret:secret/db/prod/username:file=.credentials
- -cn=secret:secret/db/prod/password:retries=true
- -cn=secret:secret/db/prod/password
- -cn=aws:aws/creds/s3_backup_policy:file=.s3_creds
volumeMounts:
- name: secrets
mountPath: /etc/secrets
```
The above equates to:
The above say's
- Write all the secrets to the /etc/secrets directory
- Retrieve a dynamic certificate pair for me, with the common name: 'commons.example.com' and renew the cert when it expires automatically
@ -90,7 +67,7 @@ The above equates to:
**Authentication**
An authentication file can be specified in either yaml of json format which contains a method field, indicating one of the authentication
A authentication file can be specified in either yaml of json format which contains a method field, indicating one of the authentication
methods provided by vault i.e. userpass, token, github etc and then followed by the required arguments for that plugin.
If the required arguments for that plugin are not contained in the authentication file, fallbacks from environment variables are used.
@ -159,7 +136,6 @@ bundle format is very similar in the sense it similar takes the private key and
**Resource Options**
- **file**: (filaname) by default all file are relative to the output directory specified and will have the name NAME.RESOURCE; the fn options allows you to switch names and paths to write the files
- **mode**: (mode) overrides the default file permissions of the secret from 0664
- **create**: (create) create the resource
- **update**: (update) override the lease time of this resource and get/renew a secret on the specified duration e.g 1m, 2d, 5m10s
- **renew**: (renewal) override the default behavour on this resource, renew the resource when coming close to expiration e.g true, TRUE
@ -167,5 +143,3 @@ bundle format is very similar in the sense it similar takes the private key and
- **revoke**: (revoke) revoke the old lease when you get retrieve a old one e.g. true, TRUE (default to allow the lease to expire and naturally revoke)
- **fmt**: (format) allows you to specify the output format of the resource / secret, e.g json, yaml, ini, txt
- **exec** (execute) execute's a command when resource is updated or changed
- **retries**: (retries) the maximum number of times to retry retrieving a resource. If not set, resources will be retried indefinitely
* **jitter**: (jitter) an optional maximum jitter duration. If specified, a random duration between 0 and `jitter` will be subtracted from the renewal time for the resource

View file

@ -28,8 +28,8 @@ type authAppRolePlugin struct {
}
type appRoleLogin struct {
RoleID string `json:"role_id,omitempty"`
SecretID string `json:"secret_id,omitempty"`
RoleId string `json:"role_id,omitempty"`
SecretId string `json:"secret_id,omitempty"`
}
// NewAppRolePlugin creates a new App Role plugin
@ -40,17 +40,21 @@ func NewAppRolePlugin(client *api.Client) AuthInterface {
}
// Create a approle plugin with the secret id and role id provided in the file
func (r authAppRolePlugin) Create(cfg *vaultAuthOptions) (string, error) {
if cfg.RoleID == "" {
cfg.RoleID = os.Getenv("VAULT_SIDEKICK_ROLE_ID")
func (r authAppRolePlugin) Create(cfg map[string]string) (string, error) {
// step: extract the options
roleId, _ := cfg["role_id"]
secretId, _ := cfg["secret_id"]
if roleId == "" {
roleId = os.Getenv("VAULT_SIDEKICK_ROLE_ID")
}
if cfg.SecretID == "" {
cfg.SecretID = os.Getenv("VAULT_SIDEKICK_SECRET_ID")
if secretId == "" {
secretId = os.Getenv("VAULT_SIDEKICK_SECRET_ID")
}
// step: create the token request
request := r.client.NewRequest("POST", "/v1/auth/approle/login")
login := appRoleLogin{SecretID: cfg.SecretID, RoleID: cfg.RoleID}
login := appRoleLogin{SecretId: secretId, RoleId: roleId}
if err := request.SetJSONBody(login); err != nil {
return "", err
}

View file

@ -1,65 +0,0 @@
package main
import (
"io/ioutil"
"os"
"github.com/hashicorp/vault/api"
)
type authKubernetesPlugin struct {
client *api.Client
}
type kubernetesLogin struct {
Role string `json:"role"`
JWT string `json:"jwt"`
}
func NewKubernetesPlugin(client *api.Client) AuthInterface {
return &authKubernetesPlugin{
client: client,
}
}
func (p authKubernetesPlugin) Create(cfg *vaultAuthOptions) (string, error) {
if cfg.RoleID == "" {
cfg.RoleID = os.Getenv("VAULT_SIDEKICK_K8S_ROLE")
}
if cfg.FileName == "" {
cfg.FileName = os.Getenv("VAULT_SIDEKICK_K8S_TOKEN_FILE")
// default to the typical location for this
if cfg.FileName == "" {
cfg.FileName = "/var/run/secrets/kubernetes.io/serviceaccount/token"
}
}
// read kubernetes serviceaccount token file (a jwt token)
tokenBytes, err := ioutil.ReadFile(cfg.FileName)
if err != nil {
return "", err
}
// create token request
request := p.client.NewRequest("POST", "/v1/auth/kubernetes/login")
body := kubernetesLogin{Role: cfg.RoleID, JWT: string(tokenBytes)}
err = request.SetJSONBody(body)
if err != nil {
return "", err
}
// execute api request
resp, err := p.client.RawRequest(request)
if err != nil {
return "", err
}
defer resp.Body.Close()
// parse secret response
secret, err := api.ParseSecret(resp.Body)
if err != nil {
return "", err
}
return secret.Auth.ClientToken, nil
}

View file

@ -37,16 +37,17 @@ func NewUserTokenPlugin(client *api.Client) AuthInterface {
}
// Create retrieves the token from an environment variable or file
func (r authTokenPlugin) Create(cfg *vaultAuthOptions) (string, error) {
if cfg.FileName != "" {
content, err := readConfigFile(cfg.FileName, cfg.FileFormat)
func (r authTokenPlugin) Create(cfg map[string]string) (string, error) {
filename, _ := cfg["filename"]
if filename != "" {
content, err := readConfigFile(filename)
if err != nil {
return "", err
}
// check: ensure we have a token in the file
token := content.Token
if token == "" {
return "", fmt.Errorf("the auth file: %s does not contain a token", cfg.FileName)
token, found := content["token"]
if !found {
return "", fmt.Errorf("the auth file: %s does not contain a token", filename)
}
return token, nil

View file

@ -41,18 +41,21 @@ func NewUserPassPlugin(client *api.Client) AuthInterface {
}
// Create a userpass plugin with the username and password provide in the file
func (r authUserPassPlugin) Create(cfg *vaultAuthOptions) (string, error) {
func (r authUserPassPlugin) Create(cfg map[string]string) (string, error) {
// step: extract the options
if cfg.Username == "" {
cfg.Username = os.Getenv("VAULT_SIDEKICK_USERNAME")
username, _ := cfg["username"]
password, _ := cfg["password"]
if username == "" {
username = os.Getenv("VAULT_SIDEKICK_USERNAME")
}
if cfg.Password == "" {
cfg.Password = os.Getenv("VAULT_SIDEKICK_PASSWORD")
if password == "" {
password = os.Getenv("VAULT_SIDEKICK_PASSWORD")
}
// step: create the token request
request := r.client.NewRequest("POST", fmt.Sprintf("/v1/auth/userpass/login/%s", cfg.Username))
if err := request.SetJSONBody(userPassLogin{Password: cfg.Password}); err != nil {
request := r.client.NewRequest("POST", fmt.Sprintf("/v1/auth/userpass/login/%s", username))
if err := request.SetJSONBody(userPassLogin{Password: password}); err != nil {
return "", err
}
// step: make the request

View file

@ -20,34 +20,16 @@ import (
"flag"
"fmt"
"net/url"
"os"
"time"
)
type vaultAuthOptions struct {
ClientToken string
Token string
LeaseDuration int
Renewable bool
Method string
VaultURL string `json:"vaultAddr"`
RoleID string `json:"role_id" yaml:"role_id"`
SecretID string `json:"secret_id" yaml:"secret_id"`
FileName string
FileFormat string
Username string
Password string
}
type config struct {
// the url for th vault server
vaultURL string
// a file containing the authenticate options
vaultAuthFile string
// whether or not the auth file format is default
vaultAuthFileFormat string
// the authentication options
vaultAuthOptions *vaultAuthOptions
vaultAuthOptions map[string]string
// the vault ca file
vaultCaFile string
// the place to write the resources
@ -55,17 +37,13 @@ type config struct {
// switch on dry run
dryRun bool
// skip tls verify
skipTLSVerify bool
tlsVerify bool
// the resource items to retrieve
resources *VaultResources
// the interval for producing statistics
statsInterval time.Duration
// the timeout for a exec command
execTimeout time.Duration
// version flag
showVersion bool
// one-shot mode
oneShot bool
}
var (
@ -75,60 +53,42 @@ var (
func init() {
// step: setup some defaults
options.resources = new(VaultResources)
options.vaultAuthOptions = &vaultAuthOptions{
Method: "token",
}
options.vaultAuthOptions = map[string]string{VaultAuth: "token"}
flag.StringVar(&options.vaultURL, "vault", getEnv("VAULT_ADDR", "https://127.0.0.1:8200"), "url the vault service or VAULT_ADDR")
flag.StringVar(&options.vaultAuthFile, "auth", getEnv("AUTH_FILE", ""), "a configuration file in json or yaml containing authentication arguments")
flag.StringVar(&options.vaultAuthFileFormat, "format", getEnv("AUTH_FORMAT", "default"), "the auth file format")
flag.StringVar(&options.vaultAuthOptions.Method, "method", getEnv("AUTH_METHOD", "token"), "the authentication method to use (use of an auth file will override this setting)")
flag.StringVar(&options.vaultAuthFile, "auth", "", "a configuration file in json or yaml containing authentication arguments")
flag.StringVar(&options.outputDir, "output", getEnv("VAULT_OUTPUT", "/etc/secrets"), "the full path to write resources or VAULT_OUTPUT")
flag.BoolVar(&options.dryRun, "dryrun", false, "perform a dry run, printing the content to screen")
flag.BoolVar(&options.skipTLSVerify, "tls-skip-verify", false, "whether to check and verify the vault service certificate")
flag.BoolVar(&options.tlsVerify, "tls-skip-verify", false, "whether to check and verify the vault service certificate")
flag.StringVar(&options.vaultCaFile, "ca-cert", "", "the path to the file container the CA used to verify the vault service")
flag.DurationVar(&options.statsInterval, "stats", time.Duration(1)*time.Hour, "the interval to produce statistics on the accessed resources")
flag.DurationVar(&options.execTimeout, "exec-timeout", time.Duration(60)*time.Second, "the timeout applied to commands on the exec option")
flag.BoolVar(&options.showVersion, "version", false, "show the vault-sidekick version")
flag.Var(options.resources, "cn", "a resource to retrieve and monitor from vault")
flag.BoolVar(&options.oneShot, "one-shot", false, "retrieve resources from vault once and then exit")
}
// parseOptions validate the command line options and validates them
func parseOptions() error {
flag.Parse()
return validateOptions(&options)
}
// validateOptions parses and validates the command line options
func validateOptions(cfg *config) (err error) {
// step: read in the token if required
// step: validate the vault url
if _, err = url.Parse(cfg.vaultURL); err != nil {
return fmt.Errorf("invalid vault url: '%s' specified", cfg.vaultURL)
}
// step: read in the token if required
if cfg.vaultAuthFile != "" {
if exists, _ := fileExists(cfg.vaultAuthFile); !exists {
return fmt.Errorf("the token file: %s does not exists, please check", cfg.vaultAuthFile)
}
cfg.vaultAuthOptions, err = readConfigFile(cfg.vaultAuthFile, cfg.vaultAuthFileFormat)
options.vaultAuthOptions, err = readConfigFile(options.vaultAuthFile)
if err != nil {
return fmt.Errorf("unable to read in authentication options from: %s, error: %s", cfg.vaultAuthFile, err)
}
if cfg.vaultAuthOptions.VaultURL != "" {
cfg.vaultURL = cfg.vaultAuthOptions.VaultURL
}
}
if cfg.vaultURL == "" {
cfg.vaultURL = os.Getenv("VAULT_ADDR")
}
if cfg.vaultURL == "" {
return fmt.Errorf("VAULT_ADDR is unset")
}
// step: validate the vault url
if _, err = url.Parse(cfg.vaultURL); err != nil {
return fmt.Errorf("invalid vault url: '%s' specified", cfg.vaultURL)
}
if cfg.vaultCaFile != "" {
@ -137,7 +97,7 @@ func validateOptions(cfg *config) (err error) {
}
}
if cfg.skipTLSVerify == true && cfg.vaultCaFile != "" {
if cfg.tlsVerify == true && cfg.vaultCaFile != "" {
return fmt.Errorf("you are skipping the tls but supplying a CA, doesn't make sense")
}

View file

@ -1,77 +0,0 @@
package main
import (
"os"
"testing"
)
func TestValidateOptionsWithoutVaultURL(t *testing.T) {
os.Setenv("VAULT_ADDR", "")
cfg := &config{}
err := validateOptions(cfg)
if err == nil {
t.Errorf("should have raised error: %v", err)
}
}
func TestValidateOptionsWithEnvFallback(t *testing.T) {
os.Setenv("VAULT_ADDR", "http://testurl:8080")
cfg := &config{}
err := validateOptions(cfg)
if err != nil {
t.Errorf("raised an error: %v", err)
}
actual := cfg.vaultURL
expected := "http://testurl:8080"
if actual != expected {
t.Errorf("Expected Vault URL to be %s got %s", expected, actual)
}
}
func TestValidateOptionsWithInvalidVaultURL(t *testing.T) {
cfg := &config{
vaultURL: "%invalid_url",
}
err := validateOptions(cfg)
if err == nil {
t.Errorf("should have raised error")
}
}
func TestValidateOptionsWithInvalidVaultURLFromAuthFile(t *testing.T) {
cfg := &config{
vaultAuthFile: "tests/invalid_kubernetes_vault_auth_file.json",
}
err := validateOptions(cfg)
if err == nil {
t.Errorf("should have raised error")
}
}
func TestValidateOptionsWithVaultURLFromAuthFile(t *testing.T) {
cfg := &config{
vaultAuthFile: "tests/kubernetes_vault_auth_file.json",
}
err := validateOptions(cfg)
if err != nil {
t.Errorf("raising an error %v", err)
}
actual := cfg.vaultURL
expected := "http://testurl:8080"
if actual != expected {
t.Errorf("Expected Vault URL to be %s got %s", expected, actual)
}
}

View file

@ -19,94 +19,68 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/golang/glog"
"gopkg.in/yaml.v2"
)
func writeIniFile(filename string, data map[string]interface{}, mode os.FileMode) error {
func writeIniFile(filename string, data map[string]interface{}) error {
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s = %v\n", key, val))
}
return writeFile(filename, buf.Bytes(), mode)
return writeFile(filename, buf.Bytes())
}
func writeCSVFile(filename string, data map[string]interface{}, mode os.FileMode) error {
func writeCSVFile(filename string, data map[string]interface{}) error {
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s,%v\n", key, val))
}
return writeFile(filename, buf.Bytes(), mode)
return writeFile(filename, buf.Bytes())
}
func writeYAMLFile(filename string, data map[string]interface{}, mode os.FileMode) error {
func writeYAMLFile(filename string, data map[string]interface{}) error {
// marshall the content to yaml
content, err := yaml.Marshal(data)
if err != nil {
return err
}
return writeFile(filename, content, mode)
return writeFile(filename, content)
}
func writeEnvFile(filename string, data map[string]interface{}, mode os.FileMode) error {
func writeEnvFile(filename string, data map[string]interface{}) error {
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s=%v\n", strings.ToUpper(key), val))
}
return writeFile(filename, buf.Bytes(), mode)
return writeFile(filename, buf.Bytes())
}
func getCombinedCaChain(dataValue interface{}) (string, error) {
arr, ok := dataValue.([]interface{})
if !ok {
return "", errors.New("ca_chain not of type array")
}
caChain := make([]string, len(arr))
for i := range arr {
caChain[i] = arr[i].(string)
}
return strings.Join(caChain, "\n"), nil
}
func writeCertificateFile(filename string, data map[string]interface{}, mode os.FileMode) error {
func writeCertificateFile(filename string, data map[string]interface{}) error {
files := map[string]string{
"certificate": "crt",
"issuing_ca": "ca",
"ca_chain": "ca.pem",
"private_key": "key",
}
for key, suffix := range files {
name := fmt.Sprintf("%s.%s", filename, suffix)
filename := fmt.Sprintf("%s.%s", filename, suffix)
content, found := data[key]
if !found {
glog.Errorf("didn't find the certification option: %s in the resource: %s", key, name)
glog.Errorf("didn't find the certification option: %s in the resource: %s", key, filename)
continue
}
// step: write the file
var contentToWrite string
var err error
if key == "ca_chain" {
contentToWrite, err = getCombinedCaChain(data[key])
if err != nil {
glog.Errorf("failed to parse ca_chain: %s", err)
}
} else {
contentToWrite = fmt.Sprintf("%s", content)
}
if err := writeFile(name, []byte(contentToWrite), mode); err != nil {
glog.Errorf("failed to write resource: %s, element: %s, filename: %s, error: %s", filename, suffix, name, err)
if err := writeFile(filename, []byte(fmt.Sprintf("%s", content))); err != nil {
glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s", filename, suffix, filename, err)
continue
}
}
@ -115,44 +89,33 @@ func writeCertificateFile(filename string, data map[string]interface{}, mode os.
}
func writeCertificateBundleFile(filename string, data map[string]interface{}, mode os.FileMode) error {
func writeCertificateBundleFile(filename string, data map[string]interface{}) error {
bundleFile := fmt.Sprintf("%s-bundle.pem", filename)
keyFile := fmt.Sprintf("%s-key.pem", filename)
caFile := fmt.Sprintf("%s-ca.pem", filename)
caChainFile := fmt.Sprintf("%s-ca-chain.pem", filename)
certFile := fmt.Sprintf("%s.pem", filename)
bundle := fmt.Sprintf("%s\n\n%s", data["certificate"], data["issuing_ca"])
key := fmt.Sprintf("%s\n", data["private_key"])
ca := fmt.Sprintf("%s\n", data["issuing_ca"])
certificate := fmt.Sprintf("%s\n", data["certificate"])
caChain, err := getCombinedCaChain(data["ca_chain"])
if err != nil {
glog.Errorf("failed to parse ca_chain: %s", err)
return err
}
if err := writeFile(bundleFile, []byte(bundle), mode); err != nil {
if err := writeFile(bundleFile, []byte(bundle)); err != nil {
glog.Errorf("failed to write the bundled certificate file, error: %s", err)
return err
}
if err := writeFile(certFile, []byte(certificate), mode); err != nil {
if err := writeFile(certFile, []byte(certificate)); err != nil {
glog.Errorf("failed to write the certificate file, errro: %s", err)
return err
}
if err := writeFile(caFile, []byte(ca), mode); err != nil {
if err := writeFile(caFile, []byte(ca)); err != nil {
glog.Errorf("failed to write the ca file, errro: %s", err)
return err
}
if err := writeFile(caChainFile, []byte(caChain), mode); err != nil {
glog.Errorf("failed to write the ca_chain file, errro: %s", err)
return err
}
if err := writeFile(keyFile, []byte(key), mode); err != nil {
if err := writeFile(keyFile, []byte(key)); err != nil {
glog.Errorf("failed to write the key file, errro: %s", err)
return err
}
@ -160,60 +123,15 @@ func writeCertificateBundleFile(filename string, data map[string]interface{}, mo
return nil
}
func writeKeyCertificateBundleFile(filename string, data map[string]interface{}, mode os.FileMode) error {
bundleFile := fmt.Sprintf("%s-bundle.pem", filename)
keyFile := fmt.Sprintf("%s-key.pem", filename)
caFile := fmt.Sprintf("%s-ca.pem", filename)
caChainFile := fmt.Sprintf("%s-ca-chain.pem", filename)
certFile := fmt.Sprintf("%s.pem", filename)
bundle := fmt.Sprintf("%s\n%s", data["private_key"], data["certificate"])
key := fmt.Sprintf("%s\n", data["private_key"])
ca := fmt.Sprintf("%s\n", data["issuing_ca"])
certificate := fmt.Sprintf("%s\n", data["certificate"])
caChain, err := getCombinedCaChain(data["ca_chain"])
if err != nil {
glog.Errorf("failed to parse ca_chain: %s", err)
return err
}
if err := writeFile(bundleFile, []byte(bundle), mode); err != nil {
glog.Errorf("failed to write the bundled certificate file, error: %s", err)
return err
}
if err := writeFile(certFile, []byte(certificate), mode); err != nil {
glog.Errorf("failed to write the certificate file, errro: %s", err)
return err
}
if err := writeFile(caFile, []byte(ca), mode); err != nil {
glog.Errorf("failed to write the ca file, errro: %s", err)
return err
}
if err := writeFile(caChainFile, []byte(caChain), mode); err != nil {
glog.Errorf("failed to write the ca_chain file, errro: %s", err)
return err
}
if err := writeFile(keyFile, []byte(key), mode); err != nil {
glog.Errorf("failed to write the key file, errro: %s", err)
return err
}
return nil
}
func writeTxtFile(filename string, data map[string]interface{}, mode os.FileMode) error {
func writeTxtFile(filename string, data map[string]interface{}) error {
keys := getKeys(data)
if len(keys) > 1 {
// step: for plain formats we need to iterate the keys and produce a file per key
for suffix, content := range data {
name := fmt.Sprintf("%s.%s", filename, suffix)
if err := writeFile(name, []byte(fmt.Sprintf("%v", content)), mode); err != nil {
filename := fmt.Sprintf("%s.%s", filename, suffix)
if err := writeFile(filename, []byte(fmt.Sprintf("%v", content))); err != nil {
glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s",
filename, suffix, name, err)
filename, suffix, filename, err)
continue
}
}
@ -224,20 +142,22 @@ func writeTxtFile(filename string, data map[string]interface{}, mode os.FileMode
value, _ := data[keys[0]]
content := []byte(fmt.Sprintf("%s", value))
return writeFile(filename, content, mode)
return writeFile(filename, content)
}
func writeJSONFile(filename string, data map[string]interface{}, mode os.FileMode) error {
func writeJSONFile(filename string, data map[string]interface{}) error {
content, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
return writeFile(filename, content, mode)
return writeFile(filename, content)
}
// writeFile writes the file to stdout or an actual file
func writeFile(filename string, content []byte, mode os.FileMode) error {
// writeFile ... writes the content to a file .. dah
// filename : the path to the file
// content : the content to be written
func writeFile(filename string, content []byte) error {
if options.dryRun {
glog.Infof("dry-run: filename: %s, content:", filename)
fmt.Printf("%s\n", string(content))
@ -245,5 +165,5 @@ func writeFile(filename string, content []byte, mode os.FileMode) error {
}
glog.V(3).Infof("saving the file: %s", filename)
return ioutil.WriteFile(filename, content, mode)
return ioutil.WriteFile(filename, content, 0664)
}

View file

@ -21,31 +21,32 @@ import (
"io"
)
var stdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+,.?/:;{}[]`~")
var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+,.?/:;{}[]`~")
func newPassword(length int) string {
return randString(length, stdChars)
func NewPassword(length int) string {
return rand_char(length, StdChars)
}
func randString(length int, chars []byte) string {
pass := make([]byte, length)
data := make([]byte, length+(length/4)) // storage for random bytes.
func rand_char(length int, chars []byte) string {
new_pword := make([]byte, length)
random_data := make([]byte, length+(length/4)) // storage for random bytes.
clen := byte(len(chars))
maxrb := byte(256 - (256 % len(chars)))
i := 0
for {
if _, err := io.ReadFull(rand.Reader, data); err != nil {
if _, err := io.ReadFull(rand.Reader, random_data); err != nil {
panic(err)
}
for _, c := range data {
for _, c := range random_data {
if c >= maxrb {
continue
}
pass[i] = chars[c%clen]
new_pword[i] = chars[c%clen]
i++
if i == length {
return string(pass)
return string(new_pword)
}
}
}
panic("unreachable")
}

View file

@ -1,87 +0,0 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: vault-sidekick
spec:
replicas: 1
template:
metadata:
labels:
name: vault-sidekick
annotations:
build: https://github.com/UKHomeOffice/vault-sidekick
spec:
containers:
- name: sidekick
image: quay.io/ukhomeofficedigital/vault-sidekick:v0.3.1
imagePullPolicy: Always
resources:
limits:
cpu: 100m
memory: 50Mi
args:
- -cn=pki:services/${NAMESPACE}/pki/issue/default:fmt=bundle,common_name=demo.${NAMESPACE}.svc.cluster.local,file=platform,mode=0600
- -ca-cert=/ca/caroot.bundle
- -logtostderr=true
- -v=3
env:
- name: VAULT_ADDR
value: https://vault.vault.svc.cluster.local:8200
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: store-token
key: token
- name: NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
volumeMounts:
- name: secrets
mountPath: /etc/secrets
- name: ca-bundle
mountPath: /ca
- name: nginx
image: quay.io/ukhomeofficedigital/nginx-proxy:v3.0.0
resources:
limits:
cpu: 400m
memory: 256Mi
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
env:
- name: LOAD_BALANCER_CIDR
value: 10.0.0.0/8
- name: PROXY_SERVICE_HOST
value: 127.0.0.1
- name: PROXY_SERVICE_PORT
value: "8080"
- name: SERVER_CERT
value: /etc/secrets/platform.pem
- name: SERVER_KEY
value: /etc/secrets/platform-key.pem
- name: SSL_CIPHERS
value: ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:AES256+EDH:!aNULL
- name: ENABLE_UUID_PARAM
value: "FALSE"
- name: NAXSI_USE_DEFAULT_RULES
value: "FALSE"
- name: PORT_IN_HOST_HEADER
value: "FALSE"
- name: ERROR_REDIRECT_CODES
value: "599"
- name: ADD_NGINX_LOCATION_CFG
value: "add_header Strict-Transport-Security \"max-age=31536000; includeSubdomains\";"
volumeMounts:
- name: secrets
mountPath: /etc/secrets
volumes:
- name: secrets
emptyDir: {}
- name: ca-bundle
secret:
secretName: ca-bundle

60
main.go
View file

@ -17,36 +17,26 @@ limitations under the License.
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"github.com/golang/glog"
)
var (
prog = "vault-sidekick"
release = "v0.3.4"
gitsha = ""
const (
Prog = "vault-sidekick"
Version = "v0.2.1"
)
func main() {
version := fmt.Sprintf("%s (git+sha %s)", release, gitsha)
// step: parse and validate the command line / environment options
if err := parseOptions(); err != nil {
showUsage("invalid options, %s", err)
}
if options.showVersion {
fmt.Printf("%s %s\n", prog, version)
return
}
glog.Infof("starting the %s, %s", prog, version)
if options.oneShot {
glog.Infof("running in one-shot mode")
}
glog.Infof("starting the %s, version: %s", Prog, Version)
// step: create a client to vault
vault, err := NewVaultService(options.vaultURL)
@ -69,50 +59,14 @@ func main() {
vault.Watch(rn)
}
toProcess := options.resources.items
toProcessLock := &sync.Mutex{}
failedResource := false
if options.oneShot && len(toProcess) == 0 {
glog.Infof("nothing to retrieve from vault. exiting...")
os.Exit(0)
}
// step: we simply wait for events i.e. secrets from vault and write them to the output directory
for {
select {
case evt := <-updates:
glog.V(10).Infof("recieved an update from the resource: %s", evt.Resource)
go func(r VaultEvent) {
toProcessLock.Lock()
defer toProcessLock.Unlock()
switch r.Type {
case EventTypeSuccess:
if err := processResource(evt.Resource, evt.Secret); err != nil {
glog.Errorf("failed to write out the update, error: %s", err)
}
if options.oneShot {
for i, r := range toProcess {
if evt.Resource == r {
toProcess = append(toProcess[:i], toProcess[i+1:]...)
}
}
}
case EventTypeFailure:
if evt.Resource.maxRetries > 0 && evt.Resource.maxRetries < evt.Resource.retries {
for i, r := range toProcess {
if evt.Resource == r {
toProcess = append(toProcess[:i], toProcess[i+1:]...)
failedResource = true
}
}
}
}
if len(toProcess) == 0 {
glog.Infof("no resources left to process. exiting...")
if failedResource {
os.Exit(1)
} else {
os.Exit(0)
}
if err := processResource(evt.Resource, evt.Secret); err != nil {
glog.Errorf("failed to write out the update, error: %s", err)
}
}(evt)
case <-signalChannel:

7
services/demo-ns.yml Normal file
View file

@ -0,0 +1,7 @@
---
kind: Namespace
apiVersion: v1
metadata:
name: demo
labels:
name: demo

58
services/demo-rc.yaml Normal file
View file

@ -0,0 +1,58 @@
---
apiVersion: v1
kind: ReplicationController
metadata:
namespace: demo
name: vault-demo
spec:
replicas: 1
selector:
name: vault-demo
template:
metadata:
labels:
name: vault-demo
spec:
containers:
- name: vault-sidekick
image: gambol99/vault-sidekick:0.0.1
imagePullPolicy: Always
args:
- -logtostderr=true
- -v=4
- -tls-skip-verify=true
- -auth=/etc/token/vault-token.yml
- -output=/etc/secrets
- -cn=secret:db:update=3h,revoke=true
- -cn=pki:example-dot-com:cn=demo.example.com,fmt=cert,file=demo.example.com
- -vault=https://vault.services.cluster.local:8200
volumeMounts:
- name: secrets
mountPath: /etc/secrets
- name: token
mountPath: /etc/token
- name: nginx-tls-sidekick
image: quay.io/ukhomeofficedigital/nginx-tls-sidekick
imagePullPolicy: Always
args:
- ./run.sh
- -p
- 443:127.0.0.1:80:demo.example.com
ports:
- containerPort: 443
volumeMounts:
- name: secrets
mountPath: /etc/secrets
- name: apache
image: fedora/apache
ports:
- containerPort: 80
volumeMounts:
- name: secrets
mountPath: /etc/secrets
volumes:
- name: secrets
emptyDir: {}
- name: token
secret:
secretName: vault-token

15
services/demo-secrets.yml Normal file
View file

@ -0,0 +1,15 @@
---
apiVersion: v1
kind: Secret
metadata:
namespace: demo
name: vault-token
data:
#
# vault auth-enable userpass
# vault write auth/userpass/users/demo password=SOME_PASSWORD policies=root
#
vault-token.yml: |
method: userpass
username: demo
password: SOME_PASSWORD

View file

@ -1,3 +1,4 @@
---
apiVersion: v1
kind: Service
metadata:

View file

@ -1 +0,0 @@
{"method": "approle", "role_id": "admin", "secret_id": "foobar"}

View file

@ -1,3 +0,0 @@
method: approle
role_id: admin
secret_id: foobar

View file

@ -1 +0,0 @@
{"method": "userpass", "username": "admin", "password": "foobar"}

View file

@ -1 +0,0 @@
{"renewable": true, "leaseDuration": 60, "vaultAddr": "%invalid_url", "token": "foobar"}

View file

@ -1 +0,0 @@
{"renewable": true, "leaseDuration": 60, "vaultAddr": "http://testurl:8080", "token": "foobar"}

View file

@ -1 +0,0 @@
{"method": "token", "token": "foobar"}

View file

@ -1,2 +0,0 @@
method: token
token: foobar

View file

@ -1 +0,0 @@
{"method": "userpass", "username": "admin", "password": "foobar"}

View file

@ -1,3 +0,0 @@
method: userpass
username: admin
password: foobar

View file

@ -27,11 +27,10 @@ import (
"strings"
"time"
"os/exec"
"path/filepath"
"github.com/golang/glog"
"gopkg.in/yaml.v2"
"os/exec"
"path/filepath"
)
func init() {
@ -70,7 +69,7 @@ func getKeys(data map[string]interface{}) []string {
// readConfigFile read in a configuration file
// filename : the path to the file
func readConfigFile(filename, fileFormat string) (*vaultAuthOptions, error) {
func readConfigFile(filename string) (map[string]string, error) {
// step: check the file exists
if exists, err := fileExists(filename); !exists {
return nil, fmt.Errorf("the file: %s does not exist", filename)
@ -85,49 +84,43 @@ func readConfigFile(filename, fileFormat string) (*vaultAuthOptions, error) {
case ".yml":
return readYAMLFile(filename)
default:
return readJSONFile(filename, fileFormat)
return readJSONFile(filename)
}
return nil, fmt.Errorf("unsupported config file format: %s", suffix)
}
// readJsonFile read in and unmarshall the data into a map
// filename : the path to the file container the json data
func readJSONFile(filename, format string) (*vaultAuthOptions, error) {
opts := &vaultAuthOptions{}
func readJSONFile(filename string) (map[string]string, error) {
data := make(map[string]string, 0)
content, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
return data, err
}
// unmarshall the data
err = json.Unmarshal(content, &opts)
if err != nil && format == "default" {
return nil, err
}
err = json.Unmarshal(content, &data)
if err != nil {
return nil, err
}
if format == "kubernetes-vault" && opts.ClientToken != "" {
opts.Method = "token"
opts.Token = opts.ClientToken
return data, err
}
return opts, nil
return data, nil
}
// readYAMLFile read in and unmarshall the data into a map
// filename : the path to the file container the yaml data
func readYAMLFile(filename string) (*vaultAuthOptions, error) {
o := &vaultAuthOptions{}
func readYAMLFile(filename string) (map[string]string, error) {
data := make(map[string]string, 0)
content, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
return data, err
}
err = yaml.Unmarshal(content, o)
err = yaml.Unmarshal(content, data)
if err != nil {
return nil, err
return data, err
}
return o, nil
return data, nil
}
// getDurationWithin generate a random integer between min and max
@ -176,23 +169,21 @@ func processResource(rn *VaultResource, data map[string]interface{}) (err error)
case "yaml":
fallthrough
case "yml":
err = writeYAMLFile(filename, data, rn.fileMode)
err = writeYAMLFile(filename, data)
case "json":
err = writeJSONFile(filename, data, rn.fileMode)
err = writeJSONFile(filename, data)
case "ini":
err = writeIniFile(filename, data, rn.fileMode)
err = writeIniFile(filename, data)
case "csv":
err = writeCSVFile(filename, data, rn.fileMode)
err = writeCSVFile(filename, data)
case "env":
err = writeEnvFile(filename, data, rn.fileMode)
err = writeEnvFile(filename, data)
case "cert":
err = writeCertificateFile(filename, data, rn.fileMode)
err = writeCertificateFile(filename, data)
case "txt":
err = writeTxtFile(filename, data, rn.fileMode)
err = writeTxtFile(filename, data)
case "bundle":
err = writeCertificateBundleFile(filename, data, rn.fileMode)
case "key-cert-bundle":
err = writeKeyCertificateBundleFile(filename, data, rn.fileMode)
err = writeCertificateBundleFile(filename, data)
default:
return fmt.Errorf("unknown output format: %s", rn.format)
}
@ -207,7 +198,7 @@ func processResource(rn *VaultResource, data map[string]interface{}) (err error)
cmd := exec.Command(rn.execPath, filename)
cmd.Start()
timer := time.AfterFunc(options.execTimeout, func() {
if err = cmd.Process.Kill(); err != nil {
if err := cmd.Process.Kill(); err != nil {
glog.Errorf("failed to kill the command, pid: %d, error: %s", cmd.Process.Pid, err)
}
})

View file

@ -1,115 +0,0 @@
package main
import (
"testing"
)
func TestReadConfigFileKubernetesVault(t *testing.T) {
o, err := readConfigFile("tests/kubernetes_vault_auth_file.json", "kubernetes-vault")
if err != nil {
t.Errorf("raising an error: %v", err)
}
tokenExpected := "foobar"
if o.Token != tokenExpected {
t.Errorf("Expected user %s got %s", tokenExpected, o.Token)
}
}
func TestReadConfigUserPassJSON(t *testing.T) {
o, err := readConfigFile("tests/userpass_auth_file.json", "default")
if err != nil {
t.Errorf("raising an error: %v", err)
}
userExpected := "admin"
passwordExpected := "foobar"
if o.Username != userExpected {
t.Errorf("Expected user %s got %s", userExpected, o.Username)
}
if o.Password != passwordExpected {
t.Errorf("Expected user %s got %s", passwordExpected, o.Password)
}
}
func TestReadConfigUserPassYAML(t *testing.T) {
o, err := readConfigFile("tests/userpass_auth_file.yml", "default")
if err != nil {
t.Errorf("raising an error: %v", err)
}
userExpected := "admin"
passwordExpected := "foobar"
if o.Username != userExpected {
t.Errorf("Expected user %s got %s", userExpected, o.Username)
}
if o.Password != passwordExpected {
t.Errorf("Expected user %s got %s", passwordExpected, o.Password)
}
}
func TestReadConfigAppRoleJSON(t *testing.T) {
o, err := readConfigFile("tests/approle_auth_file.json", "default")
if err != nil {
t.Errorf("raising an error: %v", err)
}
roleIDExpected := "admin"
secretIDExpected := "foobar"
if o.RoleID != roleIDExpected {
t.Errorf("Expected roleID %s got %s", roleIDExpected, o.RoleID)
}
if o.SecretID != secretIDExpected {
t.Errorf("Expected secretID %s got %s", secretIDExpected, o.SecretID)
}
}
func TestReadConfigAppRoleYAML(t *testing.T) {
o, err := readConfigFile("tests/approle_auth_file.yml", "default")
if err != nil {
t.Errorf("raising an error: %v", err)
}
roleIDExpected := "admin"
secretIDExpected := "foobar"
if o.RoleID != roleIDExpected {
t.Errorf("Expected roleID %s got %s", roleIDExpected, o.RoleID)
}
if o.SecretID != secretIDExpected {
t.Errorf("Expected secretID %s got %s", secretIDExpected, o.SecretID)
}
}
func TestReadConfigTokenJSON(t *testing.T) {
o, err := readConfigFile("tests/token_auth_file.json", "default")
if err != nil {
t.Errorf("raising an error: %v", err)
}
expected := "foobar"
if o.Token != expected {
t.Errorf("Expected token %s got %s", expected, o.Token)
}
}
func TestReadConfigTokenYAML(t *testing.T) {
o, err := readConfigFile("tests/token_auth_file.yml", "default")
if err != nil {
t.Errorf("raising an error: %v", err)
}
expected := "foobar"
if o.Token != expected {
t.Errorf("Expected token %s got %s", expected, o.Token)
}
}

View file

@ -31,10 +31,15 @@ import (
"github.com/hashicorp/vault/api"
)
const (
// VaultAuth the method to use when authenticating to vault
VaultAuth = "method"
)
// AuthInterface is the authentication interface
type AuthInterface interface {
// Create and handle renewals of the token
Create(*vaultAuthOptions) (string, error)
Create(map[string]string) (string, error)
}
// VaultService is the main interface into the vault API - placing into a structure
@ -59,17 +64,8 @@ type VaultEvent struct {
Resource *VaultResource
// the secret associated
Secret map[string]interface{}
// type of this event (success or failure)
Type EventType
}
type EventType int
const (
EventTypeSuccess EventType = iota
EventTypeFailure EventType = iota
)
// NewVaultService creates a new implementation to speak to vault and retrieve the resources
// url : the url of the vault service
func NewVaultService(url string) (*VaultService, error) {
@ -136,12 +132,6 @@ func (r *VaultService) vaultServiceProcessor() {
// - if we error attempting to retrieve the secret, we background and reschedule an attempt to add it
// - if ok, we grab the lease it and lease time, we setup a notification on renewal
case x := <-retrieveChannel:
// step: skip this resource if it's reached maxRetries
if x.resource.maxRetries > 0 && x.resource.retries > x.resource.maxRetries {
glog.V(4).Infof("skipping resource %s as it's failed %d/%d times", x.resource.retries, x.resource.maxRetries+1)
break
}
// step: save the current lease if we have one
leaseID := ""
if x.secret != nil && x.secret.LeaseID != "" {
@ -154,16 +144,10 @@ func (r *VaultService) vaultServiceProcessor() {
glog.Errorf("failed to retrieve the resource: %s from vault, error: %s", x.resource, err)
// reschedule the attempt for later
r.scheduleIn(x, retrieveChannel, getDurationWithin(3, 10))
x.resource.retries++
r.upstream(VaultEvent{
Resource: x.resource,
Type: EventTypeFailure,
})
break
}
glog.V(4).Infof("successfully retrieved resource: %s, leaseID: %s", x.resource, x.secret.LeaseID)
x.resource.retries = 0
// step: if we had a previous lease and the option is to revoke, lets throw into the revoke channel
if leaseID != "" && x.resource.revoked {
@ -181,22 +165,13 @@ func (r *VaultService) vaultServiceProcessor() {
x.notifyOnRenewal(renewChannel)
// step: update the upstream consumers
r.upstream(VaultEvent{
Resource: x.resource,
Secret: x.secret.Data,
Type: EventTypeSuccess,
})
r.upstream(x)
// A watched resource is coming up for renewal
// - we attempt to renew the resource from vault
// - if we encounter an error, we reschedule the attempt for the future
// - if we're ok, we update the watchedResource and we send a notification of the change upstream
case x := <-renewChannel:
// step: skip this resource if it's reached maxRetries
if x.resource.maxRetries > 0 && x.resource.retries > x.resource.maxRetries {
glog.V(4).Infof("skipping resource %s as it's failed %d/%d times", x.resource.retries, x.resource.maxRetries+1)
break
}
glog.V(4).Infof("resource: %s, lease: %s up for renewal, renewable: %t, revoked: %t", x.resource,
x.secret.LeaseID, x.resource.renewable, x.resource.revoked)
@ -221,19 +196,11 @@ func (r *VaultService) vaultServiceProcessor() {
// step: lets renew the resource
err := r.renew(x)
if err != nil {
glog.Errorf("failed to renew the resource: %s for renewal, error: %s", x.resource, err)
glog.Errorf("failed to renew the resounce: %s for renewal, error: %s", x.resource, err)
// reschedule the attempt for later
r.scheduleIn(x, renewChannel, getDurationWithin(3, 10))
x.resource.retries++
r.upstream(VaultEvent{
Resource: x.resource,
Type: EventTypeFailure,
})
break
}
glog.V(4).Infof("successfully renewed resource: %s, leaseID: %s", x.resource, x.secret.LeaseID)
x.resource.retries = 0
}
// step: the option for this resource is not to renew the secret but regenerate a new secret
@ -247,11 +214,7 @@ func (r *VaultService) vaultServiceProcessor() {
x.notifyOnRenewal(renewChannel)
// step: update any listener upstream
r.upstream(VaultEvent{
Resource: x.resource,
Secret: x.secret.Data,
Type: EventTypeSuccess,
})
r.upstream(x)
// We receive a lease ID along on the channel, just revoke the lease when you can
case x := <-revokeChannel:
@ -297,11 +260,14 @@ func (r VaultService) scheduleIn(rn *watchedResource, ch chan *watchedResource,
// upstream ... the resource has changed thus we notify the upstream listener
// item : the item which has changed
func (r VaultService) upstream(item VaultEvent) {
func (r VaultService) upstream(item *watchedResource) {
// step: chunk this into a go-routine not to block us
for _, listener := range r.listeners {
go func(ch chan VaultEvent) {
ch <- item
ch <- VaultEvent{
Resource: item.resource,
Secret: item.secret.Data,
}
}(listener)
}
}
@ -346,8 +312,7 @@ func (r VaultService) revoke(lease string) error {
// get retrieves a secret from the vault
// rn : the watched resource
func (r VaultService) get(rn *watchedResource) error {
var err error
func (r VaultService) get(rn *watchedResource) (err error) {
var secret *api.Secret
// step: not sure who to cast map[string]string to map[string]interface{} doesn't like it anyway i try and do it
@ -355,7 +320,7 @@ func (r VaultService) get(rn *watchedResource) error {
for k, v := range rn.resource.options {
params[k] = interface{}(v)
}
glog.V(10).Infof("resource: %s, path: %s, params: %v", rn.resource.resource, rn.resource.path, params)
glog.V(10).Infof("get, resource: %s, path: %s, params: %v", rn.resource.resource, rn.resource.path, params)
glog.V(5).Infof("attempting to retrieve the resource: %s from vault", rn.resource)
// step: perform a request to vault
@ -367,6 +332,7 @@ func (r VaultService) get(rn *watchedResource) error {
}
resp, err := r.client.RawRequest(request)
if err != nil {
fmt.Printf("FAILED HERE")
return err
}
// step: read the response
@ -404,7 +370,7 @@ func (r VaultService) get(rn *watchedResource) error {
// We must generate the secret if we have the create flag
if rn.resource.create && secret == nil && err == nil {
glog.V(3).Infof("Create param specified, creating resource: %s", rn.resource.path)
params["value"] = newPassword(int(rn.resource.size))
params["value"] = NewPassword(int(rn.resource.size))
secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path), params)
glog.V(3).Infof("Secret created: %s", rn.resource.path)
if err == nil {
@ -459,17 +425,14 @@ func newVaultClient(opts *config) (*api.Client, error) {
return nil, err
}
plugin := opts.vaultAuthOptions.Method
plugin, _ := opts.vaultAuthOptions[VaultAuth]
switch plugin {
case "userpass":
token, err = NewUserPassPlugin(client).Create(opts.vaultAuthOptions)
case "kubernetes":
token, err = NewKubernetesPlugin(client).Create(opts.vaultAuthOptions)
case "approle":
token, err = NewAppRolePlugin(client).Create(opts.vaultAuthOptions)
case "token":
opts.vaultAuthOptions.FileName = options.vaultAuthFile
opts.vaultAuthOptions.FileFormat = options.vaultAuthFileFormat
opts.vaultAuthOptions["filename"] = options.vaultAuthFile
token, err = NewUserTokenPlugin(client).Create(opts.vaultAuthOptions)
default:
return nil, fmt.Errorf("unsupported authentication plugin: %s", plugin)
@ -494,22 +457,23 @@ func buildHTTPTransport(opts *config) (*http.Transport, error) {
KeepAlive: 10 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: opts.skipTLSVerify,
},
}
if opts.skipTLSVerify {
glog.Warning("skipping TLS verification is not recommended")
// step: are we skip the tls verify?
if options.tlsVerify {
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
// step: are we loading a CA file
if opts.vaultCaFile != "" {
glog.V(3).Infof("loading the ca certificate: %s", opts.vaultCaFile)
// step: load the ca file
caCert, err := ioutil.ReadFile(opts.vaultCaFile)
if err != nil {
return nil, fmt.Errorf("unable to read in the ca: %s, reason: %s", opts.vaultCaFile, err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// step: add the ca to the root
transport.TLSClientConfig.RootCAs = caCertPool
}

View file

@ -18,7 +18,6 @@ package main
import (
"fmt"
"os"
"regexp"
"time"
)
@ -44,20 +43,12 @@ const (
optionCreate = "create"
// optionSize sets the initial size of a password secret
optionSize = "size"
// optionsMode is the file permissions on the secret
optionMode = "mode"
// optionMaxRetries is the maximum number of retries that should be attempted
optionMaxRetries = "retries"
// optionMaxJitter is the maximum amount of jitter that should be applied
// to updates for this resource. If non-zero, a random value between 0 and
// maxJitter will be subtracted from the update period.
optionMaxJitter = "jitter"
// defaultSize sets the default size of a generic secret
defaultSize = 20
)
var (
resourceFormatRegex = regexp.MustCompile("^(yaml|yml|json|env|ini|txt|cert|bundle|key-cert-bundle|csv)$")
resourceFormatRegex = regexp.MustCompile("^(yaml|yml|json|env|ini|txt|cert|bundle|csv)$")
// a map of valid resource to retrieve from vault
validResources = map[string]bool{
@ -76,11 +67,10 @@ var (
func defaultVaultResource() *VaultResource {
return &VaultResource{
fileMode: os.FileMode(0664),
format: "yaml",
options: make(map[string]string, 0),
renewable: false,
revoked: false,
options: make(map[string]string, 0),
size: defaultSize,
}
}
@ -113,17 +103,6 @@ type VaultResource struct {
execPath string
// additional options to the resource
options map[string]string
// the file permissions on the resource
fileMode os.FileMode
// maxRetries is the maximum number of times this resource should be
// attempted to be retrieved from Vault before failing
maxRetries int
// retries is the number of times this resource has been retried since it
// last succeeded
retries int
// maxJitter is the maximum jitter duration to use for this resource when
// performing renewals
maxJitter time.Duration
}
// GetFilename generates a resource filename by default the resource name and resource type, which
@ -173,9 +152,5 @@ func (r *VaultResource) isValidResource() error {
// String returns a string representation of the struct
func (r VaultResource) String() string {
str := fmt.Sprintf("type: %s, path: %s", r.resource, r.path)
if r.maxRetries > 0 {
str = fmt.Sprintf("%s, attempts: %d/%d", str, r.retries, r.maxRetries+1)
}
return str
return fmt.Sprintf("type: %s, path:%s", r.resource, r.path)
}

View file

@ -17,7 +17,6 @@ limitations under the License.
package main
import (
"errors"
"fmt"
"os"
"strconv"
@ -64,23 +63,11 @@ func (r *VaultResources) Set(value string) error {
return fmt.Errorf("invalid resource option: %s, must have a value", x)
}
// step: set the name and value
name := strings.TrimSpace(kp[0])
name := kp[0]
value := strings.Replace(kp[1], "|", ",", -1)
// step: extract the control options from the path resource parameters
switch name {
case optionMode:
if !strings.HasPrefix(value, "0") {
value = "0" + value
}
if len(value) != 4 {
return errors.New("the file permission invalid, should be octal 0444 or alike")
}
v, err := strconv.ParseUint(value, 0, 32)
if err != nil {
return errors.New("invalid file permissions on resource")
}
rn.fileMode = os.FileMode(v)
case optionFormat:
if matched := resourceFormatRegex.MatchString(value); !matched {
return fmt.Errorf("unsupported output format: %s", value)
@ -131,18 +118,6 @@ func (r *VaultResources) Set(value string) error {
rn.filename = value
case optionTemplatePath:
rn.templateFile = value
case optionMaxRetries:
maxRetries, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return fmt.Errorf("the retries option: %s is invalid, should be an integer", value)
}
rn.maxRetries = int(maxRetries)
case optionMaxJitter:
maxJitter, err := time.ParseDuration(value)
if err != nil {
return fmt.Errorf("the jitter option: %s is invalid, should be in duration format", value)
}
rn.maxJitter = maxJitter
default:
rn.options[name] = value
}

View file

@ -19,6 +19,7 @@ package main
import (
"time"
"fmt"
"github.com/golang/glog"
"github.com/hashicorp/vault/api"
)
@ -50,19 +51,8 @@ func (r *watchedResource) notifyOnRenewal(ch chan *watchedResource) {
r.renewalTime = r.resource.update
// step: if the answer is no, we set the notification between 80-95% of the lease time of the secret
if r.renewalTime <= 0 {
// if there is no lease time, we canout set a renewal, just fade into the background
if r.secret.LeaseDuration <= 0 {
glog.Warningf("resource: %s has no lease duration, no custom update set, so item will not be updated", r.resource.path)
return
}
r.renewalTime = r.calculateRenewal()
}
if r.resource.maxJitter != 0 {
glog.V(4).Infof("using maxJitter (%s) to calculate renewal time", r.resource.maxJitter)
r.renewalTime = time.Duration(getDurationWithin(
int((r.renewalTime-r.resource.maxJitter)/time.Second),
int(r.renewalTime/time.Second),
))
fmt.Printf("seconds: %s", r.renewalTime)
}
glog.V(3).Infof("setting a renewal notification on resource: %s, time: %s", r.resource, r.renewalTime)
// step: wait for the duration