- added the csv output format

- adding the auth backends
- general cleanup of the code
- fixed up the tests
This commit is contained in:
Rohith 2015-09-21 11:31:12 +01:00
parent 096e365598
commit 5c06dc0e14
13 changed files with 283 additions and 324 deletions

202
LICENSE
View file

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -53,6 +53,10 @@ The above say's
- Apply the IAM policy, renew the policy when required and file the API tokens to .s3_creds in the /etc/secrets directory - Apply the IAM policy, renew the policy when required and file the API tokens to .s3_creds in the /etc/secrets directory
- Read the template at /etc/templates/db.tmpl, produce the content from Vault and write to /etc/credentials file - Read the template at /etc/templates/db.tmpl, produce the content from Vault and write to /etc/credentials file
**Authentication**
A authentication file can be specified
**Secret Renewals** **Secret Renewals**
The default behaviour of vault-sidekick is **not** to renew a lease, but to retrieve a new secret and allow the previous to The default behaviour of vault-sidekick is **not** to renew a lease, but to retrieve a new secret and allow the previous to

61
auth_userpass.go Normal file
View file

@ -0,0 +1,61 @@
/*
Copyright 2015 Home Office All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"github.com/hashicorp/vault/api"
"github.com/golang/glog"
)
// the userpass authentication plugin
type authUserPass struct {
// the vault client
client *api.Client
}
// auth token
type UserPassLogin struct {
// the password for the account
Password string `json:"password,omitempty"`
}
func newUserPass(client *api.Client) *authUserPass {
return &authUserPass{
client: client,
}
}
// create ... login with the username and password an
func (r authUserPass) create(username, password string) (*api.Secret, error) {
glog.V(10).Infof("using the userpass plugin, username: %s, password: %s", username, password)
req := r.client.NewRequest("POST", fmt.Sprintf("/v1/auth/userpass/login/%s", username))
// step: create the token request
if err := req.SetJSONBody(UserPassLogin{Password: password}); err != nil {
return nil, err
}
// step: make the request
resp, err := r.client.RawRequest(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// step: parse and return auth
return api.ParseSecret(resp.Body)
}

View file

@ -19,11 +19,8 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"net/url" "net/url"
"time" "time"
"github.com/golang/glog"
) )
// config ... the command line configuration // config ... the command line configuration
@ -32,10 +29,12 @@ type config struct {
vaultURL string vaultURL string
// the token to connect to vault with // the token to connect to vault with
vaultToken string vaultToken string
// a file / path containing a token for vault // a file containing the authenticate options
vaultTokenFile string vaultAuthFile string
// the authentication options
vaultAuthOptions map[string]string
// the place to write the resources // the place to write the resources
secretsDirectory string outputDir string
// whether of not to remove the token post connection // whether of not to remove the token post connection
deleteToken bool deleteToken bool
// switch on dry run // switch on dry run
@ -54,10 +53,10 @@ func init() {
options.resources = new(vaultResources) options.resources = new(vaultResources)
flag.StringVar(&options.vaultURL, "vault", getEnv("VAULT_ADDR", "https://127.0.0.1:8200"), "the url the vault service is running behind (VAULT_ADDR if available)") flag.StringVar(&options.vaultURL, "vault", getEnv("VAULT_ADDR", "https://127.0.0.1:8200"), "the url the vault service is running behind (VAULT_ADDR if available)")
flag.StringVar(&options.vaultToken, "token", "", "the token used to authenticate to teh vault service (VAULT_TOKEN if available)") flag.StringVar(&options.vaultToken, "token", "", "the token used to authenticate to teh vault service (VAULT_TOKEN if available)")
flag.StringVar(&options.vaultTokenFile, "tokenfile", getEnv("VAULT_TOKEN_FILE", ""), "the full path to file containing the vault token used for authentication (VAULT_TOKEN_FILE if available)") flag.StringVar(&options.vaultAuthFile, "auth", "", "a configuration file in a json or yaml containing authentication arguments")
flag.StringVar(&options.secretsDirectory, "output", getEnv("VAULT_OUTPUT", "/etc/secrets"), "the full path to write the protected resources (VAULT_OUTPUT if available)") flag.StringVar(&options.outputDir, "output", getEnv("VAULT_OUTPUT", "/etc/secrets"), "the full path to write the protected resources (VAULT_OUTPUT if available)")
flag.BoolVar(&options.deleteToken, "delete-token", false, "once the we have connected to vault, delete the token file from disk") flag.BoolVar(&options.deleteToken, "delete-token", false, "once the we have connected to vault, delete the token file from disk")
flag.BoolVar(&options.dryRun, "dry-run", 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.DurationVar(&options.statsInterval, "stats", time.Duration(5)*time.Minute, "the interval to produce statistics on the accessed resources") flag.DurationVar(&options.statsInterval, "stats", time.Duration(5)*time.Minute, "the interval to produce statistics on the accessed resources")
flag.Var(options.resources, "cn", "a resource to retrieve and monitor from vault (e.g. pki:name:cert.name, secret:db_password, aws:s3_backup)") flag.Var(options.resources, "cn", "a resource to retrieve and monitor from vault (e.g. pki:name:cert.name, secret:db_password, aws:s3_backup)")
} }
@ -72,14 +71,10 @@ func parseOptions() error {
// validateOptions ... parses and validates the command line options // validateOptions ... parses and validates the command line options
func validateOptions(cfg *config) error { func validateOptions(cfg *config) error {
// step: validate the vault url // step: validate the vault url
url, err := url.Parse(cfg.vaultURL) _, err := url.Parse(cfg.vaultURL)
if err != nil { if err != nil {
return fmt.Errorf("invalid vault url: '%s' specified", cfg.vaultURL) return fmt.Errorf("invalid vault url: '%s' specified", cfg.vaultURL)
} }
// step: ensure the protocol scheme
if url.Scheme != "https" {
glog.Warningf("protocol scheme: %s is not secure and should be https", url.Scheme)
}
// step: check if the token is in the VAULT_TOKEN var // step: check if the token is in the VAULT_TOKEN var
if cfg.vaultToken == "" { if cfg.vaultToken == "" {
@ -87,25 +82,19 @@ func validateOptions(cfg *config) error {
} }
// step: ensure we have a token // step: ensure we have a token
if cfg.vaultToken == "" && cfg.vaultTokenFile == "" { if cfg.vaultToken == "" && cfg.vaultAuthFile == "" {
return fmt.Errorf("you have not either a token or a token file to authenticate with") return fmt.Errorf("you have not either a token or a authentication file to authenticate to vault with")
} }
// step: read in the token if required // step: read in the token if required
if cfg.vaultTokenFile != "" { if cfg.vaultAuthFile != "" {
exists, err := fileExists(cfg.vaultTokenFile) if exists, _ := fileExists(cfg.vaultAuthFile); !exists {
if !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.vaultTokenFile)
} }
if err != nil {
return fmt.Errorf("unable to check for token file: %s, error: %s", cfg.vaultTokenFile, err) if options.vaultAuthOptions, err = readConfigFile(options.vaultAuthFile); err != nil {
return fmt.Errorf("unable to read in authentication options from: %s, error: %s", cfg.vaultAuthFile, err)
} }
// step: read in the token
content, err := ioutil.ReadFile(cfg.vaultTokenFile)
if err != nil {
return fmt.Errorf("unable to read in token from file: %s, error: %s", cfg.vaultTokenFile, err)
}
cfg.vaultToken = string(content)
} }
return nil return nil

View file

@ -24,11 +24,10 @@ import (
func TestValidateOptions(t *testing.T) { func TestValidateOptions(t *testing.T) {
cfg := config{ cfg := config{
vaultToken: "",
vaultURL: "https://127.0.0.1:8200", vaultURL: "https://127.0.0.1:8200",
vaultToken: "d0ds09dis09ids09ifsd",
} }
assert.NotNil(t, validateOptions(&cfg))
cfg.vaultToken = "dkskdkskdsjdkjs"
assert.Nil(t, validateOptions(&cfg)) assert.Nil(t, validateOptions(&cfg))
cfg.vaultURL = "https://127.0.0.1" cfg.vaultURL = "https://127.0.0.1"
assert.Nil(t, validateOptions(&cfg)) assert.Nil(t, validateOptions(&cfg))

16
main.go
View file

@ -37,9 +37,9 @@ func main() {
} }
// step: create a client to vault // step: create a client to vault
vault, err := newVaultService(options.vaultURL, options.vaultToken) vault, err := newVaultService(options.vaultURL)
if err != nil { if err != nil {
glog.Errorf("failed to create a vault client, error: %s", err) showUsage("unable to create the vault client: %s", err)
} }
// step: setup the termination signals // step: setup the termination signals
@ -47,7 +47,7 @@ func main() {
signal.Notify(signalChannel, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) signal.Notify(signalChannel, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
// step: create a channel to receive events upon and add our resources for renewal // step: create a channel to receive events upon and add our resources for renewal
ch := make(vaultEventsChannel, 10) ch := make(chan vaultResourceEvent, 10)
for _, rn := range options.resources.items { for _, rn := range options.resources.items {
// step: valid the resource // step: valid the resource
@ -80,7 +80,7 @@ func processResource(rn *vaultResource, data map[string]interface{}) error {
// step: determine the resource path // step: determine the resource path
resourcePath := rn.filename() resourcePath := rn.filename()
if !strings.HasPrefix(resourcePath, "/") { if !strings.HasPrefix(resourcePath, "/") {
resourcePath = fmt.Sprintf("%s/%s", options.secretsDirectory, resourcePath) resourcePath = fmt.Sprintf("%s/%s", options.outputDir, resourcePath)
} }
// step: get the output format // step: get the output format
@ -120,13 +120,19 @@ func processResource(rn *vaultResource, data map[string]interface{}) error {
} }
return nil return nil
case "csv":
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s,%s\n", key, val))
}
content = buf.Bytes()
case "txt": case "txt":
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 {
filename := fmt.Sprintf("%s.%s", resourcePath, suffix) filename := fmt.Sprintf("%s.%s", resourcePath, suffix)
// step: write the file
if err := writeFile(filename, []byte(fmt.Sprintf("%s", content))); err != nil { if err := writeFile(filename, []byte(fmt.Sprintf("%s", 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",
rn, suffix, filename, err) rn, suffix, filename, err)

View file

@ -1,17 +0,0 @@
/*
Copyright 2015 Home Office All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main

3
tests/auth_file.yml Normal file
View file

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

View file

@ -22,6 +22,10 @@ import (
"math/rand" "math/rand"
"os" "os"
"time" "time"
"io/ioutil"
"encoding/json"
"gopkg.in/yaml.v2"
"path"
) )
func init() { func init() {
@ -40,12 +44,23 @@ func showUsage(message string, args ...interface{}) {
os.Exit(0) os.Exit(0)
} }
// randomWait ... wait for a random amout of time // randomWait ... wait for a random amount of time
// min : the minimum amount of time willing to wait
// max : the maximum amount of time willing to wait
func randomWait(min, max int) <-chan time.Time { func randomWait(min, max int) <-chan time.Time {
return time.After(time.Duration(getRandomWithin(min, max)) * time.Second) return time.After(time.Duration(getRandomWithin(min, max)) * time.Second)
} }
// hasKey ... checks to see if a key is present
// key : the key we are looking for
// data : a map of strings to something we are looking at
func hasKey(key string, data map [string]interface{}) bool {
_, found := data[key]
return found
}
// getKeys ... retrieve a list of keys from the map // getKeys ... retrieve a list of keys from the map
// data : the map which you wish to extract the keys from
func getKeys(data map[string]interface{}) []string { func getKeys(data map[string]interface{}) []string {
var list []string var list []string
for key := range data { for key := range data {
@ -54,6 +69,57 @@ func getKeys(data map[string]interface{}) []string {
return list return list
} }
// readConfigFile ... read in a configuration file
// filename : the path to the file
func readConfigFile(filename string) (map[string]string, error) {
suffix := path.Ext(filename)
switch suffix {
case ".json":
return readJsonFile(filename)
case ".yaml":
return readYamlFile(filename)
case ".yml":
return readYamlFile(filename)
}
return nil, fmt.Errorf("unsupported config file format: %s", suffix)
}
// readJsonFile ... read in and unmarshall the data into a map
// filename : the path to the file container the json data
func readJsonFile(filename string) (map[string]string, error) {
data := make(map[string]string, 0)
content, err := ioutil.ReadFile(filename)
if err != nil {
return data, err
}
// unmarshall the data
err = json.Unmarshal(content, data)
if err != nil {
return data, err
}
return data, nil
}
// readYamlFile ... read in and unmarshall the data into a map
// filename : the path to the file container the yaml data
func readYamlFile(filename string) (map[string]string, error) {
data := make(map[string]string, 0)
content, err := ioutil.ReadFile(filename)
if err != nil {
return data, err
}
// unmarshall the data
err = yaml.Unmarshal(content, data)
if err != nil {
return data, err
}
return data, nil
}
// randomInt ... generate a random integer between min and max // randomInt ... generate a random integer between min and max
// min : the smallest number we can accept // min : the smallest number we can accept
// max : the largest number we can accept // max : the largest number we can accept

103
vault.go
View file

@ -45,52 +45,9 @@ type vaultResourceEvent struct {
secret map[string]interface{} secret map[string]interface{}
} }
// a channel of events
type vaultEventsChannel chan vaultResourceEvent
// watchedResource ... is a resource which is being watched - i.e. when the item is coming up for renewal
// lets grab it and renew the lease
type watchedResource struct {
// the upstream listener to the event
listener vaultEventsChannel
// the resource itself
resource *vaultResource
// the last time the resource was retrieved
lastUpdated time.Time
// the time which the lease expires
leaseExpireTime time.Time
// the duration until we next time to renew lease
renewalTime time.Duration
// the secret
secret *api.Secret
}
// notifyOnRenewal ... creates a trigger and notifies when a resource is up for renewal
func (r *watchedResource) notifyOnRenewal(ch chan *watchedResource) {
go func() {
// step: check if the resource has a pre-configured renewal time
r.renewalTime = r.resource.update
// step: if the answer is no, we set the notification between 80-95% of the lease time of the secret
if r.renewalTime <= 0 {
glog.V(10).Infof("calculating the renewal between 80-95 pcent of lease time: %d seconds", r.secret.LeaseDuration)
r.renewalTime = time.Duration(getRandomWithin(
int(float64(r.secret.LeaseDuration)*0.8),
int(float64(r.secret.LeaseDuration)*0.95))) * time.Second
}
glog.V(3).Infof("setting a renewal notification on resource: %s, time: %s", r.resource, r.renewalTime)
// step: wait for the duration
<-time.After(r.renewalTime)
// step: send the notification on the renewal channel
ch <- r
}()
}
// 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
// token : the token to use when speaking to vault func newVaultService(url string) (*vaultService, error) {
func newVaultService(url, token string) (*vaultService, error) {
var err error var err error
glog.Infof("creating a new vault client: %s", url) glog.Infof("creating a new vault client: %s", url)
@ -108,8 +65,16 @@ func newVaultService(url, token string) (*vaultService, error) {
return nil, err return nil, err
} }
// step: are we using a token? or do we need to authenticate and grab a token
if options.vaultToken == "" {
options.vaultToken, err = service.authenticate(options.vaultAuthOptions)
if err != nil {
return nil, err
}
}
// step: set the token for the client // step: set the token for the client
service.client.SetToken(token) service.client.SetToken(options.vaultToken)
// step: start the service processor off // step: start the service processor off
service.vaultServiceProcessor() service.vaultServiceProcessor()
@ -172,7 +137,7 @@ func (r vaultService) vaultServiceProcessor() {
x.notifyOnRenewal(renewChannel) x.notifyOnRenewal(renewChannel)
// step: update the upstream consumers // step: update the upstream consumers
r.upstream(x, x.secret) r.upstream(x)
// 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
@ -221,7 +186,7 @@ func (r vaultService) vaultServiceProcessor() {
x.notifyOnRenewal(renewChannel) x.notifyOnRenewal(renewChannel)
// step: update any listener upstream // step: update any listener upstream
r.upstream(x, x.secret) r.upstream(x)
case lease := <-revokeChannel: case lease := <-revokeChannel:
@ -242,6 +207,42 @@ func (r vaultService) vaultServiceProcessor() {
}() }()
} }
// authenticate ... we need to authenticate to teh vault to grab a toke
// auth : a map containing the options required for authentication
func (r vaultService) authenticate(auth map[string]string) (string, error) {
var secret *api.Secret
var err error
plugin, _ := auth["method"]
switch plugin {
case "userpass":
// step: get the options for this plugin
username, _ := auth["username"]
password, _ := auth["password"]
secret, err = newUserPass(r.client).create(username, password)
default:
return "", fmt.Errorf("unsupported authentication plugin: %s", plugin)
}
// step: was there an error?
if err != nil {
return "", err
}
// step: do we have auth information
if secret.Auth == nil {
return "", fmt.Errorf("invalid authentication response, no auth response")
}
// step: return the client token
return secret.Auth.ClientToken, nil
}
// reschedule ... reschedules an event back into a channel after n seconds
// rn : a pointer to the watched resource you wish to reschedule
// ch : the channel the resource should be placed into
// min : the minimum amount of time i'm willing to wait
// max : the maximum amount of time i'm willing to wait
func (r vaultService) reschedule(rn *watchedResource, ch chan *watchedResource, min, max int) { func (r vaultService) reschedule(rn *watchedResource, ch chan *watchedResource, min, max int) {
go func(x *watchedResource) { go func(x *watchedResource) {
glog.V(3).Infof("rescheduling the resource: %s, channel: %s", rn.resource, ch) glog.V(3).Infof("rescheduling the resource: %s, channel: %s", rn.resource, ch)
@ -250,13 +251,15 @@ func (r vaultService) reschedule(rn *watchedResource, ch chan *watchedResource,
}(rn) }(rn)
} }
func (r vaultService) upstream(item *watchedResource, s *api.Secret) { // upstream ... the resource has changed thus we notify the upstream listener
// item : the item which has changed
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
go func() { go func() {
glog.V(6).Infof("sending the event for resource: %s upstream to listener: %v", item.resource, item.listener) glog.V(6).Infof("sending the event for resource: %s upstream to listener: %v", item.resource, item.listener)
item.listener <- vaultResourceEvent{ item.listener <- vaultResourceEvent{
resource: item.resource, resource: item.resource,
secret: s.Data, secret: item.secret.Data,
} }
}() }()
} }
@ -344,7 +347,7 @@ func (r vaultService) get(rn *watchedResource) (err error) {
// watch ... add a watch on a resource and inform, renew which required and inform us when // watch ... add a watch on a resource and inform, renew which required and inform us when
// the resource is ready // the resource is ready
func (r *vaultService) watch(rn *vaultResource, ch vaultEventsChannel) { func (r *vaultService) watch(rn *vaultResource, ch chan vaultResourceEvent) {
glog.V(6).Infof("adding the resource: %s, listener: %v to service processor", rn, ch) glog.V(6).Infof("adding the resource: %s, listener: %v to service processor", rn, ch)
r.resourceChannel <- &watchedResource{ r.resourceChannel <- &watchedResource{

View file

@ -172,5 +172,5 @@ func (r vaultResource) filename() string {
// String ... a string representation of the struct // String ... a string representation of the struct
func (r vaultResource) String() string { func (r vaultResource) String() string {
return fmt.Sprintf("%s/%s (%s|%t|%t)", r.resource, r.name, r.update, r.renewable, r.revoked) return fmt.Sprintf("%s/%s", r.resource, r.name)
} }

View file

@ -1,17 +0,0 @@
/*
Copyright 2015 Home Office All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main

64
watched_resource.go Normal file
View file

@ -0,0 +1,64 @@
/*
Copyright 2015 Home Office All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"time"
"github.com/golang/glog"
"github.com/hashicorp/vault/api"
)
// watchedResource ... is a resource which is being watched - i.e. when the item is coming up for renewal
// lets grab it and renew the lease
type watchedResource struct {
// the upstream listener to the event
listener chan vaultResourceEvent
// the resource itself
resource *vaultResource
// the last time the resource was retrieved
lastUpdated time.Time
// the time which the lease expires
leaseExpireTime time.Time
// the duration until we next time to renew lease
renewalTime time.Duration
// the secret
secret *api.Secret
}
// notifyOnRenewal ... creates a trigger and notifies when a resource is up for renewal
func (r *watchedResource) notifyOnRenewal(ch chan *watchedResource) {
go func() {
// step: check if the resource has a pre-configured renewal time
r.renewalTime = r.resource.update
// step: if the answer is no, we set the notification between 80-95% of the lease time of the secret
if r.renewalTime <= 0 {
glog.V(10).Infof("calculating the renewal between 80-95 pcent of lease time: %d seconds", r.secret.LeaseDuration)
r.renewalTime = time.Duration(getRandomWithin(
int(float64(r.secret.LeaseDuration)*0.8),
int(float64(r.secret.LeaseDuration)*0.95))) * time.Second
}
glog.V(3).Infof("setting a renewal notification on resource: %s, time: %s", r.resource, r.renewalTime)
// step: wait for the duration
<-time.After(r.renewalTime)
// step: send the notification on the renewal channel
ch <- r
}()
}