Skip to content

Commit

Permalink
Merge pull request #20 from nmaupu/use-client-jwt-k8s
Browse files Browse the repository at this point in the history
Using client's service account to make a k8s auth
  • Loading branch information
nmaupu authored Jul 20, 2020
2 parents 71acba0 + b6221bf commit 7caa81f
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 36 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

**Solution:** Use vault-secret custom resource to specify Vault server, path and keys and the operator will retrieve all the needed information from vault and push them into a Kubernetes secret resource ready to be used in the cluster.

# Note on upgrading to 1.0.1 onward

From version `1.0.1`, k8s auth method switches from using the local *service account* configured on the operator side to using the one from the client's namespace defined in the *custom resource*.
This is improving security but as a result, you will probably have to check your vault configuration is in adequation with this change.

# Installation

## Kubernetes version requirements
Expand Down
2 changes: 2 additions & 0 deletions deploy/crds/maupu.org_vaultsecrets_crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ spec:
type: string
role:
type: string
serviceAccount:
type: string
required:
- cluster
- role
Expand Down
1 change: 1 addition & 0 deletions deploy/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ rules:
- events
- configmaps
- secrets
- serviceaccounts
verbs:
- '*'
- apiGroups:
Expand Down
42 changes: 28 additions & 14 deletions pkg/apis/maupu/v1beta1/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,25 @@ package v1beta1
import (
"errors"

"github.com/nmaupu/vault-secret/pkg/k8sutils"
nmvault "github.com/nmaupu/vault-secret/pkg/vault"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// Get VaultAuthProvider implem from custom resource object
func (cr *VaultSecret) GetVaultAuthProvider() (nmvault.VaultAuthProvider, error) {
// BySecretKey allows sorting an array of VaultSecretSpecSecret by SecretKey
type BySecretKey []VaultSecretSpecSecret

// Len returns the len of a BySecretKey object
func (a BySecretKey) Len() int { return len(a) }

// Swap swaps two elements of a BySecretKey object
func (a BySecretKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// Less checks if a given SecretKey object is lexicographically inferior to another SecretKey object
func (a BySecretKey) Less(i, j int) bool { return a[i].SecretKey < a[j].SecretKey }

// GetVaultAuthProvider implem from custom resource object
func (cr *VaultSecret) GetVaultAuthProvider(c client.Client) (nmvault.VaultAuthProvider, error) {
// Checking order:
// - Token
// - AppRole
Expand All @@ -25,23 +39,23 @@ func (cr *VaultSecret) GetVaultAuthProvider() (nmvault.VaultAuthProvider, error)
cr.Spec.Config.Auth.AppRole.SecretID,
), nil
} else if cr.Spec.Config.Auth.Kubernetes.Role != "" {
// Retrieving token from the serviceAccount configured
saName := cr.Spec.Config.Auth.Kubernetes.ServiceAccount
if saName == "" {
saName = "default"
}

tok, err := k8sutils.GetTokenFromSA(c, cr.Namespace, saName)
if err != nil {
return nil, err
}

return nmvault.NewKubernetesProvider(
cr.Spec.Config.Auth.Kubernetes.Role,
cr.Spec.Config.Auth.Kubernetes.Cluster,
tok,
), nil
}

return nil, errors.New("Cannot find a way to authenticate, please choose between Token, AppRole or Kubernetes")
}

// BySecretKey allows sorting an array of VaultSecretSpecSecret by SecretKey
type BySecretKey []VaultSecretSpecSecret

// Len returns the len of a BySecretKey object
func (a BySecretKey) Len() int { return len(a) }

// Swap swaps two elements of a BySecretKey object
func (a BySecretKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// Less checks if a given SecretKey object is lexicographically inferior to another SecretKey object
func (a BySecretKey) Less(i, j int) bool { return a[i].SecretKey < a[j].SecretKey }
2 changes: 2 additions & 0 deletions pkg/apis/maupu/v1beta1/vaultsecret_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type VaultSecretSpecConfigAuth struct {
type KubernetesAuthType struct {
Role string `json:"role,required"`
Cluster string `json:"cluster,required"`
// ServiceAccount to use for authentication, using "default" if not provided
ServiceAccount string `json:"serviceAccount,omitempty"`
}

// AppRoleAuthType AppRole authentication type
Expand Down
16 changes: 8 additions & 8 deletions pkg/controller/vaultsecret/vaultsecret_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/source"
)

var _ reconcile.Reconciler = &ReconcileVaultSecret{}

const (
// OperatorAppName is the name of the operator
OperatorAppName = "vaultsecret-operator"
Expand All @@ -44,10 +46,10 @@ var (
// the same secret if it changes very fast (like with database KV backend or OTP)
secretsLastUpdateTime = make(map[string]time.Time)
secretsLastUpdateTimeMutex sync.Mutex
)

// LabelsFilter filters events on labels
var LabelsFilter map[string]string
// LabelsFilter filters events on labels
LabelsFilter map[string]string
)

// AddLabelFilter adds a label for filtering events
func AddLabelFilter(key, value string) {
Expand Down Expand Up @@ -150,8 +152,6 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error {
return nil
}

var _ reconcile.Reconciler = &ReconcileVaultSecret{}

// ReconcileVaultSecret reconciles a VaultSecret object
type ReconcileVaultSecret struct {
// This client, initialized using mgr.Client() above, is a split client
Expand Down Expand Up @@ -194,7 +194,7 @@ func (r *ReconcileVaultSecret) Reconcile(request reconcile.Request) (reconcile.R
now := time.Now()
if now.Sub(ti) > MinTimeMsBetweenSecretUpdate {
// Define a new Secret object from CR specs
secretFromCR, err := newSecretForCR(CRInstance)
secretFromCR, err := r.newSecretForCR(CRInstance)
if err != nil && secretFromCR == nil {
// An error occurred, requeue
reqLogger.Error(err, "An error occurred when creating secret from CR, requeuing.")
Expand Down Expand Up @@ -245,7 +245,7 @@ func (r *ReconcileVaultSecret) Reconcile(request reconcile.Request) (reconcile.R
return reconcile.Result{RequeueAfter: CRInstance.Spec.SyncPeriod.Duration}, err
}

func newSecretForCR(cr *maupuv1beta1.VaultSecret) (*corev1.Secret, error) {
func (r *ReconcileVaultSecret) newSecretForCR(cr *maupuv1beta1.VaultSecret) (*corev1.Secret, error) {
reqLogger := log.WithValues("func", "newSecretForCR")
operatorName := os.Getenv("OPERATOR_NAME")
if operatorName == "" {
Expand Down Expand Up @@ -280,7 +280,7 @@ func newSecretForCR(cr *maupuv1beta1.VaultSecret) (*corev1.Secret, error) {
}

// Authentication provider
authProvider, err := cr.GetVaultAuthProvider()
authProvider, err := cr.GetVaultAuthProvider(r.client)
if err != nil {
return nil, err
}
Expand Down
40 changes: 40 additions & 0 deletions pkg/k8sutils/resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package k8sutils

import (
"context"
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// GetTokenFromSA gets the token associated to the first secret located in a k8s' service account
func GetTokenFromSA(cli client.Client, ns, saName string) (string, error) {
if cli == nil {
return "", fmt.Errorf("Cannot get token from service account, k8s client is nil")
}

// Getting SA
saClient := &corev1.ServiceAccount{}
err := cli.Get(context.TODO(), types.NamespacedName{Name: saName, Namespace: ns}, saClient)
if err != nil && errors.IsNotFound(err) {
return "", fmt.Errorf("Unable to retrieve service account, err=%v", err)
}

if len(saClient.Secrets) == 0 {
return "", fmt.Errorf("No secret associated with the service account %s/%s", ns, saName)
}

// TODO See how to handle this slice of Secrets instead of taking the first one
saSecret := saClient.Secrets[0]
secret := &corev1.Secret{}
err = cli.Get(context.TODO(), types.NamespacedName{Name: saSecret.Name, Namespace: ns}, secret)
if err != nil {
return "", fmt.Errorf("Unable to retrieve the secret from the service account, err=%v", err)
}

// Finally, set the token
return string(secret.Data["token"]), nil
}
39 changes: 25 additions & 14 deletions pkg/vault/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,49 @@ package vault
import (
"crypto/tls"
"fmt"
vapi "github.com/hashicorp/vault/api"
"io/ioutil"
"net/http"
)

const (
KubernetesTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
vapi "github.com/hashicorp/vault/api"
)

var (
_ VaultAuthProvider = KubernetesProvider{}
)

// KubernetesProvider is a provider to authenticate using the Vault Kubernetes Auth Method plugin
// https://www.vaultproject.io/docs/auth/kubernetes
type KubernetesProvider struct {
Role string
// Role to use for the authentication
Role string
// Cluster is the path to use to call the login URL
Cluster string
// JWT token to use for the authentication
jwt string
}

func NewKubernetesProvider(role, cluster string) *KubernetesProvider {
// NewKubernetesProvider creates a new KubernetesProvider object
func NewKubernetesProvider(role, cluster, jwt string) *KubernetesProvider {
return &KubernetesProvider{
Role: role,
Cluster: cluster,
jwt: jwt,
}
}

// SetJWT set the jwt token to use for authentication
func (k *KubernetesProvider) SetJWT(jwt string) {
k.jwt = jwt
}

// Login - godoc
func (k KubernetesProvider) Login(c *VaultConfig) (*vapi.Client, error) {
log.Info("Authenticating using Kubernetes auth method")
reqLogger := log.WithValues("func", "KubernetesProvider.Login")
reqLogger.Info("Authenticating using Kubernetes auth method")

if k.jwt == "" {
return nil, fmt.Errorf("Token is empty, please provide a valid jwt token")
}

config := vapi.DefaultConfig()
config.Address = c.Address
config.HttpClient.Transport = &http.Transport{
Expand All @@ -46,14 +62,9 @@ func (k KubernetesProvider) Login(c *VaultConfig) (*vapi.Client, error) {
vclient.SetNamespace(vaultNamespace)
}

jwtData, err := ioutil.ReadFile(KubernetesTokenFile)
if err != nil {
return nil, err
}

data := map[string]interface{}{
"role": k.Role,
"jwt": string(jwtData),
"jwt": k.jwt,
}
s, err := vclient.Logical().Write(fmt.Sprintf("auth/%s/login", k.Cluster), data)
if err != nil {
Expand Down

0 comments on commit 7caa81f

Please sign in to comment.