Skip to main content

vector/aws/
auth.rs

1//! Authentication settings for AWS components.
2use std::time::Duration;
3
4use aws_config::{
5    default_provider::credentials::DefaultCredentialsChain, identity::IdentityCache, imds,
6    profile::ProfileFileCredentialsProvider, provider_config::ProviderConfig,
7    sts::AssumeRoleProviderBuilder,
8};
9use aws_credential_types::{Credentials, provider::SharedCredentialsProvider};
10use aws_runtime::env_config::file::{EnvConfigFileKind, EnvConfigFiles};
11use aws_smithy_async::time::SystemTimeSource;
12use aws_smithy_runtime_api::client::identity::SharedIdentityCache;
13use aws_types::{SdkConfig, region::Region};
14use serde_with::serde_as;
15use vector_lib::{
16    config::proxy::ProxyConfig, configurable::configurable_component,
17    sensitive_string::SensitiveString, tls::TlsConfig,
18};
19
20// matches default load timeout from the SDK as of 0.10.1, but lets us confidently document the
21// default rather than relying on the SDK default to not change
22const DEFAULT_LOAD_TIMEOUT: Duration = Duration::from_secs(5);
23const DEFAULT_PROFILE_NAME: &str = "default";
24
25/// IMDS Client Configuration for authenticating with AWS.
26#[serde_as]
27#[configurable_component]
28#[derive(Copy, Clone, Debug, Derivative, Eq, PartialEq)]
29#[derivative(Default)]
30#[serde(deny_unknown_fields)]
31pub struct ImdsAuthentication {
32    /// Number of IMDS retries for fetching tokens and metadata.
33    #[serde(default = "default_max_attempts")]
34    #[derivative(Default(value = "default_max_attempts()"))]
35    max_attempts: u32,
36
37    /// Connect timeout for IMDS.
38    #[serde(default = "default_timeout")]
39    #[serde(rename = "connect_timeout_seconds")]
40    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
41    #[derivative(Default(value = "default_timeout()"))]
42    connect_timeout: Duration,
43
44    /// Read timeout for IMDS.
45    #[serde(default = "default_timeout")]
46    #[serde(rename = "read_timeout_seconds")]
47    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
48    #[derivative(Default(value = "default_timeout()"))]
49    read_timeout: Duration,
50}
51
52const fn default_max_attempts() -> u32 {
53    4
54}
55
56const fn default_timeout() -> Duration {
57    Duration::from_secs(1)
58}
59
60/// Configuration of the authentication strategy for interacting with AWS services.
61#[configurable_component]
62#[derive(Clone, Debug, Derivative, Eq, PartialEq)]
63#[derivative(Default)]
64#[serde(deny_unknown_fields, untagged)]
65pub enum AwsAuthentication {
66    /// Authenticate using a fixed access key and secret pair.
67    AccessKey {
68        /// The AWS access key ID.
69        #[configurable(metadata(docs::examples = "AKIAIOSFODNN7EXAMPLE"))]
70        access_key_id: SensitiveString,
71
72        /// The AWS secret access key.
73        #[configurable(metadata(docs::examples = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"))]
74        secret_access_key: SensitiveString,
75
76        /// The AWS session token.
77        /// See [AWS temporary credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html)
78        #[configurable(metadata(docs::examples = "AQoDYXdz...AQoDYXdz..."))]
79        session_token: Option<SensitiveString>,
80
81        /// The ARN of an [IAM role][iam_role] to assume.
82        ///
83        /// [iam_role]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html
84        #[configurable(metadata(docs::examples = "arn:aws:iam::123456789098:role/my_role"))]
85        assume_role: Option<String>,
86
87        /// The optional unique external ID in conjunction with role to assume.
88        ///
89        /// [external_id]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html
90        #[configurable(metadata(docs::examples = "randomEXAMPLEidString"))]
91        external_id: Option<String>,
92
93        /// The [AWS region][aws_region] to send STS requests to.
94        ///
95        /// If not set, this will default to the configured region
96        /// for the service itself.
97        ///
98        /// [aws_region]: https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints
99        #[configurable(metadata(docs::examples = "us-west-2"))]
100        region: Option<String>,
101
102        /// The optional [RoleSessionName][role_session_name] is a unique session identifier for your assumed role.
103        ///
104        /// Should be unique per principal or reason.
105        /// If not set, the session name is autogenerated like assume-role-provider-1736428351340
106        ///
107        /// [role_session_name]: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
108        #[configurable(metadata(docs::examples = "vector-indexer-role"))]
109        session_name: Option<String>,
110    },
111
112    /// Authenticate using credentials stored in a file.
113    ///
114    /// Additionally, the specific credential profile to use can be set.
115    /// The file format must match the credentials file format outlined in
116    /// <https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html>.
117    File {
118        /// Path to the credentials file.
119        #[configurable(metadata(docs::examples = "/my/aws/credentials"))]
120        credentials_file: String,
121
122        /// The credentials profile to use.
123        ///
124        /// Used to select AWS credentials from a provided credentials file.
125        #[configurable(metadata(docs::examples = "develop"))]
126        #[serde(default = "default_profile")]
127        profile: String,
128
129        /// The [AWS region][aws_region] to send STS requests to.
130        ///
131        /// If not set, this defaults to the configured region
132        /// for the service itself.
133        ///
134        /// [aws_region]: https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints
135        #[configurable(metadata(docs::examples = "us-west-2"))]
136        region: Option<String>,
137    },
138
139    /// Assume the given role ARN.
140    Role {
141        /// The ARN of an [IAM role][iam_role] to assume.
142        ///
143        /// [iam_role]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html
144        #[configurable(metadata(docs::examples = "arn:aws:iam::123456789098:role/my_role"))]
145        assume_role: String,
146
147        /// The optional unique external ID in conjunction with role to assume.
148        ///
149        /// [external_id]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html
150        #[configurable(metadata(docs::examples = "randomEXAMPLEidString"))]
151        external_id: Option<String>,
152
153        /// Timeout for assuming the role, in seconds.
154        ///
155        /// Relevant when the default credentials chain or `assume_role` is used.
156        #[configurable(metadata(docs::type_unit = "seconds"))]
157        #[configurable(metadata(docs::examples = 30))]
158        #[configurable(metadata(docs::human_name = "Load Timeout"))]
159        load_timeout_secs: Option<u64>,
160
161        /// Configuration for authenticating with AWS through IMDS.
162        #[serde(default)]
163        imds: ImdsAuthentication,
164
165        /// The [AWS region][aws_region] to send STS requests to.
166        ///
167        /// If not set, this defaults to the configured region
168        /// for the service itself.
169        ///
170        /// [aws_region]: https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints
171        #[configurable(metadata(docs::examples = "us-west-2"))]
172        region: Option<String>,
173
174        /// The optional [RoleSessionName][role_session_name] is a unique session identifier for your assumed role.
175        ///
176        /// Should be unique per principal or reason.
177        /// If not set, the session name is autogenerated like assume-role-provider-1736428351340
178        ///
179        /// [role_session_name]: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
180        #[configurable(metadata(docs::examples = "vector-indexer-role"))]
181        session_name: Option<String>,
182    },
183
184    /// Default authentication strategy which tries a variety of substrategies in sequential order.
185    #[derivative(Default)]
186    Default {
187        /// Timeout for successfully loading any credentials, in seconds.
188        ///
189        /// Relevant when the default credentials chain or `assume_role` is used.
190        #[configurable(metadata(docs::type_unit = "seconds"))]
191        #[configurable(metadata(docs::examples = 30))]
192        #[configurable(metadata(docs::human_name = "Load Timeout"))]
193        load_timeout_secs: Option<u64>,
194
195        /// Configuration for authenticating with AWS through IMDS.
196        #[serde(default)]
197        imds: ImdsAuthentication,
198
199        /// The [AWS region][aws_region] to send STS requests to.
200        ///
201        /// If not set, this defaults to the configured region
202        /// for the service itself.
203        ///
204        /// [aws_region]: https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints
205        #[configurable(metadata(docs::examples = "us-west-2"))]
206        region: Option<String>,
207    },
208}
209
210fn default_profile() -> String {
211    DEFAULT_PROFILE_NAME.to_string()
212}
213
214impl AwsAuthentication {
215    /// Creates the identity cache to store credentials based on the authentication mechanism chosen.
216    pub(super) async fn credentials_cache(&self) -> crate::Result<SharedIdentityCache> {
217        match self {
218            AwsAuthentication::Role {
219                load_timeout_secs, ..
220            }
221            | AwsAuthentication::Default {
222                load_timeout_secs, ..
223            } => {
224                let credentials_cache = IdentityCache::lazy()
225                    .load_timeout(
226                        load_timeout_secs
227                            .map(Duration::from_secs)
228                            .unwrap_or(DEFAULT_LOAD_TIMEOUT),
229                    )
230                    .build();
231
232                Ok(credentials_cache)
233            }
234            _ => Ok(IdentityCache::lazy().build()),
235        }
236    }
237
238    /// Create the AssumeRoleProviderBuilder, ensuring we create the HTTP client with
239    /// the correct proxy and TLS options.
240    fn assume_role_provider_builder(
241        proxy: &ProxyConfig,
242        tls_options: Option<&TlsConfig>,
243        region: &Region,
244        assume_role: &str,
245        external_id: Option<&str>,
246        session_name: Option<&str>,
247    ) -> crate::Result<AssumeRoleProviderBuilder> {
248        let connector = super::connector(proxy, tls_options)?;
249        let config = SdkConfig::builder()
250            .http_client(connector)
251            .region(region.clone())
252            .time_source(SystemTimeSource::new())
253            .build();
254
255        let mut builder = AssumeRoleProviderBuilder::new(assume_role)
256            .region(region.clone())
257            .configure(&config);
258
259        if let Some(external_id) = external_id {
260            builder = builder.external_id(external_id)
261        }
262
263        if let Some(session_name) = session_name {
264            builder = builder.session_name(session_name)
265        }
266
267        Ok(builder)
268    }
269
270    /// Returns the provider for the credentials based on the authentication mechanism chosen.
271    pub async fn credentials_provider(
272        &self,
273        service_region: Region,
274        proxy: &ProxyConfig,
275        tls_options: Option<&TlsConfig>,
276    ) -> crate::Result<SharedCredentialsProvider> {
277        match self {
278            Self::AccessKey {
279                access_key_id,
280                secret_access_key,
281                assume_role,
282                external_id,
283                region,
284                session_name,
285                session_token,
286            } => {
287                let provider = SharedCredentialsProvider::new(Credentials::from_keys(
288                    access_key_id.inner(),
289                    secret_access_key.inner(),
290                    session_token.clone().map(|v| v.inner().into()),
291                ));
292                if let Some(assume_role) = assume_role {
293                    let auth_region = region.clone().map(Region::new).unwrap_or(service_region);
294                    let builder = Self::assume_role_provider_builder(
295                        proxy,
296                        tls_options,
297                        &auth_region,
298                        assume_role,
299                        external_id.as_deref(),
300                        session_name.as_deref(),
301                    )?;
302
303                    let provider = builder.build_from_provider(provider).await;
304
305                    return Ok(SharedCredentialsProvider::new(provider));
306                }
307                Ok(provider)
308            }
309            AwsAuthentication::File {
310                credentials_file,
311                profile,
312                region,
313            } => {
314                let connector = super::connector(proxy, tls_options)?;
315
316                // The SDK uses the default profile out of the box, but doesn't provide an optional
317                // type in the builder. We can just hardcode it so that everything works.
318                let profile_files = EnvConfigFiles::builder()
319                    .with_file(EnvConfigFileKind::Credentials, credentials_file)
320                    .build();
321
322                let auth_region = region.clone().map(Region::new).unwrap_or(service_region);
323                let provider_config = ProviderConfig::empty()
324                    .with_region(Option::from(auth_region))
325                    .with_http_client(connector);
326
327                let profile_provider = ProfileFileCredentialsProvider::builder()
328                    .profile_files(profile_files)
329                    .profile_name(profile)
330                    .configure(&provider_config)
331                    .build();
332                Ok(SharedCredentialsProvider::new(profile_provider))
333            }
334            AwsAuthentication::Role {
335                assume_role,
336                external_id,
337                imds,
338                region,
339                session_name,
340                ..
341            } => {
342                let auth_region = region.clone().map(Region::new).unwrap_or(service_region);
343                let builder = Self::assume_role_provider_builder(
344                    proxy,
345                    tls_options,
346                    &auth_region,
347                    assume_role,
348                    external_id.as_deref(),
349                    session_name.as_deref(),
350                )?;
351
352                let provider = builder
353                    .build_from_provider(
354                        default_credentials_provider(auth_region, proxy, tls_options, *imds)
355                            .await?,
356                    )
357                    .await;
358
359                Ok(SharedCredentialsProvider::new(provider))
360            }
361            AwsAuthentication::Default { imds, region, .. } => Ok(SharedCredentialsProvider::new(
362                default_credentials_provider(
363                    region.clone().map(Region::new).unwrap_or(service_region),
364                    proxy,
365                    tls_options,
366                    *imds,
367                )
368                .await?,
369            )),
370        }
371    }
372
373    #[cfg(test)]
374    /// Creates dummy authentication for tests.
375    pub fn test_auth() -> AwsAuthentication {
376        AwsAuthentication::AccessKey {
377            access_key_id: "dummy".to_string().into(),
378            secret_access_key: "dummy".to_string().into(),
379            assume_role: None,
380            external_id: None,
381            region: None,
382            session_name: None,
383            session_token: None,
384        }
385    }
386}
387
388async fn default_credentials_provider(
389    region: Region,
390    proxy: &ProxyConfig,
391    tls_options: Option<&TlsConfig>,
392    imds: ImdsAuthentication,
393) -> crate::Result<SharedCredentialsProvider> {
394    let connector = super::connector(proxy, tls_options)?;
395
396    let provider_config = ProviderConfig::empty()
397        .with_region(Some(region.clone()))
398        .with_http_client(connector);
399
400    let client = imds::Client::builder()
401        .max_attempts(imds.max_attempts)
402        .connect_timeout(imds.connect_timeout)
403        .read_timeout(imds.read_timeout)
404        .configure(&provider_config)
405        .build();
406
407    let credentials_provider = DefaultCredentialsChain::builder()
408        .region(region)
409        .imds_client(client)
410        .configure(provider_config)
411        .build()
412        .await;
413
414    Ok(SharedCredentialsProvider::new(credentials_provider))
415}
416
417#[cfg(test)]
418mod tests {
419    use indoc::indoc;
420    use serde::{Deserialize, Serialize};
421
422    use super::*;
423
424    const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
425    const READ_TIMEOUT: Duration = Duration::from_secs(10);
426
427    #[derive(Serialize, Deserialize, Clone, Debug)]
428    struct ComponentConfig {
429        assume_role: Option<String>,
430        external_id: Option<String>,
431        #[serde(default)]
432        auth: AwsAuthentication,
433    }
434
435    #[test]
436    fn parsing_default() {
437        let config = serde_yaml::from_str::<ComponentConfig>("").unwrap();
438
439        assert!(matches!(config.auth, AwsAuthentication::Default { .. }));
440    }
441
442    #[test]
443    fn parsing_default_with_load_timeout() {
444        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {"
445            auth:
446              load_timeout_secs: 10
447        "})
448        .unwrap();
449
450        assert!(matches!(
451            config.auth,
452            AwsAuthentication::Default {
453                load_timeout_secs: Some(10),
454                imds: ImdsAuthentication { .. },
455                region: None,
456            }
457        ));
458    }
459
460    #[test]
461    fn parsing_default_with_region() {
462        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
463            auth:
464              region: "us-east-2"
465        "#})
466        .unwrap();
467
468        match config.auth {
469            AwsAuthentication::Default { region, .. } => {
470                assert_eq!(region.unwrap(), "us-east-2");
471            }
472            _ => panic!(),
473        }
474    }
475
476    #[test]
477    fn parsing_default_with_imds_client() {
478        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {"
479            auth:
480              imds:
481                max_attempts: 5
482                connect_timeout_seconds: 30
483                read_timeout_seconds: 10
484        "})
485        .unwrap();
486
487        assert!(matches!(
488            config.auth,
489            AwsAuthentication::Default {
490                load_timeout_secs: None,
491                region: None,
492                imds: ImdsAuthentication {
493                    max_attempts: 5,
494                    connect_timeout: CONNECT_TIMEOUT,
495                    read_timeout: READ_TIMEOUT,
496                },
497            }
498        ));
499    }
500
501    #[test]
502    fn parsing_old_assume_role() {
503        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
504            assume_role: "root"
505        "#})
506        .unwrap();
507
508        assert!(matches!(config.auth, AwsAuthentication::Default { .. }));
509    }
510
511    #[test]
512    fn parsing_assume_role() {
513        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
514            auth:
515              assume_role: "root"
516              load_timeout_secs: 10
517        "#})
518        .unwrap();
519
520        assert!(matches!(config.auth, AwsAuthentication::Role { .. }));
521    }
522
523    #[test]
524    fn parsing_external_id_with_assume_role() {
525        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
526            auth:
527              assume_role: "root"
528              external_id: "id"
529              load_timeout_secs: 10
530        "#})
531        .unwrap();
532
533        assert!(matches!(config.auth, AwsAuthentication::Role { .. }));
534    }
535
536    #[test]
537    fn parsing_session_name_with_assume_role() {
538        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
539            auth:
540              assume_role: "root"
541              session_name: "session_name"
542              load_timeout_secs: 10
543        "#})
544        .unwrap();
545
546        match config.auth {
547            AwsAuthentication::Role { session_name, .. } => {
548                assert_eq!(session_name.unwrap(), "session_name");
549            }
550            _ => panic!(),
551        }
552    }
553
554    #[test]
555    fn parsing_assume_role_with_imds_client() {
556        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
557            auth:
558              assume_role: "root"
559              imds:
560                max_attempts: 5
561                connect_timeout_seconds: 30
562                read_timeout_seconds: 10
563        "#})
564        .unwrap();
565
566        match config.auth {
567            AwsAuthentication::Role {
568                assume_role,
569                external_id,
570                load_timeout_secs,
571                imds,
572                region,
573                session_name,
574            } => {
575                assert_eq!(&assume_role, "root");
576                assert_eq!(external_id, None);
577                assert_eq!(load_timeout_secs, None);
578                assert_eq!(session_name, None);
579                assert!(matches!(
580                    imds,
581                    ImdsAuthentication {
582                        max_attempts: 5,
583                        connect_timeout: CONNECT_TIMEOUT,
584                        read_timeout: READ_TIMEOUT,
585                    }
586                ));
587                assert_eq!(region, None);
588            }
589            _ => panic!(),
590        }
591    }
592
593    #[test]
594    fn parsing_both_assume_role() {
595        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
596            assume_role: "root"
597            auth:
598              assume_role: "auth.root"
599              load_timeout_secs: 10
600              region: "us-west-2"
601        "#})
602        .unwrap();
603
604        match config.auth {
605            AwsAuthentication::Role {
606                assume_role,
607                external_id,
608                load_timeout_secs,
609                imds,
610                region,
611                session_name,
612            } => {
613                assert_eq!(&assume_role, "auth.root");
614                assert_eq!(external_id, None);
615                assert_eq!(load_timeout_secs, Some(10));
616                assert_eq!(session_name, None);
617                assert!(matches!(imds, ImdsAuthentication { .. }));
618                assert_eq!(region.unwrap(), "us-west-2");
619            }
620            _ => panic!(),
621        }
622    }
623
624    #[test]
625    fn parsing_static() {
626        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
627            auth:
628              access_key_id: "key"
629              secret_access_key: "other"
630        "#})
631        .unwrap();
632
633        assert!(matches!(config.auth, AwsAuthentication::AccessKey { .. }));
634    }
635
636    #[test]
637    fn parsing_static_with_assume_role() {
638        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
639            auth:
640              access_key_id: "key"
641              secret_access_key: "other"
642              assume_role: "root"
643        "#})
644        .unwrap();
645
646        match config.auth {
647            AwsAuthentication::AccessKey {
648                access_key_id,
649                secret_access_key,
650                assume_role,
651                ..
652            } => {
653                assert_eq!(&access_key_id, &SensitiveString::from("key".to_string()));
654                assert_eq!(
655                    &secret_access_key,
656                    &SensitiveString::from("other".to_string())
657                );
658                assert_eq!(&assume_role, &Some("root".to_string()));
659            }
660            _ => panic!(),
661        }
662    }
663
664    #[test]
665    fn parsing_static_with_assume_role_and_external_id() {
666        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
667            auth:
668              access_key_id: "key"
669              secret_access_key: "other"
670              assume_role: "root"
671              external_id: "id"
672        "#})
673        .unwrap();
674
675        match config.auth {
676            AwsAuthentication::AccessKey {
677                access_key_id,
678                secret_access_key,
679                assume_role,
680                external_id,
681                ..
682            } => {
683                assert_eq!(&access_key_id, &SensitiveString::from("key".to_string()));
684                assert_eq!(
685                    &secret_access_key,
686                    &SensitiveString::from("other".to_string())
687                );
688                assert_eq!(&assume_role, &Some("root".to_string()));
689                assert_eq!(&external_id, &Some("id".to_string()));
690            }
691            _ => panic!(),
692        }
693    }
694
695    #[test]
696    fn parsing_file() {
697        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
698            auth:
699              credentials_file: "/path/to/file"
700              profile: "foo"
701              region: "us-west-2"
702        "#})
703        .unwrap();
704
705        match config.auth {
706            AwsAuthentication::File {
707                credentials_file,
708                profile,
709                region,
710            } => {
711                assert_eq!(&credentials_file, "/path/to/file");
712                assert_eq!(&profile, "foo");
713                assert_eq!(region.unwrap(), "us-west-2");
714            }
715            _ => panic!(),
716        }
717
718        let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
719            auth:
720              credentials_file: "/path/to/file"
721        "#})
722        .unwrap();
723
724        match config.auth {
725            AwsAuthentication::File {
726                credentials_file,
727                profile,
728                ..
729            } => {
730                assert_eq!(&credentials_file, "/path/to/file");
731                assert_eq!(profile, "default".to_string());
732            }
733            _ => panic!(),
734        }
735    }
736}