diff --git a/examples/gcp-projected-service-account-values.yaml b/examples/gcp-projected-service-account-values.yaml new file mode 100644 index 0000000..dc8625b --- /dev/null +++ b/examples/gcp-projected-service-account-values.yaml @@ -0,0 +1,32 @@ +providerConfig: + - name: jfrog-credentials-provider + artifactoryUrl: "" + matchImages: + - "*." + defaultCacheDuration: 5h + tokenAttributes: + enabled: true + gcp: + enabled: true + google_service_account_email: "" + jfrog_oidc_audience: "artifactory" + jfrog_oidc_provider_name: "" + +rbac: + create: true + +serviceAccount: + create: true + annotations: + "iam.gke.io/gcp-service-account": "" + "JFrogExchange": "true" + +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: credentialsProviderEnabled + operator: In + values: + - "true" diff --git a/helm/CHANGELOG.md b/helm/CHANGELOG.md index dbcdf1c..dbeb11f 100644 --- a/helm/CHANGELOG.md +++ b/helm/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this Helm chart will be documented in this file. +## [1.0.1] - 12th Mar, 2026 +* Added KEP-4412 - Pod Level Identity Support For JFrog Artifactory on GCP + ## [1.0.0] - 23rd Feb, 2026 * Allow using an existing ServiceAccount when `serviceAccount.create=false` * Fixed `defaultCacheDuration` for AWS diff --git a/helm/Chart.yaml b/helm/Chart.yaml index fc1f0a2..6c4b6ca 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: jfrog-credential-provider description: A Helm chart for JFrog Credential Provider supporting AWS, Azure, and GCP type: application -version: 1.0.0 -appVersion: "1.0.0" +version: 1.0.1 +appVersion: "1.0.1" keywords: - jfrog - credential-provider diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index dfd2eee..b1a6064 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -127,3 +127,29 @@ Default RBAC rules for azure with service account token projection verbs: ["get", "list"] {{- end }} + +{{/* +Default RBAC rules for gcp with service account token projection +*/}} +{{- define "jfrog-credential-provider.defaultRBACRulesGcp" }} +- apiGroups: [""] + resources: + - {{ include "jfrog-credential-provider.jfrogGCPAudience" . | quote }} + verbs: + - request-serviceaccounts-token-audience +{{- end }} + +{{/* +Fetching JFrog audience from values configuration +*/}} +{{- define "jfrog-credential-provider.jfrogGCPAudience" -}} +{{- $default := "artifactory" -}} +{{- $audience := $default -}} +{{- range .Values.providerConfig | default list }} + {{- if and (.tokenAttributes) (.gcp) (.tokenAttributes.enabled) (.gcp.enabled) }} + {{- $audience = (default $default .gcp.jfrog_oidc_audience) }} + {{- break -}} + {{- end }} +{{- end }} +{{- $audience -}} +{{- end }} \ No newline at end of file diff --git a/helm/templates/configmap-provider.yaml b/helm/templates/configmap-provider.yaml index be7204f..86614e5 100644 --- a/helm/templates/configmap-provider.yaml +++ b/helm/templates/configmap-provider.yaml @@ -24,8 +24,9 @@ data: "defaultCacheDuration": {{ .defaultCacheDuration | toJson }}, "apiVersion": "credentialprovider.kubelet.k8s.io/v1", {{- end }} - - {{- /* This is only supported for AWS and Azure at the moment */ -}} + + {{- /* This is only supported for AWS, Azure and GCP at the moment */ -}} + {{- if and .tokenAttributes .tokenAttributes.enabled (eq $cloudProvider "aws") }} "tokenAttributes": { "serviceAccountTokenAudience": "sts.amazonaws.com", @@ -38,16 +39,28 @@ data: }, {{- else if and .tokenAttributes .tokenAttributes.enabled (eq $cloudProvider "azure") }} tokenAttributes: - {{- if .azure.azure_app_audience }} + {{- if .azure.azure_app_audience }} serviceAccountTokenAudience: {{ .azure.azure_app_audience }} - {{- else}} + {{- else}} serviceAccountTokenAudience: api://AzureADTokenExchange - {{- end }} + {{- end }} cacheType: ServiceAccount requireServiceAccount: true requiredServiceAccountAnnotationKeys: - azure.workload.identity/client-id - JFrogExchange + {{- else if and .tokenAttributes .tokenAttributes.enabled (eq $cloudProvider "gcp") }} + tokenAttributes: + {{- if .gcp.jfrog_oidc_audience }} + serviceAccountTokenAudience: {{ .gcp.jfrog_oidc_audience }} + {{- else}} + serviceAccountTokenAudience: "artifactory" + {{- end }} + cacheType: ServiceAccount + requireServiceAccount: true + requiredServiceAccountAnnotationKeys: + - iam.gke.io/gcp-service-account + - JFrogExchange {{- end }} {{- if eq $cloudProvider "aws" }} diff --git a/helm/templates/configmap-setup.yaml b/helm/templates/configmap-setup.yaml index e5d42fc..ad93c80 100644 --- a/helm/templates/configmap-setup.yaml +++ b/helm/templates/configmap-setup.yaml @@ -31,7 +31,6 @@ data: export JFROG_CREDENTIAL_PROVIDER_BINARY_DIR="/home/kubernetes/bin" export KUBELET_CREDENTIAL_PROVIDER_CONFIG_PATH="/etc/srv/kubernetes/cri_auth_config.yaml" {{- end }} - {{- range .Values.providerConfig }} JFROG_CONFIG_FILE="jfrog-provider" diff --git a/helm/templates/role.yaml b/helm/templates/role.yaml index 1808627..98c954f 100644 --- a/helm/templates/role.yaml +++ b/helm/templates/role.yaml @@ -1,5 +1,5 @@ {{- $cloudProvider := include "jfrog-credential-provider.cloudProvider" . }} -{{- if and .Values.rbac.create (or (eq $cloudProvider "azure") (eq $cloudProvider "aws")) }} +{{- if and .Values.rbac.create (or (eq $cloudProvider "azure") (eq $cloudProvider "aws") (eq $cloudProvider "gcp")) }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -12,6 +12,8 @@ rules: {{- include "jfrog-credential-provider.defaultRBACRulesAWS" . | nindent 2 }} {{- else if (eq $cloudProvider "azure") }} {{- include "jfrog-credential-provider.defaultRBACRulesAzure" . | nindent 2 }} + {{- else if (eq $cloudProvider "gcp") }} + {{- include "jfrog-credential-provider.defaultRBACRulesGcp" . | nindent 2 }} {{- end }} {{- range .Values.rbac.role.additionalRules }} - {{- toYaml . | nindent 4 }} diff --git a/helm/templates/rolebinding.yaml b/helm/templates/rolebinding.yaml index d1a144f..f498948 100644 --- a/helm/templates/rolebinding.yaml +++ b/helm/templates/rolebinding.yaml @@ -1,5 +1,5 @@ {{- $cloudProvider := include "jfrog-credential-provider.cloudProvider" . }} -{{- if and .Values.rbac.create (or (eq $cloudProvider "aws") (eq $cloudProvider "azure")) }} +{{- if and .Values.rbac.create (or (eq $cloudProvider "aws") (eq $cloudProvider "azure") (eq $cloudProvider "gcp")) }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: diff --git a/helm/values.yaml b/helm/values.yaml index 8f3ad76..46b0f05 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -120,4 +120,6 @@ serviceAccount: create: true name: "" annotations: {} - + ## Example for GCP Pod Identity + # "iam.gke.io/gcp-service-account": "@.iam.gserviceaccount.com" + # "JFrogExchange": "true" diff --git a/internal/provider/provider.go b/internal/provider/provider.go index cc4e63f..1475530 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -143,7 +143,7 @@ func cloudProviderAuth(svc *service.Service, ctx context.Context, logs *logger.L return rtUsername, rtToken case utils.CloudProviderGoogle: logs.Debug("Detected Google cloud provider") - rtUsername, rtToken = handleGoogleAuth(svc, ctx, logs, artifactoryUrl) + rtUsername, rtToken = handleGoogleAuth(svc, ctx, logs, artifactoryUrl, request) return rtUsername, rtToken default: logs.Exit("ERROR in JFrog Credentials provider, cloud_provider value should be either aws, azure, or google", 1) @@ -256,6 +256,7 @@ func handleAzureAuth(svc *service.Service, ctx context.Context, logs *logger.Log logs.Info(fmt.Sprintf("getting envs - azureAppClientId: %s, azureNodepoolClientId: %s, azureAppTenantId: %s, azureAppAudience: %s, jfrogOidcProviderName: %s", azureAppClientId, azureNodepoolClientId, azureAppTenantId, azureAppAudience, jfrogOidcProviderName)) } + logs.Info("Service Account Token obtained using Node Identity (VM Service Account)") // Get Azure OIDC token token, err = handlers.GetAzureOIDCToken(svc, ctx, azureAppTenantId, azureAppClientId, azureNodepoolClientId, azureAppAudience) } else { @@ -265,6 +266,7 @@ func handleAzureAuth(svc *service.Service, ctx context.Context, logs *logger.Log logs.Info(fmt.Sprintf("getting envs - azureAppClientId: %s, azureAppAudience: %s, jfrogOidcProviderName: %s", azureAppClientId, azureAppAudience, jfrogOidcProviderName)) } + logs.Info("Service Account Token obtained using Pod Identity (Kubernetes Workload Identity)") token = request.ServiceAccountToken } if err != nil { @@ -280,12 +282,13 @@ func handleAzureAuth(svc *service.Service, ctx context.Context, logs *logger.Log return rtUsername, rtToken } -func handleGoogleAuth(svc *service.Service, ctx context.Context, logs *logger.Logger, artifactoryUrl string) (string, string) { +func handleGoogleAuth(svc *service.Service, ctx context.Context, logs *logger.Logger, artifactoryUrl string, request utils.CredentialProviderRequest) (string, string) { // get required env variables googleServiceAccountEmail := utils.GetEnvs(logs, "google_service_account_email", "") jfrogOidcProviderAudience := utils.GetEnvs(logs, "jfrog_oidc_audience", "") jfrogOidcProviderName := utils.GetEnvs(logs, "jfrog_oidc_provider_name", "") - + var token string + var err error if googleServiceAccountEmail == "" || jfrogOidcProviderAudience == "" || jfrogOidcProviderName == "" { logs.Exit("ERROR in JFrog Credentials provider, environment variables missing: google_service_account_email, jfrog_oidc_audience, jfrog_oidc_provider_name", 1) } else { @@ -293,10 +296,16 @@ func handleGoogleAuth(svc *service.Service, ctx context.Context, logs *logger.Lo googleServiceAccountEmail, jfrogOidcProviderAudience, jfrogOidcProviderName)) } - // Get Google OIDC token - token, err := handlers.GetGoogleOIDCToken(svc, ctx, googleServiceAccountEmail, jfrogOidcProviderAudience) - if err != nil { - logs.Exit("ERROR in GetGoogleOIDCToken :"+err.Error(), 1) + if request.ServiceAccountAnnotations["JFrogExchange"] == "true" { + logs.Info("Service Account Token obtained using Pod Identity (Kubernetes Workload Identity)") + token = request.ServiceAccountToken + } else { + // Get Google OIDC token + logs.Info("Service Account Token obtained using Node Identity (VM Service Account)") + token, err = handlers.GetGoogleOIDCToken(svc, ctx, googleServiceAccountEmail, jfrogOidcProviderAudience) + if err != nil { + logs.Exit("ERROR in GetGoogleOIDCToken :"+err.Error(), 1) + } } // Exchange Google OIDC token with JFrog Artifactory token @@ -304,7 +313,6 @@ func handleGoogleAuth(svc *service.Service, ctx context.Context, logs *logger.Lo if err != nil { logs.Exit("ERROR in JFrog Credentials provider, error in createArtifactoryToken :"+err.Error(), 1) } - return rtUsername, rtToken } @@ -326,6 +334,5 @@ func generateAndOutputResponse(logs *logger.Logger, request utils.CredentialProv if err != nil { logs.Exit("Error marshaling JSON :"+err.Error(), 1) } - os.Stdout.Write(jsonBytes) }