Skip to content

Commit c6680b8

Browse files
committed
Add assumerole with creds from metadata
fix sts_client remove commented code add docs
1 parent f668e30 commit c6680b8

File tree

4 files changed

+158
-11
lines changed

4 files changed

+158
-11
lines changed

docs/en/sql-reference/table-functions/s3.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -283,19 +283,21 @@ SELECT count() FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.co
283283

284284
## Role Assumption
285285

286-
ClickHouse supports assuming an AWS IAM role using a set of AWS credentials (`access_key_id`, `secret_access_key`, `session_token`).
287-
This allows ClickHouse to obtain temporary credentials for accessing an S3 bucket, even if the original credentials do not have direct access.
286+
ClickHouse supports assuming an AWS IAM role using a set of AWS credentials (`access_key_id`, `secret_access_key`, `session_token`) or EC2 metadata (only when running on EC2 instance).
287+
This allows ClickHouse to obtain temporary credentials for accessing an S3 bucket, even if the original credentials or instance do not have direct access.
288288

289289
For example, if the provided credentials have permission to assume a role but lack direct access to the S3 bucket, ClickHouse will first request temporary credentials from AWS STS and then use those credentials to access S3.
290290

291291
For more details on role assumption, read [AWS AssumeRole documentation](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html).
292292

293-
To enable role assumption, pass parameters via the extra_credentials argument in the s3 function. The following keys are supported:
293+
To use this mechanism, pass parameters via the `extra_credentials` argument to the `s3` function. The following keys are supported:
294294

295-
* `role_arn` (required) — ARN of the IAM role to assume. **If this key is not provided, ClickHouse will not attempt to assume a role and will use the original credentials as-is.**
296-
* `role_session_name` (optional) — Custom session name to include in the AssumeRole request.
295+
* `role_arn` (required) — ARN of the IAM role to assume. **If this key is not provided, ClickHouse will not attempt to assume a role and will try to access the bucket as-is.**
296+
* `role_session_name` (optional) — Custom session name to include in the AssumeRole request. If not specified, a random UUID will be assigned.
297297
* `sts_endpoint_override` (optional) — Overrides the default AWS STS endpoint (https://sts.amazonaws.com). Useful for testing with a mock or when using another STS-compatible service.
298298

299+
If explicit `access_key_id` and `secret_access_key` are provided as parameters to `s3(...)` function, then they will be used for retrieving temporary credentials from STS:
300+
299301
```sql
300302
SELECT count() FROM s3(
301303
'<s3_bucket_uri>/*.csv',
@@ -309,6 +311,20 @@ SELECT count() FROM s3(
309311
)
310312
)
311313
```
314+
315+
Otherwise, ClickHouse will attempt to extract credentials from EC2 metadata:
316+
317+
```sql
318+
SELECT count() FROM s3(
319+
'<s3_bucket_uri>/*.csv',
320+
'CSVWithNames',
321+
extra_credentials(
322+
role_arn = 'arn:aws:iam::111111111111:role/BucketAccessRole-001',
323+
role_session_name = 'ClickHouseSession'
324+
)
325+
)
326+
```
327+
312328
Further examples can be found [here](/docs/cloud/security/secure-s3#access-your-s3-bucket-with-the-clickhouseaccess-role)
313329

314330
## Working with archives {#working-with-archives}

src/IO/S3/Credentials.cpp

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,86 @@ void AwsAuthSTSAssumeRoleWebIdentityCredentialsProvider::refreshIfExpired()
563563
Reload();
564564
}
565565

566+
AWSInstanceMetadataAssumeRoleCredentialsProvider::AWSInstanceMetadataAssumeRoleCredentialsProvider(
567+
const Aws::String & role_arn_,
568+
const Aws::String & session_name_,
569+
DB::S3::PocoHTTPClientConfiguration aws_client_configuration,
570+
uint64_t expiration_window_seconds_)
571+
: role_arn(role_arn_)
572+
, session_name(session_name_.empty() ? Aws::String(Aws::Utils::UUID::RandomUUID()) : session_name_)
573+
, logger(getLogger("AWSInstanceMetadataAssumeRoleCredentialsProvider"))
574+
, expiration_window_seconds(expiration_window_seconds_)
575+
{
576+
// Create metadata credentials provider
577+
auto ec2_metadata_client = createEC2MetadataClient(aws_client_configuration);
578+
auto config_loader = std::make_shared<AWSEC2InstanceProfileConfigLoader>(ec2_metadata_client, true);
579+
metadata_provider = std::make_shared<AWSInstanceProfileCredentialsProvider>(config_loader);
580+
581+
aws_client_configuration.scheme = Aws::Http::Scheme::HTTPS;
582+
583+
std::vector<Aws::String> retryable_errors;
584+
retryable_errors.push_back("IDPCommunicationError");
585+
retryable_errors.push_back("InvalidIdentityToken");
586+
aws_client_configuration.retryStrategy = std::make_shared<Aws::Client::SpecifiedRetryableErrorsRetryStrategy>(
587+
retryable_errors, /*maxRetries=*/ 3);
588+
589+
sts_client = std::make_unique<Aws::STS::STSClient>(metadata_provider, aws_client_configuration);
590+
591+
LOG_INFO(logger, "Created STS AssumeRole provider using EC2 instance metadata");
592+
}
593+
594+
Aws::Auth::AWSCredentials AWSInstanceMetadataAssumeRoleCredentialsProvider::GetAWSCredentials()
595+
{
596+
refreshIfExpired();
597+
Aws::Utils::Threading::ReaderLockGuard guard(m_reloadLock);
598+
return credentials;
599+
}
600+
601+
void AWSInstanceMetadataAssumeRoleCredentialsProvider::Reload()
602+
{
603+
// Get fresh metadata credentials
604+
auto metadata_creds = metadata_provider->GetAWSCredentials();
605+
if (metadata_creds.IsEmpty())
606+
{
607+
LOG_ERROR(logger, "Failed to obtain instance metadata credentials");
608+
return;
609+
}
610+
611+
// Perform AssumeRole
612+
Aws::STS::Model::AssumeRoleRequest request;
613+
request.SetRoleArn(role_arn);
614+
request.SetRoleSessionName(session_name);
615+
616+
auto outcome = sts_client->AssumeRole(request);
617+
if (!outcome.IsSuccess())
618+
{
619+
LOG_ERROR(logger, "Failed to assume role: {}", outcome.GetError().GetMessage());
620+
return;
621+
}
622+
623+
const auto & result = outcome.GetResult().GetCredentials();
624+
credentials = Aws::Auth::AWSCredentials(
625+
result.GetAccessKeyId(),
626+
result.GetSecretAccessKey(),
627+
result.GetSessionToken(),
628+
result.GetExpiration().SecondsWithMSPrecision());
629+
630+
LOG_TRACE(logger, "Successfully assumed role using metadata credentials");
631+
}
632+
633+
void AWSInstanceMetadataAssumeRoleCredentialsProvider::refreshIfExpired()
634+
{
635+
Aws::Utils::Threading::ReaderLockGuard guard(m_reloadLock);
636+
if (!areCredentialsEmptyOrExpired(credentials, expiration_window_seconds))
637+
return;
638+
639+
guard.UpgradeToWriterLock();
640+
if (!areCredentialsEmptyOrExpired(credentials, expiration_window_seconds))
641+
return;
642+
643+
Reload();
644+
}
645+
566646

567647
SSOCredentialsProvider::SSOCredentialsProvider(DB::S3::PocoHTTPClientConfiguration aws_client_configuration_, uint64_t expiration_window_seconds_)
568648
: profile_to_use(Aws::Auth::GetConfigProfileName())
@@ -707,13 +787,17 @@ S3CredentialsProviderChain::S3CredentialsProviderChain(
707787
if (credentials_configuration.no_sign_request)
708788
return;
709789

710-
/// add explicit credentials to the front of the chain
711-
/// because it's manually defined by the user
712-
if (!credentials.IsEmpty())
790+
if (credentials_configuration.role_arn.empty())
713791
{
714-
if (credentials_configuration.role_arn.empty())
792+
if (!credentials.IsEmpty())
793+
{
715794
AddProvider(std::make_shared<Aws::Auth::SimpleAWSCredentialsProvider>(credentials));
716-
else
795+
return;
796+
}
797+
}
798+
else
799+
{
800+
if (!credentials.IsEmpty())
717801
{
718802
auto sts_client_config = Aws::STS::STSClientConfiguration();
719803

@@ -740,6 +824,26 @@ S3CredentialsProviderChain::S3CredentialsProviderChain(
740824
)
741825
);
742826
}
827+
else
828+
{
829+
DB::S3::PocoHTTPClientConfiguration aws_client_configuration = DB::S3::ClientFactory::instance().createClientConfiguration(
830+
configuration.region,
831+
configuration.remote_host_filter,
832+
configuration.s3_max_redirects,
833+
configuration.s3_retry_attempts,
834+
configuration.enable_s3_requests_logging,
835+
configuration.for_disk_s3,
836+
configuration.get_request_throttler,
837+
configuration.put_request_throttler);
838+
839+
// Use metadata credentials for AssumeRole
840+
AddProvider(std::make_shared<AWSInstanceMetadataAssumeRoleCredentialsProvider>(
841+
Aws::String(credentials_configuration.role_arn),
842+
Aws::String(credentials_configuration.role_session_name),
843+
std::move(aws_client_configuration),
844+
credentials_configuration.expiration_window_seconds)
845+
);
846+
}
743847
return;
744848
}
745849

src/IO/S3/Credentials.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
# include <aws/core/config/AWSProfileConfigLoader.h>
1212
# include <aws/core/auth/AWSCredentialsProviderChain.h>
1313
# include <aws/core/auth/bearer-token-provider/SSOBearerTokenProvider.h>
14+
# include <aws/sts/STSClient.h>
15+
# include <aws/sts/model/AssumeRoleRequest.h>
1416

1517
# include <IO/S3/PocoHTTPClient.h>
1618
# include <IO/S3Defines.h>
@@ -108,6 +110,32 @@ class AWSInstanceProfileCredentialsProvider : public Aws::Auth::AWSCredentialsPr
108110
LoggerPtr logger;
109111
};
110112

113+
class AWSInstanceMetadataAssumeRoleCredentialsProvider : public Aws::Auth::AWSCredentialsProvider
114+
{
115+
public:
116+
explicit AWSInstanceMetadataAssumeRoleCredentialsProvider(
117+
const Aws::String & role_arn_,
118+
const Aws::String & session_name_,
119+
DB::S3::PocoHTTPClientConfiguration aws_client_configuration,
120+
uint64_t expiration_window_seconds_);
121+
122+
Aws::Auth::AWSCredentials GetAWSCredentials() override;
123+
124+
protected:
125+
void Reload() override;
126+
127+
private:
128+
void refreshIfExpired();
129+
130+
std::shared_ptr<AWSInstanceProfileCredentialsProvider> metadata_provider;
131+
std::unique_ptr<Aws::STS::STSClient> sts_client;
132+
Aws::Auth::AWSCredentials credentials;
133+
Aws::String role_arn;
134+
Aws::String session_name;
135+
LoggerPtr logger;
136+
uint64_t expiration_window_seconds;
137+
};
138+
111139
class AwsAuthSTSAssumeRoleWebIdentityCredentialsProvider : public Aws::Auth::AWSCredentialsProvider
112140
{
113141
/// See STSAssumeRoleWebIdentityCredentialsProvider.

src/Storages/ObjectStorage/S3/Configuration.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,6 @@ void StorageS3Configuration::extractExtraCreds(ASTs & args, ContextPtr context)
233233
}
234234

235235
extra_creds_it = arg_it;
236-
continue;
237236
}
238237
}
239238

0 commit comments

Comments
 (0)