diff --git a/CONDUCT_CODE.md b/CONDUCT_CODE.md new file mode 100644 index 0000000..8ac706e --- /dev/null +++ b/CONDUCT_CODE.md @@ -0,0 +1,36 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open and +welcoming community, we pledge to respect all people who contribute through reporting issues, +posting feature requests, updating documentation, submitting pull requests or patches, and other +activities. + +We are committed to making participation in this project a harassment-free experience for everyone, +regardless of level of experience, gender, gender identity and expression, sexual orientation, +disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, without explicit + permission +* Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, +code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By +adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently +applying these principles to every aspect of managing this project. Project maintainers who do not +follow or enforce the Code of Conduct may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces when an individual is +representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an +issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), +version 1.2.0, available at +[http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9353c56 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,54 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +1. Ensure any install or build dependencies are removed before the end of the layer when doing a + build. +2. Update the README.md with details of changes to the interface, this includes new environment + variables, exposed ports, useful file locations and container parameters. +3. Increase the version numbers in any examples files and the README.md to the new version that this + Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). +4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. + +## Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open and +welcoming community, we pledge to respect all people who contribute through reporting issues, +posting feature requests, updating documentation, submitting pull requests or patches, and other +activities. + +We are committed to making participation in this project a harassment-free experience for everyone, +regardless of level of experience, gender, gender identity and expression, sexual orientation, +disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, without explicit + permission +* Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, +code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By +adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently +applying these principles to every aspect of managing this project. Project maintainers who do not +follow or enforce the Code of Conduct may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces when an individual is +representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an +issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), +version 1.2.0, available at +[http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) \ No newline at end of file diff --git a/README.md b/README.md index 8b987a7..360426d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ Usage of bin/vault-sidekick: -vmodule=: comma-separated list of pattern=N settings for file-filtered logging ``` +**Building** + +There is a Makefile in the base repository, so assuming you have make and go: # make + **Example Usage** The below is taken from a [Kubernetes](https://github.com/kubernetes/kubernetes) pod specification; diff --git a/config.go b/config.go index a823e68..dfc0ba2 100644 --- a/config.go +++ b/config.go @@ -31,6 +31,8 @@ type config struct { vaultAuthFile string // the authentication options vaultAuthOptions map[string]string + // the vault ca file + vaultCaFile string // the place to write the resources outputDir string // switch on dry run @@ -52,13 +54,14 @@ func init() { options.resources = new(VaultResources) options.vaultAuthOptions = map[string]string{VaultAuth: "token"} - 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.vaultAuthFile, "auth", "", "a configuration file in a json or yaml containing authentication arguments") - flag.StringVar(&options.outputDir, "output", getEnv("VAULT_OUTPUT", "/etc/secrets"), "the full path to write the protected resources (VAULT_OUTPUT if available)") + 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", "", "a configuration file in json or yaml containing authentication arguments") + flag.StringVar(&options.outputDir, "output", getEnv("VAULT_OUTPUT", "/etc/secrets"), "the full path to write resources or VAULT_OUTPUT") flag.BoolVar(&options.dryRun, "dryrun", false, "perform a dry run, printing the content to screen") flag.BoolVar(&options.tlsVerify, "tls-skip-verify", false, "whether to check and verify the vault service certificate") + flag.StringVar(&options.vaultCaFile, "ca-cert", "", "the path to the file container the CA used to verify the vault service") flag.DurationVar(&options.statsInterval, "stats", time.Duration(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") } // parseOptions ... validate the command line options and validates them @@ -87,5 +90,15 @@ func validateOptions(cfg *config) error { } } + if cfg.vaultCaFile != "" { + if exists, _ := fileExists(cfg.vaultCaFile); !exists { + return fmt.Errorf("the ca certificate file: %s does not exist", 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 nil } diff --git a/main.go b/main.go index 4d4aac3..a5c86e0 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,6 @@ import ( const ( Prog = "vault-sidekick" Version = "0.0.1" - GitSha = "" ) func main() { diff --git a/vault.go b/vault.go index 4ee1d20..2f6ae8d 100644 --- a/vault.go +++ b/vault.go @@ -18,7 +18,9 @@ package main import ( "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" "net/http" "time" @@ -46,6 +48,7 @@ type VaultService struct { config *api.Config // the token to authenticate with token string + // the listener channel - technically we only have the one listener but there a long term reasons for adding this listeners []chan VaultEvent // a channel to inform of a new resource to processor @@ -71,11 +74,10 @@ func NewVaultService(url string) (*VaultService, error) { service.config.Address = url service.listeners = make([]chan VaultEvent, 0) - // step: skip the cert verification if requested - if options.tlsVerify { - service.config.HttpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } + // step: setup and generate the tls options + service.config.HttpClient.Transport, err = service.getHttpTransport() + if err != nil { + return nil, err } // step: create the service processor channels @@ -99,6 +101,29 @@ func NewVaultService(url string) (*VaultService, error) { return service, nil } +func (r *VaultService) getHttpTransport() (*http.Transport, error) { + transport := &http.Transport{} + + // step: are we skip the tls verify? + if options.tlsVerify { + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + // step: are we loading a CA file + if options.vaultCaFile != "" { + // step: load the ca file + caCert, err := ioutil.ReadFile(options.vaultCaFile) + if err != nil { + return nil, fmt.Errorf("unable to read in the ca: %s, reason: %s", options.vaultCaFile, err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + // step: add the ca to the root + transport.TLSClientConfig.RootCAs = caCertPool + } + + return transport, nil +} + // AddListener ... add a listener to the events listeners func (r *VaultService) AddListener(ch chan VaultEvent) { glog.V(10).Infof("adding the listener: %v", ch) @@ -127,16 +152,17 @@ func (r *VaultService) vaultServiceProcessor() { for { select { // A new resource is being added to the service processor; - // - we retrieve the resource from vault - // - 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 + // - schedule the resource for retrieval case x := <-r.resourceChannel: glog.V(4).Infof("adding a resource into the service processor, resource: %s", x.resource) // step: add to the list of resources items = append(items, x) // step: push into the retrieval channel - retrieveChannel <- x - + r.scheduleNow(x, retrieveChannel) + // Retrieve a resource from vault + // - we retrieve the resource from vault + // - if we error attempting to retrieve the secret, we background and reschedule an attempt to add it + // - if ok, we grab the lease it and lease time, we setup a notification on renewal case x := <-retrieveChannel: // step: save the current lease if we have one leaseID := "" @@ -150,7 +176,7 @@ func (r *VaultService) vaultServiceProcessor() { if err != nil { glog.Errorf("failed to retrieve the resource: %s from vault, error: %s", x.resource, err) // reschedule the attempt for later - r.reschedule(x, retrieveChannel, 3, 10) + r.scheduleIn(x, retrieveChannel, 3, 10) break } @@ -180,7 +206,7 @@ func (r *VaultService) vaultServiceProcessor() { if time.Now().Before(x.leaseExpireTime) { glog.V(3).Infof("the lease on resource: %s has expired, we need to get a new lease", x.resource) // push into the retrieval channel and break - retrieveChannel <- x + r.scheduleNow(x, retrieveChannel) break } @@ -189,7 +215,7 @@ func (r *VaultService) vaultServiceProcessor() { // step: is the underlining resource even renewable? - otherwise we can just grab a new lease if !x.secret.Renewable { glog.V(10).Infof("the resource: %s is not renewable, retrieving a new lease instead", x.resource) - retrieveChannel <- x + r.scheduleNow(x, retrieveChannel) break } @@ -198,7 +224,7 @@ func (r *VaultService) vaultServiceProcessor() { if err != nil { glog.Errorf("failed to renew the resounce: %s for renewal, error: %s", x.resource, err) // reschedule the attempt for later - r.reschedule(x, renewChannel, 3, 10) + r.scheduleIn(x, renewChannel, 3, 10) break } } @@ -206,7 +232,7 @@ func (r *VaultService) vaultServiceProcessor() { // step: the option for this resource is not to renew the secret but regenerate a new secret if !x.resource.renewable { glog.V(4).Infof("resource: %s flagged as not renewable, shifting to regenerating the resource", x.resource) - retrieveChannel <- x + r.scheduleNow(x, retrieveChannel) break } @@ -255,15 +281,25 @@ func (r VaultService) authenticate(auth map[string]string) (string, error) { return secret, err } -// reschedule ... reschedules an event back into a channel after n seconds +// scheduleNow ... a helper method to perform an immediate reschedule into a channel +// rn : a pointer to the watched resource you wish to reschedule +// ch : the channel the resource should be placed into +func (r VaultService) scheduleNow(rn *watchedResource, ch chan *watchedResource) { + r.scheduleIn(rn, ch, 0, 0) +} + +// scheduleIn ... schedules 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) scheduleIn(rn *watchedResource, ch chan *watchedResource, min, max int) { go func(x *watchedResource) { glog.V(3).Infof("rescheduling the resource: %s, channel: %v", rn.resource, ch) - <-randomWait(min, max) + // step: are we doing a random wait? + if min > 0 { + <-randomWait(min, max) + } ch <- x }(rn) }