Skip to content

Commit

Permalink
Create a new client on each request (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
hectorhuertas committed Oct 31, 2019
1 parent 1c01009 commit 2d19ad9
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 162 deletions.
8 changes: 5 additions & 3 deletions README.md
@@ -1,7 +1,8 @@
# vault-kube-aws-credentials

This is a specialised sidecar for Kubernetes pods that fetches and serves AWS credentials from Hashicorp's Vault on behalf of a
service account.
This is a specialised sidecar for Kubernetes pods that fetches AWS credentials from [Vault](https://www.vaultproject.io) and serves them via http to be consumed by the AWS SDK.

It is intended to be used with a Vault setup like [this](https://github.com/utilitywarehouse/vault-manifests). The sidecar logs with Vault using its Kubernetes Service Account, requests credentials from an AWS secrets engine and serves the acquired credentials via http. The sidecar detects the lease expiration and keeps the served credentials updated and valid.

## Usage

Expand All @@ -21,7 +22,8 @@ Optional:
- `VKAC_AWS_SECRET_BACKEND_PATH`: path of the aws secret backend (default: `aws`)
- `VKAC_KUBE_AUTH_BACKEND_PATH`: path of the kubernetes auth backend (default: `kubernetes`)
- `VKAC_KUBE_SA_TOKEN_PATH`: path to a file containing the Kubernetes service account token (default: `/var/run/secrets/kubernetes.io/serviceaccount/token`)
- `VKAC_LISTEN_ADDRESS`: address to bind to (default: `127.0.0.1:8000`)
- `VKAC_LISTEN_HOST`: host to bind to (default: `127.0.0.1`)
- `VKAC_LISTEN_PORT`: port to bind to (default: `8000`)

Additionally, you can use any of the [environment variables supported by the Vault
client](https://www.vaultproject.io/docs/commands/#environment-variables), most applicably:
Expand Down
122 changes: 72 additions & 50 deletions credentials.go
Expand Up @@ -28,67 +28,89 @@ type lease struct {

// CredentialsRenewer renews the credentials
type CredentialsRenewer struct {
Client *vault.Client
Credentials chan<- *AWSCredentials
Errors chan<- error
Rand *rand.Rand
Role string
SecretsBackend string
Credentials chan<- *AWSCredentials
Errors chan<- error
AwsPath string
AwsRole string
KubePath string
KubeRole string
TokenPath string
}

// Start the renewer
func (cr *CredentialsRenewer) Start() {
for {
// A token is required to authenticate, this should be set by the login renewer
if len(cr.Client.Token()) > 0 {
l := lease{}
// Create Vault client
client, err := vault.NewClient(vault.DefaultConfig())
if err != nil {
cr.Errors <- err
return
}

// Get a credentials secret from vault for the role
secret, err := cr.Client.Logical().Read(cr.SecretsBackend + "/sts/" + cr.Role)
if err != nil {
cr.Errors <- err
return
}
// Login into Vault via kube SA
jwt, err := ioutil.ReadFile(cr.TokenPath)
if err != nil {
cr.Errors <- err
return
}
secret, err := client.Logical().Write("auth/"+cr.KubePath+"/login", map[string]interface{}{
"jwt": string(jwt),
"role": cr.KubeRole,
})
if err != nil {
cr.Errors <- err
return
}
client.SetToken(secret.Auth.ClientToken)

// Convert the secret's lease duration into a time.Duration
leaseDuration := time.Duration(secret.LeaseDuration) * time.Second
// Get a credentials secret from vault for the role
secret, err = client.Logical().Read(cr.AwsPath + "/sts/" + cr.AwsRole)
if err != nil {
cr.Errors <- err
return
}

// Get the expiration date of the lease from vault
req := cr.Client.NewRequest("PUT", "/v1/sys/leases/lookup")
if err = req.SetJSONBody(map[string]interface{}{
"lease_id": secret.LeaseID,
}); err != nil {
cr.Errors <- err
return
}
resp, err := cr.Client.RawRequest(req)
if err == nil {
defer func() {
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
}()
} else {
cr.Errors <- err
return
}
err = json.NewDecoder(resp.Body).Decode(&l)
if err != nil {
cr.Errors <- err
return
}
// Convert the secret's lease duration into a time.Duration
leaseDuration := time.Duration(secret.LeaseDuration) * time.Second

log.Printf("new aws credentials: %s, expiring %s", secret.Data["access_key"].(string), l.Data.ExpireTime.Format("2006-01-02 15:04:05"))
// Get the expiration date of the lease from vault
l := lease{}
req := client.NewRequest("PUT", "/v1/sys/leases/lookup")
if err = req.SetJSONBody(map[string]interface{}{
"lease_id": secret.LeaseID,
}); err != nil {
cr.Errors <- err
return
}
resp, err := client.RawRequest(req)
if err == nil {
defer func() {
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
}()
} else {
cr.Errors <- err
return
}
err = json.NewDecoder(resp.Body).Decode(&l)
if err != nil {
cr.Errors <- err
return
}

// Send the new credentials down the channel
cr.Credentials <- &AWSCredentials{
AccessKeyID: secret.Data["access_key"].(string),
SecretAccessKey: secret.Data["secret_key"].(string),
Token: secret.Data["security_token"].(string),
Expiration: l.Data.ExpireTime,
}
log.Printf("new aws credentials: %s, expiring %s", secret.Data["access_key"].(string), l.Data.ExpireTime.Format("2006-01-02 15:04:05"))

// Sleep until its time to renew the creds
time.Sleep(sleepDuration(leaseDuration, cr.Rand))
// Send the new credentials down the channel
cr.Credentials <- &AWSCredentials{
AccessKeyID: secret.Data["access_key"].(string),
SecretAccessKey: secret.Data["secret_key"].(string),
Token: secret.Data["security_token"].(string),
Expiration: l.Data.ExpireTime,
}
// Used to generate random values for sleeping between renewals
random := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))

// Sleep until its time to renew the creds
time.Sleep(sleepDuration(leaseDuration, random))
}
}
56 changes: 0 additions & 56 deletions login.go

This file was deleted.

83 changes: 34 additions & 49 deletions main.go
Expand Up @@ -2,93 +2,78 @@ package main

import (
"log"
"math/rand"
"os"
"time"

vault "github.com/hashicorp/vault/api"
)

var (
listenAddress = os.Getenv("VKAC_LISTEN_ADDRESS")
awsSecretBackend = os.Getenv("VKAC_AWS_SECRET_BACKEND")
awsSecretRole = os.Getenv("VKAC_AWS_SECRET_ROLE")
kubeAuthBackend = os.Getenv("VKAC_KUBE_AUTH_BACKEND")
kubeAuthRole = os.Getenv("VKAC_KUBE_AUTH_ROLE")
kubeTokenPath = os.Getenv("VKAC_KUBE_SA_TOKEN_PATH")
awsPath = os.Getenv("VKAC_AWS_SECRET_BACKEND_PATH")
awsRole = os.Getenv("VKAC_AWS_SECRET_ROLE")
kubePath = os.Getenv("VKAC_KUBE_AUTH_BACKEND_PATH")
kubeRole = os.Getenv("VKAC_KUBE_AUTH_ROLE")
tokenPath = os.Getenv("VKAC_KUBE_SA_TOKEN_PATH")
listenHost = os.Getenv("VKAC_LISTEN_HOST")
listenPort = os.Getenv("VKAC_LISTEN_PORT")
)

func validate() {
if len(awsSecretBackend) == 0 {
awsSecretBackend = "aws"
}

if len(awsSecretRole) == 0 {
if len(awsRole) == 0 {
log.Fatalf("error: must set VKAC_AWS_SECRET_ROLE")
}

if len(kubeAuthRole) == 0 {
if len(kubeRole) == 0 {
log.Fatalf("error: must set VKAC_KUBE_AUTH_ROLE")
}

if len(kubeAuthBackend) == 0 {
kubeAuthBackend = "kubernetes"
if len(awsPath) == 0 {
awsPath = "aws"
}

if len(kubeTokenPath) == 0 {
kubeTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
if len(kubePath) == 0 {
kubePath = "kubernetes"
}

if len(listenAddress) == 0 {
listenAddress = "127.0.0.1:8000"
if len(tokenPath) == 0 {
tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
}
}

func main() {
validate()
if len(listenHost) == 0 {
listenHost = "127.0.0.1"
}

// Vault client
client, err := vault.NewClient(vault.DefaultConfig())
if err != nil {
log.Fatalf("error creating vault client: %v", err)
if len(listenPort) == 0 {
listenPort = "8000"
}
}

// Used to generate random values for sleeping between renewals
random := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
func main() {
validate()

// Channel for goroutines to send errors to
errors := make(chan error)

// This channel communicates changes in credentials between the credentials renewer and the webserver
creds := make(chan *AWSCredentials)

// Login, and stay logged in
loginRenewer := &LoginRenewer{
AuthBackend: kubeAuthBackend,
Client: client,
Errors: errors,
Role: kubeAuthRole,
Rand: random,
TokenPath: kubeTokenPath,
}
listenAddress := listenHost + ":" + listenPort

// Keep credentials up to date
credentialsRenewer := &CredentialsRenewer{
Client: client,
Credentials: creds,
Errors: errors,
Role: awsSecretRole,
Rand: random,
SecretsBackend: awsSecretBackend,
Credentials: creds,
Errors: errors,
AwsPath: awsPath,
AwsRole: awsRole,
KubePath: kubePath,
KubeRole: kubeRole,
TokenPath: tokenPath,
}

// Serve the credentials
webserver := &Webserver{
Credentials: creds,
Errors: errors,
Credentials: creds,
Errors: errors,
ListenAddress: listenAddress,
}

go loginRenewer.Start()
go credentialsRenewer.Start()
go webserver.Start()

Expand Down
9 changes: 5 additions & 4 deletions webserver.go
Expand Up @@ -9,8 +9,9 @@ import (

// Webserver serves the credentials
type Webserver struct {
Credentials <-chan *AWSCredentials
Errors chan<- error
Credentials <-chan *AWSCredentials
Errors chan<- error
ListenAddress string
}

// Start the webserver
Expand Down Expand Up @@ -39,6 +40,6 @@ func (w *Webserver) Start() {
enc.Encode(latestCredentials)
})

log.Printf("Listening on %s", listenAddress)
w.Errors <- http.ListenAndServe(listenAddress, nil)
log.Printf("Listening on %s", w.ListenAddress)
w.Errors <- http.ListenAndServe(w.ListenAddress, nil)
}

0 comments on commit 2d19ad9

Please sign in to comment.