Skip to content

Commit

Permalink
Add Fleet Workload Identity support for GKE on-prem environments (Goo…
Browse files Browse the repository at this point in the history
  • Loading branch information
sshcherbakov committed Apr 10, 2024
1 parent 17c849c commit bb70387
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 11 deletions.
102 changes: 102 additions & 0 deletions docs/fleet-wif-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Fleet Workload Identity Authentication

This page contains example configuration to configure the `gcs-fuse-csi-driver`
with [Fleet Workload Identity](https://cloud.google.com/anthos/fleet-management/docs/use-workload-identity)
authentication in environments configured for
[Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation)
outside of the Google Cloud.

## `external_account` Credentials

Instead of the Google Service Account key file, it is possible to pass a Fleet Workload Identity configuration
JSON file to the process that needs authenticating to the Google API in a Kubernetes cluster configured for
the Workload Identity Federation. The `gcs-fuse-csi-driver` pods are such processes
that need to authenticate to the Google Cloud Storage service API to provide access to the data stores in the
Google Cloud Storage buckets.

Such configuration file contains `external_account` type of credential that does not contain any secrets similar
to the Google Service Account key. The configuration should be passed via the `GOOGLE_APPLICATION_CREDENTIALS`
environment variable, which requires the file name of the file containing the configuration on
the pod's local file system.

A ConfigMap to host the contents of the configuration file for the `GOOGLE_APPLICATION_CREDENTIALS` environment variable
of pods on Kubernetes clusters, such as Anthos on Bare Metal clusters, that require accessing Google Cloud API using
[Fleet Workload Identity](https://cloud.google.com/kubernetes-engine/fleet-management/docs/use-workload-identity) can be created
like illustrated in the following snippet:

```yaml
cat <<EOF | kubectl apply -f -
kind: ConfigMap
apiVersion: v1
metadata:
namespace: gcs-fuse-csi-driver
name: default-creds-config
data:
config: |
{
"type": "external_account",
"audience": "identitynamespace:$FLEET_PROJECT_ID.svc.id.goog:https://gkehub.googleapis.com/projects/$FLEET_PROJECT_ID/locations/global/memberships/$CLUSTER_NAME",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/GSA_NAME@GSA_PROJECT_ID.iam.gserviceaccount.com:generateAccessToken",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"file": "/var/run/secrets/tokens/gcp-ksa/token"
}
}
EOF
```

Please note, that the `service_account_impersonation_url` attribute in the snippet above is only necessary if you
link a Google Service Account with the Kubernetes Service account using `iam.gke.io/gcp-service-account` annotation
and `roles/iam.workloadIdentityUser` IAM role. Otherwise, you can omit this attribute in the configuration.

## Pass `GOOGLE_APPLICATION_CREDENTIALS`

Following snippet illustrates passing the ConfigMap with `external_account` credential to the
`gcs-fuse-csi-driver` pods of the `gcsfusecsi-node` DaemonSet that needs Fleet Workload Identity Authentication
for accessing data in Google Cloud Storage buckets using the `GOOGLE_APPLICATION_CREDENTIALS` environment variable.

```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: gcsfusecsi-node
namespace: gcs-fuse-csi-driver
...
spec:
...
template:
...
spec:
...
containers:
- name: gcs-fuse-csi-driver
image: gcr.io/$PROJECT_ID/gcp-filestore-csi-driver/gcs-fuse-csi-driver:$GCP_PROVIDER_SHA
...
env:
...
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/run/secrets/tokens/gcp-ksa/google-application-credentials.json
volumeMounts:
...
- mountPath: /var/run/secrets/tokens/gcp-ksa
name: gcp-ksa
readOnly: true
...
volumes:
...
- name: gcp-ksa
projected:
defaultMode: 420
sources:
- serviceAccountToken:
audience: $FLEET_PROJECT_ID.svc.id.goog
expirationSeconds: 172800
path: token
- configMap:
items:
- key: config
path: google-application-credentials.json
name: default-creds-config
optional: false
```
80 changes: 69 additions & 11 deletions pkg/cloud_provider/metadata/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package metadata

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"cloud.google.com/go/compute/metadata"
Expand Down Expand Up @@ -48,19 +51,13 @@ func NewMetadataService(identityPool, identityProvider string, clientset clients
return nil, fmt.Errorf("failed to get project: %w", err)
}

if identityPool == "" {
klog.Infof("got empty identityPool, constructing the identityPool using projectID")
identityPool = projectID + ".svc.id.goog"
}

if identityProvider == "" {
klog.Infof("got empty identityProvider, constructing the identityProvider using the gke-metadata-server flags")
ds, err := clientset.GetDaemonSet(context.TODO(), "kube-system", "gke-metadata-server")
identityPool, identityProvider, err = gkeWorkloadIdentity(projectID, identityPool, identityProvider, clientset)
if err != nil {
err2 := err
identityPool, identityProvider, err = fleetWorkloadIdentity()
if err != nil {
return nil, fmt.Errorf("failed to get gke-metadata-server DaemonSet spec: %w", err)
return nil, err2
}

identityProvider = getIdentityProvider(ds)
}

return &metadataServiceManager{
Expand Down Expand Up @@ -92,3 +89,64 @@ func getIdentityProvider(ds *appsv1.DaemonSet) string {

return ""
}

// JSON key file types.
const (
externalAccountKey = "external_account"
)

// credentialsFile is the unmarshalled representation of a credentials file.
type credentialsFile struct {
Type string `json:"type"`
// External Account fields
Audience string `json:"audience"`
}

func gkeWorkloadIdentity(projectID string, identityPool string, identityProvider string, clientset clientset.Interface) (string, string, error) {
if identityPool == "" {
klog.Infof("got empty identityPool, constructing the identityPool using projectID")
identityPool = projectID + ".svc.id.goog"
}

if identityProvider == "" {
klog.Infof("got empty identityProvider, constructing the identityProvider using the gke-metadata-server flags")
ds, err := clientset.GetDaemonSet(context.TODO(), "kube-system", "gke-metadata-server")
if err != nil {
return "", "", fmt.Errorf("failed to get gke-metadata-server DaemonSet spec: %w", err)
}

identityProvider = getIdentityProvider(ds)
}
return identityPool, identityProvider, nil
}

func fleetWorkloadIdentity() (string, string, error) {
const envVar = "GOOGLE_APPLICATION_CREDENTIALS"
var jsonData []byte
var err error
if filename := os.Getenv(envVar); filename != "" {
jsonData, err = os.ReadFile(filepath.Clean(filename))
if err != nil {
return "", "", fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err)
}
}

// Parse jsonData as one of the other supported credentials files.
var f credentialsFile
if err := json.Unmarshal(jsonData, &f); err != nil {
return "", "", err
}

if f.Type != externalAccountKey {
return "", "", fmt.Errorf("google: unexpected credentials type: %v, expected: %v", f.Type, externalAccountKey)
}

split := strings.SplitN(f.Audience, ":", 3)
if split == nil || len(split) < 3 {
return "", "", fmt.Errorf("google: unexpected audience value: %v", f.Audience)
}
idPool := split[1]
idProvider := split[2]

return idPool, idProvider, nil
}

0 comments on commit bb70387

Please sign in to comment.