Skip to content

Commit

Permalink
Bugfix: kubernetes login creates too many tokens (digitalis-io#32)
Browse files Browse the repository at this point in the history
* Bugfix: kubernetes login creates too many tokens
  • Loading branch information
digiserg committed Mar 2, 2022
1 parent 82810d0 commit 5e25072
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 122 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ If you're using Vault as backend you can also enable the Kubernetes Auth login m

You will need to add two additional environment variables to the `vals-operator` installation:

* VAULT_ROLE_ID: name of the Kubernetes Role you created granting access to read the secrets
* VAULT_AUTH_METHOD: set it to `kubernetes`
* *VAULT_ROLE_ID*: required to enable Kubernetes Login
* *VAULT_ADDR*: URL to the Vault server, ie, http://vault:8200
* *VAULT_LOGIN_USER* and *VAULT_LOGIN_PASSWORD*: to use `userpass` authentication (insecure, not recommended)
* *VAULT_APP_ROLE* and *VAULT_SECRET_ID*: to use `approle` authentication

# Usage

Expand Down
4 changes: 2 additions & 2 deletions charts/vals-operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ kubeVersion: ">= 1.19"
type: application

# Chart version
version: 0.4.0
version: 0.5.0
# Latest container tag
appVersion: "v0.5.0"
appVersion: "v0.6.0"

kubeVersion: '>= 1.19'
maintainers:
Expand Down
2 changes: 0 additions & 2 deletions charts/vals-operator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ vals-operator
=============
This helm chart installs the Digitalis Vals Operator to manage sync secrets from supported backends into Kubernetes

Current chart version is `0.3.0`


## Chart Values

Expand Down
1 change: 1 addition & 0 deletions controllers/valssecret_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func (r *ValsSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request)
secretYaml[k] = v.Ref
}
}

valsRendered, err := vals.Eval(secretYaml, vals.Options{})
if err != nil {
r.Log.Error(err, "Failed to get secrets from secrets store", "name", secret.Name)
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ require (
github.com/go-logr/logr v0.3.0
github.com/go-sql-driver/mysql v1.5.0
github.com/gocql/gocql v0.0.0-20211015133455-b225f9b53fa1
github.com/hashicorp/vault/api v1.0.4
github.com/hashicorp/vault/api v1.3.0
github.com/hashicorp/vault/api/auth/approle v0.1.1
github.com/hashicorp/vault/api/auth/kubernetes v0.1.0
github.com/hashicorp/vault/api/auth/userpass v0.1.0
github.com/lib/pq v1.2.0
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/variantdev/vals v0.15.0
k8s.io/api v0.20.2
k8s.io/apimachinery v0.20.2
Expand Down
114 changes: 99 additions & 15 deletions go.sum

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@ func main() {
os.Exit(1)
}

if os.Getenv("VAULT_TOKEN") != "" || os.Getenv("VAULT_AUTH_METHOD") != "" {
if os.Getenv("VAULT_AUTH_METHOD") != "" {
panic("Please remove the VAULT_AUTH_METHOD environment variable as it conflicts with `vals` backend engine")
}

if os.Getenv("VAULT_ADDR") != "" {
if err := vault.Start(); err != nil {
setupLog.Error(err, "unable authenticate with Vault")
os.Exit(1)
Expand Down
218 changes: 120 additions & 98 deletions vault/vault.go
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
package vault

import (
"context"
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"

"github.com/go-logr/logr"
"github.com/hashicorp/vault/api"
vault "github.com/hashicorp/vault/api"
vaultApprole "github.com/hashicorp/vault/api/auth/approle"
vaultKube "github.com/hashicorp/vault/api/auth/kubernetes"
vaultUserpass "github.com/hashicorp/vault/api/auth/userpass"

"github.com/go-logr/logr"
ctrl "sigs.k8s.io/controller-runtime"
)

const (
kubernetesJwtTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
kubernetesAuthURL = "auth/kubernetes/login"
kubernetesMountPath = "kubernetes"
approleMountPath = "approle"
userpassRoleMountPath = "userpass"
)

var log logr.Logger
var vaultURL string = getEnv("VAULT_ADDR", "http://vault:8200")

func fileExists(name string) (bool, error) {
_, err := os.Stat(name)
if err == nil {
return true, nil
}
if errors.Is(err, os.ErrNotExist) {
return false, nil
func getEnv(key string, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return false, err
return fallback
}

func vaultClient() (*api.Client, error) {
var vaultSkipVerify bool = false

vaultURL := os.Getenv("VAULT_ADDR")
if os.Getenv("VAULT_SKIP_VERIFY") != "" && os.Getenv("VAULT_SKIP_VERIFY") == "true" {
vaultSkipVerify = true
}
Expand All @@ -53,129 +53,151 @@ func vaultClient() (*api.Client, error) {
return api.NewClient(&api.Config{Address: vaultURL, HttpClient: httpClient})
}

func renewToken() (float64, error) {
var tokenTTL float64
client, err := vaultClient()
if err != nil {
return -1, err
}

r, err := client.Logical().Read("auth/token/lookup-self")
if err != nil {
return -1, err
func tokenRenewer(client *vault.Client) {
// Default
login := loginKube
if getEnv("VAULT_LOGIN_USER", "") != "" && getEnv("VAULT_LOGIN_PASSWORD", "") != "" {
login = loginUserPass
} else if getEnv("VAULT_APP_ROLE", "") != "" && getEnv("VAULT_SECRET_ID", "") != "" {
login = loginAppRole
}

t, err := r.TokenTTL()
if err != nil {
return -1, err
}
tokenTTL = t.Seconds()
for {
vaultLoginResp, err := login(client)
if err != nil {
log.Error(err, "unable to authenticate to Vault")
}
err = os.Setenv("VAULT_TOKEN", vaultLoginResp.Auth.ClientToken)
if err != nil {
log.Error(err, "Cannot set VAULT_TOKEN env variable")
return
}

/* FIXME: is this a sensible minimum? */
if tokenTTL > 120 {
return tokenTTL, nil
tokenErr := manageTokenLifecycle(client, vaultLoginResp)
if tokenErr != nil {
log.Error(err, "unable to start managing token lifecycle")
}
}
}

if ok, err := r.TokenIsRenewable(); !ok {
return tokenTTL, err
// Starts token lifecycle management. Returns only fatal errors as errors,
// otherwise returns nil so we can attempt login again.
func manageTokenLifecycle(client *vault.Client, token *vault.Secret) error {
renew := token.Auth.Renewable
if !renew {
log.Info("Token is not configured to be renewable. Re-attempting login.")
return nil
}

r, err = client.Logical().Write("/auth/token/renew-self", map[string]interface{}{
"token": os.Getenv("VAULT_TOKEN"),
watcher, err := client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{
Secret: token,
})
if err != nil {
return tokenTTL, err
return fmt.Errorf("unable to initialize new lifetime watcher for renewing auth token: %w", err)
}

tokenTTL = float64(r.Auth.LeaseDuration)
log.Info(fmt.Sprintf("Vault token renewed. New TTL is %v seconds", tokenTTL))
go watcher.Start()
defer watcher.Stop()

return tokenTTL, nil
for {
select {
case err := <-watcher.DoneCh():
if err != nil {
log.Error(err, "Failed to renew token")
return nil
}
// This occurs once the token has reached max TTL.
log.Info("Token can no longer be renewed. Re-attempting login.")
return nil

// Successfully completed renewal
case renewal := <-watcher.RenewCh():
log.Info("Successfully renewed vault token")
err = os.Setenv("VAULT_TOKEN", renewal.Secret.Auth.ClientToken)
if err != nil {
return err
}
}
}
}

func kubeLogin() error {
if ok, err := fileExists(kubernetesJwtTokenPath); !ok {
return err
}
if os.Getenv("VAULT_ROLE_ID") == "" {
return fmt.Errorf("VAULT_ROLE_ID not defined")
func loginKube(client *vault.Client) (*vault.Secret, error) {
roleId := getEnv("VAULT_ROLE_ID", "")
if roleId == "" {
return nil, fmt.Errorf("VAULT_ROLE_ID is not defined")
}
client, err := vaultClient()

kubeAuth, err := vaultKube.NewKubernetesAuth(roleId,
vaultKube.WithMountPath(getEnv("VAULT_KUBERNETES_MOUNT_POINT", kubernetesMountPath)))
if err != nil {
return err
return nil, err
}
fd, err := os.Open(kubernetesJwtTokenPath)
authInfo, err := client.Auth().Login(context.TODO(), kubeAuth)
if err != nil {
return err
return nil, fmt.Errorf("unable to login to kubernetes auth method: %w", err)
}
if authInfo == nil {
return nil, fmt.Errorf("no auth info was returned after login")
}
defer fd.Close()

jwt, err := ioutil.ReadAll(fd)
return authInfo, nil
}

func loginUserPass(client *vault.Client) (*vault.Secret, error) {
loginUser := getEnv("VAULT_LOGIN_USER", "")

userpassAuth, err := vaultUserpass.NewUserpassAuth(loginUser,
&vaultUserpass.Password{FromEnv: "VAULT_LOGIN_PASSWORD"},
vaultUserpass.WithMountPath(getEnv("VAULT_USERPASS_MOUNT_PATH", userpassRoleMountPath)))

if err != nil {
return err
return nil, fmt.Errorf("unable to initialize userpass auth method: %w", err)
}

params := map[string]interface{}{
"jwt": string(jwt),
"role": os.Getenv("VAULT_ROLE_ID"),
authInfo, err := client.Auth().Login(context.TODO(), userpassAuth)
if err != nil {
return nil, fmt.Errorf("unable to login to userpass auth method: %w", err)
}

var loginURL string
if os.Getenv("VAULT_KUBERNETES_PATH") == "" {
loginURL = kubernetesAuthURL
} else {
loginURL = fmt.Sprintf("auth/%s/login", os.Getenv("VAULT_KUBERNETES_PATH"))
if authInfo == nil {
return nil, fmt.Errorf("no auth info was returned after login")
}

r, err := client.Logical().Write(loginURL, params)
return authInfo, nil
}

func loginAppRole(client *vault.Client) (*vault.Secret, error) {
roleId := getEnv("VAULT_APP_ROLE", "")

appRoleAuth, err := vaultApprole.NewAppRoleAuth(roleId,
&vaultApprole.SecretID{FromEnv: "VAULT_SECRET_ID"},
vaultApprole.WithMountPath(getEnv("VAULT_APPROLE_MOUNT_PATH", approleMountPath)))

if err != nil {
return err
return nil, fmt.Errorf("unable to initialize approle auth method: %w", err)
}

client.SetToken(r.Auth.ClientToken)

err = os.Setenv("VAULT_TOKEN", r.Auth.ClientToken)
authInfo, err := client.Auth().Login(context.TODO(), appRoleAuth)
if err != nil {
return err
return nil, fmt.Errorf("unable to login to approle auth method: %w", err)
}
if authInfo == nil {
return nil, fmt.Errorf("no auth info was returned after login")
}

return nil
return authInfo, nil
}

// Start background process to check vault tokens
func Start() error {
log = ctrl.Log.WithName("vault")

if os.Getenv("VAULT_AUTH_METHOD") == "kubernetes" {
err := kubeLogin()
if err != nil {
return err
}
client, err := vaultClient()
if err != nil {
log.Error(err, "Error setting up vault client")
return err
}
go func() {
for {
tokenTTL, err := renewToken()
if err != nil {
log.Error(err, "Error renewing vault token")
time.Sleep(time.Second * 60)
continue
}

/* Wait for near the time when token expires */
if tokenTTL > 0 {
var sleepTime float64 = tokenTTL
if tokenTTL > 120 {
sleepTime = sleepTime - 120
}
time.Sleep(time.Second * time.Duration(sleepTime))
} else if tokenTTL == 0 {
log.Info("Vault token TTL is 0. It is a root token or set to not expire.")
return
} else {
time.Sleep(time.Second * 60)
}
}
}()
go tokenRenewer(client)

return nil
}

0 comments on commit 5e25072

Please sign in to comment.