From ef85116f772e3ae948551255ca9769140d3ec124 Mon Sep 17 00:00:00 2001
From: John DiSanti <jdisanti@amazon.com>
Date: Tue, 27 Sep 2022 09:33:16 -0700
Subject: [PATCH] Add ability to programmatically customize profile files
 (#1770)

---
 CHANGELOG.next.toml                           |  28 ++
 .../aws-config/src/imds/client.rs             |   6 +-
 .../aws-config/src/profile/app_name.rs        |   9 +-
 .../aws-config/src/profile/credentials.rs     |  79 +++--
 .../src/profile/location_of_profile_files.md  |  19 ++
 .../aws-config/src/profile/mod.rs             |   1 +
 .../aws-config/src/profile/parser.rs          |  94 +++---
 .../src/profile/parser/normalize.rs           |  26 +-
 .../aws-config/src/profile/parser/parse.rs    |   6 +-
 .../aws-config/src/profile/parser/source.rs   | 308 +++++++++++++-----
 .../aws-config/src/profile/profile_file.rs    | 257 +++++++++++++++
 .../aws-config/src/profile/region.rs          |  17 +-
 .../aws-config/src/profile/retry_config.rs    |  15 +-
 13 files changed, 679 insertions(+), 186 deletions(-)
 create mode 100644 aws/rust-runtime/aws-config/src/profile/location_of_profile_files.md
 create mode 100644 aws/rust-runtime/aws-config/src/profile/profile_file.rs

diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml
index df706f8fa8..94b888a4d4 100644
--- a/CHANGELOG.next.toml
+++ b/CHANGELOG.next.toml
@@ -53,3 +53,31 @@ of breaking changes and how to resolve them.
 references = ["smithy-rs#1740", "smithy-rs#256"]
 meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
 author = "jdisanti"
+
+[[aws-sdk-rust]]
+message = """
+It is now possible to programmatically customize the locations of the profile config/credentials files in `aws-config`:
+```rust
+use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider};
+use aws_config::profile::profile_file::{ProfileFiles, ProfileFileKind};
+
+let profile_files = ProfileFiles::builder()
+    .with_file(ProfileFileKind::Credentials, "some/path/to/credentials-file")
+    .build();
+let credentials_provider = ProfileFileCredentialsProvider::builder()
+    .profile_files(profile_files.clone())
+    .build();
+let region_provider = ProfileFileRegionProvider::builder()
+    .profile_files(profile_files)
+    .build();
+
+let sdk_config = aws_config::from_env()
+    .credentials_provider(credentials_provider)
+    .region(region_provider)
+    .load()
+    .await;
+```
+"""
+references = ["aws-sdk-rust#237", "smithy-rs#1770"]
+meta = { "breaking" = false, "tada" = true, "bug" = false }
+author = "jdisanti"
diff --git a/aws/rust-runtime/aws-config/src/imds/client.rs b/aws/rust-runtime/aws-config/src/imds/client.rs
index a1e39def07..10e6d419ab 100644
--- a/aws/rust-runtime/aws-config/src/imds/client.rs
+++ b/aws/rust-runtime/aws-config/src/imds/client.rs
@@ -37,7 +37,7 @@ use tokio::sync::OnceCell;
 
 use crate::connector::expect_connector;
 use crate::imds::client::token::TokenMiddleware;
-use crate::profile::ProfileParseError;
+use crate::profile::credentials::ProfileFileError;
 use crate::provider_config::ProviderConfig;
 use crate::{profile, PKG_VERSION};
 use aws_sdk_sso::config::timeout::TimeoutConfig;
@@ -439,7 +439,7 @@ pub enum BuildError {
     InvalidEndpointMode(InvalidEndpointMode),
 
     /// The AWS Profile (e.g. `~/.aws/config`) was invalid
-    InvalidProfile(ProfileParseError),
+    InvalidProfile(ProfileFileError),
 
     /// The specified endpoint was not a valid URI
     InvalidEndpointUri(InvalidUri),
@@ -626,7 +626,7 @@ impl EndpointSource {
             }
             EndpointSource::Env(env, fs) => {
                 // load an endpoint override from the environment
-                let profile = profile::load(fs, env)
+                let profile = profile::load(fs, env, &Default::default())
                     .await
                     .map_err(BuildError::InvalidProfile)?;
                 let uri_override = if let Ok(uri) = env.get(env::ENDPOINT) {
diff --git a/aws/rust-runtime/aws-config/src/profile/app_name.rs b/aws/rust-runtime/aws-config/src/profile/app_name.rs
index 44b35fc174..e0af917967 100644
--- a/aws/rust-runtime/aws-config/src/profile/app_name.rs
+++ b/aws/rust-runtime/aws-config/src/profile/app_name.rs
@@ -5,6 +5,7 @@
 
 //! Load an app name from an AWS profile
 
+use super::profile_file::ProfileFiles;
 use crate::provider_config::ProviderConfig;
 use aws_types::app_name::AppName;
 use aws_types::os_shim_internal::{Env, Fs};
@@ -14,6 +15,8 @@ use aws_types::os_shim_internal::{Env, Fs};
 /// This provider will attempt to shared AWS shared configuration and then read the
 /// `sdk-ua-app-id` property from the active profile.
 ///
+#[doc = include_str!("location_of_profile_files.md")]
+///
 /// # Examples
 ///
 /// **Loads "my-app" as the app name**
@@ -35,6 +38,7 @@ pub struct ProfileFileAppNameProvider {
     fs: Fs,
     env: Env,
     profile_override: Option<String>,
+    profile_files: ProfileFiles,
 }
 
 impl ProfileFileAppNameProvider {
@@ -46,6 +50,7 @@ impl ProfileFileAppNameProvider {
             fs: Fs::real(),
             env: Env::real(),
             profile_override: None,
+            profile_files: Default::default(),
         }
     }
 
@@ -56,7 +61,7 @@ impl ProfileFileAppNameProvider {
 
     /// Parses the profile config and attempts to find an app name.
     pub async fn app_name(&self) -> Option<AppName> {
-        let profile = super::parser::load(&self.fs, &self.env)
+        let profile = super::parser::load(&self.fs, &self.env, &self.profile_files)
             .await
             .map_err(|err| tracing::warn!(err = %err, "failed to parse profile"))
             .ok()?;
@@ -82,6 +87,7 @@ impl ProfileFileAppNameProvider {
 pub struct Builder {
     config: Option<ProviderConfig>,
     profile_override: Option<String>,
+    profile_files: Option<ProfileFiles>,
 }
 
 impl Builder {
@@ -104,6 +110,7 @@ impl Builder {
             env: conf.env(),
             fs: conf.fs(),
             profile_override: self.profile_override,
+            profile_files: self.profile_files.unwrap_or_default(),
         }
     }
 }
diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs
index 7889bb6e1f..1a025339cf 100644
--- a/aws/rust-runtime/aws-config/src/profile/credentials.rs
+++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs
@@ -22,22 +22,21 @@
 //! - `exec` which contains a chain representation of providers to implement passing bootstrapped credentials
 //! through a series of providers.
 
+use crate::profile::credentials::exec::named::NamedProviderFactory;
+use crate::profile::credentials::exec::{ClientConfiguration, ProviderChain};
+use crate::profile::parser::ProfileParseError;
+use crate::profile::profile_file::ProfileFiles;
+use crate::profile::Profile;
+use crate::provider_config::ProviderConfig;
+use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials};
 use std::borrow::Cow;
 use std::collections::HashMap;
 use std::error::Error;
 use std::fmt::{Display, Formatter};
+use std::path::PathBuf;
 use std::sync::Arc;
-
-use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials};
-
 use tracing::Instrument;
 
-use crate::profile::credentials::exec::named::NamedProviderFactory;
-use crate::profile::credentials::exec::{ClientConfiguration, ProviderChain};
-use crate::profile::parser::ProfileParseError;
-use crate::profile::Profile;
-use crate::provider_config::ProviderConfig;
-
 mod exec;
 mod repr;
 
@@ -142,29 +141,14 @@ impl ProvideCredentials for ProfileFileCredentialsProvider {
 ///
 /// SSO can also be used as a source profile for assume role chains.
 ///
-/// ## Location of Profile Files
-/// * The location of the config file will be loaded from the `AWS_CONFIG_FILE` environment variable
-/// with a fallback to `~/.aws/config`
-/// * The location of the credentials file will be loaded from the `AWS_SHARED_CREDENTIALS_FILE`
-/// environment variable with a fallback to `~/.aws/credentials`
-///
-/// ## Home directory resolution
-/// Home directory resolution is implemented to match the behavior of the CLI & Python. `~` is only
-/// used for home directory resolution when it:
-/// - Starts the path
-/// - Is followed immediately by `/` or a platform specific separator. (On windows, `~/` and `~\` both
-///   resolve to the home directory.
-///
-/// When determining the home directory, the following environment variables are checked:
-/// - `HOME` on all platforms
-/// - `USERPROFILE` on Windows
-/// - The concatenation of `HOMEDRIVE` and `HOMEPATH` on Windows (`$HOMEDRIVE$HOMEPATH`)
+#[doc = include_str!("location_of_profile_files.md")]
 #[derive(Debug)]
 pub struct ProfileFileCredentialsProvider {
     factory: NamedProviderFactory,
     client_config: ClientConfiguration,
     provider_config: ProviderConfig,
     profile_override: Option<String>,
+    profile_files: ProfileFiles,
 }
 
 impl ProfileFileCredentialsProvider {
@@ -178,6 +162,7 @@ impl ProfileFileCredentialsProvider {
             &self.provider_config,
             &self.factory,
             self.profile_override.as_deref(),
+            &self.profile_files,
         )
         .await
         .map_err(|err| match err {
@@ -225,6 +210,13 @@ impl ProfileFileCredentialsProvider {
     }
 }
 
+#[doc(hidden)]
+#[derive(Debug)]
+pub struct CouldNotReadProfileFile {
+    pub(crate) path: PathBuf,
+    pub(crate) cause: std::io::Error,
+}
+
 /// An Error building a Credential source from an AWS Profile
 #[derive(Debug)]
 #[non_exhaustive]
@@ -283,6 +275,10 @@ pub enum ProfileFileError {
         /// The name of the provider
         name: String,
     },
+
+    /// A custom profile file location didn't exist or could not be read
+    #[non_exhaustive]
+    CouldNotReadProfileFile(CouldNotReadProfileFile),
 }
 
 impl ProfileFileError {
@@ -326,6 +322,13 @@ impl Display for ProfileFileError {
                 "profile `{}` did not contain credential information",
                 profile
             ),
+            ProfileFileError::CouldNotReadProfileFile(details) => {
+                write!(
+                    f,
+                    "Failed to read custom profile file at {:?}",
+                    details.path
+                )
+            }
         }
     }
 }
@@ -334,16 +337,24 @@ impl Error for ProfileFileError {
     fn source(&self) -> Option<&(dyn Error + 'static)> {
         match self {
             ProfileFileError::CouldNotParseProfile(err) => Some(err),
+            ProfileFileError::CouldNotReadProfileFile(details) => Some(&details.cause),
             _ => None,
         }
     }
 }
 
+impl From<ProfileParseError> for ProfileFileError {
+    fn from(err: ProfileParseError) -> Self {
+        ProfileFileError::CouldNotParseProfile(err)
+    }
+}
+
 /// Builder for [`ProfileFileCredentialsProvider`]
 #[derive(Debug, Default)]
 pub struct Builder {
     provider_config: Option<ProviderConfig>,
     profile_override: Option<String>,
+    profile_files: Option<ProfileFiles>,
     custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
 }
 
@@ -409,6 +420,12 @@ impl Builder {
         self
     }
 
+    /// Set the profile file that should be used by the [`ProfileFileCredentialsProvider`]
+    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
+        self.profile_files = Some(profile_files);
+        self
+    }
+
     /// Builds a [`ProfileFileCredentialsProvider`]
     pub fn build(self) -> ProfileFileCredentialsProvider {
         let build_span = tracing::debug_span!("build_profile_provider");
@@ -453,6 +470,7 @@ impl Builder {
             },
             provider_config: conf,
             profile_override: self.profile_override,
+            profile_files: self.profile_files.unwrap_or_default(),
         }
     }
 }
@@ -461,13 +479,10 @@ async fn build_provider_chain(
     provider_config: &ProviderConfig,
     factory: &NamedProviderFactory,
     profile_override: Option<&str>,
+    profile_files: &ProfileFiles,
 ) -> Result<ProviderChain, ProfileFileError> {
-    let profile_set = super::parser::load(&provider_config.fs(), &provider_config.env())
-        .await
-        .map_err(|err| {
-            tracing::warn!(err = %err, "failed to parse profile");
-            ProfileFileError::CouldNotParseProfile(err)
-        })?;
+    let profile_set =
+        super::parser::load(&provider_config.fs(), &provider_config.env(), profile_files).await?;
     let repr = repr::resolve_chain(&profile_set, profile_override)?;
     tracing::info!(chain = ?repr, "constructed abstract provider from config file");
     exec::ProviderChain::from_repr(provider_config, repr, factory)
diff --git a/aws/rust-runtime/aws-config/src/profile/location_of_profile_files.md b/aws/rust-runtime/aws-config/src/profile/location_of_profile_files.md
new file mode 100644
index 0000000000..d712da7ff5
--- /dev/null
+++ b/aws/rust-runtime/aws-config/src/profile/location_of_profile_files.md
@@ -0,0 +1,19 @@
+## Location of Profile Files
+* The location of the config file will be loaded from the `AWS_CONFIG_FILE` environment variable
+with a fallback to `~/.aws/config`
+* The location of the credentials file will be loaded from the `AWS_SHARED_CREDENTIALS_FILE`
+environment variable with a fallback to `~/.aws/credentials`
+
+The location of these files can also be customized programmatically using [`ProfileFiles`](crate::profile::profile_file::ProfileFiles).
+
+## Home directory resolution
+Home directory resolution is implemented to match the behavior of the CLI & Python. `~` is only
+used for home directory resolution when it:
+- Starts the path
+- Is followed immediately by `/` or a platform specific separator. (On windows, `~/` and `~\` both
+  resolve to the home directory.
+
+When determining the home directory, the following environment variables are checked:
+- `HOME` on all platforms
+- `USERPROFILE` on Windows
+- The concatenation of `HOMEDRIVE` and `HOMEPATH` on Windows (`$HOMEDRIVE$HOMEPATH`)
diff --git a/aws/rust-runtime/aws-config/src/profile/mod.rs b/aws/rust-runtime/aws-config/src/profile/mod.rs
index 62071013ba..f1a81cad51 100644
--- a/aws/rust-runtime/aws-config/src/profile/mod.rs
+++ b/aws/rust-runtime/aws-config/src/profile/mod.rs
@@ -20,6 +20,7 @@ pub use parser::{load, Profile, ProfileSet, Property};
 
 pub mod app_name;
 pub mod credentials;
+pub mod profile_file;
 pub mod region;
 pub mod retry_config;
 
diff --git a/aws/rust-runtime/aws-config/src/profile/parser.rs b/aws/rust-runtime/aws-config/src/profile/parser.rs
index 3306cbb990..f80a5842a0 100644
--- a/aws/rust-runtime/aws-config/src/profile/parser.rs
+++ b/aws/rust-runtime/aws-config/src/profile/parser.rs
@@ -3,17 +3,19 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-mod normalize;
-mod parse;
-mod source;
-
 use crate::profile::parser::parse::parse_profile_file;
-use crate::profile::parser::source::{FileKind, Source};
+use crate::profile::parser::source::Source;
+use crate::profile::profile_file::ProfileFiles;
 use aws_types::os_shim_internal::{Env, Fs};
 use std::borrow::Cow;
 use std::collections::HashMap;
 
 pub use self::parse::ProfileParseError;
+use super::credentials::ProfileFileError;
+
+mod normalize;
+mod parse;
+mod source;
 
 /// Read & parse AWS config files
 ///
@@ -22,23 +24,7 @@ pub use self::parse::ProfileParseError;
 /// Although the basic behavior is straightforward, there are number of nuances to maintain backwards
 /// compatibility with other SDKs enumerated below.
 ///
-/// ## Location of Profile Files
-/// * The location of the config file will be loaded from the `AWS_CONFIG_FILE` environment variable
-/// with a fallback to `~/.aws/config`
-/// * The location of the credentials file will be loaded from the `AWS_SHARED_CREDENTIALS_FILE`
-/// environment variable with a fallback to `~/.aws/credentials`
-///
-/// ## Home directory resolution
-/// Home directory resolution is implemented to match the behavior of the CLI & Python. `~` is only
-/// used for home directory resolution when it:
-/// - Starts the path
-/// - Is followed immediately by `/` or a platform specific separator. (On windows, `~/` and `~\` both
-///   resolve to the home directory.
-///
-/// When determining the home directory, the following environment variables are checked:
-/// - `HOME` on all platforms
-/// - `USERPROFILE` on Windows
-/// - The concatenation of `HOMEDRIVE` and `HOMEPATH` on Windows (`$HOMEDRIVE$HOMEPATH`)
+#[doc = include_str!("location_of_profile_files.md")]
 ///
 /// ## Profile file syntax
 ///
@@ -66,9 +52,13 @@ pub use self::parse::ProfileParseError;
 /// [other]
 /// aws_access_key_id = 456
 /// ```
-pub async fn load(fs: &Fs, env: &Env) -> Result<ProfileSet, ProfileParseError> {
-    let source = source::load(env, fs).await;
-    ProfileSet::parse(source)
+pub async fn load(
+    fs: &Fs,
+    env: &Env,
+    profile_files: &ProfileFiles,
+) -> Result<ProfileSet, ProfileFileError> {
+    let source = source::load(env, fs, profile_files).await?;
+    Ok(ProfileSet::parse(source)?)
 }
 
 /// A top-level configuration source containing multiple named profiles
@@ -141,16 +131,9 @@ impl ProfileSet {
         let mut base = ProfileSet::empty();
         base.selected_profile = source.profile;
 
-        normalize::merge_in(
-            &mut base,
-            parse_profile_file(&source.config_file)?,
-            FileKind::Config,
-        );
-        normalize::merge_in(
-            &mut base,
-            parse_profile_file(&source.credentials_file)?,
-            FileKind::Credentials,
-        );
+        for file in source.files {
+            normalize::merge_in(&mut base, parse_profile_file(&file)?, file.kind);
+        }
         Ok(base)
     }
 
@@ -215,6 +198,7 @@ impl Property {
 #[cfg(test)]
 mod test {
     use crate::profile::parser::source::{File, Source};
+    use crate::profile::profile_file::ProfileFileKind;
     use crate::profile::ProfileSet;
     use arbitrary::{Arbitrary, Unstructured};
     use serde::Deserialize;
@@ -276,14 +260,18 @@ mod test {
             let (conf, creds): (Option<&str>, Option<&str>) =
                 Arbitrary::arbitrary(&mut unstructured)?;
             let profile_source = Source {
-                config_file: File {
-                    path: "~/.aws/config".to_string(),
-                    contents: conf.unwrap_or_default().to_string(),
-                },
-                credentials_file: File {
-                    path: "~/.aws/config".to_string(),
-                    contents: creds.unwrap_or_default().to_string(),
-                },
+                files: vec![
+                    File {
+                        kind: ProfileFileKind::Config,
+                        path: Some("~/.aws/config".to_string()),
+                        contents: conf.unwrap_or_default().to_string(),
+                    },
+                    File {
+                        kind: ProfileFileKind::Credentials,
+                        path: Some("~/.aws/credentials".to_string()),
+                        contents: creds.unwrap_or_default().to_string(),
+                    },
+                ],
                 profile: "default".into(),
             };
             // don't care if parse fails, just don't panic
@@ -313,14 +301,18 @@ mod test {
 
     fn make_source(input: ParserInput) -> Source {
         Source {
-            config_file: File {
-                path: "~/.aws/config".to_string(),
-                contents: input.config_file.unwrap_or_default(),
-            },
-            credentials_file: File {
-                path: "~/.aws/credentials".to_string(),
-                contents: input.credentials_file.unwrap_or_default(),
-            },
+            files: vec![
+                File {
+                    kind: ProfileFileKind::Config,
+                    path: Some("~/.aws/config".to_string()),
+                    contents: input.config_file.unwrap_or_default(),
+                },
+                File {
+                    kind: ProfileFileKind::Credentials,
+                    path: Some("~/.aws/credentials".to_string()),
+                    contents: input.credentials_file.unwrap_or_default(),
+                },
+            ],
             profile: "default".into(),
         }
     }
diff --git a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs b/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs
index 81ad64327b..2e645c3f11 100644
--- a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs
+++ b/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs
@@ -7,7 +7,7 @@ use std::borrow::Cow;
 use std::collections::HashMap;
 
 use crate::profile::parser::parse::{RawProfileSet, WHITESPACE};
-use crate::profile::parser::source::FileKind;
+use crate::profile::profile_file::ProfileFileKind;
 use crate::profile::{Profile, ProfileSet, Property};
 
 const DEFAULT: &str = "default";
@@ -38,7 +38,7 @@ impl ProfileName<'_> {
     /// 1. `name` must ALWAYS be a valid identifier
     /// 2. For Config files, the profile must either be `default` or it must have a profile prefix
     /// 3. For credentials files, the profile name MUST NOT have a profile prefix
-    fn valid_for(self, kind: FileKind) -> Result<Self, String> {
+    fn valid_for(self, kind: ProfileFileKind) -> Result<Self, String> {
         if validate_identifier(self.name).is_err() {
             return Err(format!(
                 "profile `{}` ignored because `{}` was not a valid identifier",
@@ -46,17 +46,17 @@ impl ProfileName<'_> {
             ));
         }
         match (self.name, kind, self.has_profile_prefix) {
-            (_, FileKind::Config, true) => Ok(self),
-            (DEFAULT, FileKind::Config, false) => Ok(self),
-            (_not_default, FileKind::Config, false) => Err(format!(
+            (_, ProfileFileKind::Config, true) => Ok(self),
+            (DEFAULT, ProfileFileKind::Config, false) => Ok(self),
+            (_not_default, ProfileFileKind::Config, false) => Err(format!(
                 "profile `{}` ignored because config profiles must be of the form `[profile <name>]`",
                 self.name
             )),
-            (_, FileKind::Credentials, true) => Err(format!(
+            (_, ProfileFileKind::Credentials, true) => Err(format!(
                 "profile `{}` ignored because credential profiles must NOT begin with `profile`",
                 self.name
             )),
-            (_, FileKind::Credentials, false) => Ok(self),
+            (_, ProfileFileKind::Credentials, false) => Ok(self),
         }
     }
 }
@@ -68,7 +68,11 @@ impl ProfileName<'_> {
 /// - Profile names are validated (see `validate_profile_name`)
 /// - A profile named `profile default` takes priority over a profile named `default`.
 /// - Profiles with identical names are merged
-pub(super) fn merge_in(base: &mut ProfileSet, raw_profile_set: RawProfileSet<'_>, kind: FileKind) {
+pub(super) fn merge_in(
+    base: &mut ProfileSet,
+    raw_profile_set: RawProfileSet<'_>,
+    kind: ProfileFileKind,
+) {
     // parse / validate profile names
     let validated_profiles = raw_profile_set
         .into_iter()
@@ -148,11 +152,11 @@ mod tests {
     use tracing_test::traced_test;
 
     use crate::profile::parser::parse::RawProfileSet;
-    use crate::profile::parser::source::FileKind;
     use crate::profile::ProfileSet;
 
     use super::{merge_in, ProfileName};
     use crate::profile::parser::normalize::validate_identifier;
+    use crate::profile::profile_file::ProfileFileKind;
 
     #[test]
     fn profile_name_parsing() {
@@ -219,7 +223,7 @@ mod tests {
             out
         });
         let mut base = ProfileSet::empty();
-        merge_in(&mut base, profile, FileKind::Config);
+        merge_in(&mut base, profile, ProfileFileKind::Config);
         assert!(base
             .get_profile("default")
             .expect("contains default profile")
@@ -235,7 +239,7 @@ mod tests {
     fn invalid_profile_generates_warning() {
         let mut profile: RawProfileSet<'_> = HashMap::new();
         profile.insert("foo", HashMap::new());
-        merge_in(&mut ProfileSet::empty(), profile, FileKind::Config);
+        merge_in(&mut ProfileSet::empty(), profile, ProfileFileKind::Config);
         assert!(logs_contain("profile `foo` ignored"));
     }
 }
diff --git a/aws/rust-runtime/aws-config/src/profile/parser/parse.rs b/aws/rust-runtime/aws-config/src/profile/parser/parse.rs
index b1d43e58b2..1d3c3acb05 100644
--- a/aws/rust-runtime/aws-config/src/profile/parser/parse.rs
+++ b/aws/rust-runtime/aws-config/src/profile/parser/parse.rs
@@ -110,7 +110,7 @@ pub(super) fn parse_profile_file(file: &File) -> Result<RawProfileSet<'_>, Profi
         state: State::Starting,
         location: Location {
             line_number: 0,
-            path: file.path.to_string(),
+            path: file.path.clone().unwrap_or_default(),
         },
     };
     parser.parse_profile(&file.contents)?;
@@ -290,6 +290,7 @@ mod test {
     use super::{parse_profile_file, prepare_line, Location};
     use crate::profile::parser::parse::{parse_property_line, PropertyError};
     use crate::profile::parser::source::File;
+    use crate::profile::profile_file::ProfileFileKind;
 
     // most test cases covered by the JSON test suite
 
@@ -330,7 +331,8 @@ mod test {
     #[test]
     fn error_line_numbers() {
         let file = File {
-            path: "~/.aws/config".into(),
+            kind: ProfileFileKind::Config,
+            path: Some("~/.aws/config".into()),
             contents: "[default\nk=v".into(),
         };
         let err = parse_profile_file(&file).expect_err("parsing should fail");
diff --git a/aws/rust-runtime/aws-config/src/profile/parser/source.rs b/aws/rust-runtime/aws-config/src/profile/parser/source.rs
index fd99f40e7d..50057805da 100644
--- a/aws/rust-runtime/aws-config/src/profile/parser/source.rs
+++ b/aws/rust-runtime/aws-config/src/profile/parser/source.rs
@@ -4,6 +4,8 @@
  */
 
 use crate::fs_util::{home_dir, Os};
+use crate::profile::credentials::{CouldNotReadProfileFile, ProfileFileError};
+use crate::profile::profile_file::{ProfileFile, ProfileFileKind, ProfileFiles};
 use aws_types::os_shim_internal;
 use std::borrow::Cow;
 use std::io::ErrorKind;
@@ -16,11 +18,8 @@ const HOME_EXPANSION_FAILURE_WARNING: &str =
 
 /// In-memory source of profile data
 pub(super) struct Source {
-    /// Contents and path of ~/.aws/config
-    pub(super) config_file: File,
-
-    /// Contents and path of ~/.aws/credentials
-    pub(super) credentials_file: File,
+    /// Profile file sources
+    pub(super) files: Vec<File>,
 
     /// Profile to use
     ///
@@ -30,49 +29,44 @@ pub(super) struct Source {
 
 /// In-memory configuration file
 pub(super) struct File {
-    pub(super) path: String,
+    pub(super) kind: ProfileFileKind,
+    pub(super) path: Option<String>,
     pub(super) contents: String,
 }
 
-#[derive(Clone, Copy)]
-pub(super) enum FileKind {
-    Config,
-    Credentials,
-}
-
-impl FileKind {
-    fn default_path(&self) -> &'static str {
-        match &self {
-            FileKind::Credentials => "~/.aws/credentials",
-            FileKind::Config => "~/.aws/config",
-        }
-    }
+/// Load a [Source](Source) from a given environment and filesystem.
+pub(super) async fn load(
+    proc_env: &os_shim_internal::Env,
+    fs: &os_shim_internal::Fs,
+    profile_files: &ProfileFiles,
+) -> Result<Source, ProfileFileError> {
+    let home = home_dir(proc_env, Os::real());
 
-    fn override_environment_variable(&self) -> &'static str {
-        match &self {
-            FileKind::Config => "AWS_CONFIG_FILE",
-            FileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE",
-        }
+    let mut files = Vec::new();
+    for file in &profile_files.files {
+        let file = load_config_file(file, &home, fs, proc_env)
+            .instrument(tracing::debug_span!("load_config_file", file = ?file))
+            .await?;
+        files.push(file);
     }
-}
 
-/// Load a [Source](Source) from a given environment and filesystem.
-pub(super) async fn load(proc_env: &os_shim_internal::Env, fs: &os_shim_internal::Fs) -> Source {
-    let home = home_dir(proc_env, Os::real());
-    let config = load_config_file(FileKind::Config, &home, fs, proc_env)
-        .instrument(tracing::debug_span!("load_config_file"))
-        .await;
-    let credentials = load_config_file(FileKind::Credentials, &home, fs, proc_env)
-        .instrument(tracing::debug_span!("load_credentials_file"))
-        .await;
-
-    Source {
-        config_file: config,
-        credentials_file: credentials,
+    Ok(Source {
+        files,
         profile: proc_env
             .get("AWS_PROFILE")
             .map(Cow::Owned)
             .unwrap_or(Cow::Borrowed("default")),
+    })
+}
+
+fn file_contents_to_string(path: &Path, contents: Vec<u8>) -> String {
+    // if the file is not valid utf-8, log a warning and use an empty file instead
+    match String::from_utf8(contents) {
+        Ok(contents) => contents,
+        Err(e) => {
+            tracing::warn!(path = ?path, error = %e, "config file did not contain utf-8 encoded data");
+            Default::default()
+        }
     }
 }
 
@@ -87,53 +81,74 @@ pub(super) async fn load(proc_env: &os_shim_internal::Env, fs: &os_shim_internal
 /// * `fs`: Filesystem abstraction
 /// * `environment`: Process environment abstraction
 async fn load_config_file(
-    kind: FileKind,
+    source: &ProfileFile,
     home_directory: &Option<String>,
     fs: &os_shim_internal::Fs,
     environment: &os_shim_internal::Env,
-) -> File {
-    let (path_is_default, path) = environment
-        .get(kind.override_environment_variable())
-        .map(|p| (false, Cow::Owned(p)))
-        .ok()
-        .unwrap_or_else(|| (true, kind.default_path().into()));
-    let expanded = expand_home(path.as_ref(), path_is_default, home_directory);
-    if path != expanded.to_string_lossy() {
-        tracing::debug!(before = ?path, after = ?expanded, "home directory expanded");
-    }
-    // read the data at the specified path
-    // if the path does not exist, log a warning but pretend it was actually an empty file
-    let data = match fs.read_to_end(&expanded).await {
-        Ok(data) => data,
-        Err(e) => {
-            match e.kind() {
-                ErrorKind::NotFound if path == kind.default_path() => {
-                    tracing::debug!(path = %path, "config file not found")
-                }
-                ErrorKind::NotFound if path != kind.default_path() => {
-                    // in the case where the user overrode the path with an environment variable,
-                    // log more loudly than the case where the default path was missing
-                    tracing::warn!(path = %path, env = %kind.override_environment_variable(), "config file overridden via environment variable not found")
+) -> Result<File, ProfileFileError> {
+    let (path, kind, contents) = match source {
+        ProfileFile::Default(kind) => {
+            let (path_is_default, path) = environment
+                .get(kind.override_environment_variable())
+                .map(|p| (false, Cow::Owned(p)))
+                .ok()
+                .unwrap_or_else(|| (true, kind.default_path().into()));
+            let expanded = expand_home(path.as_ref(), path_is_default, home_directory);
+            if path != expanded.to_string_lossy() {
+                tracing::debug!(before = ?path, after = ?expanded, "home directory expanded");
+            }
+            // read the data at the specified path
+            // if the path does not exist, log a warning but pretend it was actually an empty file
+            let data = match fs.read_to_end(&expanded).await {
+                Ok(data) => data,
+                Err(e) => {
+                    // Important: The default config/credentials files MUST NOT return an error
+                    match e.kind() {
+                        ErrorKind::NotFound if path == kind.default_path() => {
+                            tracing::debug!(path = %path, "config file not found")
+                        }
+                        ErrorKind::NotFound if path != kind.default_path() => {
+                            // in the case where the user overrode the path with an environment variable,
+                            // log more loudly than the case where the default path was missing
+                            tracing::warn!(path = %path, env = %kind.override_environment_variable(), "config file overridden via environment variable not found")
+                        }
+                        _other => {
+                            tracing::warn!(path = %path, error = %e, "failed to read config file")
+                        }
+                    };
+                    Default::default()
                 }
-                _other => tracing::warn!(path = %path, error = %e, "failed to read config file"),
             };
-            Default::default()
+            let contents = file_contents_to_string(&expanded, data);
+            (Some(Cow::Owned(expanded)), kind, contents)
         }
-    };
-    // if the file is not valid utf-8, log a warning and use an empty file instead
-    let data = match String::from_utf8(data) {
-        Ok(data) => data,
-        Err(e) => {
-            tracing::warn!(path = %path, error = %e, "config file did not contain utf-8 encoded data");
-            Default::default()
+        ProfileFile::FilePath { kind, path } => {
+            let data = match fs.read_to_end(&path).await {
+                Ok(data) => data,
+                Err(e) => {
+                    return Err(ProfileFileError::CouldNotReadProfileFile(
+                        CouldNotReadProfileFile {
+                            path: path.clone(),
+                            cause: e,
+                        },
+                    ))
+                }
+            };
+            (
+                Some(Cow::Borrowed(path)),
+                kind,
+                file_contents_to_string(path, data),
+            )
         }
+        ProfileFile::FileContents { kind, contents } => (None, kind, contents.clone()),
     };
-    tracing::debug!(path = %path, size = ?data.len(), "config file loaded");
-    File {
+    tracing::debug!(path = ?path, size = ?contents.len(), "config file loaded");
+    Ok(File {
+        kind: *kind,
         // lossy is OK here, the name of this file is just for debugging purposes
-        path: expanded.to_string_lossy().into(),
-        contents: data,
-    }
+        path: path.map(|p| p.to_string_lossy().into()),
+        contents,
+    })
 }
 
 fn expand_home(
@@ -179,9 +194,11 @@ fn expand_home(
 
 #[cfg(test)]
 mod tests {
+    use crate::profile::credentials::ProfileFileError;
     use crate::profile::parser::source::{
-        expand_home, load, load_config_file, FileKind, HOME_EXPANSION_FAILURE_WARNING,
+        expand_home, load, load_config_file, HOME_EXPANSION_FAILURE_WARNING,
     };
+    use crate::profile::profile_file::{ProfileFile, ProfileFileKind, ProfileFiles};
     use aws_types::os_shim_internal::{Env, Fs};
     use futures_util::FutureExt;
     use serde::Deserialize;
@@ -243,7 +260,7 @@ mod tests {
 
         let fs = Fs::from_map(fs);
 
-        let _src = load(&env, &fs).now_or_never();
+        let _src = load(&env, &fs, &Default::default()).now_or_never();
         assert!(logs_contain("config file loaded"));
         assert!(logs_contain("performing home directory substitution"));
     }
@@ -254,7 +271,13 @@ mod tests {
         let env = Env::from_slice(&[]);
         let fs = Fs::from_slice(&[]);
 
-        let _src = load_config_file(FileKind::Config, &None, &fs, &env).now_or_never();
+        let _src = load_config_file(
+            &ProfileFile::Default(ProfileFileKind::Config),
+            &None,
+            &fs,
+            &env,
+        )
+        .now_or_never();
         assert!(!logs_contain(HOME_EXPANSION_FAILURE_WARNING));
     }
 
@@ -264,7 +287,13 @@ mod tests {
         let env = Env::from_slice(&[("AWS_CONFIG_FILE", "~/some/path")]);
         let fs = Fs::from_slice(&[]);
 
-        let _src = load_config_file(FileKind::Config, &None, &fs, &env).now_or_never();
+        let _src = load_config_file(
+            &ProfileFile::Default(ProfileFileKind::Config),
+            &None,
+            &fs,
+            &env,
+        )
+        .now_or_never();
         assert!(logs_contain(HOME_EXPANSION_FAILURE_WARNING));
     }
 
@@ -274,17 +303,19 @@ mod tests {
         let platform_matches = (cfg!(windows) && test_case.platform == "windows")
             || (!cfg!(windows) && test_case.platform != "windows");
         if platform_matches {
-            let source = load(&env, &fs).await;
+            let source = load(&env, &fs, &Default::default()).await.unwrap();
             if let Some(expected_profile) = test_case.profile {
                 assert_eq!(source.profile, expected_profile, "{}", &test_case.name);
             }
             assert_eq!(
-                source.config_file.path, test_case.config_location,
+                source.files[0].path,
+                Some(test_case.config_location),
                 "{}",
                 &test_case.name
             );
             assert_eq!(
-                source.credentials_file.path, test_case.credentials_location,
+                source.files[1].path,
+                Some(test_case.credentials_location),
                 "{}",
                 &test_case.name
             )
@@ -337,4 +368,115 @@ mod tests {
             "C:\\Users\\name\\.aws\\config"
         );
     }
+
+    #[tokio::test]
+    async fn programmatically_set_credentials_file_contents() {
+        let contents = "[default]\n\
+            aws_access_key_id = AKIAFAKE\n\
+            aws_secret_access_key = FAKE\n\
+            ";
+        let env = Env::from_slice(&[]);
+        let fs = Fs::from_slice(&[]);
+        let profile_files = ProfileFiles::builder()
+            .with_contents(ProfileFileKind::Credentials, contents)
+            .build();
+        let source = load(&env, &fs, &profile_files).await.unwrap();
+        assert_eq!(1, source.files.len());
+        assert_eq!("default", source.profile);
+        assert_eq!(contents, source.files[0].contents);
+    }
+
+    #[tokio::test]
+    async fn programmatically_set_credentials_file_path() {
+        let contents = "[default]\n\
+            aws_access_key_id = AKIAFAKE\n\
+            aws_secret_access_key = FAKE\n\
+            ";
+        let mut fs = HashMap::new();
+        fs.insert(
+            "/custom/path/to/credentials".to_string(),
+            contents.to_string(),
+        );
+
+        let fs = Fs::from_map(fs);
+        let env = Env::from_slice(&[]);
+        let profile_files = ProfileFiles::builder()
+            .with_file(ProfileFileKind::Credentials, "/custom/path/to/credentials")
+            .build();
+        let source = load(&env, &fs, &profile_files).await.unwrap();
+        assert_eq!(1, source.files.len());
+        assert_eq!("default", source.profile);
+        assert_eq!(contents, source.files[0].contents);
+    }
+
+    #[tokio::test]
+    async fn programmatically_include_default_files() {
+        let config_contents = "[default]\nregion = us-east-1";
+        let credentials_contents = "[default]\n\
+            aws_access_key_id = AKIAFAKE\n\
+            aws_secret_access_key = FAKE\n\
+            ";
+        let custom_contents = "[profile some-profile]\n\
+            aws_access_key_id = AKIAFAKEOTHER\n\
+            aws_secret_access_key = FAKEOTHER\n\
+            ";
+        let mut fs = HashMap::new();
+        fs.insert(
+            "/user/name/.aws/config".to_string(),
+            config_contents.to_string(),
+        );
+        fs.insert(
+            "/user/name/.aws/credentials".to_string(),
+            credentials_contents.to_string(),
+        );
+
+        let fs = Fs::from_map(fs);
+        let env = Env::from_slice(&[("HOME", "/user/name")]);
+        let profile_files = ProfileFiles::builder()
+            .with_contents(ProfileFileKind::Config, custom_contents)
+            .include_default_credentials_file(true)
+            .include_default_config_file(true)
+            .build();
+        let source = load(&env, &fs, &profile_files).await.unwrap();
+        assert_eq!(3, source.files.len());
+        assert_eq!("default", source.profile);
+        assert_eq!(config_contents, source.files[0].contents);
+        assert_eq!(credentials_contents, source.files[1].contents);
+        assert_eq!(custom_contents, source.files[2].contents);
+    }
+
+    #[tokio::test]
+    async fn default_files_must_not_error() {
+        let custom_contents = "[profile some-profile]\n\
+            aws_access_key_id = AKIAFAKEOTHER\n\
+            aws_secret_access_key = FAKEOTHER\n\
+            ";
+
+        let fs = Fs::from_slice(&[]);
+        let env = Env::from_slice(&[("HOME", "/user/name")]);
+        let profile_files = ProfileFiles::builder()
+            .with_contents(ProfileFileKind::Config, custom_contents)
+            .include_default_credentials_file(true)
+            .include_default_config_file(true)
+            .build();
+        let source = load(&env, &fs, &profile_files).await.unwrap();
+        assert_eq!(3, source.files.len());
+        assert_eq!("default", source.profile);
+        assert_eq!("", source.files[0].contents);
+        assert_eq!("", source.files[1].contents);
+        assert_eq!(custom_contents, source.files[2].contents);
+    }
+
+    #[tokio::test]
+    async fn misconfigured_programmatic_custom_profile_path_must_error() {
+        let fs = Fs::from_slice(&[]);
+        let env = Env::from_slice(&[]);
+        let profile_files = ProfileFiles::builder()
+            .with_file(ProfileFileKind::Config, "definitely-doesnt-exist")
+            .build();
+        assert!(matches!(
+            load(&env, &fs, &profile_files).await,
+            Err(ProfileFileError::CouldNotReadProfileFile(_))
+        ));
+    }
 }
diff --git a/aws/rust-runtime/aws-config/src/profile/profile_file.rs b/aws/rust-runtime/aws-config/src/profile/profile_file.rs
new file mode 100644
index 0000000000..76bdfb1fba
--- /dev/null
+++ b/aws/rust-runtime/aws-config/src/profile/profile_file.rs
@@ -0,0 +1,257 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+//! Config structs to programmatically customize the profile files that get loaded
+
+use std::fmt;
+use std::path::PathBuf;
+
+/// Provides the ability to programmatically override the profile files that get loaded by the SDK.
+///
+/// The [`Default`] for `ProfileFiles` includes the default SDK config and credential files located in
+/// `~/.aws/config` and `~/.aws/credentials` respectively.
+///
+/// Any number of config and credential files may be added to the `ProfileFiles` file set, with the
+/// only requirement being that there is at least one of them. Custom file locations that are added
+/// will produce errors if they don't exist, while the default config/credentials files paths are
+/// allowed to not exist even if they're included.
+///
+/// # Example: Using a custom profile file path for credentials and region
+///
+/// ```
+/// use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider};
+/// use aws_config::profile::profile_file::{ProfileFiles, ProfileFileKind};
+///
+/// # async fn example() {
+/// let profile_files = ProfileFiles::builder()
+///     .with_file(ProfileFileKind::Credentials, "some/path/to/credentials-file")
+///     .build();
+/// let credentials_provider = ProfileFileCredentialsProvider::builder()
+///     .profile_files(profile_files.clone())
+///     .build();
+/// let region_provider = ProfileFileRegionProvider::builder()
+///     .profile_files(profile_files)
+///     .build();
+///
+/// let sdk_config = aws_config::from_env()
+///     .credentials_provider(credentials_provider)
+///     .region(region_provider)
+///     .load()
+///     .await;
+/// # }
+/// ```
+#[derive(Clone, Debug)]
+pub struct ProfileFiles {
+    pub(crate) files: Vec<ProfileFile>,
+}
+
+impl ProfileFiles {
+    /// Returns a builder to create `ProfileFiles`
+    pub fn builder() -> Builder {
+        Builder::new()
+    }
+}
+
+impl Default for ProfileFiles {
+    fn default() -> Self {
+        Self {
+            files: vec![
+                ProfileFile::Default(ProfileFileKind::Config),
+                ProfileFile::Default(ProfileFileKind::Credentials),
+            ],
+        }
+    }
+}
+
+/// Profile file type (config or credentials)
+#[derive(Copy, Clone, Debug)]
+pub enum ProfileFileKind {
+    /// The SDK config file that typically resides in `~/.aws/config`
+    Config,
+    /// The SDK credentials file that typically resides in `~/.aws/credentials`
+    Credentials,
+}
+
+impl ProfileFileKind {
+    pub(crate) fn default_path(&self) -> &'static str {
+        match &self {
+            ProfileFileKind::Credentials => "~/.aws/credentials",
+            ProfileFileKind::Config => "~/.aws/config",
+        }
+    }
+
+    pub(crate) fn override_environment_variable(&self) -> &'static str {
+        match &self {
+            ProfileFileKind::Config => "AWS_CONFIG_FILE",
+            ProfileFileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE",
+        }
+    }
+}
+
+/// A single profile file within a [`ProfileFiles`] file set.
+#[derive(Clone)]
+pub(crate) enum ProfileFile {
+    /// One of the default profile files (config or credentials in their default locations)
+    Default(ProfileFileKind),
+    /// A profile file at a custom location
+    FilePath {
+        kind: ProfileFileKind,
+        path: PathBuf,
+    },
+    /// The direct contents of a profile file
+    FileContents {
+        kind: ProfileFileKind,
+        contents: String,
+    },
+}
+
+impl fmt::Debug for ProfileFile {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Default(kind) => f.debug_tuple("Default").field(kind).finish(),
+            Self::FilePath { kind, path } => f
+                .debug_struct("FilePath")
+                .field("kind", kind)
+                .field("path", path)
+                .finish(),
+            // Security: Redact the file contents since they may have credentials in them
+            Self::FileContents { kind, contents: _ } => f
+                .debug_struct("FileContents")
+                .field("kind", kind)
+                .field("contents", &"** redacted **")
+                .finish(),
+        }
+    }
+}
+
+/// Builder for [`ProfileFiles`].
+#[derive(Clone, Default, Debug)]
+pub struct Builder {
+    with_config: bool,
+    with_credentials: bool,
+    custom_sources: Vec<ProfileFile>,
+}
+
+impl Builder {
+    /// Creates a new builder instance.
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    /// Include the default SDK config file in the list of profile files to be loaded.
+    ///
+    /// The default SDK config typically resides in `~/.aws/config`. When this flag is enabled,
+    /// this config file will be included in the profile files that get loaded in the built
+    /// [`ProfileFiles`] file set.
+    ///
+    /// This flag defaults to `false` when using the builder to construct [`ProfileFiles`].
+    pub fn include_default_config_file(mut self, include_default_config_file: bool) -> Self {
+        self.with_config = include_default_config_file;
+        self
+    }
+
+    /// Include the default SDK credentials file in the list of profile files to be loaded.
+    ///
+    /// The default SDK config typically resides in `~/.aws/credentials`. When this flag is enabled,
+    /// this credentials file will be included in the profile files that get loaded in the built
+    /// [`ProfileFiles`] file set.
+    ///
+    /// This flag defaults to `false` when using the builder to construct [`ProfileFiles`].
+    pub fn include_default_credentials_file(
+        mut self,
+        include_default_credentials_file: bool,
+    ) -> Self {
+        self.with_credentials = include_default_credentials_file;
+        self
+    }
+
+    /// Include a custom `file` in the list of profile files to be loaded.
+    ///
+    /// The `kind` informs the parser how to treat the file. If it's intended to be like
+    /// the SDK credentials file typically in `~/.aws/config`, then use [`ProfileFileKind::Config`].
+    /// Otherwise, use [`ProfileFileKind::Credentials`].
+    pub fn with_file(mut self, kind: ProfileFileKind, file: impl Into<PathBuf>) -> Self {
+        self.custom_sources.push(ProfileFile::FilePath {
+            kind,
+            path: file.into(),
+        });
+        self
+    }
+
+    /// Include custom file `contents` in the list of profile files to be loaded.
+    ///
+    /// The `kind` informs the parser how to treat the file. If it's intended to be like
+    /// the SDK credentials file typically in `~/.aws/config`, then use [`ProfileFileKind::Config`].
+    /// Otherwise, use [`ProfileFileKind::Credentials`].
+    pub fn with_contents(mut self, kind: ProfileFileKind, contents: impl Into<String>) -> Self {
+        self.custom_sources.push(ProfileFile::FileContents {
+            kind,
+            contents: contents.into(),
+        });
+        self
+    }
+
+    /// Build the [`ProfileFiles`] file set.
+    pub fn build(self) -> ProfileFiles {
+        let mut files = self.custom_sources;
+        if self.with_credentials {
+            files.insert(0, ProfileFile::Default(ProfileFileKind::Credentials));
+        }
+        if self.with_config {
+            files.insert(0, ProfileFile::Default(ProfileFileKind::Config));
+        }
+        if files.is_empty() {
+            panic!("At least one profile file must be included in the `ProfileFiles` file set.");
+        }
+        ProfileFiles { files }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn redact_file_contents_in_profile_file_debug() {
+        let profile_file = ProfileFile::FileContents {
+            kind: ProfileFileKind::Config,
+            contents: "sensitive_contents".into(),
+        };
+        let debug = format!("{:?}", profile_file);
+        assert!(!debug.contains("sensitive_contents"));
+        assert!(debug.contains("** redacted **"));
+    }
+
+    #[test]
+    fn build_correctly_orders_default_config_credentials() {
+        let profile_files = ProfileFiles::builder()
+            .with_file(ProfileFileKind::Config, "foo")
+            .include_default_credentials_file(true)
+            .include_default_config_file(true)
+            .build();
+        assert_eq!(3, profile_files.files.len());
+        assert!(matches!(
+            profile_files.files[0],
+            ProfileFile::Default(ProfileFileKind::Config)
+        ));
+        assert!(matches!(
+            profile_files.files[1],
+            ProfileFile::Default(ProfileFileKind::Credentials)
+        ));
+        assert!(matches!(
+            profile_files.files[2],
+            ProfileFile::FilePath {
+                kind: ProfileFileKind::Config,
+                path: _
+            }
+        ));
+    }
+
+    #[test]
+    #[should_panic]
+    fn empty_builder_panics() {
+        ProfileFiles::builder().build();
+    }
+}
diff --git a/aws/rust-runtime/aws-config/src/profile/region.rs b/aws/rust-runtime/aws-config/src/profile/region.rs
index e294ed61ef..547bff175f 100644
--- a/aws/rust-runtime/aws-config/src/profile/region.rs
+++ b/aws/rust-runtime/aws-config/src/profile/region.rs
@@ -10,6 +10,7 @@ use crate::provider_config::ProviderConfig;
 use aws_types::os_shim_internal::{Env, Fs};
 use aws_types::region::Region;
 
+use super::profile_file::ProfileFiles;
 use super::ProfileSet;
 
 /// Load a region from a profile file
@@ -17,6 +18,8 @@ use super::ProfileSet;
 /// This provider will attempt to load AWS shared configuration, then read the `region` property
 /// from the active profile.
 ///
+#[doc = include_str!("location_of_profile_files.md")]
+///
 /// # Examples
 ///
 /// **Loads "us-west-2" as the region**
@@ -39,6 +42,7 @@ pub struct ProfileFileRegionProvider {
     fs: Fs,
     env: Env,
     profile_override: Option<String>,
+    profile_files: ProfileFiles,
 }
 
 /// Builder for [ProfileFileRegionProvider]
@@ -46,6 +50,7 @@ pub struct ProfileFileRegionProvider {
 pub struct Builder {
     config: Option<ProviderConfig>,
     profile_override: Option<String>,
+    profile_files: Option<ProfileFiles>,
 }
 
 impl Builder {
@@ -55,12 +60,18 @@ impl Builder {
         self
     }
 
-    /// Override the profile name used by the [ProfileFileRegionProvider]
+    /// Override the profile name used by the [`ProfileFileRegionProvider`]
     pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
         self.profile_override = Some(profile_name.into());
         self
     }
 
+    /// Set the profile file that should be used by the [`ProfileFileRegionProvider`]
+    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
+        self.profile_files = Some(profile_files);
+        self
+    }
+
     /// Build a [ProfileFileRegionProvider] from this builder
     pub fn build(self) -> ProfileFileRegionProvider {
         let conf = self.config.unwrap_or_default();
@@ -68,6 +79,7 @@ impl Builder {
             env: conf.env(),
             fs: conf.fs(),
             profile_override: self.profile_override,
+            profile_files: self.profile_files.unwrap_or_default(),
         }
     }
 }
@@ -81,6 +93,7 @@ impl ProfileFileRegionProvider {
             fs: Fs::real(),
             env: Env::real(),
             profile_override: None,
+            profile_files: ProfileFiles::default(),
         }
     }
 
@@ -90,7 +103,7 @@ impl ProfileFileRegionProvider {
     }
 
     async fn region(&self) -> Option<Region> {
-        let profile_set = super::parser::load(&self.fs, &self.env)
+        let profile_set = super::parser::load(&self.fs, &self.env, &self.profile_files)
             .await
             .map_err(|err| tracing::warn!(err = %err, "failed to parse profile"))
             .ok()?;
diff --git a/aws/rust-runtime/aws-config/src/profile/retry_config.rs b/aws/rust-runtime/aws-config/src/profile/retry_config.rs
index c876d82f02..e51a39fdde 100644
--- a/aws/rust-runtime/aws-config/src/profile/retry_config.rs
+++ b/aws/rust-runtime/aws-config/src/profile/retry_config.rs
@@ -10,6 +10,7 @@ use std::str::FromStr;
 use aws_smithy_types::retry::{RetryConfigBuilder, RetryConfigErr, RetryMode};
 use aws_types::os_shim_internal::{Env, Fs};
 
+use super::profile_file::ProfileFiles;
 use crate::provider_config::ProviderConfig;
 
 /// Load retry configuration properties from a profile file
@@ -17,6 +18,8 @@ use crate::provider_config::ProviderConfig;
 /// This provider will attempt to load AWS shared configuration, then read retry configuration properties
 /// from the active profile.
 ///
+#[doc = include_str!("location_of_profile_files.md")]
+///
 /// # Examples
 ///
 /// **Loads 2 as the `max_attempts` to make when sending a request**
@@ -38,6 +41,7 @@ pub struct ProfileFileRetryConfigProvider {
     fs: Fs,
     env: Env,
     profile_override: Option<String>,
+    profile_files: ProfileFiles,
 }
 
 /// Builder for [ProfileFileRetryConfigProvider]
@@ -45,6 +49,7 @@ pub struct ProfileFileRetryConfigProvider {
 pub struct Builder {
     config: Option<ProviderConfig>,
     profile_override: Option<String>,
+    profile_files: Option<ProfileFiles>,
 }
 
 impl Builder {
@@ -60,6 +65,12 @@ impl Builder {
         self
     }
 
+    /// Set the profile file that should be used by the [`ProfileFileRetryConfigProvider`]
+    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
+        self.profile_files = Some(profile_files);
+        self
+    }
+
     /// Build a [ProfileFileRetryConfigProvider] from this builder
     pub fn build(self) -> ProfileFileRetryConfigProvider {
         let conf = self.config.unwrap_or_default();
@@ -67,6 +78,7 @@ impl Builder {
             env: conf.env(),
             fs: conf.fs(),
             profile_override: self.profile_override,
+            profile_files: self.profile_files.unwrap_or_default(),
         }
     }
 }
@@ -80,6 +92,7 @@ impl ProfileFileRetryConfigProvider {
             fs: Fs::real(),
             env: Env::real(),
             profile_override: None,
+            profile_files: Default::default(),
         }
     }
 
@@ -90,7 +103,7 @@ impl ProfileFileRetryConfigProvider {
 
     /// Attempt to create a new RetryConfigBuilder from a profile file.
     pub async fn retry_config_builder(&self) -> Result<RetryConfigBuilder, RetryConfigErr> {
-        let profile = match super::parser::load(&self.fs, &self.env).await {
+        let profile = match super::parser::load(&self.fs, &self.env, &self.profile_files).await {
             Ok(profile) => profile,
             Err(err) => {
                 tracing::warn!(err = %err, "failed to parse profile");