| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599 |
- /*
- 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 vault
- import (
- "context"
- "crypto/tls"
- "crypto/x509"
- "errors"
- "fmt"
- "io/ioutil"
- "net/http"
- "os"
- "strings"
- "github.com/go-logr/logr"
- vault "github.com/hashicorp/vault/api"
- corev1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/types"
- ctrl "sigs.k8s.io/controller-runtime"
- kclient "sigs.k8s.io/controller-runtime/pkg/client"
- esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
- esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
- "github.com/external-secrets/external-secrets/pkg/provider"
- "github.com/external-secrets/external-secrets/pkg/provider/schema"
- )
- var (
- _ provider.Provider = &connector{}
- _ provider.SecretsClient = &client{}
- )
- const (
- serviceAccTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
- errVaultStore = "received invalid Vault SecretStore resource: %w"
- errVaultClient = "cannot setup new vault client: %w"
- errVaultCert = "cannot set Vault CA certificate: %w"
- errReadSecret = "cannot read secret data from Vault: %w"
- errAuthFormat = "cannot initialize Vault client: no valid auth method specified: %w"
- errDataField = "failed to find data field"
- errJSONUnmarshall = "failed to unmarshall JSON"
- errSecretFormat = "secret data not in expected format"
- errVaultToken = "cannot parse Vault authentication token: %w"
- errVaultReqParams = "cannot set Vault request parameters: %w"
- errVaultRequest = "error from Vault request: %w"
- errVaultResponse = "cannot parse Vault response: %w"
- errServiceAccount = "cannot read Kubernetes service account token from file system: %w"
- errGetKubeSA = "cannot get Kubernetes service account %q: %w"
- errGetKubeSASecrets = "cannot find secrets bound to service account: %q"
- errGetKubeSANoToken = "cannot find token in secrets bound to service account: %q"
- errGetKubeSecret = "cannot get Kubernetes secret %q: %w"
- errSecretKeyFmt = "cannot find secret data for key: %q"
- errClientTLSAuth = "error from Client TLS Auth: %q"
- )
- type Client interface {
- NewRequest(method, requestPath string) *vault.Request
- RawRequestWithContext(ctx context.Context, r *vault.Request) (*vault.Response, error)
- SetToken(v string)
- SetNamespace(namespace string)
- }
- type client struct {
- kube kclient.Client
- store *esv1alpha1.VaultProvider
- log logr.Logger
- client Client
- namespace string
- storeKind string
- }
- func init() {
- schema.Register(&connector{
- newVaultClient: newVaultClient,
- }, &esv1alpha1.SecretStoreProvider{
- Vault: &esv1alpha1.VaultProvider{},
- })
- }
- func newVaultClient(c *vault.Config) (Client, error) {
- return vault.NewClient(c)
- }
- type connector struct {
- newVaultClient func(c *vault.Config) (Client, error)
- }
- func (c *connector) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
- storeSpec := store.GetSpec()
- if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Vault == nil {
- return nil, errors.New(errVaultStore)
- }
- vaultSpec := storeSpec.Provider.Vault
- vStore := &client{
- kube: kube,
- store: vaultSpec,
- log: ctrl.Log.WithName("provider").WithName("vault"),
- namespace: namespace,
- storeKind: store.GetObjectKind().GroupVersionKind().Kind,
- }
- cfg, err := vStore.newConfig()
- if err != nil {
- return nil, err
- }
- client, err := c.newVaultClient(cfg)
- if err != nil {
- return nil, fmt.Errorf(errVaultClient, err)
- }
- if vaultSpec.Namespace != nil {
- client.SetNamespace(*vaultSpec.Namespace)
- }
- if err := vStore.setAuth(ctx, client, cfg); err != nil {
- return nil, err
- }
- vStore.client = client
- return vStore, nil
- }
- func (v *client) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
- data, err := v.readSecret(ctx, ref.Key, ref.Version)
- if err != nil {
- return nil, err
- }
- value, exists := data[ref.Property]
- if !exists {
- return nil, fmt.Errorf(errSecretKeyFmt, ref.Property)
- }
- return value, nil
- }
- func (v *client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
- return v.readSecret(ctx, ref.Key, ref.Version)
- }
- func (v *client) Close() error {
- return nil
- }
- func (v *client) readSecret(ctx context.Context, path, version string) (map[string][]byte, error) {
- kvPath := v.store.Path
- if v.store.Version == esv1alpha1.VaultKVStoreV2 {
- if !strings.HasSuffix(kvPath, "/data") {
- kvPath = fmt.Sprintf("%s/data", kvPath)
- }
- }
- // path formated according to vault docs for v1 and v2 API
- // v1: https://www.vaultproject.io/api-docs/secret/kv/kv-v1#read-secret
- // v2: https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version
- req := v.client.NewRequest(http.MethodGet, fmt.Sprintf("/v1/%s/%s", kvPath, path))
- if version != "" {
- req.Params.Set("version", version)
- }
- resp, err := v.client.RawRequestWithContext(ctx, req)
- if err != nil {
- return nil, fmt.Errorf(errReadSecret, err)
- }
- vaultSecret, err := vault.ParseSecret(resp.Body)
- if err != nil {
- return nil, err
- }
- secretData := vaultSecret.Data
- if v.store.Version == esv1alpha1.VaultKVStoreV2 {
- // Vault KV2 has data embedded within sub-field
- // reference - https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version
- dataInt, ok := vaultSecret.Data["data"]
- if !ok {
- return nil, errors.New(errDataField)
- }
- secretData, ok = dataInt.(map[string]interface{})
- if !ok {
- return nil, errors.New(errJSONUnmarshall)
- }
- }
- byteMap := make(map[string][]byte, len(secretData))
- for k, v := range secretData {
- switch t := v.(type) {
- case string:
- byteMap[k] = []byte(t)
- case []byte:
- byteMap[k] = t
- default:
- return nil, errors.New(errSecretFormat)
- }
- }
- return byteMap, nil
- }
- func (v *client) newConfig() (*vault.Config, error) {
- cfg := vault.DefaultConfig()
- cfg.Address = v.store.Server
- if len(v.store.CABundle) == 0 {
- return cfg, nil
- }
- caCertPool := x509.NewCertPool()
- ok := caCertPool.AppendCertsFromPEM(v.store.CABundle)
- if !ok {
- return nil, errors.New(errVaultCert)
- }
- if transport, ok := cfg.HttpClient.Transport.(*http.Transport); ok {
- transport.TLSClientConfig.RootCAs = caCertPool
- }
- return cfg, nil
- }
- func (v *client) setAuth(ctx context.Context, client Client, cfg *vault.Config) error {
- tokenRef := v.store.Auth.TokenSecretRef
- if tokenRef != nil {
- token, err := v.secretKeyRef(ctx, tokenRef)
- if err != nil {
- return err
- }
- client.SetToken(token)
- return nil
- }
- appRole := v.store.Auth.AppRole
- if appRole != nil {
- token, err := v.requestTokenWithAppRoleRef(ctx, client, appRole)
- if err != nil {
- return err
- }
- client.SetToken(token)
- return nil
- }
- kubernetesAuth := v.store.Auth.Kubernetes
- if kubernetesAuth != nil {
- token, err := v.requestTokenWithKubernetesAuth(ctx, client, kubernetesAuth)
- if err != nil {
- return err
- }
- client.SetToken(token)
- return nil
- }
- ldapAuth := v.store.Auth.Ldap
- if ldapAuth != nil {
- token, err := v.requestTokenWithLdapAuth(ctx, client, ldapAuth)
- if err != nil {
- return err
- }
- client.SetToken(token)
- return nil
- }
- jwtAuth := v.store.Auth.Jwt
- if jwtAuth != nil {
- token, err := v.requestTokenWithJwtAuth(ctx, client, jwtAuth)
- if err != nil {
- return err
- }
- client.SetToken(token)
- return nil
- }
- certAuth := v.store.Auth.Cert
- if certAuth != nil {
- token, err := v.requestTokenWithCertAuth(ctx, client, certAuth, cfg)
- if err != nil {
- return err
- }
- client.SetToken(token)
- return nil
- }
- return errors.New(errAuthFormat)
- }
- func (v *client) secretKeyRefForServiceAccount(ctx context.Context, serviceAccountRef *esmeta.ServiceAccountSelector) (string, error) {
- serviceAccount := &corev1.ServiceAccount{}
- ref := types.NamespacedName{
- Namespace: v.namespace,
- Name: serviceAccountRef.Name,
- }
- if (v.storeKind == esv1alpha1.ClusterSecretStoreKind) &&
- (serviceAccountRef.Namespace != nil) {
- ref.Namespace = *serviceAccountRef.Namespace
- }
- err := v.kube.Get(ctx, ref, serviceAccount)
- if err != nil {
- return "", fmt.Errorf(errGetKubeSA, ref.Name, err)
- }
- if len(serviceAccount.Secrets) == 0 {
- return "", fmt.Errorf(errGetKubeSASecrets, ref.Name)
- }
- for _, tokenRef := range serviceAccount.Secrets {
- retval, err := v.secretKeyRef(ctx, &esmeta.SecretKeySelector{
- Name: tokenRef.Name,
- Namespace: &ref.Namespace,
- Key: "token",
- })
- if err != nil {
- continue
- }
- return retval, nil
- }
- return "", fmt.Errorf(errGetKubeSANoToken, ref.Name)
- }
- func (v *client) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySelector) (string, error) {
- secret := &corev1.Secret{}
- ref := types.NamespacedName{
- Namespace: v.namespace,
- Name: secretRef.Name,
- }
- if (v.storeKind == esv1alpha1.ClusterSecretStoreKind) &&
- (secretRef.Namespace != nil) {
- ref.Namespace = *secretRef.Namespace
- }
- err := v.kube.Get(ctx, ref, secret)
- if err != nil {
- return "", fmt.Errorf(errGetKubeSecret, ref.Name, err)
- }
- keyBytes, ok := secret.Data[secretRef.Key]
- if !ok {
- return "", fmt.Errorf(errSecretKeyFmt, secretRef.Key)
- }
- value := string(keyBytes)
- valueStr := strings.TrimSpace(value)
- return valueStr, nil
- }
- // appRoleParameters creates the required body for Vault AppRole Auth.
- // Reference - https://www.vaultproject.io/api-docs/auth/approle#login-with-approle
- func appRoleParameters(role, secret string) map[string]string {
- return map[string]string{
- "role_id": role,
- "secret_id": secret,
- }
- }
- func (v *client) requestTokenWithAppRoleRef(ctx context.Context, client Client, appRole *esv1alpha1.VaultAppRole) (string, error) {
- roleID := strings.TrimSpace(appRole.RoleID)
- secretID, err := v.secretKeyRef(ctx, &appRole.SecretRef)
- if err != nil {
- return "", err
- }
- parameters := appRoleParameters(roleID, secretID)
- url := strings.Join([]string{"/v1", "auth", appRole.Path, "login"}, "/")
- request := client.NewRequest("POST", url)
- err = request.SetJSONBody(parameters)
- if err != nil {
- return "", fmt.Errorf(errVaultReqParams, err)
- }
- resp, err := client.RawRequestWithContext(ctx, request)
- if err != nil {
- return "", fmt.Errorf(errVaultRequest, err)
- }
- defer resp.Body.Close()
- vaultResult := vault.Secret{}
- if err = resp.DecodeJSON(&vaultResult); err != nil {
- return "", fmt.Errorf(errVaultResponse, err)
- }
- token, err := vaultResult.TokenID()
- if err != nil {
- return "", fmt.Errorf(errVaultToken, err)
- }
- return token, nil
- }
- // kubeParameters creates the required body for Vault Kubernetes auth.
- // Reference - https://www.vaultproject.io/api/auth/kubernetes#login
- func kubeParameters(role, jwt string) map[string]string {
- return map[string]string{
- "role": role,
- "jwt": jwt,
- }
- }
- func (v *client) requestTokenWithKubernetesAuth(ctx context.Context, client Client, kubernetesAuth *esv1alpha1.VaultKubernetesAuth) (string, error) {
- jwtString := ""
- if kubernetesAuth.ServiceAccountRef != nil {
- jwt, err := v.secretKeyRefForServiceAccount(ctx, kubernetesAuth.ServiceAccountRef)
- if err != nil {
- return "", err
- }
- jwtString = jwt
- } else if kubernetesAuth.SecretRef != nil {
- tokenRef := kubernetesAuth.SecretRef
- if tokenRef.Key == "" {
- tokenRef = kubernetesAuth.SecretRef.DeepCopy()
- tokenRef.Key = "token"
- }
- jwt, err := v.secretKeyRef(ctx, tokenRef)
- if err != nil {
- return "", err
- }
- jwtString = jwt
- } else {
- // Kubernetes authentication is specified, but without a referenced
- // Kubernetes secret. We check if the file path for in-cluster service account
- // exists and attempt to use the token for Vault Kubernetes auth.
- if _, err := os.Stat(serviceAccTokenPath); err != nil {
- return "", fmt.Errorf(errServiceAccount, err)
- }
- jwtByte, err := ioutil.ReadFile(serviceAccTokenPath)
- if err != nil {
- return "", fmt.Errorf(errServiceAccount, err)
- }
- jwtString = string(jwtByte)
- }
- parameters := kubeParameters(kubernetesAuth.Role, jwtString)
- url := strings.Join([]string{"/v1", "auth", kubernetesAuth.Path, "login"}, "/")
- request := client.NewRequest("POST", url)
- err := request.SetJSONBody(parameters)
- if err != nil {
- return "", fmt.Errorf(errVaultReqParams, err)
- }
- resp, err := client.RawRequestWithContext(ctx, request)
- if err != nil {
- return "", fmt.Errorf(errVaultRequest, err)
- }
- defer resp.Body.Close()
- vaultResult := vault.Secret{}
- err = resp.DecodeJSON(&vaultResult)
- if err != nil {
- return "", fmt.Errorf(errVaultResponse, err)
- }
- token, err := vaultResult.TokenID()
- if err != nil {
- return "", fmt.Errorf(errVaultToken, err)
- }
- return token, nil
- }
- func (v *client) requestTokenWithLdapAuth(ctx context.Context, client Client, ldapAuth *esv1alpha1.VaultLdapAuth) (string, error) {
- username := strings.TrimSpace(ldapAuth.Username)
- password, err := v.secretKeyRef(ctx, &ldapAuth.SecretRef)
- if err != nil {
- return "", err
- }
- parameters := map[string]string{
- "password": password,
- }
- url := strings.Join([]string{"/v1", "auth", "ldap", "login", username}, "/")
- request := client.NewRequest("POST", url)
- err = request.SetJSONBody(parameters)
- if err != nil {
- return "", fmt.Errorf(errVaultReqParams, err)
- }
- resp, err := client.RawRequestWithContext(ctx, request)
- if err != nil {
- return "", fmt.Errorf(errVaultRequest, err)
- }
- defer resp.Body.Close()
- vaultResult := vault.Secret{}
- if err = resp.DecodeJSON(&vaultResult); err != nil {
- return "", fmt.Errorf(errVaultResponse, err)
- }
- token, err := vaultResult.TokenID()
- if err != nil {
- return "", fmt.Errorf(errVaultToken, err)
- }
- return token, nil
- }
- func (v *client) requestTokenWithJwtAuth(ctx context.Context, client Client, jwtAuth *esv1alpha1.VaultJwtAuth) (string, error) {
- role := strings.TrimSpace(jwtAuth.Role)
- jwt, err := v.secretKeyRef(ctx, &jwtAuth.SecretRef)
- if err != nil {
- return "", err
- }
- parameters := map[string]string{
- "role": role,
- "jwt": jwt,
- }
- url := strings.Join([]string{"/v1", "auth", "jwt", "login"}, "/")
- request := client.NewRequest("POST", url)
- err = request.SetJSONBody(parameters)
- if err != nil {
- return "", fmt.Errorf(errVaultReqParams, err)
- }
- resp, err := client.RawRequestWithContext(ctx, request)
- if err != nil {
- return "", fmt.Errorf(errVaultRequest, err)
- }
- defer resp.Body.Close()
- vaultResult := vault.Secret{}
- if err = resp.DecodeJSON(&vaultResult); err != nil {
- return "", fmt.Errorf(errVaultResponse, err)
- }
- token, err := vaultResult.TokenID()
- if err != nil {
- return "", fmt.Errorf(errVaultToken, err)
- }
- return token, nil
- }
- func (v *client) requestTokenWithCertAuth(ctx context.Context, client Client, certAuth *esv1alpha1.VaultCertAuth, cfg *vault.Config) (string, error) {
- clientKey, err := v.secretKeyRef(ctx, &certAuth.SecretRef)
- if err != nil {
- return "", err
- }
- clientCert, err := v.secretKeyRef(ctx, &certAuth.ClientCert)
- if err != nil {
- return "", err
- }
- cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
- if err != nil {
- return "", fmt.Errorf(errClientTLSAuth, err)
- }
- if transport, ok := cfg.HttpClient.Transport.(*http.Transport); ok {
- transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
- }
- url := strings.Join([]string{"/v1", "auth", "cert", "login"}, "/")
- request := client.NewRequest("POST", url)
- resp, err := client.RawRequestWithContext(ctx, request)
- if err != nil {
- return "", fmt.Errorf(errVaultRequest, err)
- }
- defer resp.Body.Close()
- vaultResult := vault.Secret{}
- if err = resp.DecodeJSON(&vaultResult); err != nil {
- return "", fmt.Errorf(errVaultResponse, err)
- }
- token, err := vaultResult.TokenID()
- if err != nil {
- return "", fmt.Errorf(errVaultToken, err)
- }
- return token, nil
- }
|