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