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 serde::{Deserialize, Serialize};
420
421    use super::*;
422
423    const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
424    const READ_TIMEOUT: Duration = Duration::from_secs(10);
425
426    #[derive(Serialize, Deserialize, Clone, Debug)]
427    struct ComponentConfig {
428        assume_role: Option<String>,
429        external_id: Option<String>,
430        #[serde(default)]
431        auth: AwsAuthentication,
432    }
433
434    #[test]
435    fn parsing_default() {
436        let config = toml::from_str::<ComponentConfig>("").unwrap();
437
438        assert!(matches!(config.auth, AwsAuthentication::Default { .. }));
439    }
440
441    #[test]
442    fn parsing_default_with_load_timeout() {
443        let config = toml::from_str::<ComponentConfig>(
444            "
445            auth.load_timeout_secs = 10
446        ",
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 = toml::from_str::<ComponentConfig>(
463            r#"
464            auth.region = "us-east-2"
465        "#,
466        )
467        .unwrap();
468
469        match config.auth {
470            AwsAuthentication::Default { region, .. } => {
471                assert_eq!(region.unwrap(), "us-east-2");
472            }
473            _ => panic!(),
474        }
475    }
476
477    #[test]
478    fn parsing_default_with_imds_client() {
479        let config = toml::from_str::<ComponentConfig>(
480            "
481            auth.imds.max_attempts = 5
482            auth.imds.connect_timeout_seconds = 30
483            auth.imds.read_timeout_seconds = 10
484        ",
485        )
486        .unwrap();
487
488        assert!(matches!(
489            config.auth,
490            AwsAuthentication::Default {
491                load_timeout_secs: None,
492                region: None,
493                imds: ImdsAuthentication {
494                    max_attempts: 5,
495                    connect_timeout: CONNECT_TIMEOUT,
496                    read_timeout: READ_TIMEOUT,
497                },
498            }
499        ));
500    }
501
502    #[test]
503    fn parsing_old_assume_role() {
504        let config = toml::from_str::<ComponentConfig>(
505            r#"
506            assume_role = "root"
507        "#,
508        )
509        .unwrap();
510
511        assert!(matches!(config.auth, AwsAuthentication::Default { .. }));
512    }
513
514    #[test]
515    fn parsing_assume_role() {
516        let config = toml::from_str::<ComponentConfig>(
517            r#"
518            auth.assume_role = "root"
519            auth.load_timeout_secs = 10
520        "#,
521        )
522        .unwrap();
523
524        assert!(matches!(config.auth, AwsAuthentication::Role { .. }));
525    }
526
527    #[test]
528    fn parsing_external_id_with_assume_role() {
529        let config = toml::from_str::<ComponentConfig>(
530            r#"
531            auth.assume_role = "root"
532            auth.external_id = "id"
533            auth.load_timeout_secs = 10
534        "#,
535        )
536        .unwrap();
537
538        assert!(matches!(config.auth, AwsAuthentication::Role { .. }));
539    }
540
541    #[test]
542    fn parsing_session_name_with_assume_role() {
543        let config = toml::from_str::<ComponentConfig>(
544            r#"
545            auth.assume_role = "root"
546            auth.session_name = "session_name"
547            auth.load_timeout_secs = 10
548        "#,
549        )
550        .unwrap();
551
552        match config.auth {
553            AwsAuthentication::Role { session_name, .. } => {
554                assert_eq!(session_name.unwrap(), "session_name");
555            }
556            _ => panic!(),
557        }
558    }
559
560    #[test]
561    fn parsing_assume_role_with_imds_client() {
562        let config = toml::from_str::<ComponentConfig>(
563            r#"
564            auth.assume_role = "root"
565            auth.imds.max_attempts = 5
566            auth.imds.connect_timeout_seconds = 30
567            auth.imds.read_timeout_seconds = 10
568        "#,
569        )
570        .unwrap();
571
572        match config.auth {
573            AwsAuthentication::Role {
574                assume_role,
575                external_id,
576                load_timeout_secs,
577                imds,
578                region,
579                session_name,
580            } => {
581                assert_eq!(&assume_role, "root");
582                assert_eq!(external_id, None);
583                assert_eq!(load_timeout_secs, None);
584                assert_eq!(session_name, None);
585                assert!(matches!(
586                    imds,
587                    ImdsAuthentication {
588                        max_attempts: 5,
589                        connect_timeout: CONNECT_TIMEOUT,
590                        read_timeout: READ_TIMEOUT,
591                    }
592                ));
593                assert_eq!(region, None);
594            }
595            _ => panic!(),
596        }
597    }
598
599    #[test]
600    fn parsing_both_assume_role() {
601        let config = toml::from_str::<ComponentConfig>(
602            r#"
603            assume_role = "root"
604            auth.assume_role = "auth.root"
605            auth.load_timeout_secs = 10
606            auth.region = "us-west-2"
607        "#,
608        )
609        .unwrap();
610
611        match config.auth {
612            AwsAuthentication::Role {
613                assume_role,
614                external_id,
615                load_timeout_secs,
616                imds,
617                region,
618                session_name,
619            } => {
620                assert_eq!(&assume_role, "auth.root");
621                assert_eq!(external_id, None);
622                assert_eq!(load_timeout_secs, Some(10));
623                assert_eq!(session_name, None);
624                assert!(matches!(imds, ImdsAuthentication { .. }));
625                assert_eq!(region.unwrap(), "us-west-2");
626            }
627            _ => panic!(),
628        }
629    }
630
631    #[test]
632    fn parsing_static() {
633        let config = toml::from_str::<ComponentConfig>(
634            r#"
635            auth.access_key_id = "key"
636            auth.secret_access_key = "other"
637        "#,
638        )
639        .unwrap();
640
641        assert!(matches!(config.auth, AwsAuthentication::AccessKey { .. }));
642    }
643
644    #[test]
645    fn parsing_static_with_assume_role() {
646        let config = toml::from_str::<ComponentConfig>(
647            r#"
648            auth.access_key_id = "key"
649            auth.secret_access_key = "other"
650            auth.assume_role = "root"
651        "#,
652        )
653        .unwrap();
654
655        match config.auth {
656            AwsAuthentication::AccessKey {
657                access_key_id,
658                secret_access_key,
659                assume_role,
660                ..
661            } => {
662                assert_eq!(&access_key_id, &SensitiveString::from("key".to_string()));
663                assert_eq!(
664                    &secret_access_key,
665                    &SensitiveString::from("other".to_string())
666                );
667                assert_eq!(&assume_role, &Some("root".to_string()));
668            }
669            _ => panic!(),
670        }
671    }
672
673    #[test]
674    fn parsing_static_with_assume_role_and_external_id() {
675        let config = toml::from_str::<ComponentConfig>(
676            r#"
677            auth.access_key_id = "key"
678            auth.secret_access_key = "other"
679            auth.assume_role = "root"
680            auth.external_id = "id"
681        "#,
682        )
683        .unwrap();
684
685        match config.auth {
686            AwsAuthentication::AccessKey {
687                access_key_id,
688                secret_access_key,
689                assume_role,
690                external_id,
691                ..
692            } => {
693                assert_eq!(&access_key_id, &SensitiveString::from("key".to_string()));
694                assert_eq!(
695                    &secret_access_key,
696                    &SensitiveString::from("other".to_string())
697                );
698                assert_eq!(&assume_role, &Some("root".to_string()));
699                assert_eq!(&external_id, &Some("id".to_string()));
700            }
701            _ => panic!(),
702        }
703    }
704
705    #[test]
706    fn parsing_file() {
707        let config = toml::from_str::<ComponentConfig>(
708            r#"
709            auth.credentials_file = "/path/to/file"
710            auth.profile = "foo"
711            auth.region = "us-west-2"
712        "#,
713        )
714        .unwrap();
715
716        match config.auth {
717            AwsAuthentication::File {
718                credentials_file,
719                profile,
720                region,
721            } => {
722                assert_eq!(&credentials_file, "/path/to/file");
723                assert_eq!(&profile, "foo");
724                assert_eq!(region.unwrap(), "us-west-2");
725            }
726            _ => panic!(),
727        }
728
729        let config = toml::from_str::<ComponentConfig>(
730            r#"
731            auth.credentials_file = "/path/to/file"
732        "#,
733        )
734        .unwrap();
735
736        match config.auth {
737            AwsAuthentication::File {
738                credentials_file,
739                profile,
740                ..
741            } => {
742                assert_eq!(&credentials_file, "/path/to/file");
743                assert_eq!(profile, "default".to_string());
744            }
745            _ => panic!(),
746        }
747    }
748}