Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions docs/rotate-oidc-key.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ When OpenShift is configured to use temporary credentials (AZWI, STS, WIF) to au
CURRENT_ISSUER=$(oc get authentication cluster -o jsonpath='{.spec.serviceAccountIssuer}')

GCP_BUCKET=$(echo ${CURRENT_ISSUER} | cut -d "/" -f4)

CLUSTER_NAME=${GCP_BUCKET%-*}
```

1. Confirm that your cluster is in a stable state.
Expand Down Expand Up @@ -100,11 +102,16 @@ When OpenShift is configured to use temporary credentials (AZWI, STS, WIF) to au
az storage blob download --container-name ${AZURE_STORAGE_CONTAINER} --account-name ${AZURE_STORAGE_ACCOUNT} --name 'openid/v1/jwks' -f ${TEMPDIR}/jwks.current.json
```

GCP
GCP public-bucket
```bash
gcloud storage cp gs://${GCP_BUCKET}/keys.json ${TEMPDIR}/jwks.current.json
```

GCP pool-jwk-file
```bash
gcloud iam workload-identity-pools providers describe --format json --location global --workload-identity-pool ${CLUSTER_NAME} ${CLUSTER_NAME} | jq -r ".oidc.jwksJson" > ${TEMPDIR}/jwks.current.json
```

1. Combine the current and new keys

Combine the key(s) downloaded from the cloud provider with the new key. The resulting file will enable authentication for both the old and new keys during the transistion.
Expand All @@ -127,11 +134,16 @@ When OpenShift is configured to use temporary credentials (AZWI, STS, WIF) to au
az storage blob upload --overwrite --account-name ${AZURE_STORAGE_ACCOUNT} --container-name ${AZURE_STORAGE_CONTAINER} --name 'openid/v1/jwks' -f ${TEMPDIR}/jwks.combined.json
```

GCP
GCP public-bucket
```bash
gcloud storage cp ${TEMPDIR}/jwks.combined.json gs://${GCP_BUCKET}/keys.json
```

GCP pool-jwk-file
```bash
gcloud iam workload-identity-pools providers update-oidc ${CLUSTER_NAME} --location=global --workload-identity-pool=${CLUSTER_NAME} --jwk-json-path=${TEMPDIR}/jwks.combined.json
```

1. Wait for kube-apiserver to update to the new key

Wait for the kube-apiserver pods to be using the new key before proceeding. The kube-apiserver operator enters the progressing state until all of the pods are cycled and using the new key.
Expand Down Expand Up @@ -168,7 +180,12 @@ When OpenShift is configured to use temporary credentials (AZWI, STS, WIF) to au
az storage blob upload --overwrite --account-name ${AZURE_STORAGE_ACCOUNT} --container-name ${AZURE_STORAGE_CONTAINER} --name 'openid/v1/jwks' -f ${TEMPDIR}/jwks.new.json
```

GCP
GCP public-bucket
```bash
gcloud storage cp ${TEMPDIR}/jwks.new.json gs://${GCP_BUCKET}/keys.json
```

GCP pool-jwk-file
```bash
gcloud iam workload-identity-pools providers update-oidc ${CLUSTER_NAME} --location=global --workload-identity-pool=${CLUSTER_NAME} --jwk-json-path=${TEMPDIR}/jwks.new.json
```
8 changes: 7 additions & 1 deletion pkg/cmd/provisioning/gcp/create_all.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gcp

import (
"context"
"fmt"
"log"
"os"
"path"
Expand Down Expand Up @@ -46,7 +47,7 @@ func createAllCmd(cmd *cobra.Command, args []string) {
log.Fatalf("Failed to create workload identity pool: %s", err)
}

if err = createWorkloadIdentityProvider(ctx, gcpClient, CreateAllOpts.Name, CreateAllOpts.Region, CreateAllOpts.Project, CreateAllOpts.Name, publicKeyPath, CreateAllOpts.TargetDir, false); err != nil {
if err = createWorkloadIdentityProvider(ctx, gcpClient, CreateAllOpts.Name, CreateAllOpts.Region, CreateAllOpts.Project, CreateAllOpts.Name, publicKeyPath, CreateAllOpts.TargetDir, CreateAllOpts.KeyStorageMethod, false); err != nil {
log.Fatalf("Failed to create workload identity provider: %s", err)
}

Expand All @@ -63,6 +64,10 @@ func validationForCreateAllCmd(cmd *cobra.Command, args []string) {
log.Fatalf("Name can be at most 32 characters long")
}

if err := validateKeyStorageMethod(CreateAllOpts.KeyStorageMethod); err != nil {
log.Fatalf("%s", err)
}

if CreateAllOpts.TargetDir == "" {
pwd, err := os.Getwd()
if err != nil {
Expand Down Expand Up @@ -117,6 +122,7 @@ func NewCreateAllCmd() *cobra.Command {
createAllCmd.PersistentFlags().StringVar(&CreateAllOpts.TargetDir, "output-dir", "", "Directory to place generated files (defaults to current directory)")
createAllCmd.PersistentFlags().BoolVar(&CreateAllOpts.EnableTechPreview, "enable-tech-preview", false, "Opt into processing CredentialsRequests marked as tech-preview")
createAllCmd.PersistentFlags().StringVar(&CreateAllOpts.PublicKeyPath, "public-key-file", "", "Path to public ServiceAccount signing key")
createAllCmd.PersistentFlags().StringVar(&CreateAllOpts.KeyStorageMethod, "key-storage-method", KeyStorageMethodPublicBucket, fmt.Sprintf("Method for storing OIDC JWK files. %q (default) creates a public GCS bucket; %q attaches the JWK directly to the workload identity pool provider without creating a bucket", KeyStorageMethodPublicBucket, KeyStorageMethodPoolJWKFile))

return createAllCmd
}
144 changes: 125 additions & 19 deletions pkg/cmd/provisioning/gcp/create_workload_identity_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,35 @@ const (
createIdentityProviderScriptName = "05-create-workload-identity-provider.sh"
// createIdentityProviderCmd is gcloud cli command to create workload identity provider
createIdentityProviderCmd = "gcloud iam workload-identity-pools providers create-oidc %s --location=global --workload-identity-pool=%s --display-name=%s --description=\"%s\" --issuer-uri=%s --allowed-audiences=%s --attribute-mapping=\"google.subject=assertion.sub\""
// createIdentityProviderWithJWKFileCmd is gcloud cli command to create workload identity provider with attached JWK file
createIdentityProviderWithJWKFileCmd = "gcloud iam workload-identity-pools providers create-oidc %s --location=global --workload-identity-pool=%s --display-name=%s --description=\"%s\" --issuer-uri=%s --allowed-audiences=%s --attribute-mapping=\"google.subject=assertion.sub\" --jwk-json-path=%s"
// openShiftAudience is the only acceptable value for the `aud` field (audience) in the OIDC token shared by
// OpenShift components
openShiftAudience = "openshift"

// KeyStorageMethodPublicBucket is the default storage method that creates a public GCS bucket to host OIDC config
KeyStorageMethodPublicBucket = "public-bucket"
// KeyStorageMethodPoolJWKFile is the storage method that attaches the JWK directly to the workload identity pool provider
KeyStorageMethodPoolJWKFile = "pool-jwk-file"

// oidcJWKSUpdateMask is the field mask used when patching the JWKS on a workload identity pool provider
oidcJWKSUpdateMask = "oidc.jwks_json"

// workloadIdentityPoolResourceFmt is the GCP resource path format for a workload identity pool
workloadIdentityPoolResourceFmt = "projects/%s/locations/global/workloadIdentityPools/%s"
// workloadIdentityProviderResourceFmt is the GCP resource path format for a workload identity provider
workloadIdentityProviderResourceFmt = "projects/%s/locations/global/workloadIdentityPools/%s/providers/%s"
)

func validateKeyStorageMethod(method string) error {
switch method {
case KeyStorageMethodPublicBucket, KeyStorageMethodPoolJWKFile:
return nil
default:
return fmt.Errorf("invalid --key-storage-method %q, must be one of: %s, %s", method, KeyStorageMethodPublicBucket, KeyStorageMethodPoolJWKFile)
}
}

func createWorkloadIdentityProviderCmd(cmd *cobra.Command, args []string) {
ctx := context.Background()

Expand All @@ -71,39 +95,116 @@ func createWorkloadIdentityProviderCmd(cmd *cobra.Command, args []string) {
publicKeyPath = filepath.Join(CreateWorkloadIdentityProviderOpts.TargetDir, provisioning.PublicKeyFile)
}

err = createWorkloadIdentityProvider(ctx, gcpClient, CreateWorkloadIdentityProviderOpts.Name, CreateWorkloadIdentityProviderOpts.Region, CreateWorkloadIdentityProviderOpts.Project, CreateWorkloadIdentityProviderOpts.WorkloadIdentityPool, publicKeyPath, CreateWorkloadIdentityProviderOpts.TargetDir, CreateWorkloadIdentityProviderOpts.DryRun)
err = createWorkloadIdentityProvider(ctx, gcpClient, CreateWorkloadIdentityProviderOpts.Name, CreateWorkloadIdentityProviderOpts.Region, CreateWorkloadIdentityProviderOpts.Project, CreateWorkloadIdentityProviderOpts.WorkloadIdentityPool, publicKeyPath, CreateWorkloadIdentityProviderOpts.TargetDir, CreateWorkloadIdentityProviderOpts.KeyStorageMethod, CreateWorkloadIdentityProviderOpts.DryRun)
if err != nil {
log.Fatal(err)
}
}

func createWorkloadIdentityProvider(ctx context.Context, client gcp.Client, name, region, project, workloadIdentityPool string, publicKeyPath, targetDir string, generateOnly bool) error {
// Create a storage bucket
func createWorkloadIdentityProvider(ctx context.Context, client gcp.Client, name, region, project, workloadIdentityPool string, publicKeyPath, targetDir, keyStorageMethod string, generateOnly bool) error {
bucketName := fmt.Sprintf("%s-oidc", name)
if err := createOIDCBucket(ctx, client, bucketName, region, project, targetDir, generateOnly); err != nil {
return err
}
issuerURL := fmt.Sprintf("https://storage.googleapis.com/%s", bucketName)

// Create the OIDC config file
if err := createOIDCConfiguration(ctx, client, bucketName, issuerURL, targetDir, generateOnly); err != nil {
return err
switch keyStorageMethod {
case KeyStorageMethodPoolJWKFile:
// Build the JWKS and attach it directly to the identity pool provider
if err := createIdentityProviderWithJWKFile(ctx, client, name, project, issuerURL, workloadIdentityPool, publicKeyPath, targetDir, generateOnly); err != nil {
return err
}
case KeyStorageMethodPublicBucket:
if err := createOIDCBucket(ctx, client, bucketName, region, project, targetDir, generateOnly); err != nil {
return err
}

// Create the OIDC config file
if err := createOIDCConfiguration(ctx, client, bucketName, issuerURL, targetDir, generateOnly); err != nil {
return err
}

// Create the OIDC key list
if err := createJSONWebKeySet(ctx, client, publicKeyPath, bucketName, targetDir, generateOnly); err != nil {
return err
}

// Create the workload identity provider
if err := createIdentityProvider(ctx, client, name, project, issuerURL, workloadIdentityPool, targetDir, generateOnly); err != nil {
return err
}
default:
return fmt.Errorf("unsupported key-storage-method %q", keyStorageMethod)
}

// Create the OIDC key list
if err := createJSONWebKeySet(ctx, client, publicKeyPath, bucketName, targetDir, generateOnly); err != nil {
// Create the installer manifest file
if err := provisioning.CreateClusterAuthentication(issuerURL, targetDir); err != nil {
return err
}

// Create the workload identity provider
err := createIdentityProvider(ctx, client, name, project, issuerURL, workloadIdentityPool, targetDir, generateOnly)
return nil
}

func createIdentityProviderWithJWKFile(ctx context.Context, client gcp.Client, name, project, issuerURL, workloadIdentityPool, publicKeyPath, targetDir string, generateOnly bool) error {
jwks, err := provisioning.BuildJsonWebKeySet(publicKeyPath)
if err != nil {
return err
return fmt.Errorf("failed to build JSON web key set from the public key: %w", err)
}

// Create the installer manifest file
if err := provisioning.CreateClusterAuthentication(issuerURL, targetDir); err != nil {
return err
if generateOnly {
jwksFilePath := filepath.Join(targetDir, gcpOidcKeysFilename)
log.Printf("Saving JSON web key set (JWKS) locally at %s", jwksFilePath)
if err := os.WriteFile(jwksFilePath, jwks, fileModeCcoctlDryRun); err != nil {
return fmt.Errorf("failed to save JSON web key set (JWKS) locally at %s: %w", jwksFilePath, err)
}

createIdentityProviderScript := provisioning.CreateShellScript([]string{createIdentityProviderWithJWKFileCmd})
createIdentityProviderScriptFilepath := filepath.Join(targetDir, createIdentityProviderScriptName)
script := fmt.Sprintf(createIdentityProviderScript, name, workloadIdentityPool, name, createdByCcoctl, issuerURL, openShiftAudience, gcpOidcKeysFilename)
log.Printf("Saving shell script to create workload identity provider locally at %s", createIdentityProviderScriptFilepath)
if err := os.WriteFile(createIdentityProviderScriptFilepath, []byte(script), fileModeCcoctlDryRun); err != nil {
return fmt.Errorf("failed to save shell script to create workload identity provider locally at %s: %w", createIdentityProviderScriptFilepath, err)
}
return nil
}

providerResource := fmt.Sprintf(workloadIdentityProviderResourceFmt, project, workloadIdentityPool, name)
existingProvider, err := client.GetWorkloadIdentityProvider(ctx, providerResource)
if err != nil {
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 {
provider := &iam.WorkloadIdentityPoolProvider{
Name: name,
DisplayName: name,
Description: createdByCcoctl,
Disabled: false,
State: "ACTIVE",
Oidc: &iam.Oidc{
AllowedAudiences: []string{openShiftAudience},
IssuerUri: issuerURL,
JwksJson: string(jwks),
},
AttributeMapping: map[string]string{
"google.subject": "assertion.sub",
},
}

_, err := client.CreateWorkloadIdentityProvider(ctx, fmt.Sprintf(workloadIdentityPoolResourceFmt, project, workloadIdentityPool), name, provider)
if err != nil {
return fmt.Errorf("failed to create workload identity provider %s: %w", name, err)
}
log.Printf("workload identity provider created with name %s", name)
} else {
return fmt.Errorf("failed to check if there is existing workload identity provider %s in pool %s: %w", name, workloadIdentityPool, err)
}
} else {
log.Printf("Workload identity provider %s already exists in pool %s, updating JWK set", existingProvider.Name, workloadIdentityPool)
updatedProvider := &iam.WorkloadIdentityPoolProvider{
Oidc: &iam.Oidc{
JwksJson: string(jwks),
},
}
_, err := client.UpdateWorkloadIdentityProvider(ctx, providerResource, updatedProvider, oidcJWKSUpdateMask)
if err != nil {
return fmt.Errorf("failed to update workload identity provider %s: %w", name, err)
}
log.Printf("workload identity provider %s updated with new JWK set", name)
}

return nil
Expand Down Expand Up @@ -226,7 +327,7 @@ func createIdentityProvider(ctx context.Context, client gcp.Client, name, projec
return errors.Wrap(err, fmt.Sprintf("Failed to save shell script to create workload identity provider locally at %s", createIdentityProviderScriptFilepath))
}
} else {
providerResource := fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s/providers/%s", project, workloadIdentityPool, name)
providerResource := fmt.Sprintf(workloadIdentityProviderResourceFmt, project, workloadIdentityPool, name)
_, err := client.GetWorkloadIdentityProvider(ctx, providerResource)
if err != nil {
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && strings.Contains(gerr.Message, "Requested entity was not found") {
Expand All @@ -248,7 +349,7 @@ func createIdentityProvider(ctx context.Context, client gcp.Client, name, projec
},
}

_, err := client.CreateWorkloadIdentityProvider(ctx, fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s", project, workloadIdentityPool), name, provider)
_, err := client.CreateWorkloadIdentityProvider(ctx, fmt.Sprintf(workloadIdentityPoolResourceFmt, project, workloadIdentityPool), name, provider)
if err != nil {
return errors.Wrapf(err, "failed to create workload identity provider %s", name)
}
Expand All @@ -271,6 +372,10 @@ func validationForCreateWorkloadIdentityProviderCmd(cmd *cobra.Command, args []s
log.Fatalf("Name can be at most 32 characters long")
}

if err := validateKeyStorageMethod(CreateWorkloadIdentityProviderOpts.KeyStorageMethod); err != nil {
log.Fatalf("%s", err)
}

if CreateWorkloadIdentityProviderOpts.TargetDir == "" {
pwd, err := os.Getwd()
if err != nil {
Expand Down Expand Up @@ -318,6 +423,7 @@ func NewCreateWorkloadIdentityProviderCmd() *cobra.Command {
createWorkloadIdentityProviderCmd.PersistentFlags().StringVar(&CreateWorkloadIdentityProviderOpts.PublicKeyPath, "public-key-file", "", "Path to public ServiceAccount signing key")
createWorkloadIdentityProviderCmd.PersistentFlags().BoolVar(&CreateWorkloadIdentityProviderOpts.DryRun, "dry-run", false, "Skip creating objects, and just save what would have been created into files")
createWorkloadIdentityProviderCmd.PersistentFlags().StringVar(&CreateWorkloadIdentityProviderOpts.TargetDir, "output-dir", "", "Directory to place generated files (defaults to current directory)")
createWorkloadIdentityProviderCmd.PersistentFlags().StringVar(&CreateWorkloadIdentityProviderOpts.KeyStorageMethod, "key-storage-method", KeyStorageMethodPublicBucket, fmt.Sprintf("Method for storing OIDC JWK files. %q (default) creates a public GCS bucket; %q attaches the JWK directly to the workload identity pool provider without creating a bucket", KeyStorageMethodPublicBucket, KeyStorageMethodPoolJWKFile))

return createWorkloadIdentityProviderCmd
}
Loading