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