Compare commits
1 commit
master
...
vault_user
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7c7bfac372 |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||||
|
|
29
.travis.yml
29
.travis.yml
|
@ -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
|
|
||||||
|
|
45
CHANGELOG.md
45
CHANGELOG.md
|
@ -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**
|
#### **Version v0.1.0**
|
||||||
|
|
||||||
##### FEATURES
|
##### FEATURES
|
||||||
|
|
||||||
BUGS
|
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
|
and the FILENAME.pem certificate
|
||||||
|
|
||||||
#### **Version v0.0.9-1**
|
#### **Version v0.0.9-1**
|
||||||
|
|
||||||
##### FEATURES
|
##### FEATURES
|
||||||
|
|
||||||
* Adding the ability to perform environment variable substituted of the resource path i.e.
|
* Adding the ability to perform environment variable substituted of the resource path i.e.
|
||||||
-resource=secret:/secrets/%ENV%/myset : %ENV% will substituted
|
-resource=secret:/secrets/%ENV%/myset : %ENV% will substituted
|
||||||
|
|
||||||
#### **Version v0.0.9**
|
#### **Version v0.0.9**
|
||||||
|
|
||||||
##### FEATURES
|
##### FEATURES
|
||||||
|
@ -55,7 +24,7 @@ BUGS
|
||||||
|
|
||||||
##### FEATURES
|
##### 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)
|
condfigurable timeout (default to 60s)
|
||||||
-cn=secret:platform/secrets/se2:fmt=yaml,exec=tests/runme.sh,update=1s
|
-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
|
* 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 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
|
* 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'
|
-cn=pki:platform/pki/issue/example-dot-com:common_name=blah.example.com,alt_names='me.example.com|ted.example.com'
|
||||||
|
|
||||||
##### BREAKING CHANGES:
|
##### 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
|
same as those for vault, i.e. for pki cn -> common_name
|
||||||
|
|
||||||
##### BUGS:
|
##### BUGS:
|
||||||
|
|
10
Dockerfile
10
Dockerfile
|
@ -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
|
||||||
|
|
10
Makefile
10
Makefile
|
@ -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"
|
||||||
|
|
74
README.md
74
README.md
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
66
config.go
66
config.go
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
140
formats.go
140
formats.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
21
generate.go
21
generate.go
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
60
main.go
|
@ -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,50 +59,14 @@ 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()
|
if err := processResource(evt.Resource, evt.Secret); err != nil {
|
||||||
defer toProcessLock.Unlock()
|
glog.Errorf("failed to write out the update, error: %s", err)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(evt)
|
}(evt)
|
||||||
case <-signalChannel:
|
case <-signalChannel:
|
||||||
|
|
7
services/demo-ns.yml
Normal file
7
services/demo-ns.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
kind: Namespace
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: demo
|
||||||
|
labels:
|
||||||
|
name: demo
|
58
services/demo-rc.yaml
Normal file
58
services/demo-rc.yaml
Normal 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
15
services/demo-secrets.yml
Normal 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
|
|
@ -1,3 +1,4 @@
|
||||||
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
|
@ -1 +0,0 @@
|
||||||
{"method": "approle", "role_id": "admin", "secret_id": "foobar"}
|
|
|
@ -1,3 +0,0 @@
|
||||||
method: approle
|
|
||||||
role_id: admin
|
|
||||||
secret_id: foobar
|
|
|
@ -1 +0,0 @@
|
||||||
{"method": "userpass", "username": "admin", "password": "foobar"}
|
|
|
@ -1 +0,0 @@
|
||||||
{"renewable": true, "leaseDuration": 60, "vaultAddr": "%invalid_url", "token": "foobar"}
|
|
|
@ -1 +0,0 @@
|
||||||
{"renewable": true, "leaseDuration": 60, "vaultAddr": "http://testurl:8080", "token": "foobar"}
|
|
|
@ -1 +0,0 @@
|
||||||
{"method": "token", "token": "foobar"}
|
|
|
@ -1,2 +0,0 @@
|
||||||
method: token
|
|
||||||
token: foobar
|
|
|
@ -1 +0,0 @@
|
||||||
{"method": "userpass", "username": "admin", "password": "foobar"}
|
|
|
@ -1,3 +0,0 @@
|
||||||
method: userpass
|
|
||||||
username: admin
|
|
||||||
password: foobar
|
|
61
utils.go
61
utils.go
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
115
utils_test.go
115
utils_test.go
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
90
vault.go
90
vault.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in a new issue