Skip to main content

vector/sinks/azure_common/
config.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4#[cfg(test)]
5use base64::prelude::*;
6
7use azure_core::http::ClientMethodOptions;
8
9use azure_core::credentials::{TokenCredential, TokenRequestOptions};
10use azure_core::{Error, error::ErrorKind};
11
12use azure_identity::{
13    AzureCliCredential, ClientAssertion, ClientAssertionCredential, ClientCertificateCredential,
14    ClientCertificateCredentialOptions, ClientSecretCredential, ManagedIdentityCredential,
15    ManagedIdentityCredentialOptions, UserAssignedId, WorkloadIdentityCredential,
16    WorkloadIdentityCredentialOptions,
17};
18
19use vector_lib::{configurable::configurable_component, sensitive_string::SensitiveString};
20
21/// TLS configuration.
22#[configurable_component]
23#[derive(Clone, Debug, Default)]
24#[serde(deny_unknown_fields)]
25pub struct AzureBlobTlsConfig {
26    /// Absolute path to an additional CA certificate file.
27    ///
28    /// The certificate must be in PEM (X.509) format.
29    #[serde(alias = "ca_path")]
30    #[configurable(metadata(docs::examples = "/path/to/certificate_authority.crt"))]
31    #[configurable(metadata(docs::human_name = "CA File Path"))]
32    pub ca_file: Option<PathBuf>,
33}
34
35/// Azure service principal authentication.
36#[configurable_component]
37#[derive(Clone, Debug, Eq, PartialEq)]
38#[serde(deny_unknown_fields, untagged)]
39pub enum AzureAuthentication {
40    #[configurable(metadata(docs::enum_tag_description = "The kind of Azure credential to use."))]
41    Specific(SpecificAzureCredential),
42
43    /// Mock credential for testing — returns a static fake token
44    #[cfg(test)]
45    #[serde(skip)]
46    MockCredential,
47}
48
49impl Default for AzureAuthentication {
50    // This should never be actually used.
51    // This is only needed when using Default::default() (such as unit tests),
52    // as serde requires `azure_credential_kind` to be specified.
53    fn default() -> Self {
54        Self::Specific(SpecificAzureCredential::ManagedIdentity {
55            user_assigned_managed_identity_id: None,
56            user_assigned_managed_identity_id_type: None,
57        })
58    }
59}
60
61#[configurable_component]
62#[derive(Clone, Debug, Eq, PartialEq)]
63#[serde(deny_unknown_fields, rename_all = "snake_case")]
64#[derive(Default)]
65/// User Assigned Managed Identity Types.
66pub enum UserAssignedManagedIdentityIdType {
67    #[default]
68    /// Client ID
69    ClientId,
70    /// Object ID
71    ObjectId,
72    /// Resource ID
73    ResourceId,
74}
75
76/// Specific Azure credential types.
77#[configurable_component]
78#[derive(Clone, Debug, Eq, PartialEq)]
79#[serde(
80    tag = "azure_credential_kind",
81    rename_all = "snake_case",
82    deny_unknown_fields
83)]
84pub enum SpecificAzureCredential {
85    /// Use Azure CLI credentials
86    #[cfg(not(target_arch = "wasm32"))]
87    AzureCli {},
88
89    /// Use certificate credentials
90    ClientCertificateCredential {
91        /// The [Azure Tenant ID][azure_tenant_id].
92        ///
93        /// [azure_tenant_id]: https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal
94        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
95        #[configurable(metadata(docs::examples = "${AZURE_TENANT_ID:?err}"))]
96        azure_tenant_id: String,
97
98        /// The [Azure Client ID][azure_client_id].
99        ///
100        /// [azure_client_id]: https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal
101        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
102        #[configurable(metadata(docs::examples = "${AZURE_CLIENT_ID:?err}"))]
103        azure_client_id: String,
104
105        /// PKCS12 certificate with RSA private key.
106        #[configurable(metadata(docs::examples = "path/to/certificate.pfx"))]
107        #[configurable(metadata(docs::examples = "${AZURE_CLIENT_CERTIFICATE_PATH:?err}"))]
108        certificate_path: PathBuf,
109
110        /// The password for the client certificate, if applicable.
111        #[configurable(metadata(docs::examples = "${AZURE_CLIENT_CERTIFICATE_PASSWORD}"))]
112        certificate_password: Option<SensitiveString>,
113    },
114
115    /// Use client ID/secret credentials
116    ClientSecretCredential {
117        /// The [Azure Tenant ID][azure_tenant_id].
118        ///
119        /// [azure_tenant_id]: https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal
120        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
121        #[configurable(metadata(docs::examples = "${AZURE_TENANT_ID:?err}"))]
122        azure_tenant_id: String,
123
124        /// The [Azure Client ID][azure_client_id].
125        ///
126        /// [azure_client_id]: https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal
127        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
128        #[configurable(metadata(docs::examples = "${AZURE_CLIENT_ID:?err}"))]
129        azure_client_id: String,
130
131        /// The [Azure Client Secret][azure_client_secret].
132        ///
133        /// [azure_client_secret]: https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal
134        #[configurable(metadata(docs::examples = "00-00~000000-0000000~0000000000000000000"))]
135        #[configurable(metadata(docs::examples = "${AZURE_CLIENT_SECRET:?err}"))]
136        azure_client_secret: SensitiveString,
137    },
138
139    /// Use Managed Identity credentials
140    ManagedIdentity {
141        /// The User Assigned Managed Identity to use.
142        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
143        #[serde(default, skip_serializing_if = "Option::is_none")]
144        user_assigned_managed_identity_id: Option<String>,
145
146        /// The type of the User Assigned Managed Identity ID provided (Client ID, Object ID,
147        /// or Resource ID). Defaults to Client ID.
148        user_assigned_managed_identity_id_type: Option<UserAssignedManagedIdentityIdType>,
149    },
150
151    /// Use Managed Identity with Client Assertion credentials
152    ManagedIdentityClientAssertion {
153        /// The User Assigned Managed Identity to use for the managed identity.
154        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
155        #[configurable(metadata(
156            docs::examples = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-vector/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-vector-uami"
157        ))]
158        #[serde(default, skip_serializing_if = "Option::is_none")]
159        user_assigned_managed_identity_id: Option<String>,
160
161        /// The type of the User Assigned Managed Identity ID provided (Client ID, Object ID, or Resource ID). Defaults to Client ID.
162        user_assigned_managed_identity_id_type: Option<UserAssignedManagedIdentityIdType>,
163
164        /// The target Tenant ID to use.
165        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
166        client_assertion_tenant_id: String,
167
168        /// The target Client ID to use.
169        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
170        client_assertion_client_id: String,
171    },
172
173    /// Use Workload Identity credentials
174    WorkloadIdentity {
175        /// The [Azure Tenant ID][azure_tenant_id]. Defaults to the value of the environment variable `AZURE_TENANT_ID`.
176        ///
177        /// [azure_tenant_id]: https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal
178        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
179        #[configurable(metadata(docs::examples = "${AZURE_TENANT_ID}"))]
180        tenant_id: Option<String>,
181
182        /// The [Azure Client ID][azure_client_id]. Defaults to the value of the environment variable `AZURE_CLIENT_ID`.
183        ///
184        /// [azure_client_id]: https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal
185        #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
186        #[configurable(metadata(docs::examples = "${AZURE_CLIENT_ID}"))]
187        client_id: Option<String>,
188
189        /// Path of a file containing a Kubernetes service account token. Defaults to the value of the environment variable `AZURE_FEDERATED_TOKEN_FILE`.
190        #[configurable(metadata(
191            docs::examples = "/var/run/secrets/azure/tokens/azure-identity-token"
192        ))]
193        #[configurable(metadata(docs::examples = "${AZURE_FEDERATED_TOKEN_FILE}"))]
194        token_file_path: Option<PathBuf>,
195    },
196}
197
198#[derive(Debug)]
199struct ManagedIdentityClientAssertion {
200    credential: Arc<dyn TokenCredential>,
201    scope: String,
202}
203
204#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
205#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
206impl ClientAssertion for ManagedIdentityClientAssertion {
207    async fn secret(&self, options: Option<ClientMethodOptions<'_>>) -> azure_core::Result<String> {
208        Ok(self
209            .credential
210            .get_token(
211                &[&self.scope],
212                Some(TokenRequestOptions {
213                    method_options: options.unwrap_or_default(),
214                }),
215            )
216            .await?
217            .token
218            .secret()
219            .to_string())
220    }
221}
222
223impl AzureAuthentication {
224    /// Returns the provider for the credentials based on the authentication mechanism chosen.
225    pub async fn credential(&self) -> azure_core::Result<Arc<dyn TokenCredential>> {
226        match self {
227            Self::Specific(specific) => specific.credential().await,
228
229            #[cfg(test)]
230            Self::MockCredential => Ok(Arc::new(MockTokenCredential) as Arc<dyn TokenCredential>),
231        }
232    }
233}
234
235impl SpecificAzureCredential {
236    /// Returns the provider for the credentials based on the specific credential type.
237    pub async fn credential(&self) -> azure_core::Result<Arc<dyn TokenCredential>> {
238        let credential: Arc<dyn TokenCredential> = match self {
239            #[cfg(not(target_arch = "wasm32"))]
240            Self::AzureCli {} => AzureCliCredential::new(None)?,
241
242            // requires azure_identity feature 'client_certificate'
243            Self::ClientCertificateCredential {
244                azure_tenant_id,
245                azure_client_id,
246                certificate_path,
247                certificate_password,
248            } => {
249                let certificate_bytes: Vec<u8> = std::fs::read(certificate_path).map_err(|e| {
250                    Error::with_message(
251                        ErrorKind::Credential,
252                        format!(
253                            "Failed to read certificate file {}: {e}",
254                            certificate_path.display()
255                        ),
256                    )
257                })?;
258
259                let mut options: ClientCertificateCredentialOptions =
260                    ClientCertificateCredentialOptions::default();
261                if let Some(password) = certificate_password {
262                    options.password = Some(password.inner().to_string().into());
263                }
264
265                ClientCertificateCredential::new(
266                    azure_tenant_id.clone(),
267                    azure_client_id.clone(),
268                    certificate_bytes.into(),
269                    Some(options),
270                )?
271            }
272
273            Self::ClientSecretCredential {
274                azure_tenant_id,
275                azure_client_id,
276                azure_client_secret,
277            } => {
278                if azure_tenant_id.is_empty() {
279                    return Err(Error::with_message(ErrorKind::Credential,
280                        "`auth.azure_tenant_id` is blank; either use `auth.azure_credential_kind`, or provide tenant ID, client ID, and secret.".to_string()
281                    ));
282                }
283                if azure_client_id.is_empty() {
284                    return Err(Error::with_message(ErrorKind::Credential,
285                        "`auth.azure_client_id` is blank; either use `auth.azure_credential_kind`, or provide tenant ID, client ID, and secret.".to_string()
286                    ));
287                }
288                if azure_client_secret.inner().is_empty() {
289                    return Err(Error::with_message(ErrorKind::Credential,
290                        "`auth.azure_client_secret` is blank; either use `auth.azure_credential_kind`, or provide tenant ID, client ID, and secret.".to_string()
291                    ));
292                }
293
294                let secret: String = azure_client_secret.inner().into();
295                ClientSecretCredential::new(
296                    &azure_tenant_id.clone(),
297                    azure_client_id.clone(),
298                    secret.into(),
299                    None,
300                )?
301            }
302
303            Self::ManagedIdentity {
304                user_assigned_managed_identity_id,
305                user_assigned_managed_identity_id_type,
306            } => {
307                let mut options = ManagedIdentityCredentialOptions::default();
308                if let Some(id) = user_assigned_managed_identity_id {
309                    options.user_assigned_id = match user_assigned_managed_identity_id_type
310                        .as_ref()
311                        .unwrap_or(&Default::default())
312                    {
313                        UserAssignedManagedIdentityIdType::ClientId => {
314                            Some(UserAssignedId::ClientId(id.clone()))
315                        }
316                        UserAssignedManagedIdentityIdType::ObjectId => {
317                            Some(UserAssignedId::ObjectId(id.clone()))
318                        }
319                        UserAssignedManagedIdentityIdType::ResourceId => {
320                            Some(UserAssignedId::ResourceId(id.clone()))
321                        }
322                    };
323                }
324                ManagedIdentityCredential::new(Some(options))?
325            }
326
327            Self::ManagedIdentityClientAssertion {
328                user_assigned_managed_identity_id,
329                user_assigned_managed_identity_id_type,
330                client_assertion_tenant_id,
331                client_assertion_client_id,
332            } => {
333                let mut options = ManagedIdentityCredentialOptions::default();
334                if let Some(id) = user_assigned_managed_identity_id {
335                    options.user_assigned_id = match user_assigned_managed_identity_id_type
336                        .as_ref()
337                        .unwrap_or(&Default::default())
338                    {
339                        UserAssignedManagedIdentityIdType::ClientId => {
340                            Some(UserAssignedId::ClientId(id.clone()))
341                        }
342                        UserAssignedManagedIdentityIdType::ObjectId => {
343                            Some(UserAssignedId::ObjectId(id.clone()))
344                        }
345                        UserAssignedManagedIdentityIdType::ResourceId => {
346                            Some(UserAssignedId::ResourceId(id.clone()))
347                        }
348                    };
349                }
350                let msi: Arc<dyn TokenCredential> = ManagedIdentityCredential::new(Some(options))?;
351                let assertion = ManagedIdentityClientAssertion {
352                    credential: msi,
353                    // Future: make this configurable for sovereign clouds? (no way to test...)
354                    scope: "api://AzureADTokenExchange/.default".to_string(),
355                };
356
357                ClientAssertionCredential::new(
358                    client_assertion_tenant_id.clone(),
359                    client_assertion_client_id.clone(),
360                    assertion,
361                    None,
362                )?
363            }
364
365            Self::WorkloadIdentity {
366                tenant_id,
367                client_id,
368                token_file_path,
369            } => {
370                let options = WorkloadIdentityCredentialOptions {
371                    tenant_id: tenant_id.clone(),
372                    client_id: client_id.clone(),
373                    token_file_path: token_file_path.clone(),
374                    ..Default::default()
375                };
376
377                WorkloadIdentityCredential::new(Some(options))?
378            }
379        };
380        Ok(credential)
381    }
382}
383
384#[cfg(test)]
385#[derive(Debug)]
386struct MockTokenCredential;
387
388#[cfg(test)]
389#[async_trait::async_trait]
390impl TokenCredential for MockTokenCredential {
391    async fn get_token(
392        &self,
393        scopes: &[&str],
394        _options: Option<azure_core::credentials::TokenRequestOptions<'_>>,
395    ) -> azure_core::Result<azure_core::credentials::AccessToken> {
396        let Some(scope) = scopes.first() else {
397            return Err(Error::with_message(
398                ErrorKind::Credential,
399                "no scopes were provided",
400            ));
401        };
402
403        // serde_json sometimes does and sometimes doesn't preserve order, be careful to sort
404        // the claims in alphabetical order to ensure a consistent base64 encoding for testing
405        let jwt = serde_json::json!({
406            "aud": scope.strip_suffix("/.default").unwrap_or(*scope),
407            "exp": 2147483647,
408            "iat": 0,
409            "iss": "https://sts.windows.net/",
410            "nbf": 0,
411        });
412
413        // JWTs do not include standard base64 padding.
414        // this seemed cleaner than importing a new crates just for this function
415        let jwt_base64 = format!(
416            "e30.{}.",
417            BASE64_STANDARD
418                .encode(serde_json::to_string(&jwt).unwrap())
419                .trim_end_matches("=")
420        )
421        .to_string();
422
423        warn!(
424            "Using mock token credential, JWT: {}, base64: {}",
425            serde_json::to_string(&jwt).unwrap(),
426            jwt_base64
427        );
428
429        Ok(azure_core::credentials::AccessToken::new(
430            jwt_base64,
431            azure_core::time::OffsetDateTime::now_utc() + std::time::Duration::from_secs(3600),
432        ))
433    }
434}
435
436#[cfg(test)]
437#[tokio::test]
438async fn azure_mock_token_credential_test() {
439    let credential = MockTokenCredential;
440    let access_token = credential
441        .get_token(&["https://example.com/.default"], None)
442        .await
443        .expect("valid credential should return a token");
444    assert_eq!(
445        access_token.token.secret(),
446        "e30.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tIiwiZXhwIjoyMTQ3NDgzNjQ3LCJpYXQiOjAsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LyIsIm5iZiI6MH0."
447    );
448}