Skip to content

Commit

Permalink
feat(operator): Add support for managed GCP WorkloadIdentity
Browse files Browse the repository at this point in the history
  • Loading branch information
periklis committed Nov 4, 2024
1 parent f5b0fb6 commit 6fdc7e2
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 19 deletions.
17 changes: 17 additions & 0 deletions operator/internal/config/managed_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ type AzureEnvironment struct {
Region string
}

type GCPEnvironment struct {
Audience string
ServiceAccountEmail string
}

type TokenCCOAuthConfig struct {
AWS *AWSEnvironment
Azure *AzureEnvironment
GCP *GCPEnvironment
}

func discoverTokenCCOAuthConfig() *TokenCCOAuthConfig {
Expand All @@ -28,6 +34,10 @@ func discoverTokenCCOAuthConfig() *TokenCCOAuthConfig {
subscriptionID := os.Getenv("SUBSCRIPTIONID")
region := os.Getenv("REGION")

// GCP
audience := os.Getenv("AUDIENCE")
serviceAccountEmail := os.Getenv("SERVICE_ACCOUNT_EMAIL")

switch {
case roleARN != "":
return &TokenCCOAuthConfig{
Expand All @@ -44,6 +54,13 @@ func discoverTokenCCOAuthConfig() *TokenCCOAuthConfig {
Region: region,
},
}
case audience != "" && serviceAccountEmail != "":
return &TokenCCOAuthConfig{
GCP: &GCPEnvironment{
Audience: audience,
ServiceAccountEmail: serviceAccountEmail,
},
}
}

return nil
Expand Down
43 changes: 28 additions & 15 deletions operator/internal/handlers/internal/storage/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ var (
errSecretUnknownSSEType = errors.New("unsupported SSE type (supported: SSE-KMS, SSE-S3)")
errSecretHashError = errors.New("error calculating hash for secret")

errSecretUnknownCredentialMode = errors.New("unknown credential mode")
errSecretUnsupportedCredentialMode = errors.New("combination of storage type and credential mode not supported")
errSecretUnknownCredentialMode = errors.New("unknown credential mode")

errAzureManagedIdentityNoOverride = errors.New("when in managed mode, storage secret can not contain credentials")
errAzureInvalidEnvironment = errors.New("azure environment invalid (valid values: AzureGlobal, AzureChinaCloud, AzureGermanCloud, AzureUSGovernment)")
Expand All @@ -47,6 +46,7 @@ var (

errGCPParseCredentialsFile = errors.New("gcp storage secret cannot be parsed from JSON content")
errGCPWrongCredentialSourceFile = errors.New("credential source in secret needs to point to token file")
errGCPInvalidCredentialsFile = errors.New("GCP credentials file contains invalid fields")

azureValidEnvironments = map[string]bool{
"AzureGlobal": true,
Expand Down Expand Up @@ -138,7 +138,7 @@ func extractSecrets(secretSpec lokiv1.ObjectStorageSecretSpec, objStore, tokenCC
case lokiv1.ObjectStorageSecretAzure:
storageOpts.Azure, err = extractAzureConfigSecret(objStore, credentialMode)
case lokiv1.ObjectStorageSecretGCS:
storageOpts.GCS, err = extractGCSConfigSecret(objStore, credentialMode)
storageOpts.GCS, err = extractGCSConfigSecret(objStore, tokenCCOAuth, credentialMode)
case lokiv1.ObjectStorageSecretS3:
storageOpts.S3, err = extractS3ConfigSecret(objStore, credentialMode)
case lokiv1.ObjectStorageSecretSwift:
Expand Down Expand Up @@ -182,7 +182,7 @@ func determineCredentialMode(spec lokiv1.ObjectStorageSecretSpec, secret *corev1
return lokiv1.CredentialModeToken, nil
}
case lokiv1.ObjectStorageSecretGCS:
_, credentialType, err := extractGoogleCredentialSource(secret)
_, credentialType, _, err := extractGoogleCredentialSource(secret, storage.KeyGCPServiceAccountKeyFilename)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -326,13 +326,14 @@ func validateBase64(data []byte) error {
return err
}

func extractGoogleCredentialSource(secret *corev1.Secret) (sourceFile, sourceType string, err error) {
keyJSON := secret.Data[storage.KeyGCPServiceAccountKeyFilename]
func extractGoogleCredentialSource(secret *corev1.Secret, key string) (sourceFile, sourceType, audience string, err error) {
keyJSON := secret.Data[key]
if len(keyJSON) == 0 {
return "", "", fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyGCPServiceAccountKeyFilename)
return "", "", "", fmt.Errorf("%w: %s", errSecretMissingField, key)
}

credentialsFile := struct {
Audience string `json:"audience"`
CredentialsType string `json:"type"`
CredentialsSource struct {
File string `json:"file"`
Expand All @@ -341,20 +342,35 @@ func extractGoogleCredentialSource(secret *corev1.Secret) (sourceFile, sourceTyp

err = json.Unmarshal(keyJSON, &credentialsFile)
if err != nil {
return "", "", errGCPParseCredentialsFile
return "", "", "", errGCPParseCredentialsFile
}

return credentialsFile.CredentialsSource.File, credentialsFile.CredentialsType, nil
return credentialsFile.CredentialsSource.File, credentialsFile.CredentialsType, credentialsFile.Audience, nil
}

func extractGCSConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMode) (*storage.GCSStorageConfig, error) {
func extractGCSConfigSecret(s, tokenCCOAuth *corev1.Secret, credentialMode lokiv1.CredentialMode) (*storage.GCSStorageConfig, error) {
// Extract and validate mandatory fields
bucket := s.Data[storage.KeyGCPStorageBucketName]
if len(bucket) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyGCPStorageBucketName)
}

switch credentialMode {
case lokiv1.CredentialModeTokenCCO:
if _, ok := s.Data[storage.KeyGCPServiceAccountKeyFilename]; ok {
return nil, fmt.Errorf("%w: %s", errGCPInvalidCredentialsFile, "key.json must not be set for CredentialModeTokenCCO")
}

_, _, audience, err := extractGoogleCredentialSource(tokenCCOAuth, storage.KeyGCPManagedServiceAccountKeyFilename)
if err != nil {
return nil, err
}

return &storage.GCSStorageConfig{
Audience: audience,
Bucket: string(bucket),
WorkloadIdentity: true,
}, nil
case lokiv1.CredentialModeStatic:
return &storage.GCSStorageConfig{
Bucket: string(bucket),
Expand All @@ -366,7 +382,7 @@ func extractGCSConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMo
}

// Check if correct credential source is used
credentialSource, _, err := extractGoogleCredentialSource(s)
credentialSource, _, _, err := extractGoogleCredentialSource(s, storage.KeyGCPServiceAccountKeyFilename)
if err != nil {
return nil, err
}
Expand All @@ -380,12 +396,9 @@ func extractGCSConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMo
WorkloadIdentity: true,
Audience: audience,
}, nil
case lokiv1.CredentialModeTokenCCO:
return nil, fmt.Errorf("%w: type: %s credentialMode: %s", errSecretUnsupportedCredentialMode, lokiv1.ObjectStorageSecretGCS, credentialMode)
default:
return nil, fmt.Errorf("%w: %s", errSecretUnknownCredentialMode, credentialMode)
}

return nil, fmt.Errorf("%w: %s", errSecretUnknownCredentialMode, credentialMode)
}

func extractS3ConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMode) (*storage.S3StorageConfig, error) {
Expand Down
43 changes: 42 additions & 1 deletion operator/internal/handlers/internal/storage/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ func TestGCSExtract(t *testing.T) {
type test struct {
name string
secret *corev1.Secret
tokenAuth *corev1.Secret
featureGates configv1.FeatureGates
wantError string
wantCredentialMode lokiv1.CredentialMode
}
Expand Down Expand Up @@ -343,6 +345,45 @@ func TestGCSExtract(t *testing.T) {
},
wantCredentialMode: lokiv1.CredentialModeToken,
},
{
name: "invalid for token CCO",
featureGates: configv1.FeatureGates{
OpenShift: configv1.OpenShiftFeatureGates{
Enabled: true,
TokenCCOAuthEnv: true,
},
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
"key.json": []byte("{\"type\": \"external_account\", \"audience\": \"\", \"service_account_id\": \"\"}"),
},
},
wantError: "GCP credentials file contains invalid fields: key.json must not be set for CredentialModeTokenCCO",
},
{
name: "valid for token CCO",
featureGates: configv1.FeatureGates{
OpenShift: configv1.OpenShiftFeatureGates{
Enabled: true,
TokenCCOAuthEnv: true,
},
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
},
},
tokenAuth: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "token-auth-config"},
Data: map[string][]byte{
"service_account.json": []byte("{\"type\": \"external_account\", \"audience\": \"test\", \"service_account_id\": \"\"}"),
},
},
wantCredentialMode: lokiv1.CredentialModeTokenCCO,
},
}
for _, tst := range table {
t.Run(tst.name, func(t *testing.T) {
Expand All @@ -352,7 +393,7 @@ func TestGCSExtract(t *testing.T) {
Type: lokiv1.ObjectStorageSecretGCS,
}

opts, err := extractSecrets(spec, tst.secret, nil, configv1.FeatureGates{})
opts, err := extractSecrets(spec, tst.secret, tst.tokenAuth, tst.featureGates)
if tst.wantError == "" {
require.NoError(t, err)
require.Equal(t, tst.wantCredentialMode, opts.CredentialMode)
Expand Down
9 changes: 9 additions & 0 deletions operator/internal/manifests/openshift/credentialsrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ func encodeProviderSpec(env *config.TokenCCOAuthConfig) (*runtime.RawExtension,
AzureSubscriptionID: azure.SubscriptionID,
AzureTenantID: azure.TenantID,
}
case env.GCP != nil:
spec = &cloudcredentialv1.GCPProviderSpec{
PredefinedRoles: []string{
"roles/iam.workloadIdentityUser",
"roles/storage.objectAdmin",
},
Audience: env.GCP.Audience,
ServiceAccountEmail: env.GCP.ServiceAccountEmail,
}
}

encodedSpec, err := cloudcredentialv1.Codec.EncodeProviderSpec(spec.DeepCopyObject())
Expand Down
14 changes: 11 additions & 3 deletions operator/internal/manifests/storage/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ func ensureObjectStoreCredentials(p *corev1.PodSpec, opts Options) corev1.PodSpe
volumes = append(volumes, saTokenVolume(opts))
container.VolumeMounts = append(container.VolumeMounts, saTokenVolumeMount)

if opts.OpenShift.TokenCCOAuthEnabled() && opts.S3 != nil && opts.S3.STS {
isSTS := opts.S3 != nil && opts.S3.STS
isWIF := opts.GCS != nil && opts.GCS.WorkloadIdentity
if opts.OpenShift.TokenCCOAuthEnabled() && (isSTS || isWIF) {
volumes = append(volumes, tokenCCOAuthConfigVolume(opts))
container.VolumeMounts = append(container.VolumeMounts, tokenCCOAuthConfigVolumeMount)
}
Expand Down Expand Up @@ -223,8 +225,14 @@ func tokenAuthCredentials(opts Options) []corev1.EnvVar {
envVarFromValue(EnvAzureFederatedTokenFile, ServiceAccountTokenFilePath),
}
case lokiv1.ObjectStorageSecretGCS:
return []corev1.EnvVar{
envVarFromValue(EnvGoogleApplicationCredentials, path.Join(secretDirectory, KeyGCPServiceAccountKeyFilename)),
if opts.OpenShift.TokenCCOAuthEnabled() {
return []corev1.EnvVar{
envVarFromValue(EnvGoogleApplicationCredentials, path.Join(tokenAuthConfigDirectory, KeyGCPManagedServiceAccountKeyFilename)),
}
} else {
return []corev1.EnvVar{
envVarFromValue(EnvGoogleApplicationCredentials, path.Join(secretDirectory, KeyGCPServiceAccountKeyFilename)),
}
}
default:
return []corev1.EnvVar{}
Expand Down
98 changes: 98 additions & 0 deletions operator/internal/manifests/storage/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,104 @@ func TestConfigureDeploymentForStorageType(t *testing.T) {
},
},
},
{
desc: "object storage GCS with Workload Identity and OpenShift Managed Credentials",
opts: Options{
SecretName: "test",
SharedStore: lokiv1.ObjectStorageSecretGCS,
CredentialMode: lokiv1.CredentialModeTokenCCO,
GCS: &GCSStorageConfig{
Audience: "test",
WorkloadIdentity: true,
},
OpenShift: OpenShiftOptions{
Enabled: true,
CloudCredentials: CloudCredentials{
SecretName: "cloud-credentials",
SHA1: "deadbeef",
},
},
},
dpl: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "loki-ingester",
},
},
},
},
},
},
want: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "loki-ingester",
VolumeMounts: []corev1.VolumeMount{
{
Name: "test",
ReadOnly: false,
MountPath: "/etc/storage/secrets",
},
{
Name: saTokenVolumeName,
ReadOnly: false,
MountPath: saTokenVolumeMountPath,
},
tokenCCOAuthConfigVolumeMount,
},
Env: []corev1.EnvVar{
{
Name: EnvGoogleApplicationCredentials,
Value: "/etc/storage/token-auth/service_account.json",
},
},
},
},
Volumes: []corev1.Volume{
{
Name: "test",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "test",
},
},
},
{
Name: saTokenVolumeName,
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: []corev1.VolumeProjection{
{
ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
Audience: "test",
ExpirationSeconds: ptr.To[int64](3600),
Path: corev1.ServiceAccountTokenKey,
},
},
},
},
},
},
{
Name: tokenAuthConfigVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "cloud-credentials",
},
},
},
},
},
},
},
},
},
{
desc: "object storage S3",
opts: Options{
Expand Down
2 changes: 2 additions & 0 deletions operator/internal/manifests/storage/var.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ const (
KeyGCPStorageBucketName = "bucketname"
// KeyGCPServiceAccountKeyFilename is the service account key filename containing the Google authentication credentials.
KeyGCPServiceAccountKeyFilename = "key.json"
// KeyGCPManagedServiceAccountKeyFilename is the service account key filename for the managed Google service account.
KeyGCPManagedServiceAccountKeyFilename = "service_account.json"

// KeySwiftAuthURL is the secret data key for the OpenStack Swift authentication URL.
KeySwiftAuthURL = "auth_url"
Expand Down

0 comments on commit 6fdc7e2

Please sign in to comment.