From 7b8fef2c182702c018b8ed078ae310d52ecd8ad5 Mon Sep 17 00:00:00 2001 From: Gaurav Dasson Date: Thu, 11 May 2023 04:10:07 -0500 Subject: [PATCH] :sparkles: Enabling Vault IAM auth (#2208) * Enabling Vault IAM auth Signed-off-by: Gaurav Dasson * Adding spec Signed-off-by: Gaurav Dasson * Adding test cases and decoupling vault provider from aws for iam auth Signed-off-by: Gaurav Dasson * Fixing comments Signed-off-by: Gaurav Dasson * Fixing linter issues Signed-off-by: Gaurav Dasson * Fixing the check-diff errors Signed-off-by: Gaurav Dasson * Adding support for assumeRole operations when using static creds Signed-off-by: Gaurav Dasson * Bumping the dependencies to fix the go.mod/go.sum conflicts Signed-off-by: Gaurav Dasson * Bumping up e2e go mod files Signed-off-by: Gaurav Dasson --------- Signed-off-by: Gaurav Dasson --- .../v1beta1/secretstore_vault_types.go | 59 ++++ .../v1beta1/zz_generated.deepcopy.go | 97 ++++++ ...ternal-secrets.io_clustersecretstores.yaml | 129 ++++++++ .../external-secrets.io_secretstores.yaml | 129 ++++++++ ...ternal-secrets.io_vaultdynamicsecrets.yaml | 128 ++++++++ deploy/crds/bundle.yaml | 264 +++++++++++++++ docs/api/spec.md | 265 +++++++++++++++ docs/provider/hashicorp-vault.md | 53 ++- ...ult-iam-store-controller-pod-identity.yaml | 21 ++ docs/snippets/vault-iam-store-sa.yaml | 7 + .../vault-iam-store-static-creds.yaml | 33 ++ docs/snippets/vault-iam-store.yaml | 24 ++ e2e/go.mod | 1 - e2e/go.sum | 1 - go.mod | 5 + go.sum | 17 +- pkg/provider/vault/iamauth/iamauth.go | 309 ++++++++++++++++++ pkg/provider/vault/iamauth/iamauth_test.go | 60 ++++ pkg/provider/vault/util/vault.go | 3 + pkg/provider/vault/vault.go | 193 ++++++++++- pkg/provider/vault/vault_test.go | 62 ++++ 21 files changed, 1854 insertions(+), 6 deletions(-) create mode 100644 docs/snippets/vault-iam-store-controller-pod-identity.yaml create mode 100644 docs/snippets/vault-iam-store-sa.yaml create mode 100644 docs/snippets/vault-iam-store-static-creds.yaml create mode 100644 docs/snippets/vault-iam-store.yaml create mode 100644 pkg/provider/vault/iamauth/iamauth.go create mode 100644 pkg/provider/vault/iamauth/iamauth_test.go diff --git a/apis/externalsecrets/v1beta1/secretstore_vault_types.go b/apis/externalsecrets/v1beta1/secretstore_vault_types.go index a6b095be5..6230e1502 100644 --- a/apis/externalsecrets/v1beta1/secretstore_vault_types.go +++ b/apis/externalsecrets/v1beta1/secretstore_vault_types.go @@ -112,6 +112,11 @@ type VaultAuth struct { // Cert authentication method // +optional Cert *VaultCertAuth `json:"cert,omitempty"` + + // Iam authenticates with vault by passing a special AWS request signed with AWS IAM credentials + // AWS IAM authentication method + // +optional + Iam *VaultIamAuth `json:"iam,omitempty"` } // VaultAppRole authenticates with Vault using the App Role auth mechanism, @@ -178,6 +183,37 @@ type VaultLdapAuth struct { SecretRef esmeta.SecretKeySelector `json:"secretRef,omitempty"` } +// VaultAwsAuth tells the controller how to do authentication with aws. +// Only one of secretRef or jwt can be specified. +// if none is specified the controller will try to load credentials from its own service account assuming it is IRSA enabled. +type VaultAwsAuth struct { + // +optional + SecretRef *VaultAwsAuthSecretRef `json:"secretRef,omitempty"` + // +optional + JWTAuth *VaultAwsJWTAuth `json:"jwt,omitempty"` +} + +// VaultAWSAuthSecretRef holds secret references for AWS credentials +// both AccessKeyID and SecretAccessKey must be defined in order to properly authenticate. +type VaultAwsAuthSecretRef struct { + // The AccessKeyID is used for authentication + AccessKeyID esmeta.SecretKeySelector `json:"accessKeyIDSecretRef,omitempty"` + + // The SecretAccessKey is used for authentication + SecretAccessKey esmeta.SecretKeySelector `json:"secretAccessKeySecretRef,omitempty"` + + // The SessionToken used for authentication + // This must be defined if AccessKeyID and SecretAccessKey are temporary credentials + // see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html + // +Optional + SessionToken *esmeta.SecretKeySelector `json:"sessionTokenSecretRef,omitempty"` +} + +// Authenticate against AWS using service account tokens. +type VaultAwsJWTAuth struct { + ServiceAccountRef *esmeta.ServiceAccountSelector `json:"serviceAccountRef,omitempty"` +} + // VaultKubernetesServiceAccountTokenAuth authenticates with Vault using a temporary // Kubernetes service account token retrieved by the `TokenRequest` API. type VaultKubernetesServiceAccountTokenAuth struct { @@ -237,3 +273,26 @@ type VaultCertAuth struct { // authenticate with Vault using the Cert authentication method SecretRef esmeta.SecretKeySelector `json:"secretRef,omitempty"` } + +// VaultIamAuth authenticates with Vault using the Vault's AWS IAM authentication method. Refer: https://developer.hashicorp.com/vault/docs/auth/aws +type VaultIamAuth struct { + + // Path where the AWS auth method is enabled in Vault, e.g: "aws" + Path string `json:"path,omitempty"` + // AWS region + Region string `json:"region,omitempty"` + // This is the AWS role to be assumed before talking to vault + AWSIAMRole string `json:"role,omitempty"` + // Vault Role. In vault, a role describes an identity with a set of permissions, groups, or policies you want to attach a user of the secrets engine + Role string `json:"vaultRole"` + // AWS External ID set on assumed IAM roles + ExternalID string `json:"externalID,omitempty"` + // X-Vault-AWS-IAM-Server-ID is an additional header used by Vault IAM auth method to mitigate against different types of replay attacks. More details here: https://developer.hashicorp.com/vault/docs/auth/aws + VaultAWSIAMServerID string `json:"vaultAwsIamServerID,omitempty"` + // Specify credentials in a Secret object + // +optional + SecretRef *VaultAwsAuthSecretRef `json:"secretRef,omitempty"` + // Specify a service account with IRSA enabled + // +optional + JWTAuth *VaultAwsJWTAuth `json:"jwt,omitempty"` +} diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go index dfe5ce8eb..2a97b7ddc 100644 --- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go @@ -2051,6 +2051,11 @@ func (in *VaultAuth) DeepCopyInto(out *VaultAuth) { *out = new(VaultCertAuth) (*in).DeepCopyInto(*out) } + if in.Iam != nil { + in, out := &in.Iam, &out.Iam + *out = new(VaultIamAuth) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAuth. @@ -2063,6 +2068,73 @@ func (in *VaultAuth) DeepCopy() *VaultAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultAwsAuth) DeepCopyInto(out *VaultAwsAuth) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(VaultAwsAuthSecretRef) + (*in).DeepCopyInto(*out) + } + if in.JWTAuth != nil { + in, out := &in.JWTAuth, &out.JWTAuth + *out = new(VaultAwsJWTAuth) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAwsAuth. +func (in *VaultAwsAuth) DeepCopy() *VaultAwsAuth { + if in == nil { + return nil + } + out := new(VaultAwsAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultAwsAuthSecretRef) DeepCopyInto(out *VaultAwsAuthSecretRef) { + *out = *in + in.AccessKeyID.DeepCopyInto(&out.AccessKeyID) + in.SecretAccessKey.DeepCopyInto(&out.SecretAccessKey) + if in.SessionToken != nil { + in, out := &in.SessionToken, &out.SessionToken + *out = new(metav1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAwsAuthSecretRef. +func (in *VaultAwsAuthSecretRef) DeepCopy() *VaultAwsAuthSecretRef { + if in == nil { + return nil + } + out := new(VaultAwsAuthSecretRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultAwsJWTAuth) DeepCopyInto(out *VaultAwsJWTAuth) { + *out = *in + if in.ServiceAccountRef != nil { + in, out := &in.ServiceAccountRef, &out.ServiceAccountRef + *out = new(metav1.ServiceAccountSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAwsJWTAuth. +func (in *VaultAwsJWTAuth) DeepCopy() *VaultAwsJWTAuth { + if in == nil { + return nil + } + out := new(VaultAwsJWTAuth) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VaultCertAuth) DeepCopyInto(out *VaultCertAuth) { *out = *in @@ -2080,6 +2152,31 @@ func (in *VaultCertAuth) DeepCopy() *VaultCertAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultIamAuth) DeepCopyInto(out *VaultIamAuth) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(VaultAwsAuthSecretRef) + (*in).DeepCopyInto(*out) + } + if in.JWTAuth != nil { + in, out := &in.JWTAuth, &out.JWTAuth + *out = new(VaultAwsJWTAuth) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultIamAuth. +func (in *VaultIamAuth) DeepCopy() *VaultIamAuth { + if in == nil { + return nil + } + out := new(VaultIamAuth) + in.DeepCopyInto(out) + return out +} + // 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 diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml index 7aaedbc23..55b8a77b3 100644 --- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml +++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml @@ -3003,6 +3003,135 @@ spec: type: string type: object type: object + iam: + description: Iam authenticates with vault by passing a + special AWS request signed with AWS IAM credentials + AWS IAM authentication method + properties: + externalID: + description: AWS External ID set on assumed IAM roles + type: string + jwt: + description: Specify a service account with IRSA enabled + properties: + serviceAccountRef: + description: A reference to a ServiceAccount resource. + properties: + audiences: + description: Audience specifies the `aud` + claim for the service account token If the + service account uses a well-known annotation + for e.g. IRSA or GCP Workload Identity then + this audiences will be appended to the list + items: + type: string + type: array + 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 + type: object + path: + description: 'Path where the AWS auth method is enabled + in Vault, e.g: "aws"' + type: string + region: + description: AWS region + type: string + role: + description: This is the AWS role to be assumed before + talking to vault + type: string + secretRef: + description: Specify credentials in a Secret object + properties: + accessKeyIDSecretRef: + description: The AccessKeyID is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + secretAccessKeySecretRef: + description: The SecretAccessKey is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + sessionTokenSecretRef: + description: 'The SessionToken used for authentication + This must be defined if AccessKeyID and SecretAccessKey + are temporary credentials see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html' + 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. + type: string + name: + description: The name of the Secret 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 + type: object + type: object + vaultAwsIamServerID: + description: 'X-Vault-AWS-IAM-Server-ID is an additional + header used by Vault IAM auth method to mitigate + against different types of replay attacks. More + details here: https://developer.hashicorp.com/vault/docs/auth/aws' + type: string + vaultRole: + description: Vault Role. In vault, a role describes + an identity with a set of permissions, groups, or + policies you want to attach a user of the secrets + engine + type: string + required: + - vaultRole + type: object jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml index 91cab3799..875fe420e 100644 --- a/config/crds/bases/external-secrets.io_secretstores.yaml +++ b/config/crds/bases/external-secrets.io_secretstores.yaml @@ -3003,6 +3003,135 @@ spec: type: string type: object type: object + iam: + description: Iam authenticates with vault by passing a + special AWS request signed with AWS IAM credentials + AWS IAM authentication method + properties: + externalID: + description: AWS External ID set on assumed IAM roles + type: string + jwt: + description: Specify a service account with IRSA enabled + properties: + serviceAccountRef: + description: A reference to a ServiceAccount resource. + properties: + audiences: + description: Audience specifies the `aud` + claim for the service account token If the + service account uses a well-known annotation + for e.g. IRSA or GCP Workload Identity then + this audiences will be appended to the list + items: + type: string + type: array + 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 + type: object + path: + description: 'Path where the AWS auth method is enabled + in Vault, e.g: "aws"' + type: string + region: + description: AWS region + type: string + role: + description: This is the AWS role to be assumed before + talking to vault + type: string + secretRef: + description: Specify credentials in a Secret object + properties: + accessKeyIDSecretRef: + description: The AccessKeyID is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + secretAccessKeySecretRef: + description: The SecretAccessKey is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + sessionTokenSecretRef: + description: 'The SessionToken used for authentication + This must be defined if AccessKeyID and SecretAccessKey + are temporary credentials see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html' + 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. + type: string + name: + description: The name of the Secret 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 + type: object + type: object + vaultAwsIamServerID: + description: 'X-Vault-AWS-IAM-Server-ID is an additional + header used by Vault IAM auth method to mitigate + against different types of replay attacks. More + details here: https://developer.hashicorp.com/vault/docs/auth/aws' + type: string + vaultRole: + description: Vault Role. In vault, a role describes + an identity with a set of permissions, groups, or + policies you want to attach a user of the secrets + engine + type: string + required: + - vaultRole + type: object jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method diff --git a/config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml b/config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml index 3fb8a2e18..b53d5f442 100644 --- a/config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml +++ b/config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml @@ -139,6 +139,134 @@ spec: type: string type: object type: object + iam: + description: Iam authenticates with vault by passing a special + AWS request signed with AWS IAM credentials AWS IAM authentication + method + properties: + externalID: + description: AWS External ID set on assumed IAM roles + type: string + jwt: + description: Specify a service account with IRSA enabled + properties: + serviceAccountRef: + description: A reference to a ServiceAccount resource. + properties: + audiences: + description: Audience specifies the `aud` claim + for the service account token If the service + account uses a well-known annotation for e.g. + IRSA or GCP Workload Identity then this audiences + will be appended to the list + items: + type: string + type: array + 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 + type: object + path: + description: 'Path where the AWS auth method is enabled + in Vault, e.g: "aws"' + type: string + region: + description: AWS region + type: string + role: + description: This is the AWS role to be assumed before + talking to vault + type: string + secretRef: + description: Specify credentials in a Secret object + properties: + accessKeyIDSecretRef: + description: The AccessKeyID is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + secretAccessKeySecretRef: + description: The SecretAccessKey is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + sessionTokenSecretRef: + description: 'The SessionToken used for authentication + This must be defined if AccessKeyID and SecretAccessKey + are temporary credentials see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html' + 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. + type: string + name: + description: The name of the Secret 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 + type: object + type: object + vaultAwsIamServerID: + description: 'X-Vault-AWS-IAM-Server-ID is an additional + header used by Vault IAM auth method to mitigate against + different types of replay attacks. More details here: + https://developer.hashicorp.com/vault/docs/auth/aws' + type: string + vaultRole: + description: Vault Role. In vault, a role describes an + identity with a set of permissions, groups, or policies + you want to attach a user of the secrets engine + type: string + required: + - vaultRole + type: object jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml index 7b697c7f0..54c1533cd 100644 --- a/deploy/crds/bundle.yaml +++ b/deploy/crds/bundle.yaml @@ -2656,6 +2656,94 @@ spec: type: string type: object type: object + iam: + description: Iam authenticates with vault by passing a special AWS request signed with AWS IAM credentials AWS IAM authentication method + properties: + externalID: + description: AWS External ID set on assumed IAM roles + type: string + jwt: + description: Specify a service account with IRSA enabled + properties: + serviceAccountRef: + description: A reference to a ServiceAccount resource. + properties: + audiences: + description: Audience specifies the `aud` claim for the service account token If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity then this audiences will be appended to the list + items: + type: string + type: array + 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 + type: object + path: + description: 'Path where the AWS auth method is enabled in Vault, e.g: "aws"' + type: string + region: + description: AWS region + type: string + role: + description: This is the AWS role to be assumed before talking to vault + type: string + secretRef: + description: Specify credentials in a Secret object + properties: + accessKeyIDSecretRef: + description: The AccessKeyID is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + secretAccessKeySecretRef: + description: The SecretAccessKey is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + sessionTokenSecretRef: + description: 'The SessionToken used for authentication This must be defined if AccessKeyID and SecretAccessKey are temporary credentials see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html' + 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. + type: string + name: + description: The name of the Secret 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 + type: object + type: object + vaultAwsIamServerID: + description: 'X-Vault-AWS-IAM-Server-ID is an additional header used by Vault IAM auth method to mitigate against different types of replay attacks. More details here: https://developer.hashicorp.com/vault/docs/auth/aws' + type: string + vaultRole: + description: Vault Role. In vault, a role describes an identity with a set of permissions, groups, or policies you want to attach a user of the secrets engine + type: string + required: + - vaultRole + type: object jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: @@ -6113,6 +6201,94 @@ spec: type: string type: object type: object + iam: + description: Iam authenticates with vault by passing a special AWS request signed with AWS IAM credentials AWS IAM authentication method + properties: + externalID: + description: AWS External ID set on assumed IAM roles + type: string + jwt: + description: Specify a service account with IRSA enabled + properties: + serviceAccountRef: + description: A reference to a ServiceAccount resource. + properties: + audiences: + description: Audience specifies the `aud` claim for the service account token If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity then this audiences will be appended to the list + items: + type: string + type: array + 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 + type: object + path: + description: 'Path where the AWS auth method is enabled in Vault, e.g: "aws"' + type: string + region: + description: AWS region + type: string + role: + description: This is the AWS role to be assumed before talking to vault + type: string + secretRef: + description: Specify credentials in a Secret object + properties: + accessKeyIDSecretRef: + description: The AccessKeyID is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + secretAccessKeySecretRef: + description: The SecretAccessKey is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + sessionTokenSecretRef: + description: 'The SessionToken used for authentication This must be defined if AccessKeyID and SecretAccessKey are temporary credentials see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html' + 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. + type: string + name: + description: The name of the Secret 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 + type: object + type: object + vaultAwsIamServerID: + description: 'X-Vault-AWS-IAM-Server-ID is an additional header used by Vault IAM auth method to mitigate against different types of replay attacks. More details here: https://developer.hashicorp.com/vault/docs/auth/aws' + type: string + vaultRole: + description: Vault Role. In vault, a role describes an identity with a set of permissions, groups, or policies you want to attach a user of the secrets engine + type: string + required: + - vaultRole + type: object jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: @@ -7154,6 +7330,94 @@ spec: type: string type: object type: object + iam: + description: Iam authenticates with vault by passing a special AWS request signed with AWS IAM credentials AWS IAM authentication method + properties: + externalID: + description: AWS External ID set on assumed IAM roles + type: string + jwt: + description: Specify a service account with IRSA enabled + properties: + serviceAccountRef: + description: A reference to a ServiceAccount resource. + properties: + audiences: + description: Audience specifies the `aud` claim for the service account token If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity then this audiences will be appended to the list + items: + type: string + type: array + 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 + type: object + path: + description: 'Path where the AWS auth method is enabled in Vault, e.g: "aws"' + type: string + region: + description: AWS region + type: string + role: + description: This is the AWS role to be assumed before talking to vault + type: string + secretRef: + description: Specify credentials in a Secret object + properties: + accessKeyIDSecretRef: + description: The AccessKeyID is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + secretAccessKeySecretRef: + description: The SecretAccessKey is used for authentication + 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. + type: string + name: + description: The name of the Secret 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 + type: object + sessionTokenSecretRef: + description: 'The SessionToken used for authentication This must be defined if AccessKeyID and SecretAccessKey are temporary credentials see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html' + 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. + type: string + name: + description: The name of the Secret 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 + type: object + type: object + vaultAwsIamServerID: + description: 'X-Vault-AWS-IAM-Server-ID is an additional header used by Vault IAM auth method to mitigate against different types of replay attacks. More details here: https://developer.hashicorp.com/vault/docs/auth/aws' + type: string + vaultRole: + description: Vault Role. In vault, a role describes an identity with a set of permissions, groups, or policies you want to attach a user of the secrets engine + type: string + required: + - vaultRole + type: object jwt: description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method properties: diff --git a/docs/api/spec.md b/docs/api/spec.md index a03ccb898..428ddd506 100644 --- a/docs/api/spec.md +++ b/docs/api/spec.md @@ -5543,6 +5543,158 @@ VaultCertAuth Cert authentication method

+ + +iam
+ + +VaultIamAuth + + + + +(Optional) +

Iam authenticates with vault by passing a special AWS request signed with AWS IAM credentials +AWS IAM authentication method

+ + + + +

VaultAwsAuth +

+

+

VaultAwsAuth tells the controller how to do authentication with aws. +Only one of secretRef or jwt can be specified. +if none is specified the controller will try to load credentials from its own service account assuming it is IRSA enabled.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+secretRef
+ + +VaultAwsAuthSecretRef + + +
+(Optional) +
+jwt
+ + +VaultAwsJWTAuth + + +
+(Optional) +
+

VaultAwsAuthSecretRef +

+

+(Appears on: +VaultAwsAuth, +VaultIamAuth) +

+

+

VaultAWSAuthSecretRef holds secret references for AWS credentials +both AccessKeyID and SecretAccessKey must be defined in order to properly authenticate.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+accessKeyIDSecretRef
+ + +External Secrets meta/v1.SecretKeySelector + + +
+

The AccessKeyID is used for authentication

+
+secretAccessKeySecretRef
+ + +External Secrets meta/v1.SecretKeySelector + + +
+

The SecretAccessKey is used for authentication

+
+sessionTokenSecretRef
+ + +External Secrets meta/v1.SecretKeySelector + + +
+

The SessionToken used for authentication +This must be defined if AccessKeyID and SecretAccessKey are temporary credentials +see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html

+
+

VaultAwsJWTAuth +

+

+(Appears on: +VaultAwsAuth, +VaultIamAuth) +

+

+

Authenticate against AWS using service account tokens.

+

+ + + + + + + + + + + +
FieldDescription
+serviceAccountRef
+ + +External Secrets meta/v1.ServiceAccountSelector + + +
+

VaultCertAuth @@ -5594,6 +5746,119 @@ authenticate with Vault using the Cert authentication method

+

VaultIamAuth +

+

+(Appears on: +VaultAuth) +

+

+

VaultIamAuth authenticates with Vault using the Vault’s AWS IAM authentication method. Refer: https://developer.hashicorp.com/vault/docs/auth/aws

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+path
+ +string + +
+

Path where the AWS auth method is enabled in Vault, e.g: “aws”

+
+region
+ +string + +
+

AWS region

+
+role
+ +string + +
+

This is the AWS role to be assumed before talking to vault

+
+vaultRole
+ +string + +
+

Vault Role. In vault, a role describes an identity with a set of permissions, groups, or policies you want to attach a user of the secrets engine

+
+externalID
+ +string + +
+

AWS External ID set on assumed IAM roles

+
+vaultAwsIamServerID
+ +string + +
+

X-Vault-AWS-IAM-Server-ID is an additional header used by Vault IAM auth method to mitigate against different types of replay attacks. More details here: https://developer.hashicorp.com/vault/docs/auth/aws

+
+secretRef
+ + +VaultAwsAuthSecretRef + + +
+(Optional) +

Specify credentials in a Secret object

+
+jwt
+ + +VaultAwsJWTAuth + + +
+(Optional) +

Specify a service account with IRSA enabled

+

VaultJwtAuth

diff --git a/docs/provider/hashicorp-vault.md b/docs/provider/hashicorp-vault.md index 891666a64..20ced7d04 100644 --- a/docs/provider/hashicorp-vault.md +++ b/docs/provider/hashicorp-vault.md @@ -271,8 +271,9 @@ We support five different modes for authentication: [token-based](https://www.vaultproject.io/docs/auth/token), [appRole](https://www.vaultproject.io/docs/auth/approle), [kubernetes-native](https://www.vaultproject.io/docs/auth/kubernetes), -[ldap](https://www.vaultproject.io/docs/auth/ldap) and -[jwt/oidc](https://www.vaultproject.io/docs/auth/jwt), each one comes with it's own +[ldap](https://www.vaultproject.io/docs/auth/ldap), +[jwt/oidc](https://www.vaultproject.io/docs/auth/jwt) and +[awsAuth](https://developer.hashicorp.com/vault/docs/auth/aws), each one comes with it's own trade-offs. Depending on the authentication method you need to adapt your environment. #### Token-based authentication @@ -333,6 +334,54 @@ or `Kind=ClusterSecretStore` resource. ``` **NOTE:** In case of a `ClusterSecretStore`, Be sure to provide `namespace` in `secretRef` with the namespace where the secret resides. +#### AWS IAM authentication + +[AWS IAM](https://developer.hashicorp.com/vault/docs/auth/aws) uses either a +set of AWS Programmatic access credentials stored in a `Kind=Secret` and referenced by the +`secretRef` or by getting the authentication token from an [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) enabled service account + +### Access Key ID & Secret Access Key +You can store Access Key ID & Secret Access Key in a `Kind=Secret` and reference it from a SecretStore. + +```yaml +{% include 'vault-iam-store-static-creds.yaml' %} +``` + +**NOTE:** In case of a `ClusterSecretStore`, Be sure to provide `namespace` in `accessKeyIDSecretRef`, `secretAccessKeySecretRef` with the namespaces where the secrets reside. + +### EKS Service Account credentials + +This feature lets you use short-lived service account tokens to authenticate with AWS. +You must have [Service Account Volume Projection](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-token-volume-projection) enabled - it is by default on EKS. See [EKS guide](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html) on how to set up IAM roles for service accounts. + +The big advantage of this approach is that ESO runs without any credentials. + +```yaml +{% include 'vault-iam-store-sa.yaml' %} +``` + +Reference the service account from above in the Secret Store: + +```yaml +{% include 'vault-iam-store.yaml' %} +``` +### Controller's Pod Identity + +This is basicially a zero-configuration authentication approach that inherits the credentials from the controller's pod identity + +This approach assumes that appropriate IRSA setup is done controller's pod (i.e. IRSA enabled IAM role is created appropriately and controller's service account is annotated appropriately with the annotation "eks.amazonaws.com/role-arn" to enable IRSA) + +```yaml +{% include 'vault-iam-store-controller-pod-identity.yaml' %} +``` + +**NOTE:** In case of a `ClusterSecretStore`, Be sure to provide `namespace` for `serviceAccountRef` with the namespace where the service account resides. + +```yaml +{% include 'vault-jwt-store.yaml' %} +``` +**NOTE:** In case of a `ClusterSecretStore`, Be sure to provide `namespace` in `secretRef` with the namespace where the secret resides. + ### PushSecret Vault supports PushSecret features which allow you to sync a given kubernetes secret key into a hashicorp vault secret. In order to do so, it is expected that the secret key is a valid JSON object. diff --git a/docs/snippets/vault-iam-store-controller-pod-identity.yaml b/docs/snippets/vault-iam-store-controller-pod-identity.yaml new file mode 100644 index 000000000..55f99695c --- /dev/null +++ b/docs/snippets/vault-iam-store-controller-pod-identity.yaml @@ -0,0 +1,21 @@ +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: vault-backend-aws-iam +spec: + provider: + vault: + server: "http://my.vault.server:8200" + path: secret + version: v2 + namespace: + auth: + iam: + # Path where the AWS auth method is enabled in Vault, e.g: "aws/". Defaults to aws + path: aws + # AWS Region. Defaults to us-east-1 + region: us-east-1 + # Vault Role. In vault, a role describes an identity with a set of permissions, groups, or policies you want to attach a user of the secrets engine + vaultRole: vault-role-for-aws-iam-auth + # Optional. Placeholder to supply header X-Vault-AWS-IAM-Server-ID. It is an additional (optional) header used by Vault IAM auth method to mitigate against different types of replay attacks. More details here: https://developer.hashicorp.com/vault/docs/auth/aws + vaultAwsIamServerID: example-vaultAwsIamServerID \ No newline at end of file diff --git a/docs/snippets/vault-iam-store-sa.yaml b/docs/snippets/vault-iam-store-sa.yaml new file mode 100644 index 000000000..2ceb3cc28 --- /dev/null +++ b/docs/snippets/vault-iam-store-sa.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-irsa-enabled-role + name: my-serviceaccount + namespace: default \ No newline at end of file diff --git a/docs/snippets/vault-iam-store-static-creds.yaml b/docs/snippets/vault-iam-store-static-creds.yaml new file mode 100644 index 000000000..2a929b93a --- /dev/null +++ b/docs/snippets/vault-iam-store-static-creds.yaml @@ -0,0 +1,33 @@ +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: vault-backend-aws-iam +spec: + provider: + vault: + server: "http://my.vault.server:8200" + path: secret + version: v2 + namespace: + auth: + iam: + # Path where the AWS auth method is enabled in Vault, e.g: "aws/". Defaults to aws + path: aws + # AWS Region. Defaults to us-east-1 + region: us-east-1 + # optional: assume role before fetching secrets + role: arn:aws:iam::1234567890:role/role-a + # Vault Role. In vault, a role describes an identity with a set of permissions, groups, or policies you want to attach a user of the secrets engine + vaultRole: vault-role-for-aws-iam-auth + # Optional. Placeholder to supply header X-Vault-AWS-IAM-Server-ID. It is an additional (optional) header used by Vault IAM auth method to mitigate against different types of replay attacks. More details here: https://developer.hashicorp.com/vault/docs/auth/aws + vaultAwsIamServerID: example-vaultAwsIamServerID + secretRef: #Use this method when you have static AWS creds. + accessKeyIDSecretRef: + name: vault-iam-creds-secret + key: access-key + secretAccessKeySecretRef: + name: vault-iam-creds-secret + key: secret-access-key + sessionTokenSecretRef: + name: vault-iam-creds-secret + key: secret-session-token \ No newline at end of file diff --git a/docs/snippets/vault-iam-store.yaml b/docs/snippets/vault-iam-store.yaml new file mode 100644 index 000000000..c78610f68 --- /dev/null +++ b/docs/snippets/vault-iam-store.yaml @@ -0,0 +1,24 @@ +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: vault-backend-aws-iam +spec: + provider: + vault: + server: "http://my.vault.server:8200" + path: secret + version: v2 + namespace: + auth: + iam: + # Path where the AWS auth method is enabled in Vault, e.g: "aws/". Defaults to aws + path: aws + # AWS Region. Defaults to us-east-1 + region: us-east-1 + # Vault Role. In vault, a role describes an identity with a set of permissions, groups, or policies you want to attach a user of the secrets engine + vaultRole: vault-role-for-aws-iam-auth + # Optional. Placeholder to supply header X-Vault-AWS-IAM-Server-ID. It is an additional (optional) header used by Vault IAM auth method to mitigate against different types of replay attacks. More details here: https://developer.hashicorp.com/vault/docs/auth/aws + vaultAwsIamServerID: example-vaultAwsIamServerID + jwt: + serviceAccountRef: + name: my-serviceaccount #Provide service account with IRSA enabled \ No newline at end of file diff --git a/e2e/go.mod b/e2e/go.mod index 5f5a801af..aec41a34e 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -179,7 +179,6 @@ require ( github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/robfig/cron v1.2.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday v1.5.2 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index 394bb29d1..efa93383a 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -853,7 +853,6 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= diff --git a/go.mod b/go.mod index 25c29ad99..497a9c5ab 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,9 @@ require ( github.com/alibabacloud-go/tea-utils/v2 v2.0.1 github.com/aliyun/credentials-go v1.2.7 github.com/avast/retry-go/v4 v4.3.4 + github.com/golang-jwt/jwt/v5 v5.0.0-rc.2 github.com/hashicorp/golang-lru v0.5.4 + github.com/hashicorp/vault/api/auth/aws v0.4.0 github.com/keeper-security/secrets-manager-go/core v1.5.0 github.com/maxbrunsfeld/counterfeiter/v6 v6.6.1 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.16 @@ -90,6 +92,9 @@ require ( github.com/clbanning/mxj/v2 v2.5.7 // indirect github.com/go-playground/validator/v10 v10.13.0 // indirect github.com/google/s2a-go v0.1.3 // indirect + github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 // indirect + github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect ) diff --git a/go.sum b/go.sum index b66b25c6d..e910c0205 100644 --- a/go.sum +++ b/go.sum @@ -154,6 +154,7 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/avast/retry-go/v4 v4.3.4 h1:pHLkL7jvCvP317I8Ge+Km2Yhntv3SdkJm7uekkqbKhM= github.com/avast/retry-go/v4 v4.3.4/go.mod h1:rv+Nla6Vk3/ilU0H51VHddWHiwimzX66yZ0JT6T+UvE= +github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.41.13/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.44.254 h1:8baW4yal2xGiM/Wm5/ZU10drS8sd+BVjMjPFjJx2ooc= github.com/aws/aws-sdk-go v1.44.254/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= @@ -255,6 +256,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ= github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= @@ -272,6 +274,8 @@ github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0-rc.2 h1:hXPcSazn8wKOfSb9y2m1bdgUMlDxVDarxh3lJVbC6JE= +github.com/golang-jwt/jwt/v5 v5.0.0-rc.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -381,6 +385,8 @@ github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUD github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 h1:W9WN8p6moV1fjKLkeqEgkAMu5rauy9QeYDAmIaPuuiA= +github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= @@ -389,6 +395,8 @@ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= @@ -401,6 +409,8 @@ github.com/hashicorp/vault/api v1.9.1 h1:LtY/I16+5jVGU8rufyyAkwopgq/HpUnxFBg+QLO github.com/hashicorp/vault/api v1.9.1/go.mod h1:78kktNcQYbBGSrOjQfHjXN32OhhxXnbYl3zxpd2uPUs= github.com/hashicorp/vault/api/auth/approle v0.4.0 h1:tjJHoUkPx8zRoFlFy86uvgg/1gpTnDPp0t0BYWTKjjw= github.com/hashicorp/vault/api/auth/approle v0.4.0/go.mod h1:D2gEpR0aS/F/MEcSjmhUlOsuK1RMVZojsnIQAEf0EV0= +github.com/hashicorp/vault/api/auth/aws v0.4.0 h1:2Myo+XU3X5gQTtr3S+WGXcrLUa6iO4w97VzFFaaBOm8= +github.com/hashicorp/vault/api/auth/aws v0.4.0/go.mod h1:CGm5PAXEREuYpszyA2ERQPFBSIUD+QTqXfKvdI2Gw/Q= github.com/hashicorp/vault/api/auth/kubernetes v0.4.0 h1:f6OIOF9012JIdqYvOeeewxhtQdJosnog2CHzh33j41s= github.com/hashicorp/vault/api/auth/kubernetes v0.4.0/go.mod h1:tMewM2hPyFNKP1EXdWbc0dUHHoS5V/0qS04BEaxuy78= github.com/hashicorp/vault/api/auth/ldap v0.4.0 h1:/P2HCNmcDY6s22JBXxVhr9noaFqPEQS2qwSnWIYezkc= @@ -416,6 +426,7 @@ github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+h github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -437,6 +448,7 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -541,7 +553,9 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -812,6 +826,7 @@ golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/provider/vault/iamauth/iamauth.go b/pkg/provider/vault/iamauth/iamauth.go new file mode 100644 index 000000000..e26631834 --- /dev/null +++ b/pkg/provider/vault/iamauth/iamauth.go @@ -0,0 +1,309 @@ +/* +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. +*/ + +// Mostly sourced from ~/external-secrets/pkg/provider/aws/auth +package iamauth + +import ( + "context" + "fmt" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sts" + "github.com/aws/aws-sdk-go/service/sts/stsiface" + authv1 "k8s.io/api/authentication/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + k8scorev1 "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" + "github.com/external-secrets/external-secrets/pkg/provider/vault/util" +) + +var ( + logger = ctrl.Log.WithName("provider").WithName("vault") +) + +const ( + roleARNAnnotation = "eks.amazonaws.com/role-arn" + audienceAnnotation = "eks.amazonaws.com/audience" + defaultTokenAudience = "sts.amazonaws.com" + + STSEndpointEnv = "AWS_STS_ENDPOINT" + AWSWebIdentityTokenFileEnvVar = "AWS_WEB_IDENTITY_TOKEN_FILE" + + errInvalidClusterStoreMissingAKIDNamespace = "invalid ClusterSecretStore: missing AWS AccessKeyID Namespace" + errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing AWS SecretAccessKey Namespace" + errFetchAKIDSecret = "could not fetch accessKeyID secret: %w" + errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w" + errFetchSTSecret = "could not fetch SessionToken secret: %w" + errMissingSAK = "missing SecretAccessKey" + errMissingAKID = "missing AccessKeyID" +) + +// DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity +// 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. +func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error) { + cfg, err := ctrlcfg.GetConfig() + if err != nil { + return nil, err + } + clientset, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + handlers := defaults.Handlers() + handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets")) + awscfg := aws.NewConfig().WithEndpointResolver(ResolveEndpoint()) + if region != "" { + awscfg.WithRegion(region) + } + sess, err := session.NewSessionWithOptions(session.Options{ + Config: *awscfg, + SharedConfigState: session.SharedConfigDisable, + Handlers: handlers, + }) + if err != nil { + return nil, err + } + tokenFetcher := &authTokenFetcher{ + Namespace: namespace, + Audiences: aud, + ServiceAccount: name, + k8sClient: clientset.CoreV1(), + } + + return stscreds.NewWebIdentityRoleProviderWithOptions( + sts.New(sess), roleArn, "external-secrets-provider-vault", tokenFetcher), nil +} + +// ResolveEndpoint returns a ResolverFunc with +// customizable endpoints. +func ResolveEndpoint() endpoints.ResolverFunc { + customEndpoints := make(map[string]string) + if v := os.Getenv(STSEndpointEnv); v != "" { + customEndpoints["sts"] = v + } + return ResolveEndpointWithServiceMap(customEndpoints) +} + +func ResolveEndpointWithServiceMap(customEndpoints map[string]string) endpoints.ResolverFunc { + defaultResolver := endpoints.DefaultResolver() + return func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { + if ep, ok := customEndpoints[service]; ok { + return endpoints.ResolvedEndpoint{ + URL: ep, + }, nil + } + return defaultResolver.EndpointFor(service, region, opts...) + } +} + +// mostly taken from: +// https://github.com/aws/secrets-store-csi-driver-provider-aws/blob/main/auth/auth.go#L140-L145 + +type authTokenFetcher struct { + Namespace string + // Audience is the token aud claim + // which is verified by the aws oidc provider + // see: https://github.com/external-secrets/external-secrets/issues/1251#issuecomment-1161745849 + Audiences []string + ServiceAccount string + k8sClient k8scorev1.CoreV1Interface +} + +// FetchToken satisfies the stscreds.TokenFetcher interface +// it is used to generate service account tokens which are consumed by the aws sdk. +func (p authTokenFetcher) FetchToken(ctx credentials.Context) ([]byte, error) { + logger.V(1).Info("fetching token", "ns", p.Namespace, "sa", p.ServiceAccount) + tokRsp, err := p.k8sClient.ServiceAccounts(p.Namespace).CreateToken(ctx, p.ServiceAccount, &authv1.TokenRequest{ + Spec: authv1.TokenRequestSpec{ + Audiences: p.Audiences, + }, + }, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("error creating service account token: %w", err) + } + return []byte(tokRsp.Status.Token), nil +} + +// CredsFromServiceAccount uses a Kubernetes Service Account to acquire temporary +// credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined +// in the ServiceAccount annotation. +// If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth). +// If the ClusterSecretStore defines the namespace it will take precedence. +func CredsFromServiceAccount(ctx context.Context, auth esv1beta1.VaultIamAuth, region string, isClusterKind bool, kube kclient.Client, namespace string, jwtProvider util.JwtProviderFactory) (*credentials.Credentials, error) { + name := auth.JWTAuth.ServiceAccountRef.Name + if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil { + namespace = *auth.JWTAuth.ServiceAccountRef.Namespace + } + sa := v1.ServiceAccount{} + err := kube.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: namespace, + }, &sa) + if err != nil { + return nil, err + } + // the service account is expected to have a well-known annotation + // this is used as input to assumeRoleWithWebIdentity + roleArn := sa.Annotations[roleARNAnnotation] + if roleArn == "" { + return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace) + } + + tokenAud := sa.Annotations[audienceAnnotation] + if tokenAud == "" { + tokenAud = defaultTokenAudience + } + audiences := []string{tokenAud} + if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 { + audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...) + } + + jwtProv, err := jwtProvider(name, namespace, roleArn, audiences, region) + if err != nil { + return nil, err + } + + logger.V(1).Info("using credentials via service account", "role", roleArn, "region", region) + return credentials.NewCredentials(jwtProv), nil +} + +func CredsFromControllerServiceAccount(ctx context.Context, saname, ns, region string, kube kclient.Client, jwtProvider util.JwtProviderFactory) (*credentials.Credentials, error) { + name := saname + nmspc := ns + + sa := v1.ServiceAccount{} + err := kube.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: nmspc, + }, &sa) + if err != nil { + return nil, err + } + // the service account is expected to have a well-known annotation + // this is used as input to assumeRoleWithWebIdentity + roleArn := sa.Annotations[roleARNAnnotation] + if roleArn == "" { + return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, nmspc) + } + + tokenAud := sa.Annotations[audienceAnnotation] + if tokenAud == "" { + tokenAud = defaultTokenAudience + } + audiences := []string{tokenAud} + + jwtProv, err := jwtProvider(name, nmspc, roleArn, audiences, region) + if err != nil { + return nil, err + } + + logger.V(1).Info("using credentials via service account", "role", roleArn, "region", region) + return credentials.NewCredentials(jwtProv), nil +} + +// CredsFromSecretRef pulls access-key / secret-access-key from a secretRef to +// construct a aws.Credentials object +// The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth) +// If the ClusterSecretStore defines a namespace it will take precedence. +func CredsFromSecretRef(ctx context.Context, auth esv1beta1.VaultIamAuth, isClusterKind bool, kube kclient.Client, namespace string) (*credentials.Credentials, error) { + ke := kclient.ObjectKey{ + Name: auth.SecretRef.AccessKeyID.Name, + Namespace: namespace, + } + if isClusterKind && auth.SecretRef.AccessKeyID.Namespace != nil { + ke.Namespace = *auth.SecretRef.AccessKeyID.Namespace + } + akSecret := v1.Secret{} + err := kube.Get(ctx, ke, &akSecret) + if err != nil { + return nil, fmt.Errorf(errFetchAKIDSecret, err) + } + ke = kclient.ObjectKey{ + Name: auth.SecretRef.SecretAccessKey.Name, + Namespace: namespace, + } + if isClusterKind && auth.SecretRef.SecretAccessKey.Namespace != nil { + ke.Namespace = *auth.SecretRef.SecretAccessKey.Namespace + } + sakSecret := v1.Secret{} + err = kube.Get(ctx, ke, &sakSecret) + if err != nil { + return nil, fmt.Errorf(errFetchSAKSecret, err) + } + sak := string(sakSecret.Data[auth.SecretRef.SecretAccessKey.Key]) + aks := string(akSecret.Data[auth.SecretRef.AccessKeyID.Key]) + if sak == "" { + return nil, fmt.Errorf(errMissingSAK) + } + if aks == "" { + return nil, fmt.Errorf(errMissingAKID) + } + + var sessionToken string + if auth.SecretRef.SessionToken != nil { + ke = kclient.ObjectKey{ + Name: auth.SecretRef.SessionToken.Name, + Namespace: namespace, + } + if isClusterKind && auth.SecretRef.SessionToken.Namespace != nil { + ke.Namespace = *auth.SecretRef.SessionToken.Namespace + } + stSecret := v1.Secret{} + err = kube.Get(ctx, ke, &stSecret) + if err != nil { + return nil, fmt.Errorf(errFetchSTSecret, err) + } + sessionToken = string(stSecret.Data[auth.SecretRef.SessionToken.Key]) + } + + return credentials.NewStaticCredentials(aks, sak, sessionToken), err +} + +type STSProvider func(*session.Session) stsiface.STSAPI + +func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI { + return sts.New(sess) +} + +// getAWSSession returns the aws session or an error. +func GetAWSSession(config *aws.Config) (*session.Session, error) { + handlers := defaults.Handlers() + handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets")) + sess, err := session.NewSessionWithOptions(session.Options{ + Config: *config, + Handlers: handlers, + SharedConfigState: session.SharedConfigDisable, + }) + if err != nil { + return nil, err + } + + return sess, nil +} diff --git a/pkg/provider/vault/iamauth/iamauth_test.go b/pkg/provider/vault/iamauth/iamauth_test.go new file mode 100644 index 000000000..ecc488614 --- /dev/null +++ b/pkg/provider/vault/iamauth/iamauth_test.go @@ -0,0 +1,60 @@ +/* +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 iamauth + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/external-secrets/external-secrets/pkg/provider/util/fake" +) + +func TestTokenFetcher(t *testing.T) { + tf := &authTokenFetcher{ + ServiceAccount: "foobar", + Namespace: "example", + k8sClient: fake.NewCreateTokenMock().WithToken("FAKETOKEN"), + } + token, err := tf.FetchToken(context.Background()) + assert.Nil(t, err) + assert.Equal(t, []byte("FAKETOKEN"), token) +} + +func TestResolver(t *testing.T) { + tbl := []struct { + env string + service string + url string + }{ + { + env: STSEndpointEnv, + service: "sts", + url: "http://sts.foo", + }, + } + + for _, item := range tbl { + t.Setenv(item.env, item.url) + } + + f := ResolveEndpoint() + + for _, item := range tbl { + ep, err := f.EndpointFor(item.service, "") + assert.Nil(t, err) + assert.Equal(t, item.url, ep.URL) + } +} diff --git a/pkg/provider/vault/util/vault.go b/pkg/provider/vault/util/vault.go index 47fa93b49..5ec81241d 100644 --- a/pkg/provider/vault/util/vault.go +++ b/pkg/provider/vault/util/vault.go @@ -17,9 +17,12 @@ package util import ( "context" + "github.com/aws/aws-sdk-go/aws/credentials" vault "github.com/hashicorp/vault/api" ) +type JwtProviderFactory func(name, namespace, roleArn string, aud []string, region string) (credentials.Provider, error) + type Auth interface { Login(ctx context.Context, authMethod vault.AuthMethod) (*vault.Secret, error) } diff --git a/pkg/provider/vault/vault.go b/pkg/provider/vault/vault.go index 9cdb05963..382c539e9 100644 --- a/pkg/provider/vault/vault.go +++ b/pkg/provider/vault/vault.go @@ -28,9 +28,14 @@ import ( "strconv" "strings" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/go-logr/logr" + "github.com/golang-jwt/jwt/v5" vault "github.com/hashicorp/vault/api" approle "github.com/hashicorp/vault/api/auth/approle" + authaws "github.com/hashicorp/vault/api/auth/aws" authkubernetes "github.com/hashicorp/vault/api/auth/kubernetes" authldap "github.com/hashicorp/vault/api/auth/ldap" "github.com/spf13/pflag" @@ -51,6 +56,7 @@ import ( "github.com/external-secrets/external-secrets/pkg/feature" "github.com/external-secrets/external-secrets/pkg/find" "github.com/external-secrets/external-secrets/pkg/provider/metrics" + vaultiamauth "github.com/external-secrets/external-secrets/pkg/provider/vault/iamauth" "github.com/external-secrets/external-secrets/pkg/provider/vault/util" "github.com/external-secrets/external-secrets/pkg/utils" ) @@ -64,7 +70,9 @@ var ( ) const ( - serviceAccTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + serviceAccTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + defaultAWSRegion = "us-east-1" + defaultAWSAuthMountPath = "aws" errVaultStore = "received invalid Vault SecretStore resource: %w" errVaultCacheCreate = "cannot create Vault client cache: %s" @@ -87,6 +95,11 @@ const ( errUnsupportedKvVersion = "cannot perform find operations with kv version v1" errUnsupportedMetadataKvVersion = "cannot perform metadata fetch operations with kv version v1" errNotFound = "secret not found" + errIrsaTokenEnvVarNotFoundOnPod = "expected env variable: %s not found on controller's pod" + errIrsaTokenFileNotFoundOnPod = "web ddentity token file not found at %s location: %w" + errIrsaTokenFileNotReadable = "could not read the web identity token from the file %s: %w" + errIrsaTokenNotValidJWT = "could not parse web identity token available at %s. not a valid jwt?: %w" + errPodInfoNotFoundOnToken = "could not find pod identity info on token %s: %w" errGetKubeSA = "cannot get Kubernetes service account %q: %w" errGetKubeSASecrets = "cannot find secrets bound to service account: %q" @@ -352,6 +365,29 @@ func (c *Connector) ValidateStore(store esv1beta1.GenericStore) error { return fmt.Errorf(errInvalidTokenRef, err) } } + if p.Auth.Iam != nil { + if p.Auth.Iam.JWTAuth != nil { + if p.Auth.Iam.JWTAuth.ServiceAccountRef != nil { + if err := utils.ValidateReferentServiceAccountSelector(store, *p.Auth.Iam.JWTAuth.ServiceAccountRef); err != nil { + return fmt.Errorf(errInvalidTokenRef, err) + } + } + } + + if p.Auth.Iam.SecretRef != nil { + if err := utils.ValidateReferentSecretSelector(store, p.Auth.Iam.SecretRef.AccessKeyID); err != nil { + return fmt.Errorf(errInvalidTokenRef, err) + } + if err := utils.ValidateReferentSecretSelector(store, p.Auth.Iam.SecretRef.SecretAccessKey); err != nil { + return fmt.Errorf(errInvalidTokenRef, err) + } + if p.Auth.Iam.SecretRef.SessionToken != nil { + if err := utils.ValidateReferentSecretSelector(store, *p.Auth.Iam.SecretRef.SessionToken); err != nil { + return fmt.Errorf(errInvalidTokenRef, err) + } + } + } + } return nil } @@ -740,6 +776,15 @@ func isReferentSpec(prov *esv1beta1.VaultProvider) bool { if prov.Auth.Cert != nil && prov.Auth.Cert.SecretRef.Namespace == nil { return true } + if prov.Auth.Iam != nil && prov.Auth.Iam.JWTAuth != nil && prov.Auth.Iam.JWTAuth.ServiceAccountRef != nil && prov.Auth.Iam.JWTAuth.ServiceAccountRef.Namespace == nil { + return true + } + if prov.Auth.Iam != nil && prov.Auth.Iam.SecretRef != nil && + (prov.Auth.Iam.SecretRef.AccessKeyID.Namespace == nil || + prov.Auth.Iam.SecretRef.SecretAccessKey.Namespace == nil || + (prov.Auth.Iam.SecretRef.SessionToken != nil && prov.Auth.Iam.SecretRef.SessionToken.Namespace == nil)) { + return true + } return false } @@ -1034,6 +1079,12 @@ func (v *client) setAuth(ctx context.Context, cfg *vault.Config) error { return err } + tokenExists, err = setIamAuthToken(ctx, v, vaultiamauth.DefaultJWTProvider, vaultiamauth.DefaultSTSProvider) + if tokenExists { + v.log.V(1).Info("Retrieved new token using IAM auth") + return err + } + return errors.New(errAuthFormat) } @@ -1110,6 +1161,19 @@ func setCertAuthToken(ctx context.Context, v *client, cfg *vault.Config) (bool, return false, nil } +func setIamAuthToken(ctx context.Context, v *client, jwtProvider util.JwtProviderFactory, assumeRoler vaultiamauth.STSProvider) (bool, error) { + iamAuth := v.store.Auth.Iam + isClusterKind := v.storeKind == esv1beta1.ClusterSecretStoreKind + if iamAuth != nil { + err := v.requestTokenWithIamAuth(ctx, iamAuth, isClusterKind, v.kube, v.namespace, jwtProvider, assumeRoler) + if err != nil { + return true, err + } + return true, nil + } + return false, nil +} + func (v *client) secretKeyRefForServiceAccount(ctx context.Context, serviceAccountRef *esmeta.ServiceAccountSelector) (string, error) { serviceAccount := &corev1.ServiceAccount{} ref := types.NamespacedName{ @@ -1407,6 +1471,133 @@ func (v *client) requestTokenWithCertAuth(ctx context.Context, certAuth *esv1bet return nil } +func (v *client) requestTokenWithIamAuth(ctx context.Context, iamAuth *esv1beta1.VaultIamAuth, ick bool, k kclient.Client, n string, jwtProvider util.JwtProviderFactory, assumeRoler vaultiamauth.STSProvider) error { + jwtAuth := iamAuth.JWTAuth + secretRefAuth := iamAuth.SecretRef + regionAWS := defaultAWSRegion + awsAuthMountPath := defaultAWSAuthMountPath + if iamAuth.Region != "" { + regionAWS = iamAuth.Region + } + if iamAuth.Path != "" { + awsAuthMountPath = iamAuth.Path + } + var creds *credentials.Credentials + var err error + if jwtAuth != nil { // use credentials from a sa explicitly defined and referenced. Highest preference is given to this method/configuration. + creds, err = vaultiamauth.CredsFromServiceAccount(ctx, *iamAuth, regionAWS, ick, k, n, jwtProvider) + if err != nil { + return err + } + } else if secretRefAuth != nil { // if jwtAuth is not defined, check if secretRef is defined. Second preference. + logger.V(1).Info("using credentials from secretRef") + creds, err = vaultiamauth.CredsFromSecretRef(ctx, *iamAuth, ick, k, n) + if err != nil { + return err + } + } + + // Neither of jwtAuth or secretRefAuth defined. Last preference. + // Default to controller pod's identity + if jwtAuth == nil && secretRefAuth == nil { + // Checking if controller pod's service account is IRSA enabled and Web Identity token is available on pod + tknFile, tknFileEnvVarPresent := os.LookupEnv(vaultiamauth.AWSWebIdentityTokenFileEnvVar) + if !tknFileEnvVarPresent { + return fmt.Errorf(errIrsaTokenEnvVarNotFoundOnPod, vaultiamauth.AWSWebIdentityTokenFileEnvVar) // No Web Identity(IRSA) token found on pod + } + + // IRSA enabled service account, let's check that the jwt token filemount and file exists + if _, err := os.Stat(tknFile); err != nil { + return fmt.Errorf(errIrsaTokenFileNotFoundOnPod, tknFile, err) + } + + // everything looks good so far, let's fetch the jwt token from AWS_WEB_IDENTITY_TOKEN_FILE + jwtByte, err := os.ReadFile(tknFile) + if err != nil { + return fmt.Errorf(errIrsaTokenFileNotReadable, tknFile, err) + } + + // let's parse the jwt token + parser := jwt.NewParser(jwt.WithoutClaimsValidation()) + + token, _, err := parser.ParseUnverified(string(jwtByte), jwt.MapClaims{}) + if err != nil { + return fmt.Errorf(errIrsaTokenNotValidJWT, tknFile, err) // JWT token parser error + } + + var ns string + var sa string + + // let's fetch the namespace and serviceaccount from parsed jwt token + if claims, ok := token.Claims.(jwt.MapClaims); ok { + ns = claims["kubernetes.io"].(map[string]interface{})["namespace"].(string) + sa = claims["kubernetes.io"].(map[string]interface{})["serviceaccount"].(map[string]interface{})["name"].(string) + } else { + return fmt.Errorf(errPodInfoNotFoundOnToken, tknFile, err) + } + + creds, err = vaultiamauth.CredsFromControllerServiceAccount(ctx, sa, ns, regionAWS, k, jwtProvider) + if err != nil { + return err + } + } + + config := aws.NewConfig().WithEndpointResolver(vaultiamauth.ResolveEndpoint()) + if creds != nil { + config.WithCredentials(creds) + } + + if regionAWS != "" { + config.WithRegion(regionAWS) + } + + sess, err := vaultiamauth.GetAWSSession(config) + if err != nil { + return err + } + if iamAuth.AWSIAMRole != "" { + stsclient := assumeRoler(sess) + if iamAuth.ExternalID != "" { + var setExternalID = func(p *stscreds.AssumeRoleProvider) { + p.ExternalID = aws.String(iamAuth.ExternalID) + } + sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, iamAuth.AWSIAMRole, setExternalID)) + } else { + sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, iamAuth.AWSIAMRole)) + } + } + + getCreds, err := sess.Config.Credentials.Get() + if err != nil { + return err + } + // Set environment variables. These would be fetched by Login + os.Setenv("AWS_ACCESS_KEY_ID", getCreds.AccessKeyID) + os.Setenv("AWS_SECRET_ACCESS_KEY", getCreds.SecretAccessKey) + os.Setenv("AWS_SESSION_TOKEN", getCreds.SessionToken) + + var awsAuthClient *authaws.AWSAuth + + if iamAuth.VaultAWSIAMServerID != "" { + awsAuthClient, err = authaws.NewAWSAuth(authaws.WithRegion(regionAWS), authaws.WithIAMAuth(), authaws.WithRole(iamAuth.Role), authaws.WithMountPath(awsAuthMountPath), authaws.WithIAMServerIDHeader(iamAuth.VaultAWSIAMServerID)) + if err != nil { + return err + } + } else { + awsAuthClient, err = authaws.NewAWSAuth(authaws.WithRegion(regionAWS), authaws.WithIAMAuth(), authaws.WithRole(iamAuth.Role), authaws.WithMountPath(awsAuthMountPath)) + if err != nil { + return err + } + } + + _, err = v.auth.Login(ctx, awsAuthClient) + metrics.ObserveAPICall(metrics.ProviderHCVault, metrics.CallHCVaultLogin, err) + if err != nil { + return err + } + return nil +} + func init() { var vaultTokenCacheSize int fs := pflag.NewFlagSet("vault", pflag.ExitOnError) diff --git a/pkg/provider/vault/vault_test.go b/pkg/provider/vault/vault_test.go index a56c6d8d0..c0a85675d 100644 --- a/pkg/provider/vault/vault_test.go +++ b/pkg/provider/vault/vault_test.go @@ -161,6 +161,45 @@ func makeInvalidClusterSecretStoreWithK8sCerts() *esv1beta1.ClusterSecretStore { } } +func makeValidSecretStoreWithIamAuthSecret() *esv1beta1.SecretStore { + return &esv1beta1.SecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vault-store", + Namespace: "default", + }, + Spec: esv1beta1.SecretStoreSpec{ + Provider: &esv1beta1.SecretStoreProvider{ + Vault: &esv1beta1.VaultProvider{ + Server: "https://vault.example.com:8200", + Path: &secretStorePath, + Version: esv1beta1.VaultKVStoreV2, + Auth: esv1beta1.VaultAuth{ + Iam: &esv1beta1.VaultIamAuth{ + Path: "aws", + Region: "us-east-1", + Role: "vault-role", + SecretRef: &esv1beta1.VaultAwsAuthSecretRef{ + AccessKeyID: esmeta.SecretKeySelector{ + Name: "vault-iam-creds-secret", + Key: "access-key", + }, + SecretAccessKey: esmeta.SecretKeySelector{ + Name: "vault-iam-creds-secret", + Key: "secret-access-key", + }, + SessionToken: &esmeta.SecretKeySelector{ + Name: "vault-iam-creds-secret", + Key: "secret-session-token", + }, + }, + }, + }, + }, + }, + }, + } +} + type secretStoreTweakFn func(s *esv1beta1.SecretStore) func makeSecretStore(tweaks ...secretStoreTweakFn) *esv1beta1.SecretStore { @@ -335,6 +374,29 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE err: fmt.Errorf(errVaultCert, errors.New(`cannot find secret data for key: "cert"`)), }, }, + "SuccessfulVaultStoreWithIamAuthSecret": { + reason: "Should return a Vault provider successfully", + args: args{ + store: makeValidSecretStoreWithIamAuthSecret(), + ns: "default", + kube: clientfake.NewClientBuilder().WithObjects(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vault-iam-creds-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + "access-key": []byte("TESTING"), + "secret-access-key": []byte("ABCDEF"), + "secret-session-token": []byte("c2VjcmV0LXNlc3Npb24tdG9rZW4K"), + }, + }).Build(), + corev1: utilfake.NewCreateTokenMock().WithToken("ok"), + newClientFunc: fake.ClientWithLoginMock, + }, + want: want{ + err: nil, + }, + }, "SuccessfulVaultStoreWithK8sCertConfigMap": { reason: "Should return a Vault prodvider with the cert from k8s", args: args{