From d7022b1bef443c9b7391e462a509723925db70a2 Mon Sep 17 00:00:00 2001 From: Alfred Krohmer Date: Mon, 4 Apr 2022 21:20:58 +0200 Subject: [PATCH] feat(vault): add option for JWT backend to authenticate with Kubernetes service account token (#768) --- .../v1alpha1/secretstore_vault_types.go | 35 +++++- .../v1alpha1/zz_generated.deepcopy.go | 41 ++++++- .../v1beta1/secretstore_vault_types.go | 35 +++++- .../v1beta1/zz_generated.deepcopy.go | 41 ++++++- ...ternal-secrets.io_clustersecretstores.yaml | 96 ++++++++++++++- .../external-secrets.io_secretstores.yaml | 96 ++++++++++++++- deploy/crds/bundle.yaml | 116 +++++++++++++++++- docs/provider-hashicorp-vault.md | 4 +- docs/snippets/vault-jwt-store.yaml | 12 ++ e2e/framework/addon/vault.go | 2 + e2e/k8s/vault-config/configure-vault.sh | 15 +++ e2e/kind.yaml | 9 -- e2e/run.sh | 5 + e2e/suite/vault/provider.go | 24 +++- e2e/suite/vault/vault.go | 11 ++ pkg/provider/vault/vault.go | 83 ++++++++++++- pkg/provider/vault/vault_test.go | 4 +- 17 files changed, 583 insertions(+), 46 deletions(-) diff --git a/apis/externalsecrets/v1alpha1/secretstore_vault_types.go b/apis/externalsecrets/v1alpha1/secretstore_vault_types.go index 4cbe624c9..faec47632 100644 --- a/apis/externalsecrets/v1alpha1/secretstore_vault_types.go +++ b/apis/externalsecrets/v1alpha1/secretstore_vault_types.go @@ -203,8 +203,29 @@ type VaultLdapAuth struct { SecretRef esmeta.SecretKeySelector `json:"secretRef,omitempty"` } +// VaultKubernetesServiceAccountTokenAuth authenticates with Vault using a temporary +// Kubernetes service account token retrieved by the `TokenRequest` API. +type VaultKubernetesServiceAccountTokenAuth struct { + // Service account field containing the name of a kubernetes ServiceAccount. + ServiceAccountRef esmeta.ServiceAccountSelector `json:"serviceAccountRef"` + + // Optional audiences field that will be used to request a temporary Kubernetes service + // account token for the service account referenced by `serviceAccountRef`. + // Defaults to a single audience `vault` it not specified. + // +optional + Audiences *[]string `json:"audiences,omitempty"` + + // Optional expiration time in seconds that will be used to request a temporary + // Kubernetes service account token for the service account referenced by + // `serviceAccountRef`. + // Defaults to 10 minutes. + // +optional + ExpirationSeconds *int64 `json:"expirationSeconds,omitempty"` +} + // VaultJwtAuth authenticates with Vault using the JWT/OIDC authentication -// method, with the role name and token stored in a Kubernetes Secret resource. +// method, with the role name and a token stored in a Kubernetes Secret resource or +// a Kubernetes service account token retrieved via `TokenRequest`. type VaultJwtAuth struct { // Path where the JWT authentication backend is mounted // in Vault, e.g: "jwt" @@ -216,9 +237,15 @@ type VaultJwtAuth struct { // +optional Role string `json:"role"` - // SecretRef to a key in a Secret resource containing JWT token to - // authenticate with Vault using the JWT/OIDC authentication method - SecretRef esmeta.SecretKeySelector `json:"secretRef,omitempty"` + // Optional SecretRef that refers to a key in a Secret resource containing JWT token to + // authenticate with Vault using the JWT/OIDC authentication method. + // +optional + SecretRef *esmeta.SecretKeySelector `json:"secretRef,omitempty"` + + // Optional ServiceAccountToken specifies the Kubernetes service account for which to request + // a token for with the `TokenRequest` API. + // +optional + KubernetesServiceAccountToken *VaultKubernetesServiceAccountTokenAuth `json:"kubernetesServiceAccountToken,omitempty"` } // VaultJwtAuth authenticates with Vault using the JWT/OIDC authentication diff --git a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go index baaee5002..40cb13966 100644 --- a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go @@ -1379,7 +1379,16 @@ func (in *VaultCertAuth) DeepCopy() *VaultCertAuth { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VaultJwtAuth) DeepCopyInto(out *VaultJwtAuth) { *out = *in - in.SecretRef.DeepCopyInto(&out.SecretRef) + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(metav1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.KubernetesServiceAccountToken != nil { + in, out := &in.KubernetesServiceAccountToken, &out.KubernetesServiceAccountToken + *out = new(VaultKubernetesServiceAccountTokenAuth) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultJwtAuth. @@ -1417,6 +1426,36 @@ func (in *VaultKubernetesAuth) DeepCopy() *VaultKubernetesAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultKubernetesServiceAccountTokenAuth) DeepCopyInto(out *VaultKubernetesServiceAccountTokenAuth) { + *out = *in + in.ServiceAccountRef.DeepCopyInto(&out.ServiceAccountRef) + if in.Audiences != nil { + in, out := &in.Audiences, &out.Audiences + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + if in.ExpirationSeconds != nil { + in, out := &in.ExpirationSeconds, &out.ExpirationSeconds + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultKubernetesServiceAccountTokenAuth. +func (in *VaultKubernetesServiceAccountTokenAuth) DeepCopy() *VaultKubernetesServiceAccountTokenAuth { + if in == nil { + return nil + } + out := new(VaultKubernetesServiceAccountTokenAuth) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VaultLdapAuth) DeepCopyInto(out *VaultLdapAuth) { *out = *in diff --git a/apis/externalsecrets/v1beta1/secretstore_vault_types.go b/apis/externalsecrets/v1beta1/secretstore_vault_types.go index f08588ab5..cb0b971b3 100644 --- a/apis/externalsecrets/v1beta1/secretstore_vault_types.go +++ b/apis/externalsecrets/v1beta1/secretstore_vault_types.go @@ -203,8 +203,29 @@ type VaultLdapAuth struct { SecretRef esmeta.SecretKeySelector `json:"secretRef,omitempty"` } +// VaultKubernetesServiceAccountTokenAuth authenticates with Vault using a temporary +// Kubernetes service account token retrieved by the `TokenRequest` API. +type VaultKubernetesServiceAccountTokenAuth struct { + // Service account field containing the name of a kubernetes ServiceAccount. + ServiceAccountRef esmeta.ServiceAccountSelector `json:"serviceAccountRef"` + + // Optional audiences field that will be used to request a temporary Kubernetes service + // account token for the service account referenced by `serviceAccountRef`. + // Defaults to a single audience `vault` it not specified. + // +optional + Audiences *[]string `json:"audiences,omitempty"` + + // Optional expiration time in seconds that will be used to request a temporary + // Kubernetes service account token for the service account referenced by + // `serviceAccountRef`. + // Defaults to 10 minutes. + // +optional + ExpirationSeconds *int64 `json:"expirationSeconds,omitempty"` +} + // VaultJwtAuth authenticates with Vault using the JWT/OIDC authentication -// method, with the role name and token stored in a Kubernetes Secret resource. +// method, with the role name and a token stored in a Kubernetes Secret resource or +// a Kubernetes service account token retrieved via `TokenRequest`. type VaultJwtAuth struct { // Path where the JWT authentication backend is mounted // in Vault, e.g: "jwt" @@ -216,9 +237,15 @@ type VaultJwtAuth struct { // +optional Role string `json:"role"` - // SecretRef to a key in a Secret resource containing JWT token to - // authenticate with Vault using the JWT/OIDC authentication method - SecretRef esmeta.SecretKeySelector `json:"secretRef,omitempty"` + // Optional SecretRef that refers to a key in a Secret resource containing JWT token to + // authenticate with Vault using the JWT/OIDC authentication method. + // +optional + SecretRef *esmeta.SecretKeySelector `json:"secretRef,omitempty"` + + // Optional ServiceAccountToken specifies the Kubernetes service account for which to request + // a token for with the `TokenRequest` API. + // +optional + KubernetesServiceAccountToken *VaultKubernetesServiceAccountTokenAuth `json:"kubernetesServiceAccountToken,omitempty"` } // VaultJwtAuth authenticates with Vault using the JWT/OIDC authentication diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go index ae4e5f2f2..9b07b042d 100644 --- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go @@ -1609,7 +1609,16 @@ func (in *VaultCertAuth) DeepCopy() *VaultCertAuth { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VaultJwtAuth) DeepCopyInto(out *VaultJwtAuth) { *out = *in - in.SecretRef.DeepCopyInto(&out.SecretRef) + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(metav1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.KubernetesServiceAccountToken != nil { + in, out := &in.KubernetesServiceAccountToken, &out.KubernetesServiceAccountToken + *out = new(VaultKubernetesServiceAccountTokenAuth) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultJwtAuth. @@ -1647,6 +1656,36 @@ func (in *VaultKubernetesAuth) DeepCopy() *VaultKubernetesAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultKubernetesServiceAccountTokenAuth) DeepCopyInto(out *VaultKubernetesServiceAccountTokenAuth) { + *out = *in + in.ServiceAccountRef.DeepCopyInto(&out.ServiceAccountRef) + if in.Audiences != nil { + in, out := &in.Audiences, &out.Audiences + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + if in.ExpirationSeconds != nil { + in, out := &in.ExpirationSeconds, &out.ExpirationSeconds + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultKubernetesServiceAccountTokenAuth. +func (in *VaultKubernetesServiceAccountTokenAuth) DeepCopy() *VaultKubernetesServiceAccountTokenAuth { + if in == nil { + return nil + } + out := new(VaultKubernetesServiceAccountTokenAuth) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VaultLdapAuth) DeepCopyInto(out *VaultLdapAuth) { *out = *in diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml index a57faabdf..ad45abb64 100644 --- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml +++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml @@ -908,6 +908,48 @@ spec: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: + kubernetesServiceAccountToken: + description: Optional ServiceAccountToken specifies + the Kubernetes service account for which to request + a token for with the `TokenRequest` API. + properties: + audiences: + description: Optional audiences field that will + be used to request a temporary Kubernetes service + account token for the service account referenced + by `serviceAccountRef`. Defaults to a single + audience `vault` it not specified. + items: + type: string + type: array + expirationSeconds: + description: Optional expiration time in seconds + that will be used to request a temporary Kubernetes + service account token for the service account + referenced by `serviceAccountRef`. Defaults + to 10 minutes. + format: int64 + type: integer + serviceAccountRef: + description: Service account field containing + the name of a kubernetes ServiceAccount. + properties: + name: + description: The name of the ServiceAccount + resource being referred to. + type: string + namespace: + description: Namespace of the resource being + referred to. Ignored if referent is not + cluster-scoped. cluster-scoped defaults + to the namespace of the referent. + type: string + required: + - name + type: object + required: + - serviceAccountRef + type: object path: default: jwt description: 'Path where the JWT authentication backend @@ -918,9 +960,9 @@ spec: the JWT/OIDC Vault authentication method type: string secretRef: - description: SecretRef to a key in a Secret resource - containing JWT token to authenticate with Vault - using the JWT/OIDC authentication method + description: Optional SecretRef that refers to a key + in a Secret resource containing JWT token to authenticate + with Vault using the JWT/OIDC authentication method. properties: key: description: The key of the entry in the Secret @@ -2227,6 +2269,48 @@ spec: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: + kubernetesServiceAccountToken: + description: Optional ServiceAccountToken specifies + the Kubernetes service account for which to request + a token for with the `TokenRequest` API. + properties: + audiences: + description: Optional audiences field that will + be used to request a temporary Kubernetes service + account token for the service account referenced + by `serviceAccountRef`. Defaults to a single + audience `vault` it not specified. + items: + type: string + type: array + expirationSeconds: + description: Optional expiration time in seconds + that will be used to request a temporary Kubernetes + service account token for the service account + referenced by `serviceAccountRef`. Defaults + to 10 minutes. + format: int64 + type: integer + serviceAccountRef: + description: Service account field containing + the name of a kubernetes ServiceAccount. + properties: + name: + description: The name of the ServiceAccount + resource being referred to. + type: string + namespace: + description: Namespace of the resource being + referred to. Ignored if referent is not + cluster-scoped. cluster-scoped defaults + to the namespace of the referent. + type: string + required: + - name + type: object + required: + - serviceAccountRef + type: object path: default: jwt description: 'Path where the JWT authentication backend @@ -2237,9 +2321,9 @@ spec: the JWT/OIDC Vault authentication method type: string secretRef: - description: SecretRef to a key in a Secret resource - containing JWT token to authenticate with Vault - using the JWT/OIDC authentication method + description: Optional SecretRef that refers to a key + in a Secret resource containing JWT token to authenticate + with Vault using the JWT/OIDC authentication method. properties: key: description: The key of the entry in the Secret diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml index 02ee92e44..f9ef87fbf 100644 --- a/config/crds/bases/external-secrets.io_secretstores.yaml +++ b/config/crds/bases/external-secrets.io_secretstores.yaml @@ -908,6 +908,48 @@ spec: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: + kubernetesServiceAccountToken: + description: Optional ServiceAccountToken specifies + the Kubernetes service account for which to request + a token for with the `TokenRequest` API. + properties: + audiences: + description: Optional audiences field that will + be used to request a temporary Kubernetes service + account token for the service account referenced + by `serviceAccountRef`. Defaults to a single + audience `vault` it not specified. + items: + type: string + type: array + expirationSeconds: + description: Optional expiration time in seconds + that will be used to request a temporary Kubernetes + service account token for the service account + referenced by `serviceAccountRef`. Defaults + to 10 minutes. + format: int64 + type: integer + serviceAccountRef: + description: Service account field containing + the name of a kubernetes ServiceAccount. + properties: + name: + description: The name of the ServiceAccount + resource being referred to. + type: string + namespace: + description: Namespace of the resource being + referred to. Ignored if referent is not + cluster-scoped. cluster-scoped defaults + to the namespace of the referent. + type: string + required: + - name + type: object + required: + - serviceAccountRef + type: object path: default: jwt description: 'Path where the JWT authentication backend @@ -918,9 +960,9 @@ spec: the JWT/OIDC Vault authentication method type: string secretRef: - description: SecretRef to a key in a Secret resource - containing JWT token to authenticate with Vault - using the JWT/OIDC authentication method + description: Optional SecretRef that refers to a key + in a Secret resource containing JWT token to authenticate + with Vault using the JWT/OIDC authentication method. properties: key: description: The key of the entry in the Secret @@ -2230,6 +2272,48 @@ spec: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: + kubernetesServiceAccountToken: + description: Optional ServiceAccountToken specifies + the Kubernetes service account for which to request + a token for with the `TokenRequest` API. + properties: + audiences: + description: Optional audiences field that will + be used to request a temporary Kubernetes service + account token for the service account referenced + by `serviceAccountRef`. Defaults to a single + audience `vault` it not specified. + items: + type: string + type: array + expirationSeconds: + description: Optional expiration time in seconds + that will be used to request a temporary Kubernetes + service account token for the service account + referenced by `serviceAccountRef`. Defaults + to 10 minutes. + format: int64 + type: integer + serviceAccountRef: + description: Service account field containing + the name of a kubernetes ServiceAccount. + properties: + name: + description: The name of the ServiceAccount + resource being referred to. + type: string + namespace: + description: Namespace of the resource being + referred to. Ignored if referent is not + cluster-scoped. cluster-scoped defaults + to the namespace of the referent. + type: string + required: + - name + type: object + required: + - serviceAccountRef + type: object path: default: jwt description: 'Path where the JWT authentication backend @@ -2240,9 +2324,9 @@ spec: the JWT/OIDC Vault authentication method type: string secretRef: - description: SecretRef to a key in a Secret resource - containing JWT token to authenticate with Vault - using the JWT/OIDC authentication method + description: Optional SecretRef that refers to a key + in a Secret resource containing JWT token to authenticate + with Vault using the JWT/OIDC authentication method. properties: key: description: The key of the entry in the Secret diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml index 0fe86a001..31c5536bb 100644 --- a/deploy/crds/bundle.yaml +++ b/deploy/crds/bundle.yaml @@ -999,6 +999,33 @@ spec: jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: + kubernetesServiceAccountToken: + description: Optional ServiceAccountToken specifies the Kubernetes service account for which to request a token for with the `TokenRequest` API. + properties: + audiences: + description: Optional audiences field that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Defaults to a single audience `vault` it not specified. + items: + type: string + type: array + expirationSeconds: + description: Optional expiration time in seconds that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Defaults to 10 minutes. + format: int64 + type: integer + serviceAccountRef: + description: Service account field containing the name of a kubernetes ServiceAccount. + properties: + name: + description: The name of the ServiceAccount resource being referred to. + type: string + namespace: + description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. + type: string + required: + - name + type: object + required: + - serviceAccountRef + type: object path: default: jwt description: 'Path where the JWT authentication backend is mounted in Vault, e.g: "jwt"' @@ -1007,7 +1034,7 @@ spec: description: Role is a JWT role to authenticate using the JWT/OIDC Vault authentication method type: string secretRef: - description: SecretRef to a key in a Secret resource containing JWT token to authenticate with Vault using the JWT/OIDC authentication method + description: Optional SecretRef that refers to a key in a Secret resource containing JWT token to authenticate with Vault using the JWT/OIDC authentication method. properties: key: description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. @@ -1972,6 +1999,33 @@ spec: jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: + kubernetesServiceAccountToken: + description: Optional ServiceAccountToken specifies the Kubernetes service account for which to request a token for with the `TokenRequest` API. + properties: + audiences: + description: Optional audiences field that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Defaults to a single audience `vault` it not specified. + items: + type: string + type: array + expirationSeconds: + description: Optional expiration time in seconds that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Defaults to 10 minutes. + format: int64 + type: integer + serviceAccountRef: + description: Service account field containing the name of a kubernetes ServiceAccount. + properties: + name: + description: The name of the ServiceAccount resource being referred to. + type: string + namespace: + description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. + type: string + required: + - name + type: object + required: + - serviceAccountRef + type: object path: default: jwt description: 'Path where the JWT authentication backend is mounted in Vault, e.g: "jwt"' @@ -1980,7 +2034,7 @@ spec: description: Role is a JWT role to authenticate using the JWT/OIDC Vault authentication method type: string secretRef: - description: SecretRef to a key in a Secret resource containing JWT token to authenticate with Vault using the JWT/OIDC authentication method + description: Optional SecretRef that refers to a key in a Secret resource containing JWT token to authenticate with Vault using the JWT/OIDC authentication method. properties: key: description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. @@ -3487,6 +3541,33 @@ spec: jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: + kubernetesServiceAccountToken: + description: Optional ServiceAccountToken specifies the Kubernetes service account for which to request a token for with the `TokenRequest` API. + properties: + audiences: + description: Optional audiences field that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Defaults to a single audience `vault` it not specified. + items: + type: string + type: array + expirationSeconds: + description: Optional expiration time in seconds that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Defaults to 10 minutes. + format: int64 + type: integer + serviceAccountRef: + description: Service account field containing the name of a kubernetes ServiceAccount. + properties: + name: + description: The name of the ServiceAccount resource being referred to. + type: string + namespace: + description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. + type: string + required: + - name + type: object + required: + - serviceAccountRef + type: object path: default: jwt description: 'Path where the JWT authentication backend is mounted in Vault, e.g: "jwt"' @@ -3495,7 +3576,7 @@ spec: description: Role is a JWT role to authenticate using the JWT/OIDC Vault authentication method type: string secretRef: - description: SecretRef to a key in a Secret resource containing JWT token to authenticate with Vault using the JWT/OIDC authentication method + description: Optional SecretRef that refers to a key in a Secret resource containing JWT token to authenticate with Vault using the JWT/OIDC authentication method. properties: key: description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. @@ -4463,6 +4544,33 @@ spec: jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: + kubernetesServiceAccountToken: + description: Optional ServiceAccountToken specifies the Kubernetes service account for which to request a token for with the `TokenRequest` API. + properties: + audiences: + description: Optional audiences field that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Defaults to a single audience `vault` it not specified. + items: + type: string + type: array + expirationSeconds: + description: Optional expiration time in seconds that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Defaults to 10 minutes. + format: int64 + type: integer + serviceAccountRef: + description: Service account field containing the name of a kubernetes ServiceAccount. + properties: + name: + description: The name of the ServiceAccount resource being referred to. + type: string + namespace: + description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. + type: string + required: + - name + type: object + required: + - serviceAccountRef + type: object path: default: jwt description: 'Path where the JWT authentication backend is mounted in Vault, e.g: "jwt"' @@ -4471,7 +4579,7 @@ spec: description: Role is a JWT role to authenticate using the JWT/OIDC Vault authentication method type: string secretRef: - description: SecretRef to a key in a Secret resource containing JWT token to authenticate with Vault using the JWT/OIDC authentication method + description: Optional SecretRef that refers to a key in a Secret resource containing JWT token to authenticate with Vault using the JWT/OIDC authentication method. properties: key: description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. diff --git a/docs/provider-hashicorp-vault.md b/docs/provider-hashicorp-vault.md index bb606dad7..0e32ed262 100644 --- a/docs/provider-hashicorp-vault.md +++ b/docs/provider-hashicorp-vault.md @@ -295,9 +295,9 @@ in a `Kind=Secret` referenced by the `secretRef`. #### JWT/OIDC authentication -[JWT/OIDC](https://www.vaultproject.io/docs/auth/jwt) uses a +[JWT/OIDC](https://www.vaultproject.io/docs/auth/jwt) uses either a [JWT](https://jwt.io/) token stored in a `Kind=Secret` and referenced by the -`secretRef`. Optionally a `role` field can be defined in a `Kind=SecretStore` +`secretRef` or a temporary Kubernetes service account token retrieved via the `TokenRequest` API. Optionally a `role` field can be defined in a `Kind=SecretStore` or `Kind=ClusterSecretStore` resource. ```yaml diff --git a/docs/snippets/vault-jwt-store.yaml b/docs/snippets/vault-jwt-store.yaml index 4c71e264a..544f0befb 100644 --- a/docs/snippets/vault-jwt-store.yaml +++ b/docs/snippets/vault-jwt-store.yaml @@ -17,6 +17,18 @@ spec: path: "jwt" # JWT role configured in a Vault server, optional. role: "vault-jwt-role" + + # Retrieve JWT token from a Kubernetes secret secretRef: name: "my-secret" key: "jwt-token" + + # ... or retrieve a Kubernetes service account token via the `TokenRequest` API + kubernetesServiceAccountToken: + serviceAccountRef: + name: "my-sa" + # `audiences` defaults to `["vault"]` it not supplied + audiences: + - vault + # `expirationSeconds` defaults to 10 minutes if not supplied + expirationSeconds: 600 diff --git a/e2e/framework/addon/vault.go b/e2e/framework/addon/vault.go index 55b8c61c8..320000d64 100644 --- a/e2e/framework/addon/vault.go +++ b/e2e/framework/addon/vault.go @@ -58,6 +58,7 @@ type Vault struct { JWTToken string JWTRole string JWTPath string + JWTK8sPath string KubernetesAuthPath string KubernetesAuthRole string @@ -162,6 +163,7 @@ func (l *Vault) initVault() error { l.JWTPubkey = jwtPubkey l.JWTToken = jwtToken l.JWTPath = "myjwt" // see configure-vault.sh + l.JWTK8sPath = "myjwtk8s" // see configure-vault.sh l.JWTRole = "external-secrets-operator" // see configure-vault.sh l.KubernetesAuthPath = "mykubernetes" // see configure-vault.sh l.KubernetesAuthRole = "external-secrets-operator" // see configure-vault.sh diff --git a/e2e/k8s/vault-config/configure-vault.sh b/e2e/k8s/vault-config/configure-vault.sh index 96b12f53a..ab20457e5 100755 --- a/e2e/k8s/vault-config/configure-vault.sh +++ b/e2e/k8s/vault-config/configure-vault.sh @@ -69,6 +69,21 @@ vault write auth/myjwt/role/external-secrets-operator \ policies=external-secrets-operator \ ttl=1h +vault auth enable -path=myjwtk8s jwt + +vault write auth/myjwtk8s/config \ + oidc_discovery_url=https://kubernetes.default.svc.cluster.local \ + oidc_discovery_ca_pem=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ + bound_issuer="https://kubernetes.default.svc.cluster.local" \ + default_role="external-secrets-operator" + +vault write auth/myjwtk8s/role/external-secrets-operator \ + role_type="jwt" \ + bound_audiences="vault.client" \ + user_claim="sub" \ + policies=external-secrets-operator \ + ttl=1h + # ------------------ # Kubernetes AUTH # https://www.vaultproject.io/docs/auth/kubernetes diff --git a/e2e/kind.yaml b/e2e/kind.yaml index 9249e2911..752e993cd 100644 --- a/e2e/kind.yaml +++ b/e2e/kind.yaml @@ -1,14 +1,5 @@ kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 -kubeadmConfigPatches: -- | - kind: ClusterConfiguration - apiServer: - extraArgs: - api-audiences: "sts.amazonaws.com" - service-account-key-file: "/etc/kubernetes/pki/sa.pub" - service-account-signing-key-file: "/etc/kubernetes/pki/sa.key" - service-account-issuer: "https://s3-XXXXXXXXXX.amazonaws.com/XXXXXXXXXXXXXXXXXXXXX" nodes: - role: control-plane - role: worker diff --git a/e2e/run.sh b/e2e/run.sh index 34570e3aa..82574d276 100755 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -34,6 +34,11 @@ kubectl create clusterrolebinding permissive-binding \ --user=kubelet \ --serviceaccount=default:external-secrets-e2e || true +echo -e "Granting anonymous access to service account issuer discovery" +kubectl create clusterrolebinding service-account-issuer-discovery-binding \ + --clusterrole=system:service-account-issuer-discovery \ + --group=system:unauthenticated || true + echo -e "Waiting service account..."; \ until kubectl get secret | grep -q -e ^external-secrets-e2e-token; do \ echo -e "waiting for api token"; \ diff --git a/e2e/suite/vault/provider.go b/e2e/suite/vault/provider.go index a7f7c659b..32c6b9975 100644 --- a/e2e/suite/vault/provider.go +++ b/e2e/suite/vault/provider.go @@ -45,6 +45,7 @@ const ( appRoleAuthProviderName = "app-role-provider" kvv1ProviderName = "kv-v1-provider" jwtProviderName = "jwt-provider" + jwtK8sProviderName = "jwt-k8s-provider" kubernetesProviderName = "kubernetes-provider" ) @@ -95,6 +96,7 @@ func (s *vaultProvider) BeforeEach() { s.CreateAppRoleStore(v, ns) s.CreateV1Store(v, ns) s.CreateJWTStore(v, ns) + s.CreateJWTK8sStore(v, ns) s.CreateKubernetesAuthStore(v, ns) } @@ -249,7 +251,7 @@ func (s vaultProvider) CreateJWTStore(v *addon.Vault, ns string) { Jwt: &esv1beta1.VaultJwtAuth{ Path: v.JWTPath, Role: v.JWTRole, - SecretRef: esmeta.SecretKeySelector{ + SecretRef: &esmeta.SecretKeySelector{ Name: "jwt-provider", Key: "jwt", }, @@ -259,6 +261,26 @@ func (s vaultProvider) CreateJWTStore(v *addon.Vault, ns string) { Expect(err).ToNot(HaveOccurred()) } +func (s vaultProvider) CreateJWTK8sStore(v *addon.Vault, ns string) { + secretStore := makeStore(jwtK8sProviderName, ns, v) + secretStore.Spec.Provider.Vault.Auth = esv1beta1.VaultAuth{ + Jwt: &esv1beta1.VaultJwtAuth{ + Path: v.JWTK8sPath, + Role: v.JWTRole, + KubernetesServiceAccountToken: &esv1beta1.VaultKubernetesServiceAccountTokenAuth{ + ServiceAccountRef: esmeta.ServiceAccountSelector{ + Name: "default", + }, + Audiences: &[]string{ + "vault.client", + }, + }, + }, + } + err := s.framework.CRClient.Create(context.Background(), secretStore) + Expect(err).ToNot(HaveOccurred()) +} + func (s vaultProvider) CreateKubernetesAuthStore(v *addon.Vault, ns string) { secretStore := makeStore(kubernetesProviderName, ns, v) secretStore.Spec.Provider.Vault.Auth = esv1beta1.VaultAuth{ diff --git a/e2e/suite/vault/vault.go b/e2e/suite/vault/vault.go index aaee5a6d2..b4447b655 100644 --- a/e2e/suite/vault/vault.go +++ b/e2e/suite/vault/vault.go @@ -30,6 +30,7 @@ const ( withApprole = "with approle auth" withV1 = "with v1 provider" withJWT = "with jwt provider" + withJWTK8s = "with jwt k8s provider" withK8s = "with kubernetes provider" ) @@ -74,6 +75,12 @@ var _ = Describe("[vault]", Label("vault"), func() { framework.Compose(withJWT, f, common.JSONDataWithTemplate, useJWTProvider), framework.Compose(withJWT, f, common.DataPropertyDockerconfigJSON, useJWTProvider), framework.Compose(withJWT, f, common.JSONDataWithoutTargetName, useJWTProvider), + // use jwt k8s provider + framework.Compose(withJWTK8s, f, common.JSONDataFromSync, useJWTK8sProvider), + framework.Compose(withJWTK8s, f, common.JSONDataWithProperty, useJWTK8sProvider), + framework.Compose(withJWTK8s, f, common.JSONDataWithTemplate, useJWTK8sProvider), + framework.Compose(withJWTK8s, f, common.DataPropertyDockerconfigJSON, useJWTK8sProvider), + framework.Compose(withJWTK8s, f, common.JSONDataWithoutTargetName, useJWTK8sProvider), // use kubernetes provider framework.Compose(withK8s, f, common.FindByName, useKubernetesProvider), framework.Compose(withK8s, f, common.JSONDataFromSync, useKubernetesProvider), @@ -109,6 +116,10 @@ func useJWTProvider(tc *framework.TestCase) { tc.ExternalSecret.Spec.SecretStoreRef.Name = jwtProviderName } +func useJWTK8sProvider(tc *framework.TestCase) { + tc.ExternalSecret.Spec.SecretStoreRef.Name = jwtK8sProviderName +} + func useKubernetesProvider(tc *framework.TestCase) { tc.ExternalSecret.Spec.SecretStoreRef.Name = kubernetesProviderName } diff --git a/pkg/provider/vault/vault.go b/pkg/provider/vault/vault.go index b4f0ce32d..4642172e1 100644 --- a/pkg/provider/vault/vault.go +++ b/pkg/provider/vault/vault.go @@ -29,10 +29,15 @@ import ( "github.com/go-logr/logr" vault "github.com/hashicorp/vault/api" "github.com/tidwall/gjson" + authenticationv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" ctrl "sigs.k8s.io/controller-runtime" kclient "sigs.k8s.io/controller-runtime/pkg/client" + ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" @@ -64,10 +69,13 @@ const ( errVaultRequest = "error from Vault request: %w" errVaultResponse = "cannot parse Vault response: %w" errServiceAccount = "cannot read Kubernetes service account token from file system: %w" + errJwtNoTokenSource = "neither `secretRef` nor `kubernetesServiceAccountToken` was supplied as token source for jwt authentication" errUnsupportedKvVersion = "cannot perform find operations with kv version v1" - 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" + + 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" + errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w" errGetKubeSecret = "cannot get Kubernetes secret %q: %w" errSecretKeyFmt = "cannot find secret data for key: %q" @@ -106,6 +114,7 @@ type Client interface { type client struct { kube kclient.Client + corev1 typedcorev1.CoreV1Interface store *esv1beta1.VaultProvider log logr.Logger client Client @@ -130,6 +139,21 @@ type connector struct { } func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) { + // controller-runtime/client does not support TokenRequest or other subresource APIs + // so we need to construct our own client and use it to fetch tokens + // (for Kubernetes service account token auth) + restCfg, err := ctrlcfg.GetConfig() + if err != nil { + return nil, err + } + clientset, err := kubernetes.NewForConfig(restCfg) + if err != nil { + return nil, err + } + return c.newClient(ctx, store, kube, clientset.CoreV1(), namespace) +} + +func (c *connector) newClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, namespace string) (esv1beta1.SecretsClient, error) { storeSpec := store.GetSpec() if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Vault == nil { return nil, errors.New(errVaultStore) @@ -138,6 +162,7 @@ func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, vStore := &client{ kube: kube, + corev1: corev1, store: vaultSpec, log: ctrl.Log.WithName("provider").WithName("vault"), namespace: namespace, @@ -200,8 +225,16 @@ func (c *connector) ValidateStore(store esv1beta1.GenericStore) error { } } if p.Auth.Jwt != nil { - if err := utils.ValidateSecretSelector(store, p.Auth.Jwt.SecretRef); err != nil { - return fmt.Errorf(errInvalidJwtSec, err) + if p.Auth.Jwt.SecretRef != nil { + if err := utils.ValidateSecretSelector(store, *p.Auth.Jwt.SecretRef); err != nil { + return fmt.Errorf(errInvalidJwtSec, err) + } + } else if p.Auth.Jwt.KubernetesServiceAccountToken != nil { + if err := utils.ValidateServiceAccountSelector(store, p.Auth.Jwt.KubernetesServiceAccountToken.ServiceAccountRef); err != nil { + return fmt.Errorf(errInvalidJwtSec, err) + } + } else { + return fmt.Errorf(errJwtNoTokenSource) } } if p.Auth.Kubernetes != nil { @@ -822,6 +855,27 @@ func (v *client) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySe return valueStr, nil } +func (v *client) serviceAccountToken(ctx context.Context, serviceAccountRef esmeta.ServiceAccountSelector, audiences []string, expirationSeconds int64) (string, error) { + tokenRequest := &authenticationv1.TokenRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v.namespace, + }, + Spec: authenticationv1.TokenRequestSpec{ + Audiences: audiences, + ExpirationSeconds: &expirationSeconds, + }, + } + if (v.storeKind == esv1beta1.ClusterSecretStoreKind) && + (serviceAccountRef.Namespace != nil) { + tokenRequest.Namespace = *serviceAccountRef.Namespace + } + tokenResponse, err := v.corev1.ServiceAccounts(tokenRequest.Namespace).CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{}) + if err != nil { + return "", fmt.Errorf(errGetKubeSATokenRequest, serviceAccountRef.Name, err) + } + return tokenResponse.Status.Token, nil +} + // checkToken does a lookup and checks if the provided token exists. func checkToken(ctx context.Context, vStore *client) error { // https://www.vaultproject.io/api-docs/auth/token#lookup-a-token-self @@ -995,7 +1049,24 @@ func (v *client) requestTokenWithLdapAuth(ctx context.Context, client Client, ld func (v *client) requestTokenWithJwtAuth(ctx context.Context, client Client, jwtAuth *esv1beta1.VaultJwtAuth) (string, error) { role := strings.TrimSpace(jwtAuth.Role) - jwt, err := v.secretKeyRef(ctx, &jwtAuth.SecretRef) + var jwt string + var err error + if jwtAuth.SecretRef != nil { + jwt, err = v.secretKeyRef(ctx, jwtAuth.SecretRef) + } else if k8sServiceAccountToken := jwtAuth.KubernetesServiceAccountToken; k8sServiceAccountToken != nil { + audiences := k8sServiceAccountToken.Audiences + if audiences == nil { + audiences = &[]string{"vault"} + } + expirationSeconds := k8sServiceAccountToken.ExpirationSeconds + if expirationSeconds == nil { + tmp := int64(600) + expirationSeconds = &tmp + } + jwt, err = v.serviceAccountToken(ctx, k8sServiceAccountToken.ServiceAccountRef, *audiences, *expirationSeconds) + } else { + err = fmt.Errorf(errJwtNoTokenSource) + } if err != nil { return "", err } diff --git a/pkg/provider/vault/vault_test.go b/pkg/provider/vault/vault_test.go index 93965e60c..a60a5b53e 100644 --- a/pkg/provider/vault/vault_test.go +++ b/pkg/provider/vault/vault_test.go @@ -564,7 +564,7 @@ func vaultTest(t *testing.T, name string, tc testCase) { if tc.args.newClientFunc == nil { conn.newVaultClient = newVaultClient } - _, err := conn.NewClient(context.Background(), tc.args.store, tc.args.kube, tc.args.ns) + _, err := conn.newClient(context.Background(), tc.args.store, tc.args.kube, nil, tc.args.ns) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nvault.New(...): -want error, +got error:\n%s", tc.reason, diff) } @@ -1361,7 +1361,7 @@ func TestValidateStore(t *testing.T) { args: args{ auth: esv1beta1.VaultAuth{ Jwt: &esv1beta1.VaultJwtAuth{ - SecretRef: esmeta.SecretKeySelector{ + SecretRef: &esmeta.SecretKeySelector{ Namespace: pointer.StringPtr("invalid"), }, },