Skip to content

Commit 4e14d2e

Browse files
committed
Use AWS RDS IAM secrets from duckdb-aws
This PR builds on top of the following PR in duckdb-aws: - duckdb/duckdb-aws#144 With the RDS IAM token generation done in `duckdb-aws`, now two secrets are required for IAM auth: 1. secret of type `rds` that contains all the details used to generate the IAM token 2. secret of type `postgres` that contains all other connection details except the IAM token; the `rds` secret name is specified in this secret Example: ```sql LOAD aws; LOAD postgres; CREATE SECRET aws_rds_secret1 ( TYPE rds, PROVIDER credential_chain, PROFILE 'DatabaseAdministrator-<account_id>', CHAIN 'env;sso;', REGION 'eu-west-1', RDS_USER 'postgres', RDS_HOST 'database-1-instance-1.xxxxxxxxxxxx.eu-west-1.rds.amazonaws.com', RDS_PORT '5432' ); CREATE SECRET pg_rds_secret1 ( TYPE postgres, HOST 'database-1-instance-1.xxxxxxxxxxxx.eu-west-1.rds.amazonaws.com', PORT '5432', USER 'postgres', DATABASE 'postgres', SSLMODE require, AWS_RDS_SECRET aws_rds_secret1 ); ``` Testing: tested locally with Aurora using both `sso` and `env` providers. Ref: #464
1 parent a491bf4 commit 4e14d2e

10 files changed

Lines changed: 139 additions & 94 deletions

CMakeLists.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ add_definitions(
1111

1212
find_package(OpenSSL REQUIRED)
1313
find_package(PostgreSQL REQUIRED)
14-
find_package(AWSSDK REQUIRED COMPONENTS rds)
1514

1615
if(NOT MSVC)
1716
add_compile_options(
@@ -55,7 +54,6 @@ target_link_libraries(${LOADABLE_EXTENSION_NAME}
5554
OpenSSL::SSL
5655
OpenSSL::Crypto
5756
PostgreSQL::PostgreSQL
58-
${AWSSDK_LINK_LIBRARIES}
5957
${CURL_LIBRARIES})
6058
set_property(TARGET ${EXTENSION_NAME} PROPERTY C_STANDARD 99)
6159
set_property(TARGET ${LOADABLE_EXTENSION_NAME} PROPERTY C_STANDARD 99)

extension_config.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ duckdb_extension_load(postgres_scanner
1010
duckdb_extension_load(tpch)
1111
duckdb_extension_load(tpcds)
1212
duckdb_extension_load(json)
13+
# duckdb_extension_load(aws)
1314

1415
# Any extra extensions that should be built
1516
# e.g.: duckdb_extension_load(json)

src/include/postgres_aws.hpp

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@
1717
namespace duckdb {
1818

1919
struct PostgresAwsRdsTokenConfig {
20-
bool enabled = false;
21-
std::string hostname;
22-
std::string port;
23-
std::string username;
24-
std::string region;
25-
std::uint64_t expiration_seconds = 900; // 15 min;
20+
static const int64_t expiration_seconds = 900; // 15 min, this value is fixed, also used in duckdb-aws
21+
22+
std::string rds_secret_name;
2623
std::string base_connection_string;
24+
25+
bool Enabled();
26+
int64_t MaxAgeSeconds();
2727
};
2828

2929
struct PostgresAws {
30-
static std::string GenerateRdsAuthToken(const PostgresAwsRdsTokenConfig &token_config);
30+
static std::string GenerateRdsAuthToken(AttachedDatabase &attached_db,
31+
const PostgresAwsRdsTokenConfig &token_config);
3132

3233
static PostgresAwsRdsTokenConfig ExtractTokenConfigFromSecret(optional_ptr<SecretEntry> secret_entry);
3334
};

src/include/postgres_secrets.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ namespace duckdb {
1616
struct PostgresSecrets {
1717
static const std::vector<std::string> &ConnectionOptionNames();
1818

19+
static SecretType CreateType();
20+
21+
static SecretType CreateRdsType();
22+
1923
static unique_ptr<BaseSecret> CreateFunction(ClientContext &context, CreateSecretInput &input);
2024

2125
static void SetSecretParameters(CreateSecretFunction &function);

src/postgres_aws.cpp

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,89 @@
33
#include <mutex>
44
#include <stdexcept>
55

6-
#include <aws/core/Aws.h>
7-
#include <aws/rds/RDSClient.h>
6+
#include "dbconnector/defer.hpp"
87

8+
#include "duckdb/parser/keyword_helper.hpp"
99
#include "postgres_secrets.hpp"
1010
#include "postgres_utils.hpp"
1111

1212
namespace duckdb {
1313

14-
static std::mutex sdk_init_mutex;
15-
static bool sdk_initialized = false;
16-
static Aws::SDKOptions sdk_options;
14+
bool PostgresAwsRdsTokenConfig::Enabled() {
15+
return !rds_secret_name.empty();
16+
}
17+
18+
int64_t PostgresAwsRdsTokenConfig::MaxAgeSeconds() {
19+
return expiration_seconds - 60;
20+
}
21+
22+
static std::string MakeCreateSecretQuery(const std::string &template_secret_name, const std::string &secret_name,
23+
const KeyValueSecret &kv_secret) {
24+
std::string query("CREATE OR REPLACE TEMPORARY SECRET \"");
25+
query += secret_name;
26+
query += "\" (\n";
27+
query += " TYPE rds,\n";
28+
query += " PROVIDER credential_chain,\n";
29+
for (auto &en : kv_secret.secret_map) {
30+
query += " " + en.first + " " + en.second.ToSQLString() + ",\n";
31+
}
32+
query += " RDS_TEMPLATE_SECRET_NAME " + KeywordHelper::WriteQuoted(template_secret_name, '\'') + "\n";
33+
query += ")";
34+
return query;
35+
}
36+
37+
static void RunQuery(Connection &conn, const std::string &query, const std::string &err_msg = std::string()) {
38+
auto res = conn.Query(query);
39+
if (res->HasError()) {
40+
if (err_msg.empty()) {
41+
throw InvalidConfigurationException("Error generating RDS IAM token: %s", res->GetError());
42+
} else {
43+
std::string error_type = EnumUtil::ToString(res->GetErrorType());
44+
throw InvalidConfigurationException("Error generating RDS IAM token, type: %s, %s", error_type, err_msg);
45+
}
46+
}
47+
}
1748

18-
static void EnsureAwsSdkInitialized() {
19-
std::lock_guard<std::mutex> lock(sdk_init_mutex);
20-
if (!sdk_initialized) {
21-
Aws::InitAPI(sdk_options);
22-
sdk_initialized = true;
49+
static unique_ptr<SecretEntry> GetRdsSecret(SecretManager &secret_manager, Connection &conn, const std::string &name) {
50+
auto transaction = CatalogTransaction::GetSystemCatalogTransaction(*conn.context);
51+
auto secret_entry = secret_manager.GetSecretByName(transaction, name);
52+
if (!secret_entry) {
53+
throw InvalidConfigurationException("Specified RDS secret with name: \"%s\" not found", name);
2354
}
55+
return secret_entry;
2456
}
2557

26-
std::string PostgresAws::GenerateRdsAuthToken(const PostgresAwsRdsTokenConfig &token_config) {
27-
EnsureAwsSdkInitialized();
28-
29-
Aws::Client::ClientConfiguration config;
30-
config.region = token_config.region;
31-
Aws::RDS::RDSClient rds_client(config);
32-
33-
// https://github.com/aws/aws-sdk-cpp/issues/861#issuecomment-386643571
34-
// Aws::String token = rdsClient.GenerateConnectAuthToken(hostname.c_str(), aws_region.c_str(),
35-
// static_cast<unsigned>(port_int), username.c_str());
36-
37-
std::string host_and_port = token_config.hostname + ":" + token_config.port;
38-
std::string host_and_port_with_prefix = "http://" + host_and_port;
39-
std::string host_and_port_with_suffix = host_and_port + "/";
40-
Aws::Http::URI uri(host_and_port_with_prefix.c_str());
41-
uri.AddQueryStringParameter("Action", "connect");
42-
uri.AddQueryStringParameter("DBUser", token_config.username.c_str());
43-
auto token = rds_client.GeneratePresignedUrl(uri, Aws::Http::HttpMethod::HTTP_GET, token_config.region.c_str(),
44-
"rds-db", static_cast<long long>(token_config.expiration_seconds));
45-
Aws::Utils::StringUtils::Replace(token, host_and_port_with_prefix.c_str(), host_and_port_with_suffix.c_str());
46-
47-
std::string token_str = token.c_str();
48-
return token_str;
58+
std::string PostgresAws::GenerateRdsAuthToken(AttachedDatabase &attached_db,
59+
const PostgresAwsRdsTokenConfig &token_config) {
60+
DatabaseInstance &db = attached_db.GetDatabase();
61+
Connection conn(db);
62+
RunQuery(conn, "LOAD aws");
63+
RunQuery(conn, "BEGIN TRANSACTION");
64+
auto deferred_rollback = dbconnector::Defer([&conn] { conn.Query("ROLLBACK"); });
65+
66+
SecretManager &secret_manager = SecretManager::Get(db);
67+
auto template_secret = GetRdsSecret(secret_manager, conn, token_config.rds_secret_name);
68+
const auto &kv_template_secret = dynamic_cast<const KeyValueSecret &>(*template_secret->secret);
69+
70+
std::string secret_name = token_config.rds_secret_name + "_" + attached_db.GetName() + "_postgres_temp";
71+
std::string create_secret_query =
72+
MakeCreateSecretQuery(token_config.rds_secret_name, secret_name, kv_template_secret);
73+
74+
std::string quoted_secret_name = KeywordHelper::WriteQuoted(secret_name, '"');
75+
RunQuery(conn, create_secret_query, "error creating RDS secret from template: " + token_config.rds_secret_name);
76+
auto deferred_drop_secret =
77+
dbconnector::Defer([&conn, quoted_secret_name] { conn.Query("DROP SECRET " + quoted_secret_name); });
78+
79+
auto secret = GetRdsSecret(secret_manager, conn, secret_name);
80+
const auto &kv_secret = dynamic_cast<const KeyValueSecret &>(*secret->secret);
81+
std::string token = kv_secret.TryGetValue("session_token").ToString();
82+
83+
if (token.find("X-Amz-Algorithm") == std::string::npos) {
84+
throw InvalidConfigurationException("Unable to generate RDS IAM token for secret with name: \"%s\"",
85+
token_config.rds_secret_name);
86+
}
87+
88+
return token;
4989
}
5090

5191
static std::string ExtractString(const KeyValueSecret &kv, const std::string name) {
@@ -64,30 +104,16 @@ PostgresAwsRdsTokenConfig PostgresAws::ExtractTokenConfigFromSecret(optional_ptr
64104
return config;
65105
}
66106
const auto &kv = dynamic_cast<const KeyValueSecret &>(*secret_entry->secret);
67-
Value enabled_val = kv.TryGetValue("aws_rds_iam_auth_enabled");
68-
if (enabled_val.IsNull() || !BooleanValue::Get(enabled_val)) {
69-
return config;
70-
}
71107

72-
config.enabled = true;
73-
config.hostname = ExtractString(kv, "host");
74-
config.port = ExtractString(kv, "port");
75-
config.username = ExtractString(kv, "user");
76-
config.region = ExtractString(kv, "aws_region");
77-
if (config.hostname.empty() || config.port.empty() || config.username.empty()) {
78-
throw BinderException("Invalid AWS RDS IAM auth secret configuration: 'HOST', 'PORT', 'USER' and 'AWS_REGION' "
79-
"parameters must be specified");
108+
config.rds_secret_name = ExtractString(kv, "aws_rds_secret");
109+
if (config.rds_secret_name.empty()) {
110+
return config;
80111
}
81112

82113
std::string password = ExtractString(kv, "password");
83114
if (!password.empty()) {
84115
throw BinderException("Invalid AWS RDS IAM auth secret configuration: 'PASSWORD' parameters must not be "
85-
"specified - IAM token will be used instead of the password");
86-
}
87-
88-
Value expiration_seconds_val = kv.TryGetValue("aws_rds_iam_token_expiration_seconds");
89-
if (!expiration_seconds_val.IsNull()) {
90-
config.expiration_seconds = UBigIntValue::Get(expiration_seconds_val);
116+
"specified - generated IAM token will be used instead of the password");
91117
}
92118

93119
// Build the base connection string (without password)

src/postgres_extension.cpp

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,8 @@ static void LoadInternal(ExtensionLoader &loader) {
157157
RegisterHstoreFunctions(loader);
158158

159159
// Register the new type
160-
SecretType secret_type;
161-
secret_type.name = "postgres";
162-
secret_type.deserializer = KeyValueSecret::Deserialize<KeyValueSecret>;
163-
secret_type.default_provider = "config";
164-
165-
loader.RegisterSecretType(secret_type);
160+
loader.RegisterSecretType(PostgresSecrets::CreateType());
161+
loader.RegisterSecretType(PostgresSecrets::CreateRdsType());
166162

167163
CreateSecretFunction postgres_secret_function = {"postgres", "config", PostgresSecrets::CreateFunction};
168164
PostgresSecrets::SetSecretParameters(postgres_secret_function);

src/postgres_secrets.cpp

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ static const std::unordered_map<std::string, std::string> connection_option_alia
6969

7070
static const std::vector<std::string> other_option_names = {
7171
"uri",
72-
"aws_rds_iam_auth_enabled",
73-
"aws_rds_iam_token_expiration_seconds",
74-
"aws_region"
72+
"aws_rds_secret"
7573
};
7674
// clang-format on
7775

@@ -87,6 +85,22 @@ const std::vector<std::string> &PostgresSecrets::ConnectionOptionNames() {
8785
return connection_option_names;
8886
}
8987

88+
SecretType PostgresSecrets::CreateType() {
89+
SecretType secret_type;
90+
secret_type.name = "postgres";
91+
secret_type.deserializer = KeyValueSecret::Deserialize<KeyValueSecret>;
92+
secret_type.default_provider = "config";
93+
return secret_type;
94+
}
95+
96+
SecretType PostgresSecrets::CreateRdsType() {
97+
SecretType secret_type;
98+
secret_type.name = "rds";
99+
secret_type.deserializer = KeyValueSecret::Deserialize<KeyValueSecret>;
100+
secret_type.default_provider = "credential_chain";
101+
return secret_type;
102+
}
103+
90104
unique_ptr<BaseSecret> PostgresSecrets::CreateFunction(ClientContext &context, CreateSecretInput &input) {
91105
vector<string> prefix_paths;
92106
auto result = make_uniq<KeyValueSecret>(prefix_paths, "postgres", "config", input.name);
@@ -113,9 +127,7 @@ void PostgresSecrets::SetSecretParameters(CreateSecretFunction &function) {
113127
}
114128
// other options
115129
function.named_parameters["uri"] = LogicalType::VARCHAR;
116-
function.named_parameters["aws_rds_iam_auth_enabled"] = LogicalType::BOOLEAN;
117-
function.named_parameters["aws_rds_iam_token_expiration_seconds"] = LogicalType::UBIGINT;
118-
function.named_parameters["aws_region"] = LogicalType::VARCHAR;
130+
function.named_parameters["aws_rds_secret"] = LogicalType::VARCHAR;
119131
}
120132

121133
} // namespace duckdb

src/storage/postgres_catalog.cpp

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ namespace duckdb {
1717
unique_ptr<SecretEntry> GetSecret(ClientContext &context, const string &secret_name) {
1818
auto &secret_manager = SecretManager::Get(context);
1919
auto transaction = CatalogTransaction::GetSystemCatalogTransaction(context);
20-
// FIXME: this should be adjusted once the `GetSecretByName` API supports this use case
21-
auto secret_entry = secret_manager.GetSecretByName(transaction, secret_name, "memory");
20+
auto secret_entry = secret_manager.GetSecretByName(transaction, secret_name);
2221
if (secret_entry) {
2322
return secret_entry;
2423
}
@@ -37,7 +36,7 @@ PostgresCatalog::PostgresCatalog(ClientContext &ctx, AttachedDatabase &db_p, str
3736
connection_pool(make_shared_ptr<PostgresConnectionPool>(*this, ctx)), default_schema(schema_to_load) {
3837
auto secret_entry = GetSecretEntry(ctx, secret_name);
3938
this->rds_token_config = PostgresAws::ExtractTokenConfigFromSecret(secret_entry);
40-
if (!rds_token_config.enabled) {
39+
if (!rds_token_config.Enabled()) {
4140
this->connection_string = CreateConnectionString(secret_entry, attach_path);
4241
}
4342

@@ -103,7 +102,7 @@ string PostgresCatalog::CreateConnectionString(optional_ptr<SecretEntry> secret_
103102
}
104103

105104
string PostgresCatalog::GetConnectionString() {
106-
if (!rds_token_config.enabled) {
105+
if (!rds_token_config.Enabled()) {
107106
return connection_string;
108107
}
109108

@@ -116,12 +115,11 @@ string PostgresCatalog::GetConnectionString() {
116115
expired = true;
117116
} else {
118117
int64_t age_seconds = std::chrono::duration_cast<std::chrono::seconds>(now - rds_token_last_refreshed).count();
119-
int64_t max_age = static_cast<int64_t>(rds_token_config.expiration_seconds) - 60;
120-
expired = age_seconds > max_age;
118+
expired = age_seconds > rds_token_config.MaxAgeSeconds();
121119
}
122120

123121
if (expired) {
124-
this->rds_token = PostgresAws::GenerateRdsAuthToken(rds_token_config);
122+
this->rds_token = PostgresAws::GenerateRdsAuthToken(GetAttached(), rds_token_config);
125123
this->rds_token_last_refreshed = now;
126124
this->connection_string = rds_token_config.base_connection_string +
127125
"password=" + PostgresUtils::EscapeConnectionString(rds_token) + " " + attach_path;

test/sql/storage/attach_rds_iam.test

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,32 @@ statement ok
1010
PRAGMA enable_verification
1111

1212
statement ok
13-
CREATE TEMPORARY SECRET rds_secret (
14-
TYPE POSTGRES,
15-
HOST 'database-1-instance-1.xxxxxxxxxxx.eu-west-1.rds.amazonaws.com',
13+
LOAD postgres
14+
15+
statement ok
16+
LOAD aws
17+
18+
statement ok
19+
CREATE SECRET rds_template_secret (
20+
TYPE rds,
21+
PROVIDER credential_chain,
22+
CHAIN 'env;sso;',
23+
PROFILE 'DatabaseAdministrator-xxx',
24+
REGION 'eu-west-1',
25+
RDS_USER 'postgres',
26+
RDS_HOST 'database-1-instance-1.xxx.eu-west-1.rds.amazonaws.com',
27+
RDS_PORT '5432'
28+
);
29+
30+
statement ok
31+
CREATE SECRET rds_secret (
32+
TYPE postgres,
33+
HOST 'database-1-instance-1.xxx.eu-west-1.rds.amazonaws.com',
1634
PORT '5432',
1735
USER 'postgres',
1836
DATABASE 'postgres',
19-
SSLMODE 'require',
20-
AWS_RDS_IAM_AUTH_ENABLED TRUE,
21-
AWS_REGION 'eu-west-1'
37+
SSLMODE require,
38+
AWS_RDS_SECRET rds_template_secret
2239
);
2340

2441
statement ok

vcpkg.json

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
{
22
"dependencies": [
33
"openssl",
4-
"libpq",
5-
{
6-
"name": "aws-sdk-cpp",
7-
"features": [
8-
"rds",
9-
"sso",
10-
"sts"
11-
]
12-
}
4+
"libpq"
135
],
146
"vcpkg-configuration": {
157
"overlay-ports": [

0 commit comments

Comments
 (0)