From b25743f2bf0690686086f607f21b14286934ba46 Mon Sep 17 00:00:00 2001 From: Khris Richardson Date: Thu, 1 Oct 2026 10:56:53 -0700 Subject: [PATCH] feat(credentials): add an explicit workload identity auth type for Azure Key Vault AzureKeyVault gains an auth_type enum. AUTH_TYPE_WORKLOAD_IDENTITY authenticates as client_id through AKS Workload Identity, using the federated token the workload identity webhook projects into the pod, so no client secret is stored. AUTH_TYPE_CREDENTIALS keeps the service principal client secret, and AUTH_TYPE_UNSPECIFIED is treated as AUTH_TYPE_CREDENTIALS, so existing configurations do not change. The operator must select workload identity explicitly. The manager rejects AUTH_TYPE_CREDENTIALS without a client secret, so a forgotten secret fails at startup instead of silently switching the authentication method, and rejects AUTH_TYPE_WORKLOAD_IDENTITY with a client secret. The chart exposes secretsBackend.azureKeyVault.authType and renders client_secret only for AUTH_TYPE_CREDENTIALS. The README documents the pod label and service account annotation that workload identity needs. Assisted-by: Claude Code Signed-off-by: Khris Richardson --- deployment/chainloop/Chart.yaml | 2 +- deployment/chainloop/README.md | 28 ++++- deployment/chainloop/templates/_helpers.tpl | 10 ++ deployment/chainloop/values.yaml | 6 +- .../api/credentials/v1/config.pb.go | 118 ++++++++++++++---- .../api/credentials/v1/config.proto | 16 ++- pkg/credentials/azurekv/keyvault.go | 60 +++++++-- pkg/credentials/azurekv/keyvault_test.go | 23 +++- pkg/credentials/manager/manager.go | 6 + pkg/credentials/manager/manager_test.go | 5 +- 10 files changed, 235 insertions(+), 39 deletions(-) diff --git a/deployment/chainloop/Chart.yaml b/deployment/chainloop/Chart.yaml index adead4697..8b355dbfd 100644 --- a/deployment/chainloop/Chart.yaml +++ b/deployment/chainloop/Chart.yaml @@ -7,7 +7,7 @@ description: Chainloop is an open source software supply chain control plane, a type: application # Bump the patch (not minor, not major) version on each change in the Chart Source code -version: 1.451.0 +version: 1.451.1 # Do not update appVersion, this is handled automatically by the release process appVersion: v1.113.0 diff --git a/deployment/chainloop/README.md b/deployment/chainloop/README.md index 4847257ed..4a1244a05 100644 --- a/deployment/chainloop/README.md +++ b/deployment/chainloop/README.md @@ -388,6 +388,31 @@ secretsBackend: ``` +To authenticate without a stored secret, select [AKS Workload Identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview) explicitly and leave `clientSecret` unset. The pods need the `azure.workload.identity/use: "true"` label (set through `commonLabels`), and the controlplane and CAS service accounts need the `azure.workload.identity/client-id: [CLIENT_ID]` annotation + +```yaml +secretsBackend: + backend: azureKeyVault + azureKeyVault: + authType: AUTH_TYPE_WORKLOAD_IDENTITY + tenantID: [TENANT_ID] + clientID: [CLIENT_ID] + vaultURI: [VAULT URI] + +commonLabels: + azure.workload.identity/use: "true" + +controlplane: + serviceAccount: + annotations: + azure.workload.identity/client-id: [CLIENT_ID] + +cas: + serviceAccount: + annotations: + azure.workload.identity/client-id: [CLIENT_ID] +``` + ### Deploy in keyless mode with file-based CA You can enable keyless signing mode by providing a custom Certificate Authority. @@ -547,7 +572,8 @@ Once done, you can access with [two predefined users](https://github.com/chainlo | `secretsBackend.gcpSecretManager.serviceAccountKey` | GCP Auth Key | | | `secretsBackend.azureKeyVault.tenantID` | Active Directory Tenant ID | | | `secretsBackend.azureKeyVault.clientID` | Registered application / service principal client ID | | -| `secretsBackend.azureKeyVault.clientSecret` | Service principal client secret | | +| `secretsBackend.azureKeyVault.authType` | AUTH_TYPE_CREDENTIALS (default, client secret) or AUTH_TYPE_WORKLOAD_IDENTITY | | +| `secretsBackend.azureKeyVault.clientSecret` | Service principal client secret (AUTH_TYPE_CREDENTIALS only) | | | `secretsBackend.azureKeyVault.vaultURI` | Azure Key Vault URL | | ### Authentication diff --git a/deployment/chainloop/templates/_helpers.tpl b/deployment/chainloop/templates/_helpers.tpl index e530766fd..109745c4e 100644 --- a/deployment/chainloop/templates/_helpers.tpl +++ b/deployment/chainloop/templates/_helpers.tpl @@ -95,10 +95,20 @@ gcpSecretManager: {{- fail ".Values.secretsBackend.gcpSecretManager.serviceAccountKey not set" }} {{- end }} {{- else if eq .backend "azureKeyVault" }} +{{- $authType := .azureKeyVault.authType | default "AUTH_TYPE_CREDENTIALS" }} azure_key_vault: tenant_id: {{ required "AD tenantID required" .azureKeyVault.tenantID | quote }} client_id: {{ required "Service principal ID required" .azureKeyVault.clientID | quote }} + auth_type: {{ $authType | quote }} + {{- if eq $authType "AUTH_TYPE_CREDENTIALS" }} client_secret: {{ required "Service principal secret required" .azureKeyVault.clientSecret | quote }} + {{- else if eq $authType "AUTH_TYPE_WORKLOAD_IDENTITY" }} + {{- if .azureKeyVault.clientSecret }} + {{- fail "secretsBackend.azureKeyVault: clientSecret must not be set with authType AUTH_TYPE_WORKLOAD_IDENTITY" }} + {{- end }} + {{- else }} + {{- fail (printf "secretsBackend.azureKeyVault.authType %q is not one of AUTH_TYPE_CREDENTIALS, AUTH_TYPE_WORKLOAD_IDENTITY" $authType) }} + {{- end }} vault_uri: {{ required "Azure Vault URL required" .azureKeyVault.vaultURI | quote }} {{- end }} {{- end }} diff --git a/deployment/chainloop/values.yaml b/deployment/chainloop/values.yaml index 94cde726f..5eca62394 100644 --- a/deployment/chainloop/values.yaml +++ b/deployment/chainloop/values.yaml @@ -88,10 +88,14 @@ secretsBackend: ## @extra secretsBackend.azureKeyVault.tenantID Active Directory Tenant ID ## @extra secretsBackend.azureKeyVault.clientID Registered application / service principal client ID - ## @extra secretsBackend.azureKeyVault.clientSecret Service principal client secret + ## @extra secretsBackend.azureKeyVault.authType AUTH_TYPE_CREDENTIALS (default, client secret) or AUTH_TYPE_WORKLOAD_IDENTITY + ## @extra secretsBackend.azureKeyVault.clientSecret Service principal client secret (AUTH_TYPE_CREDENTIALS only) + ## (AUTH_TYPE_WORKLOAD_IDENTITY: set commonLabels azure.workload.identity/use: "true", and annotate the controlplane and + ## cas service accounts azure.workload.identity/client-id: ) ## @extra secretsBackend.azureKeyVault.vaultURI Azure Key Vault URL ## # azureKeyVault: + # authType: AUTH_TYPE_CREDENTIALS # tenantID: "" # clientID: "" # clientSecret: "" diff --git a/pkg/credentials/api/credentials/v1/config.pb.go b/pkg/credentials/api/credentials/v1/config.pb.go index 3b1ec870d..24926f76f 100644 --- a/pkg/credentials/api/credentials/v1/config.pb.go +++ b/pkg/credentials/api/credentials/v1/config.pb.go @@ -37,6 +37,57 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type Credentials_AzureKeyVault_AuthType int32 + +const ( + Credentials_AzureKeyVault_AUTH_TYPE_UNSPECIFIED Credentials_AzureKeyVault_AuthType = 0 + // Use the service principal client secret. + Credentials_AzureKeyVault_AUTH_TYPE_CREDENTIALS Credentials_AzureKeyVault_AuthType = 1 + // Use AKS Workload Identity for client_id: the federated token the workload identity webhook projects into the pod. + Credentials_AzureKeyVault_AUTH_TYPE_WORKLOAD_IDENTITY Credentials_AzureKeyVault_AuthType = 2 +) + +// Enum value maps for Credentials_AzureKeyVault_AuthType. +var ( + Credentials_AzureKeyVault_AuthType_name = map[int32]string{ + 0: "AUTH_TYPE_UNSPECIFIED", + 1: "AUTH_TYPE_CREDENTIALS", + 2: "AUTH_TYPE_WORKLOAD_IDENTITY", + } + Credentials_AzureKeyVault_AuthType_value = map[string]int32{ + "AUTH_TYPE_UNSPECIFIED": 0, + "AUTH_TYPE_CREDENTIALS": 1, + "AUTH_TYPE_WORKLOAD_IDENTITY": 2, + } +) + +func (x Credentials_AzureKeyVault_AuthType) Enum() *Credentials_AzureKeyVault_AuthType { + p := new(Credentials_AzureKeyVault_AuthType) + *p = x + return p +} + +func (x Credentials_AzureKeyVault_AuthType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Credentials_AzureKeyVault_AuthType) Descriptor() protoreflect.EnumDescriptor { + return file_credentials_v1_config_proto_enumTypes[0].Descriptor() +} + +func (Credentials_AzureKeyVault_AuthType) Type() protoreflect.EnumType { + return &file_credentials_v1_config_proto_enumTypes[0] +} + +func (x Credentials_AzureKeyVault_AuthType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Credentials_AzureKeyVault_AuthType.Descriptor instead. +func (Credentials_AzureKeyVault_AuthType) EnumDescriptor() ([]byte, []int) { + return file_credentials_v1_config_proto_rawDescGZIP(), []int{0, 3, 0} +} + // Where the credentials to access the backends are stored type Credentials struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -338,10 +389,13 @@ type Credentials_AzureKeyVault struct { TenantId string `protobuf:"bytes,1,opt,name=tenant_id,json=tenantId,proto3" json:"tenant_id,omitempty"` // Registered application / service principal client ID ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - // Registered application / service principal client secret + // Registered application / service principal client secret. + // Required for AUTH_TYPE_CREDENTIALS, rejected for AUTH_TYPE_WORKLOAD_IDENTITY. ClientSecret string `protobuf:"bytes,3,opt,name=client_secret,json=clientSecret,proto3" json:"client_secret,omitempty"` // Azure Key Vault URL - VaultUri string `protobuf:"bytes,4,opt,name=vault_uri,json=vaultUri,proto3" json:"vault_uri,omitempty"` + VaultUri string `protobuf:"bytes,4,opt,name=vault_uri,json=vaultUri,proto3" json:"vault_uri,omitempty"` + // How to authenticate. AUTH_TYPE_UNSPECIFIED is treated as AUTH_TYPE_CREDENTIALS. + AuthType Credentials_AzureKeyVault_AuthType `protobuf:"varint,5,opt,name=auth_type,json=authType,proto3,enum=credentials.v1.Credentials_AzureKeyVault_AuthType" json:"auth_type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -404,6 +458,13 @@ func (x *Credentials_AzureKeyVault) GetVaultUri() string { return "" } +func (x *Credentials_AzureKeyVault) GetAuthType() Credentials_AzureKeyVault_AuthType { + if x != nil { + return x.AuthType + } + return Credentials_AzureKeyVault_AUTH_TYPE_UNSPECIFIED +} + type Credentials_AWSSecretManager_Creds struct { state protoimpl.MessageState `protogen:"open.v1"` AccessKey string `protobuf:"bytes,1,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"` @@ -460,7 +521,7 @@ var File_credentials_v1_config_proto protoreflect.FileDescriptor const file_credentials_v1_config_proto_rawDesc = "" + "\n" + - "\x1bcredentials/v1/config.proto\x12\x0ecredentials.v1\x1a\x1bbuf/validate/validate.proto\"\xfa\a\n" + + "\x1bcredentials/v1/config.proto\x12\x0ecredentials.v1\x1a\x1bbuf/validate/validate.proto\"\xaf\t\n" + "\vCredentials\x12\\\n" + "\x12aws_secret_manager\x18\x01 \x01(\v2,.credentials.v1.Credentials.AWSSecretManagerH\x00R\x10awsSecretManager\x129\n" + "\x05vault\x18\x02 \x01(\v2!.credentials.v1.Credentials.VaultH\x00R\x05vault\x12\\\n" + @@ -483,12 +544,17 @@ const file_credentials_v1_config_proto_rawDesc = "" + "\x10GCPSecretManager\x12&\n" + "\n" + "project_id\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tprojectId\x12.\n" + - "\x13service_account_key\x18\x02 \x01(\tR\x11serviceAccountKey\x1a\xb0\x01\n" + + "\x13service_account_key\x18\x02 \x01(\tR\x11serviceAccountKey\x1a\xe5\x02\n" + "\rAzureKeyVault\x12$\n" + "\ttenant_id\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\btenantId\x12$\n" + - "\tclient_id\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\bclientId\x12,\n" + - "\rclient_secret\x18\x03 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\fclientSecret\x12%\n" + - "\tvault_uri\x18\x04 \x01(\tB\b\xbaH\x05r\x03\x90\x01\x01R\bvaultUriB\x10\n" + + "\tclient_id\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\bclientId\x12#\n" + + "\rclient_secret\x18\x03 \x01(\tR\fclientSecret\x12%\n" + + "\tvault_uri\x18\x04 \x01(\tB\b\xbaH\x05r\x03\x90\x01\x01R\bvaultUri\x12Y\n" + + "\tauth_type\x18\x05 \x01(\x0e22.credentials.v1.Credentials.AzureKeyVault.AuthTypeB\b\xbaH\x05\x82\x01\x02\x10\x01R\bauthType\"a\n" + + "\bAuthType\x12\x19\n" + + "\x15AUTH_TYPE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15AUTH_TYPE_CREDENTIALS\x10\x01\x12\x1f\n" + + "\x1bAUTH_TYPE_WORKLOAD_IDENTITY\x10\x02B\x10\n" + "\abackend\x12\x05\xbaH\x02\b\x01BJZHgithub.com/chainloop-dev/chainloop/pkg/credentials/api/credentials/v1;v1b\x06proto3" var ( @@ -503,26 +569,29 @@ func file_credentials_v1_config_proto_rawDescGZIP() []byte { return file_credentials_v1_config_proto_rawDescData } +var file_credentials_v1_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_credentials_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_credentials_v1_config_proto_goTypes = []any{ - (*Credentials)(nil), // 0: credentials.v1.Credentials - (*Credentials_AWSSecretManager)(nil), // 1: credentials.v1.Credentials.AWSSecretManager - (*Credentials_Vault)(nil), // 2: credentials.v1.Credentials.Vault - (*Credentials_GCPSecretManager)(nil), // 3: credentials.v1.Credentials.GCPSecretManager - (*Credentials_AzureKeyVault)(nil), // 4: credentials.v1.Credentials.AzureKeyVault - (*Credentials_AWSSecretManager_Creds)(nil), // 5: credentials.v1.Credentials.AWSSecretManager.Creds + (Credentials_AzureKeyVault_AuthType)(0), // 0: credentials.v1.Credentials.AzureKeyVault.AuthType + (*Credentials)(nil), // 1: credentials.v1.Credentials + (*Credentials_AWSSecretManager)(nil), // 2: credentials.v1.Credentials.AWSSecretManager + (*Credentials_Vault)(nil), // 3: credentials.v1.Credentials.Vault + (*Credentials_GCPSecretManager)(nil), // 4: credentials.v1.Credentials.GCPSecretManager + (*Credentials_AzureKeyVault)(nil), // 5: credentials.v1.Credentials.AzureKeyVault + (*Credentials_AWSSecretManager_Creds)(nil), // 6: credentials.v1.Credentials.AWSSecretManager.Creds } var file_credentials_v1_config_proto_depIdxs = []int32{ - 1, // 0: credentials.v1.Credentials.aws_secret_manager:type_name -> credentials.v1.Credentials.AWSSecretManager - 2, // 1: credentials.v1.Credentials.vault:type_name -> credentials.v1.Credentials.Vault - 3, // 2: credentials.v1.Credentials.gcp_secret_manager:type_name -> credentials.v1.Credentials.GCPSecretManager - 4, // 3: credentials.v1.Credentials.azure_key_vault:type_name -> credentials.v1.Credentials.AzureKeyVault - 5, // 4: credentials.v1.Credentials.AWSSecretManager.creds:type_name -> credentials.v1.Credentials.AWSSecretManager.Creds - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 2, // 0: credentials.v1.Credentials.aws_secret_manager:type_name -> credentials.v1.Credentials.AWSSecretManager + 3, // 1: credentials.v1.Credentials.vault:type_name -> credentials.v1.Credentials.Vault + 4, // 2: credentials.v1.Credentials.gcp_secret_manager:type_name -> credentials.v1.Credentials.GCPSecretManager + 5, // 3: credentials.v1.Credentials.azure_key_vault:type_name -> credentials.v1.Credentials.AzureKeyVault + 6, // 4: credentials.v1.Credentials.AWSSecretManager.creds:type_name -> credentials.v1.Credentials.AWSSecretManager.Creds + 0, // 5: credentials.v1.Credentials.AzureKeyVault.auth_type:type_name -> credentials.v1.Credentials.AzureKeyVault.AuthType + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_credentials_v1_config_proto_init() } @@ -541,13 +610,14 @@ func file_credentials_v1_config_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_credentials_v1_config_proto_rawDesc), len(file_credentials_v1_config_proto_rawDesc)), - NumEnums: 0, + NumEnums: 1, NumMessages: 6, NumExtensions: 0, NumServices: 0, }, GoTypes: file_credentials_v1_config_proto_goTypes, DependencyIndexes: file_credentials_v1_config_proto_depIdxs, + EnumInfos: file_credentials_v1_config_proto_enumTypes, MessageInfos: file_credentials_v1_config_proto_msgTypes, }.Build() File_credentials_v1_config_proto = out.File diff --git a/pkg/credentials/api/credentials/v1/config.proto b/pkg/credentials/api/credentials/v1/config.proto index 557d1654d..4e0369aa9 100644 --- a/pkg/credentials/api/credentials/v1/config.proto +++ b/pkg/credentials/api/credentials/v1/config.proto @@ -67,9 +67,21 @@ message Credentials { string tenant_id = 1 [(buf.validate.field).string.min_len = 1]; // Registered application / service principal client ID string client_id = 2 [(buf.validate.field).string.min_len = 1]; - // Registered application / service principal client secret - string client_secret = 3 [(buf.validate.field).string.min_len = 1]; + // Registered application / service principal client secret. + // Required for AUTH_TYPE_CREDENTIALS, rejected for AUTH_TYPE_WORKLOAD_IDENTITY. + string client_secret = 3; // Azure Key Vault URL string vault_uri = 4 [(buf.validate.field).string.uri_ref = true]; + + enum AuthType { + AUTH_TYPE_UNSPECIFIED = 0; + // Use the service principal client secret. + AUTH_TYPE_CREDENTIALS = 1; + // Use AKS Workload Identity for client_id: the federated token the workload identity webhook projects into the pod. + AUTH_TYPE_WORKLOAD_IDENTITY = 2; + } + + // How to authenticate. AUTH_TYPE_UNSPECIFIED is treated as AUTH_TYPE_CREDENTIALS. + AuthType auth_type = 5 [(buf.validate.field).enum.defined_only = true]; } } diff --git a/pkg/credentials/azurekv/keyvault.go b/pkg/credentials/azurekv/keyvault.go index 6d7a1a8c3..5a3f12d0f 100644 --- a/pkg/credentials/azurekv/keyvault.go +++ b/pkg/credentials/azurekv/keyvault.go @@ -50,13 +50,26 @@ type SecretsRW interface { DeleteSecret(ctx context.Context, secretName string, options *azsecrets.DeleteSecretOptions) (azsecrets.DeleteSecretResponse, error) } +// AuthType selects how the manager authenticates to Azure. The zero value is AuthTypeCredentials, so a configuration +// that does not choose keeps using the client secret. +type AuthType int + +const ( + // AuthTypeCredentials uses the service principal ClientSecret. + AuthTypeCredentials AuthType = iota + // AuthTypeWorkloadIdentity uses AKS Workload Identity for ClientID, so no secret is stored. + AuthTypeWorkloadIdentity +) + type NewManagerOpts struct { // Active Directory Tenant ID TenantID string // Registered application / service principal client ID ClientID string - // Registered application / service principal client secret + // Registered application / service principal client secret. Required for AuthTypeCredentials, rejected for + // AuthTypeWorkloadIdentity. ClientSecret string + AuthType AuthType // Vault URL VaultURI string // Optional secret prefix @@ -76,17 +89,50 @@ func (o *NewManagerOpts) Validate() error { return fmt.Errorf("%w: missing client ID", ErrValidation) } - if o.ClientSecret == "" { - return fmt.Errorf("%w: missing client secret", ErrValidation) - } - if o.VaultURI == "" { return fmt.Errorf("%w: missing VAULT URI", ErrValidation) } + // The operator chooses workload identity explicitly, so a forgotten secret fails here instead of silently + // switching the authentication method. + switch o.AuthType { + case AuthTypeCredentials: + if o.ClientSecret == "" { + return fmt.Errorf("%w: missing client secret", ErrValidation) + } + case AuthTypeWorkloadIdentity: + if o.ClientSecret != "" { + return fmt.Errorf("%w: client secret must not be set for the workload identity auth type", ErrValidation) + } + default: + return fmt.Errorf("%w: unknown auth type %d", ErrValidation, o.AuthType) + } + return nil } +// newCredential uses the service principal secret for AuthTypeCredentials, and AKS Workload Identity for the same +// tenant and client ID for AuthTypeWorkloadIdentity: the federated token file the workload identity webhook projects +// into the pod (AZURE_FEDERATED_TOKEN_FILE). +func newCredential(opts *NewManagerOpts) (azcore.TokenCredential, error) { + if opts.AuthType == AuthTypeCredentials { + credential, err := azidentity.NewClientSecretCredential(opts.TenantID, opts.ClientID, opts.ClientSecret, nil) + if err != nil { + return nil, fmt.Errorf("failed to create Azure Service principal Credential: %w", err) + } + return credential, nil + } + + credential, err := azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{ + TenantID: opts.TenantID, + ClientID: opts.ClientID, + }) + if err != nil { + return nil, fmt.Errorf("failed to create Azure Workload Identity Credential: %w", err) + } + return credential, nil +} + func NewManager(opts *NewManagerOpts) (*Manager, error) { l := opts.Logger if l == nil { @@ -100,9 +146,9 @@ func NewManager(opts *NewManagerOpts) (*Manager, error) { return nil, fmt.Errorf("invalid credentials: %w", err) } - credential, err := azidentity.NewClientSecretCredential(opts.TenantID, opts.ClientID, opts.ClientSecret, nil) + credential, err := newCredential(opts) if err != nil { - return nil, fmt.Errorf("failed to create Azure Service principal Credential: %w", err) + return nil, err } // Establish a connection to the Key Vault client diff --git a/pkg/credentials/azurekv/keyvault_test.go b/pkg/credentials/azurekv/keyvault_test.go index 27494a16c..6feae19f8 100644 --- a/pkg/credentials/azurekv/keyvault_test.go +++ b/pkg/credentials/azurekv/keyvault_test.go @@ -19,6 +19,8 @@ import ( "context" "encoding/json" "errors" + "os" + "path/filepath" "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -37,12 +39,19 @@ func (s *testSuite) TestNewManager() { clientID string clientSecret string vaultURI string + authType AuthType Role credentials.Role expectedError bool + // federatedTokenFile stands in for the AKS workload identity webhook's projected token. + federatedTokenFile bool }{ {name: "missing tenantID", tenantID: "", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, {name: "missing clientID", tenantID: "tenantID", clientID: "", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, - {name: "missing clientSecret", tenantID: "tenantID", clientID: "clientID", clientSecret: "", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, + {name: "missing clientSecret", tenantID: "tenantID", clientID: "clientID", clientSecret: "", vaultURI: "vaultURI", Role: credentials.RoleReader, federatedTokenFile: true, expectedError: true}, + {name: "workload identity", tenantID: "tenantID", clientID: "clientID", vaultURI: "vaultURI", authType: AuthTypeWorkloadIdentity, Role: credentials.RoleReader, federatedTokenFile: true}, + {name: "workload identity without a federated token", tenantID: "tenantID", clientID: "clientID", vaultURI: "vaultURI", authType: AuthTypeWorkloadIdentity, Role: credentials.RoleReader, expectedError: true}, + {name: "workload identity with a clientSecret", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", authType: AuthTypeWorkloadIdentity, Role: credentials.RoleReader, federatedTokenFile: true, expectedError: true}, + {name: "unknown auth type", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", authType: AuthType(99), Role: credentials.RoleReader, expectedError: true}, {name: "missing vaultURI", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "", Role: credentials.RoleReader, expectedError: true}, {name: "valid reader configuration", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleReader}, {name: "valid writer configuration", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleWriter}, @@ -50,7 +59,17 @@ func (s *testSuite) TestNewManager() { for _, tc := range testCases { s.T().Run(tc.name, func(t *testing.T) { - opts := &NewManagerOpts{TenantID: tc.tenantID, ClientID: tc.clientID, ClientSecret: tc.clientSecret, VaultURI: tc.vaultURI, Role: tc.Role} + tokenFile := "" + if tc.federatedTokenFile { + tokenFile = filepath.Join(t.TempDir(), "token") + assert.NoError(t, os.WriteFile(tokenFile, []byte("federated-token"), 0o600)) + } + // Setenv first so the variable is restored afterwards; an empty value would still count as set. + t.Setenv("AZURE_FEDERATED_TOKEN_FILE", tokenFile) + if tokenFile == "" { + assert.NoError(t, os.Unsetenv("AZURE_FEDERATED_TOKEN_FILE")) + } + opts := &NewManagerOpts{TenantID: tc.tenantID, ClientID: tc.clientID, ClientSecret: tc.clientSecret, AuthType: tc.authType, VaultURI: tc.vaultURI, Role: tc.Role} _, err := NewManager(opts) if tc.expectedError { assert.Error(t, err) diff --git a/pkg/credentials/manager/manager.go b/pkg/credentials/manager/manager.go index c13cc2016..92aba8ba4 100644 --- a/pkg/credentials/manager/manager.go +++ b/pkg/credentials/manager/manager.go @@ -64,10 +64,16 @@ func newAzureKBManager(conf *api.Credentials_AzureKeyVault, prefix string, r cre return nil, fmt.Errorf("uncompleted configuration for Azure Key Vault: %w", err) } + authType := azurekv.AuthTypeCredentials + if conf.GetAuthType() == api.Credentials_AzureKeyVault_AUTH_TYPE_WORKLOAD_IDENTITY { + authType = azurekv.AuthTypeWorkloadIdentity + } + opts := &azurekv.NewManagerOpts{ TenantID: conf.GetTenantId(), ClientID: conf.GetClientId(), ClientSecret: conf.GetClientSecret(), + AuthType: authType, VaultURI: conf.GetVaultUri(), Logger: l, SecretPrefix: prefix, diff --git a/pkg/credentials/manager/manager_test.go b/pkg/credentials/manager/manager_test.go index 98f524187..0fc83e260 100644 --- a/pkg/credentials/manager/manager_test.go +++ b/pkg/credentials/manager/manager_test.go @@ -68,6 +68,7 @@ func (s *testSuite) TestNewAzureManagerFromConfig() { clientID string clientSecret string vaultURI string + authType v1.Credentials_AzureKeyVault_AuthType Role credentials.Role expectedError bool }{ @@ -75,6 +76,8 @@ func (s *testSuite) TestNewAzureManagerFromConfig() { {name: "missing clientID", tenantID: "tenantID", clientID: "", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, {name: "missing clientSecret", tenantID: "tenantID", clientID: "clientID", clientSecret: "", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, {name: "missing vaultURI", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "", Role: credentials.RoleReader, expectedError: true}, + {name: "workload identity with a clientSecret", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", authType: v1.Credentials_AzureKeyVault_AUTH_TYPE_WORKLOAD_IDENTITY, Role: credentials.RoleReader, expectedError: true}, + {name: "undefined auth type", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", authType: 99, Role: credentials.RoleReader, expectedError: true}, } for _, tc := range testCases { @@ -82,7 +85,7 @@ func (s *testSuite) TestNewAzureManagerFromConfig() { conf := &v1.Credentials{ Backend: &v1.Credentials_AzureKeyVault_{ AzureKeyVault: &v1.Credentials_AzureKeyVault{ - TenantId: tc.tenantID, ClientId: tc.clientID, ClientSecret: tc.clientSecret, VaultUri: tc.vaultURI, + TenantId: tc.tenantID, ClientId: tc.clientID, ClientSecret: tc.clientSecret, VaultUri: tc.vaultURI, AuthType: tc.authType, }, }, }