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/ .idea/
bin/ bin/
release/ release/
secrets/
# Compiled Object files, Static and Dynamic libs (Shared Objects) # Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o *.o

View file

@ -1,5 +1,3 @@
services:
- docker
env: env:
global: global:
- secure: Vq4593WUn2Hng93OynvydesiWu7dAnhIFhX6KtmVwqyuWuk60D8bJAd9qiJYax6+jqsAVnouaD+0TtfLb+SFJ9+vFMqykP+iSVJyC6p3DCRqMmKN+KN20hmq5WSvoE9Rs1elNV770plZdDbAIJ36iFsSpJnrp6dA9vFs0G4E2ceLw0KTJDW7C/eFO1LFiWdgsIrq4DpSCGqjcAzrRKsAkajBmsLNO//MFlsUnnJX4YBQKzibSAyoHeTHPitkvZ6Qf11N7pFMqeX78Stqeat7Xks/C0RzCOZipIGX/yU5qTGg8yGS9FG1qDkNf2Vh4osZySRHy4QJfukTlrLBB1skPHfQpU6n3bNVkF2dhQv0M4TzkrFXB+DF5WyrvKhM7i/w6WvTmg6Zc66ut0qXbTUOSJeZpuHo7c195ly51GxmbNIDw9F2RrJbjRqJrrJ9EXGCBC1wbz4wIYXt5sGh2bl1HdZqGEYkzFfXsIrFc6e8mal/AEMwos3hfezoU9kyriM5sA4M4+yK4ERQYL3Kc4UsFg4oyZrbna6k4Pas/mdFYeM3jhQxOKWxCaSSoeKE/FrbKcMb2/4btQe12Ik9zUIca58VJ04KawEBoyF/+YTSCpQOapnoFmWJoCrXJZlIU56zdn4LXNjDL+pENnjDXhutQck3ZqErcewQQzSepIUtFQo= - 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 - AUTHOR_EMAIL=gambol99@gmail.com
- REGISTRY_USERNAME=ukhomeofficedigital+vault_sidekick - REGISTRY_USERNAME=ukhomeofficedigital+vault_sidekick
- REGISTRY=quay.io - REGISTRY=quay.io
services:
- docker
language: go language: go
go: 1.8.1 go:
- 1.7.1
install: true install:
script:
- make test - make test
- if ([[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]) || [[ -n ${TRAVIS_TAG} ]]; then - if ([[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} != "true" ]]) || [[ -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;
docker login -u ${REGISTRY_USERNAME} -p ${REGISTRY_TOKEN} -e ${AUTHOR_EMAIL} ${REGISTRY}; 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 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: deploy:
provider: releases provider: releases
skip_cleanup: true skip_cleanup: true
on: on:
go: 1.7.1
repo: UKHomeOffice/vault-sidekick repo: UKHomeOffice/vault-sidekick
tags: true tags: true
api_key: api_key:
secure: "${GITHUB_TOKEN}" secure: "${GITHUB_TOKEN}"
file: file:
- bin/vault-sidekick_linux_amd64 - bin/vault-sidekick-linux-amd64
- bin/vault-sidekick_darwin_amd64
- bin/vault-sidekick_windows_amd64.exe

View file

@ -1,35 +1,4 @@
#### **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** #### **Version v0.1.0**
##### FEATURES ##### FEATURES

View file

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

View file

@ -2,11 +2,9 @@
NAME=vault-sidekick NAME=vault-sidekick
AUTHOR ?= ukhomeofficedigital AUTHOR ?= ukhomeofficedigital
REGISTRY ?= quay.io REGISTRY ?= quay.io
GOVERSION ?= 1.8.1 GOVERSION ?= 1.7.1
HARDWARE=$(shell uname -m) HARDWARE=$(shell uname -m)
VERSION ?= $(shell awk '/release =/ { print $$3 }' main.go | sed 's/"//g') VERSION ?= $(shell awk '/Version =/ { print $$3 }' main.go | sed 's/"//g')
GIT_SHA=$(shell git --no-pager describe --always --dirty)
LFLAGS ?= -X main.gitsha=${GIT_SHA}
VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr
.PHONY: test authors changelog build docker static release .PHONY: test authors changelog build docker static release
@ -16,12 +14,12 @@ default: build
build: deps build: deps
@echo "--> Compiling the project" @echo "--> Compiling the project"
mkdir -p bin mkdir -p bin
godep go build -ldflags '-w ${LFLAGS}' -o bin/${NAME} godep go build -o bin/${NAME}
static: deps static: deps
@echo "--> Compiling the static binary" @echo "--> Compiling the static binary"
mkdir -p bin 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: docker-build:
@echo "--> Compiling the project" @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:** **Usage:**
```shell ```shell
$ sudo docker run --rm quay.io/ukhomeofficedigital/vault-sidekick:v0.3.3 -help [jest@starfury vault-sidekick]$ bin/vault-sidekick --help
Usage of /vault-sidekick: Usage of bin/vault-sidekick:
-alsologtostderr -alsologtostderr=false: log to standard error as well as files
log to standard error as well as files -auth="": a configuration file in a json or yaml containing authentication arguments
-auth string -cn=: a resource to retrieve and monitor from vault (e.g. pki:name:cert.name, secret:db_password, aws:s3_backup)
a configuration file in json or yaml containing authentication arguments -ca-cert="": a CA certificate to use in order to validate the vault service certificate
-ca-cert string -delete-token=false: once the we have connected to vault, delete the token file from disk
the path to the file container the CA used to verify the vault service -dryrun=false: perform a dry run, printing the content to screen
-cn value -log_backtrace_at=:0: when logging hits line file:N, emit a stack trace
a resource to retrieve and monitor from vault -log_dir="": If non-empty, write log files in this directory
-dryrun -logtostderr=false: log to standard error instead of files
perform a dry run, printing the content to screen -output="/etc/secrets": the full path to write the protected resources (VAULT_OUTPUT if available)
-exec-timeout duration -stats=5m0s: the interval to produce statistics on the accessed resources
the timeout applied to commands on the exec option (default 1m0s) -stderrthreshold=0: logs at or above this threshold go to stderr
-format string -tls-skip-verify=false: skip verifying the vault certificate
the auth file format (default "default") -token="": the token used to authenticate to teh vault service (VAULT_TOKEN if available)
-log_backtrace_at value -v=0: log level for V logs
when logging hits line file:N, emit a stack trace -vault="https://127.0.0.1:8200": the url the vault service is running behind (VAULT_ADDR if available)
-log_dir string -vmodule=: comma-separated list of pattern=N settings for file-filtered logging
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
``` ```
**Building** **Building**
There is a Makefile in the base repository, so assuming you have make and go: There is a Makefile in the base repository, so assuming you have make and go: # make
`$ make`
**Example Usage** **Example Usage**
@ -68,19 +45,19 @@ The below is taken from a [Kubernetes](https://github.com/kubernetes/kubernetes)
spec: spec:
containers: containers:
- name: vault-side-kick - name: vault-side-kick
image: quay.io/ukhomeofficedigital/vault-sidekick:v0.3.3 image: gambol99/vault-sidekick:latest
args: args:
- -output=/etc/secrets - -output=/etc/secrets
- -cn=pki:project1/certs/example.com:common_name=commons.example.com,revoke=true,update=2h - -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/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 - -cn=aws:aws/creds/s3_backup_policy:file=.s3_creds
volumeMounts: volumeMounts:
- name: secrets - name: secrets
mountPath: /etc/secrets mountPath: /etc/secrets
``` ```
The above equates to: The above say's
- Write all the secrets to the /etc/secrets directory - 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 - 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** **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. 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. 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** **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 - **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 - **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 - **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 - **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) - **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 - **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 - **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 { type appRoleLogin struct {
RoleID string `json:"role_id,omitempty"` RoleId string `json:"role_id,omitempty"`
SecretID string `json:"secret_id,omitempty"` SecretId string `json:"secret_id,omitempty"`
} }
// NewAppRolePlugin creates a new App Role plugin // 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 // Create a approle plugin with the secret id and role id provided in the file
func (r authAppRolePlugin) Create(cfg *vaultAuthOptions) (string, error) { func (r authAppRolePlugin) Create(cfg map[string]string) (string, error) {
if cfg.RoleID == "" { // step: extract the options
cfg.RoleID = os.Getenv("VAULT_SIDEKICK_ROLE_ID") roleId, _ := cfg["role_id"]
secretId, _ := cfg["secret_id"]
if roleId == "" {
roleId = os.Getenv("VAULT_SIDEKICK_ROLE_ID")
} }
if cfg.SecretID == "" { if secretId == "" {
cfg.SecretID = os.Getenv("VAULT_SIDEKICK_SECRET_ID") secretId = os.Getenv("VAULT_SIDEKICK_SECRET_ID")
} }
// step: create the token request // step: create the token request
request := r.client.NewRequest("POST", "/v1/auth/approle/login") 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 { if err := request.SetJSONBody(login); err != nil {
return "", err 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 // Create retrieves the token from an environment variable or file
func (r authTokenPlugin) Create(cfg *vaultAuthOptions) (string, error) { func (r authTokenPlugin) Create(cfg map[string]string) (string, error) {
if cfg.FileName != "" { filename, _ := cfg["filename"]
content, err := readConfigFile(cfg.FileName, cfg.FileFormat) if filename != "" {
content, err := readConfigFile(filename)
if err != nil { if err != nil {
return "", err return "", err
} }
// check: ensure we have a token in the file // check: ensure we have a token in the file
token := content.Token token, found := content["token"]
if token == "" { if !found {
return "", fmt.Errorf("the auth file: %s does not contain a token", cfg.FileName) return "", fmt.Errorf("the auth file: %s does not contain a token", filename)
} }
return token, nil 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 // 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 // step: extract the options
if cfg.Username == "" { username, _ := cfg["username"]
cfg.Username = os.Getenv("VAULT_SIDEKICK_USERNAME") password, _ := cfg["password"]
if username == "" {
username = os.Getenv("VAULT_SIDEKICK_USERNAME")
} }
if cfg.Password == "" { if password == "" {
cfg.Password = os.Getenv("VAULT_SIDEKICK_PASSWORD") password = os.Getenv("VAULT_SIDEKICK_PASSWORD")
} }
// step: create the token request // step: create the token request
request := r.client.NewRequest("POST", fmt.Sprintf("/v1/auth/userpass/login/%s", cfg.Username)) request := r.client.NewRequest("POST", fmt.Sprintf("/v1/auth/userpass/login/%s", username))
if err := request.SetJSONBody(userPassLogin{Password: cfg.Password}); err != nil { if err := request.SetJSONBody(userPassLogin{Password: password}); err != nil {
return "", err return "", err
} }
// step: make the request // step: make the request

View file

@ -20,34 +20,16 @@ import (
"flag" "flag"
"fmt" "fmt"
"net/url" "net/url"
"os"
"time" "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 { type config struct {
// the url for th vault server // the url for th vault server
vaultURL string vaultURL string
// a file containing the authenticate options // a file containing the authenticate options
vaultAuthFile string vaultAuthFile string
// whether or not the auth file format is default
vaultAuthFileFormat string
// the authentication options // the authentication options
vaultAuthOptions *vaultAuthOptions vaultAuthOptions map[string]string
// the vault ca file // the vault ca file
vaultCaFile string vaultCaFile string
// the place to write the resources // the place to write the resources
@ -55,17 +37,13 @@ type config struct {
// switch on dry run // switch on dry run
dryRun bool dryRun bool
// skip tls verify // skip tls verify
skipTLSVerify bool tlsVerify bool
// the resource items to retrieve // the resource items to retrieve
resources *VaultResources resources *VaultResources
// the interval for producing statistics // the interval for producing statistics
statsInterval time.Duration statsInterval time.Duration
// the timeout for a exec command // the timeout for a exec command
execTimeout time.Duration execTimeout time.Duration
// version flag
showVersion bool
// one-shot mode
oneShot bool
} }
var ( var (
@ -75,60 +53,42 @@ var (
func init() { func init() {
// step: setup some defaults // step: setup some defaults
options.resources = new(VaultResources) options.resources = new(VaultResources)
options.vaultAuthOptions = &vaultAuthOptions{ options.vaultAuthOptions = map[string]string{VaultAuth: "token"}
Method: "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.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.vaultAuthFile, "auth", "", "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.outputDir, "output", getEnv("VAULT_OUTPUT", "/etc/secrets"), "the full path to write resources or VAULT_OUTPUT") 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.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.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.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.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.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 // parseOptions validate the command line options and validates them
func parseOptions() error { func parseOptions() error {
flag.Parse() flag.Parse()
return validateOptions(&options) return validateOptions(&options)
} }
// validateOptions parses and validates the command line options // validateOptions parses and validates the command line options
func validateOptions(cfg *config) (err error) { 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 cfg.vaultAuthFile != "" {
if exists, _ := fileExists(cfg.vaultAuthFile); !exists { if exists, _ := fileExists(cfg.vaultAuthFile); !exists {
return fmt.Errorf("the token file: %s does not exists, please check", cfg.vaultAuthFile) return fmt.Errorf("the token file: %s does not exists, please check", cfg.vaultAuthFile)
} }
options.vaultAuthOptions, err = readConfigFile(options.vaultAuthFile)
cfg.vaultAuthOptions, err = readConfigFile(cfg.vaultAuthFile, cfg.vaultAuthFileFormat)
if err != nil { if err != nil {
return fmt.Errorf("unable to read in authentication options from: %s, error: %s", cfg.vaultAuthFile, err) 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 != "" { 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") 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 ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os"
"strings" "strings"
"github.com/golang/glog" "github.com/golang/glog"
"gopkg.in/yaml.v2" "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 var buf bytes.Buffer
for key, val := range data { for key, val := range data {
buf.WriteString(fmt.Sprintf("%s = %v\n", key, val)) 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 var buf bytes.Buffer
for key, val := range data { for key, val := range data {
buf.WriteString(fmt.Sprintf("%s,%v\n", key, val)) 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 // marshall the content to yaml
content, err := yaml.Marshal(data) content, err := yaml.Marshal(data)
if err != nil { if err != nil {
return err 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 var buf bytes.Buffer
for key, val := range data { for key, val := range data {
buf.WriteString(fmt.Sprintf("%s=%v\n", strings.ToUpper(key), val)) 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) { func writeCertificateFile(filename string, data map[string]interface{}) 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 {
files := map[string]string{ files := map[string]string{
"certificate": "crt", "certificate": "crt",
"issuing_ca": "ca", "issuing_ca": "ca",
"ca_chain": "ca.pem",
"private_key": "key", "private_key": "key",
} }
for key, suffix := range files { for key, suffix := range files {
name := fmt.Sprintf("%s.%s", filename, suffix) filename := fmt.Sprintf("%s.%s", filename, suffix)
content, found := data[key] content, found := data[key]
if !found { 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 continue
} }
// step: write the file // step: write the file
var contentToWrite string if err := writeFile(filename, []byte(fmt.Sprintf("%s", content))); err != nil {
var err error glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s", filename, suffix, filename, err)
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)
continue 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) bundleFile := fmt.Sprintf("%s-bundle.pem", filename)
keyFile := fmt.Sprintf("%s-key.pem", filename) keyFile := fmt.Sprintf("%s-key.pem", filename)
caFile := fmt.Sprintf("%s-ca.pem", filename) caFile := fmt.Sprintf("%s-ca.pem", filename)
caChainFile := fmt.Sprintf("%s-ca-chain.pem", filename)
certFile := fmt.Sprintf("%s.pem", filename) certFile := fmt.Sprintf("%s.pem", filename)
bundle := fmt.Sprintf("%s\n\n%s", data["certificate"], data["issuing_ca"]) bundle := fmt.Sprintf("%s\n\n%s", data["certificate"], data["issuing_ca"])
key := fmt.Sprintf("%s\n", data["private_key"]) key := fmt.Sprintf("%s\n", data["private_key"])
ca := fmt.Sprintf("%s\n", data["issuing_ca"]) ca := fmt.Sprintf("%s\n", data["issuing_ca"])
certificate := fmt.Sprintf("%s\n", data["certificate"]) 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) glog.Errorf("failed to write the bundled certificate file, error: %s", err)
return 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) glog.Errorf("failed to write the certificate file, errro: %s", err)
return 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) glog.Errorf("failed to write the ca file, errro: %s", err)
return err return err
} }
if err := writeFile(caChainFile, []byte(caChain), mode); err != nil { if err := writeFile(keyFile, []byte(key)); 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) glog.Errorf("failed to write the key file, errro: %s", err)
return err return err
} }
@ -160,60 +123,15 @@ func writeCertificateBundleFile(filename string, data map[string]interface{}, mo
return nil return nil
} }
func writeKeyCertificateBundleFile(filename string, data map[string]interface{}, mode os.FileMode) error { func writeTxtFile(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%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 {
keys := getKeys(data) keys := getKeys(data)
if len(keys) > 1 { if len(keys) > 1 {
// step: for plain formats we need to iterate the keys and produce a file per key // step: for plain formats we need to iterate the keys and produce a file per key
for suffix, content := range data { for suffix, content := range data {
name := fmt.Sprintf("%s.%s", filename, suffix) filename := fmt.Sprintf("%s.%s", filename, suffix)
if err := writeFile(name, []byte(fmt.Sprintf("%v", content)), mode); err != nil { if err := writeFile(filename, []byte(fmt.Sprintf("%v", content))); err != nil {
glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s", glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s",
filename, suffix, name, err) filename, suffix, filename, err)
continue continue
} }
} }
@ -224,20 +142,22 @@ func writeTxtFile(filename string, data map[string]interface{}, mode os.FileMode
value, _ := data[keys[0]] value, _ := data[keys[0]]
content := []byte(fmt.Sprintf("%s", value)) 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, "", " ") content, err := json.MarshalIndent(data, "", " ")
if err != nil { if err != nil {
return err return err
} }
return writeFile(filename, content, mode) return writeFile(filename, content)
} }
// writeFile writes the file to stdout or an actual file // writeFile ... writes the content to a file .. dah
func writeFile(filename string, content []byte, mode os.FileMode) error { // filename : the path to the file
// content : the content to be written
func writeFile(filename string, content []byte) error {
if options.dryRun { if options.dryRun {
glog.Infof("dry-run: filename: %s, content:", filename) glog.Infof("dry-run: filename: %s, content:", filename)
fmt.Printf("%s\n", string(content)) 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) 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" "io"
) )
var stdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+,.?/:;{}[]`~") var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+,.?/:;{}[]`~")
func newPassword(length int) string { func NewPassword(length int) string {
return randString(length, stdChars) return rand_char(length, StdChars)
} }
func randString(length int, chars []byte) string { func rand_char(length int, chars []byte) string {
pass := make([]byte, length) new_pword := make([]byte, length)
data := make([]byte, length+(length/4)) // storage for random bytes. random_data := make([]byte, length+(length/4)) // storage for random bytes.
clen := byte(len(chars)) clen := byte(len(chars))
maxrb := byte(256 - (256 % len(chars))) maxrb := byte(256 - (256 % len(chars)))
i := 0 i := 0
for { for {
if _, err := io.ReadFull(rand.Reader, data); err != nil { if _, err := io.ReadFull(rand.Reader, random_data); err != nil {
panic(err) panic(err)
} }
for _, c := range data { for _, c := range random_data {
if c >= maxrb { if c >= maxrb {
continue continue
} }
pass[i] = chars[c%clen] new_pword[i] = chars[c%clen]
i++ i++
if i == length { 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

56
main.go
View file

@ -17,36 +17,26 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"os" "os"
"os/signal" "os/signal"
"sync"
"syscall" "syscall"
"github.com/golang/glog" "github.com/golang/glog"
) )
var ( const (
prog = "vault-sidekick" Prog = "vault-sidekick"
release = "v0.3.4" Version = "v0.2.1"
gitsha = ""
) )
func main() { func main() {
version := fmt.Sprintf("%s (git+sha %s)", release, gitsha)
// step: parse and validate the command line / environment options // step: parse and validate the command line / environment options
if err := parseOptions(); err != nil { if err := parseOptions(); err != nil {
showUsage("invalid options, %s", err) 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("starting the %s, version: %s", Prog, Version)
glog.Infof("running in one-shot mode")
}
// step: create a client to vault // step: create a client to vault
vault, err := NewVaultService(options.vaultURL) vault, err := NewVaultService(options.vaultURL)
@ -69,51 +59,15 @@ func main() {
vault.Watch(rn) 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 // step: we simply wait for events i.e. secrets from vault and write them to the output directory
for { for {
select { select {
case evt := <-updates: case evt := <-updates:
glog.V(10).Infof("recieved an update from the resource: %s", evt.Resource) glog.V(10).Infof("recieved an update from the resource: %s", evt.Resource)
go func(r VaultEvent) { go func(r VaultEvent) {
toProcessLock.Lock()
defer toProcessLock.Unlock()
switch r.Type {
case EventTypeSuccess:
if err := processResource(evt.Resource, evt.Secret); err != nil { if err := processResource(evt.Resource, evt.Secret); err != nil {
glog.Errorf("failed to write out the update, error: %s", err) 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)
}
}
}(evt) }(evt)
case <-signalChannel: case <-signalChannel:
glog.Infof("recieved a termination signal, shutting down the service") glog.Infof("recieved a termination signal, shutting down the service")

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 apiVersion: v1
kind: Service kind: Service
metadata: 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" "strings"
"time" "time"
"os/exec"
"path/filepath"
"github.com/golang/glog" "github.com/golang/glog"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"os/exec"
"path/filepath"
) )
func init() { func init() {
@ -70,7 +69,7 @@ func getKeys(data map[string]interface{}) []string {
// readConfigFile read in a configuration file // readConfigFile read in a configuration file
// filename : the path to the 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 // step: check the file exists
if exists, err := fileExists(filename); !exists { if exists, err := fileExists(filename); !exists {
return nil, fmt.Errorf("the file: %s does not exist", filename) return nil, fmt.Errorf("the file: %s does not exist", filename)
@ -85,49 +84,43 @@ func readConfigFile(filename, fileFormat string) (*vaultAuthOptions, error) {
case ".yml": case ".yml":
return readYAMLFile(filename) return readYAMLFile(filename)
default: 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 // readJsonFile read in and unmarshall the data into a map
// filename : the path to the file container the json data // filename : the path to the file container the json data
func readJSONFile(filename, format string) (*vaultAuthOptions, error) { func readJSONFile(filename string) (map[string]string, error) {
opts := &vaultAuthOptions{} data := make(map[string]string, 0)
content, err := ioutil.ReadFile(filename) content, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return data, err
} }
// unmarshall the data // unmarshall the data
err = json.Unmarshal(content, &opts) err = json.Unmarshal(content, &data)
if err != nil && format == "default" {
return nil, err
}
if err != nil { if err != nil {
return nil, err return data, err
}
if format == "kubernetes-vault" && opts.ClientToken != "" {
opts.Method = "token"
opts.Token = opts.ClientToken
} }
return opts, nil return data, nil
} }
// readYAMLFile read in and unmarshall the data into a map // readYAMLFile read in and unmarshall the data into a map
// filename : the path to the file container the yaml data // filename : the path to the file container the yaml data
func readYAMLFile(filename string) (*vaultAuthOptions, error) { func readYAMLFile(filename string) (map[string]string, error) {
o := &vaultAuthOptions{} data := make(map[string]string, 0)
content, err := ioutil.ReadFile(filename) content, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return data, err
} }
err = yaml.Unmarshal(content, o) err = yaml.Unmarshal(content, data)
if err != nil { if err != nil {
return nil, err return data, err
} }
return o, nil return data, nil
} }
// getDurationWithin generate a random integer between min and max // 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": case "yaml":
fallthrough fallthrough
case "yml": case "yml":
err = writeYAMLFile(filename, data, rn.fileMode) err = writeYAMLFile(filename, data)
case "json": case "json":
err = writeJSONFile(filename, data, rn.fileMode) err = writeJSONFile(filename, data)
case "ini": case "ini":
err = writeIniFile(filename, data, rn.fileMode) err = writeIniFile(filename, data)
case "csv": case "csv":
err = writeCSVFile(filename, data, rn.fileMode) err = writeCSVFile(filename, data)
case "env": case "env":
err = writeEnvFile(filename, data, rn.fileMode) err = writeEnvFile(filename, data)
case "cert": case "cert":
err = writeCertificateFile(filename, data, rn.fileMode) err = writeCertificateFile(filename, data)
case "txt": case "txt":
err = writeTxtFile(filename, data, rn.fileMode) err = writeTxtFile(filename, data)
case "bundle": case "bundle":
err = writeCertificateBundleFile(filename, data, rn.fileMode) err = writeCertificateBundleFile(filename, data)
case "key-cert-bundle":
err = writeKeyCertificateBundleFile(filename, data, rn.fileMode)
default: default:
return fmt.Errorf("unknown output format: %s", rn.format) 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 := exec.Command(rn.execPath, filename)
cmd.Start() cmd.Start()
timer := time.AfterFunc(options.execTimeout, func() { 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) 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" "github.com/hashicorp/vault/api"
) )
const (
// VaultAuth the method to use when authenticating to vault
VaultAuth = "method"
)
// AuthInterface is the authentication interface // AuthInterface is the authentication interface
type AuthInterface interface { type AuthInterface interface {
// Create and handle renewals of the token // 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 // VaultService is the main interface into the vault API - placing into a structure
@ -59,17 +64,8 @@ type VaultEvent struct {
Resource *VaultResource Resource *VaultResource
// the secret associated // the secret associated
Secret map[string]interface{} 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 // NewVaultService creates a new implementation to speak to vault and retrieve the resources
// url : the url of the vault service // url : the url of the vault service
func NewVaultService(url string) (*VaultService, error) { 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 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 // - if ok, we grab the lease it and lease time, we setup a notification on renewal
case x := <-retrieveChannel: 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 // step: save the current lease if we have one
leaseID := "" leaseID := ""
if x.secret != nil && x.secret.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) glog.Errorf("failed to retrieve the resource: %s from vault, error: %s", x.resource, err)
// reschedule the attempt for later // reschedule the attempt for later
r.scheduleIn(x, retrieveChannel, getDurationWithin(3, 10)) r.scheduleIn(x, retrieveChannel, getDurationWithin(3, 10))
x.resource.retries++
r.upstream(VaultEvent{
Resource: x.resource,
Type: EventTypeFailure,
})
break break
} }
glog.V(4).Infof("successfully retrieved resource: %s, leaseID: %s", x.resource, x.secret.LeaseID) 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 // step: if we had a previous lease and the option is to revoke, lets throw into the revoke channel
if leaseID != "" && x.resource.revoked { if leaseID != "" && x.resource.revoked {
@ -181,22 +165,13 @@ func (r *VaultService) vaultServiceProcessor() {
x.notifyOnRenewal(renewChannel) x.notifyOnRenewal(renewChannel)
// step: update the upstream consumers // step: update the upstream consumers
r.upstream(VaultEvent{ r.upstream(x)
Resource: x.resource,
Secret: x.secret.Data,
Type: EventTypeSuccess,
})
// A watched resource is coming up for renewal // A watched resource is coming up for renewal
// - we attempt to renew the resource from vault // - we attempt to renew the resource from vault
// - if we encounter an error, we reschedule the attempt for the future // - 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 // - if we're ok, we update the watchedResource and we send a notification of the change upstream
case x := <-renewChannel: 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, 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) x.secret.LeaseID, x.resource.renewable, x.resource.revoked)
@ -221,19 +196,11 @@ func (r *VaultService) vaultServiceProcessor() {
// step: lets renew the resource // step: lets renew the resource
err := r.renew(x) err := r.renew(x)
if err != nil { 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 // reschedule the attempt for later
r.scheduleIn(x, renewChannel, getDurationWithin(3, 10)) r.scheduleIn(x, renewChannel, getDurationWithin(3, 10))
x.resource.retries++
r.upstream(VaultEvent{
Resource: x.resource,
Type: EventTypeFailure,
})
break 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 // 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) x.notifyOnRenewal(renewChannel)
// step: update any listener upstream // step: update any listener upstream
r.upstream(VaultEvent{ r.upstream(x)
Resource: x.resource,
Secret: x.secret.Data,
Type: EventTypeSuccess,
})
// We receive a lease ID along on the channel, just revoke the lease when you can // We receive a lease ID along on the channel, just revoke the lease when you can
case x := <-revokeChannel: 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 // upstream ... the resource has changed thus we notify the upstream listener
// item : the item which has changed // 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 // step: chunk this into a go-routine not to block us
for _, listener := range r.listeners { for _, listener := range r.listeners {
go func(ch chan VaultEvent) { go func(ch chan VaultEvent) {
ch <- item ch <- VaultEvent{
Resource: item.resource,
Secret: item.secret.Data,
}
}(listener) }(listener)
} }
} }
@ -346,8 +312,7 @@ func (r VaultService) revoke(lease string) error {
// get retrieves a secret from the vault // get retrieves a secret from the vault
// rn : the watched resource // rn : the watched resource
func (r VaultService) get(rn *watchedResource) error { func (r VaultService) get(rn *watchedResource) (err error) {
var err error
var secret *api.Secret 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 // 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 { for k, v := range rn.resource.options {
params[k] = interface{}(v) 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) glog.V(5).Infof("attempting to retrieve the resource: %s from vault", rn.resource)
// step: perform a request to vault // step: perform a request to vault
@ -367,6 +332,7 @@ func (r VaultService) get(rn *watchedResource) error {
} }
resp, err := r.client.RawRequest(request) resp, err := r.client.RawRequest(request)
if err != nil { if err != nil {
fmt.Printf("FAILED HERE")
return err return err
} }
// step: read the response // 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 // We must generate the secret if we have the create flag
if rn.resource.create && secret == nil && err == nil { if rn.resource.create && secret == nil && err == nil {
glog.V(3).Infof("Create param specified, creating resource: %s", rn.resource.path) 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) secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path), params)
glog.V(3).Infof("Secret created: %s", rn.resource.path) glog.V(3).Infof("Secret created: %s", rn.resource.path)
if err == nil { if err == nil {
@ -459,17 +425,14 @@ func newVaultClient(opts *config) (*api.Client, error) {
return nil, err return nil, err
} }
plugin := opts.vaultAuthOptions.Method plugin, _ := opts.vaultAuthOptions[VaultAuth]
switch plugin { switch plugin {
case "userpass": case "userpass":
token, err = NewUserPassPlugin(client).Create(opts.vaultAuthOptions) token, err = NewUserPassPlugin(client).Create(opts.vaultAuthOptions)
case "kubernetes":
token, err = NewKubernetesPlugin(client).Create(opts.vaultAuthOptions)
case "approle": case "approle":
token, err = NewAppRolePlugin(client).Create(opts.vaultAuthOptions) token, err = NewAppRolePlugin(client).Create(opts.vaultAuthOptions)
case "token": case "token":
opts.vaultAuthOptions.FileName = options.vaultAuthFile opts.vaultAuthOptions["filename"] = options.vaultAuthFile
opts.vaultAuthOptions.FileFormat = options.vaultAuthFileFormat
token, err = NewUserTokenPlugin(client).Create(opts.vaultAuthOptions) token, err = NewUserTokenPlugin(client).Create(opts.vaultAuthOptions)
default: default:
return nil, fmt.Errorf("unsupported authentication plugin: %s", plugin) return nil, fmt.Errorf("unsupported authentication plugin: %s", plugin)
@ -494,22 +457,23 @@ func buildHTTPTransport(opts *config) (*http.Transport, error) {
KeepAlive: 10 * time.Second, KeepAlive: 10 * time.Second,
}).Dial, }).Dial,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: opts.skipTLSVerify,
},
} }
if opts.skipTLSVerify { // step: are we skip the tls verify?
glog.Warning("skipping TLS verification is not recommended") if options.tlsVerify {
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
} }
// step: are we loading a CA file // step: are we loading a CA file
if opts.vaultCaFile != "" { 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) caCert, err := ioutil.ReadFile(opts.vaultCaFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to read in the ca: %s, reason: %s", opts.vaultCaFile, err) return nil, fmt.Errorf("unable to read in the ca: %s, reason: %s", opts.vaultCaFile, err)
} }
caCertPool := x509.NewCertPool() caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert) caCertPool.AppendCertsFromPEM(caCert)
// step: add the ca to the root
transport.TLSClientConfig.RootCAs = caCertPool transport.TLSClientConfig.RootCAs = caCertPool
} }

View file

@ -18,7 +18,6 @@ package main
import ( import (
"fmt" "fmt"
"os"
"regexp" "regexp"
"time" "time"
) )
@ -44,20 +43,12 @@ const (
optionCreate = "create" optionCreate = "create"
// optionSize sets the initial size of a password secret // optionSize sets the initial size of a password secret
optionSize = "size" 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 sets the default size of a generic secret
defaultSize = 20 defaultSize = 20
) )
var ( 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 // a map of valid resource to retrieve from vault
validResources = map[string]bool{ validResources = map[string]bool{
@ -76,11 +67,10 @@ var (
func defaultVaultResource() *VaultResource { func defaultVaultResource() *VaultResource {
return &VaultResource{ return &VaultResource{
fileMode: os.FileMode(0664),
format: "yaml", format: "yaml",
options: make(map[string]string, 0),
renewable: false, renewable: false,
revoked: false, revoked: false,
options: make(map[string]string, 0),
size: defaultSize, size: defaultSize,
} }
} }
@ -113,17 +103,6 @@ type VaultResource struct {
execPath string execPath string
// additional options to the resource // additional options to the resource
options map[string]string 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 // 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 // String returns a string representation of the struct
func (r VaultResource) String() string { func (r VaultResource) String() string {
str := fmt.Sprintf("type: %s, path: %s", r.resource, r.path) return 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
} }

View file

@ -17,7 +17,6 @@ limitations under the License.
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
@ -64,23 +63,11 @@ func (r *VaultResources) Set(value string) error {
return fmt.Errorf("invalid resource option: %s, must have a value", x) return fmt.Errorf("invalid resource option: %s, must have a value", x)
} }
// step: set the name and value // step: set the name and value
name := strings.TrimSpace(kp[0]) name := kp[0]
value := strings.Replace(kp[1], "|", ",", -1) value := strings.Replace(kp[1], "|", ",", -1)
// step: extract the control options from the path resource parameters // step: extract the control options from the path resource parameters
switch name { 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: case optionFormat:
if matched := resourceFormatRegex.MatchString(value); !matched { if matched := resourceFormatRegex.MatchString(value); !matched {
return fmt.Errorf("unsupported output format: %s", value) return fmt.Errorf("unsupported output format: %s", value)
@ -131,18 +118,6 @@ func (r *VaultResources) Set(value string) error {
rn.filename = value rn.filename = value
case optionTemplatePath: case optionTemplatePath:
rn.templateFile = value 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: default:
rn.options[name] = value rn.options[name] = value
} }

View file

@ -19,6 +19,7 @@ package main
import ( import (
"time" "time"
"fmt"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
) )
@ -50,19 +51,8 @@ func (r *watchedResource) notifyOnRenewal(ch chan *watchedResource) {
r.renewalTime = r.resource.update 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 // 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 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() r.renewalTime = r.calculateRenewal()
} fmt.Printf("seconds: %s", r.renewalTime)
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),
))
} }
glog.V(3).Infof("setting a renewal notification on resource: %s, time: %s", r.resource, r.renewalTime) glog.V(3).Infof("setting a renewal notification on resource: %s, time: %s", r.resource, r.renewalTime)
// step: wait for the duration // step: wait for the duration