Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

operator: Add support for running with Google Workload Identity #11869

Merged
merged 9 commits into from
Feb 8, 2024
1 change: 1 addition & 0 deletions operator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Main

- [11869](https://github.com/grafana/loki/pull/11869) **periklis**: Add support for running with Google Workload Identity
- [11868](https://github.com/grafana/loki/pull/11868) **xperimental**: Integrate support for OpenShift-managed credentials in Azure
- [11854](https://github.com/grafana/loki/pull/11854) **periklis**: Allow custom audience for managed-auth on STS
- [11802](https://github.com/grafana/loki/pull/11802) **xperimental**: Add support for running with Azure Workload Identity
Expand Down
36 changes: 35 additions & 1 deletion operator/internal/handlers/internal/storage/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package storage
import (
"context"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"sort"
Expand Down Expand Up @@ -32,8 +33,13 @@ var (
errAzureNoCredentials = errors.New("azure storage secret does contain neither account_key or client_id")
errAzureMixedCredentials = errors.New("azure storage secret can not contain both account_key and client_id")
errAzureManagedIdentityNoOverride = errors.New("when in managed mode, storage secret can not contain credentials")

errGCPParseCredentialsFile = errors.New("gcp storage secret cannot be parsed from JSON content")
errGCPCredentialsSourceNoOverride = errors.New("when managed mode, storage secret can not override the default credentials source file")
periklis marked this conversation as resolved.
Show resolved Hide resolved
)

const gcpAccountTypeExternal = "external_account"

func getSecrets(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack, fg configv1.FeatureGates) (*corev1.Secret, *corev1.Secret, error) {
var (
storageSecret corev1.Secret
Expand Down Expand Up @@ -255,8 +261,36 @@ func extractGCSConfigSecret(s *corev1.Secret) (*storage.GCSStorageConfig, error)
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyGCPServiceAccountKeyFilename)
}

credentialsFile := struct {
CredentialsType string `json:"type"`
CredentialsSource struct {
File string `json:"file"`
} `json:"credential_source"`
}{}

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

var (
audience = s.Data[storage.KeyGCPWorkloadIdentityProviderAudience]
isWorkloadIdentity = credentialsFile.CredentialsType == gcpAccountTypeExternal
)
if isWorkloadIdentity {
if len(audience) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyGCPWorkloadIdentityProviderAudience)
}

if credentialsFile.CredentialsSource.File != storage.GCPDefautCredentialsFile {
return nil, fmt.Errorf("%w: %s", errGCPCredentialsSourceNoOverride, storage.GCPDefautCredentialsFile)
periklis marked this conversation as resolved.
Show resolved Hide resolved
}
}

return &storage.GCSStorageConfig{
Bucket: string(bucket),
Bucket: string(bucket),
WorkloadIdentity: isWorkloadIdentity,
Audience: string(audience),
}, nil
}

Expand Down
36 changes: 35 additions & 1 deletion operator/internal/handlers/internal/storage/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,47 @@ func TestGCSExtract(t *testing.T) {
},
wantError: "missing secret field: key.json",
},
{
name: "missing audience",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
"key.json": []byte("{\"type\": \"external_account\"}"),
},
},
wantError: "missing secret field: audience",
},
{
name: "credential_source file no override",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
"audience": []byte("test"),
"key.json": []byte("{\"type\": \"external_account\", \"credential_source\": {\"file\": \"/custom/path/to/secret/gcp/serviceaccount/token\"}}"),
},
},
wantError: "when managed mode, storage secret can not override the default credentials source file: /var/run/secrets/gcp/serviceaccount/token",
periklis marked this conversation as resolved.
Show resolved Hide resolved
},
{
name: "all set",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
"key.json": []byte("{\"type\": \"SA\"}"),
"key.json": []byte("{\"type\": \"service_account\"}"),
},
},
},
{
name: "mandatory for workload-identity set",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"bucketname": []byte("here"),
"audience": []byte("test"),
"key.json": []byte("{\"type\": \"external_account\", \"credential_source\": {\"file\": \"/var/run/secrets/gcp/serviceaccount/token\"}}"),
},
},
},
Expand Down
10 changes: 10 additions & 0 deletions operator/internal/manifests/storage/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ func managedAuthCredentials(opts Options) []corev1.EnvVar {
envVarFromSecret(EnvAzureSubscriptionID, opts.SecretName, KeyAzureStorageSubscriptionID),
envVarFromValue(EnvAzureFederatedTokenFile, path.Join(azureTokenVolumeDirectory, "token")),
}
case lokiv1.ObjectStorageSecretGCS:
return []corev1.EnvVar{
envVarFromValue(EnvGoogleApplicationCredentials, path.Join(secretDirectory, KeyGCPServiceAccountKeyFilename)),
}
default:
return []corev1.EnvVar{}
}
Expand Down Expand Up @@ -290,6 +294,8 @@ func managedAuthEnabled(opts Options) bool {
return opts.S3 != nil && opts.S3.STS
case lokiv1.ObjectStorageSecretAzure:
return opts.Azure != nil && opts.Azure.WorkloadIdentity
case lokiv1.ObjectStorageSecretGCS:
return opts.GCS != nil && opts.GCS.WorkloadIdentity
default:
return false
}
Expand All @@ -302,6 +308,8 @@ func saTokenVolumeMount(opts Options) corev1.VolumeMount {
tokenPath = AWSTokenVolumeDirectory
case lokiv1.ObjectStorageSecretAzure:
tokenPath = azureTokenVolumeDirectory
case lokiv1.ObjectStorageSecretGCS:
tokenPath = gcpTokenVolumeDirectory
}
return corev1.VolumeMount{
Name: saTokenVolumeName,
Expand All @@ -323,6 +331,8 @@ func saTokenVolume(opts Options) corev1.Volume {
if opts.Azure.Audience != "" {
audience = opts.Azure.Audience
}
case lokiv1.ObjectStorageSecretGCS:
audience = opts.GCS.Audience
}
return corev1.Volume{
Name: saTokenVolumeName,
Expand Down
162 changes: 162 additions & 0 deletions operator/internal/manifests/storage/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,87 @@ func TestConfigureDeploymentForStorageType(t *testing.T) {
},
},
},
{
desc: "object storage GCS with WorkloadIdentity",
periklis marked this conversation as resolved.
Show resolved Hide resolved
opts: Options{
SecretName: "test",
SharedStore: lokiv1.ObjectStorageSecretGCS,
GCS: &GCSStorageConfig{
Audience: "test",
WorkloadIdentity: true,
},
},
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: "/var/run/secrets/gcp/serviceaccount",
},
},
Env: []corev1.EnvVar{
{
Name: EnvGoogleApplicationCredentials,
Value: "/etc/storage/secrets/key.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,
},
},
},
},
},
},
},
},
},
},
},
},
{
desc: "object storage S3",
opts: Options{
Expand Down Expand Up @@ -1669,6 +1750,87 @@ func TestConfigureStatefulSetForStorageType(t *testing.T) {
},
},
},
{
desc: "object storage GCS with Workload Identity",
opts: Options{
SecretName: "test",
SharedStore: lokiv1.ObjectStorageSecretGCS,
GCS: &GCSStorageConfig{
Audience: "test",
WorkloadIdentity: true,
},
},
sts: &appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "loki-ingester",
},
},
},
},
},
},
want: &appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
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: "/var/run/secrets/gcp/serviceaccount",
},
},
Env: []corev1.EnvVar{
{
Name: EnvGoogleApplicationCredentials,
Value: "/etc/storage/secrets/key.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,
},
},
},
},
},
},
},
},
},
},
},
},
{
desc: "object storage S3",
opts: Options{
Expand Down
4 changes: 3 additions & 1 deletion operator/internal/manifests/storage/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ type AzureStorageConfig struct {

// GCSStorageConfig for GCS storage config
type GCSStorageConfig struct {
Bucket string
Bucket string
Audience string
WorkloadIdentity bool
}

// S3StorageConfig for S3 storage config
Expand Down
5 changes: 5 additions & 0 deletions operator/internal/manifests/storage/var.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ const (
// KeyAzureAudience is the secret data key for customizing the audience used for the ServiceAccount token.
KeyAzureAudience = "audience"

// KeyGCPWorkloadIdentityProviderAudience is the secret data key for the GCP Workload Identity Provider audience.
KeyGCPWorkloadIdentityProviderAudience = "audience"
// KeyGCPStorageBucketName is the secret data key for the GCS bucket name.
KeyGCPStorageBucketName = "bucketname"
// KeyGCPServiceAccountKeyFilename is the service account key filename containing the Google authentication credentials.
Expand Down Expand Up @@ -144,5 +146,8 @@ const (
azureManagedCredentialKeyTenantID = "azure_tenant_id"
azureManagedCredentialKeySubscriptionID = "azure_subscription_id"

gcpTokenVolumeDirectory = "/var/run/secrets/gcp/serviceaccount"
GCPDefautCredentialsFile = gcpTokenVolumeDirectory + "/token"

AnnotationCredentialsRequestsSecretRef = "loki.grafana.com/credentials-request-secret-ref"
)
Loading