Skip to content

Commit 9d6e4ee

Browse files
refactor(drainer, router): KMS decrypt database password when kms feature is enabled (#733)
1 parent a733eaf commit 9d6e4ee

File tree

11 files changed

+146
-47
lines changed

11 files changed

+146
-47
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/config.example.toml

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,25 @@ request_body_limit = 16_384
1919

2020
# Main SQL data store credentials
2121
[master_database]
22-
username = "db_user" # DB Username
23-
password = "db_pass" # DB Password
24-
host = "localhost" # DB Host
25-
port = 5432 # DB Port
26-
dbname = "hyperswitch_db" # Name of Database
27-
pool_size = 5 # Number of connections to keep open
28-
connection_timeout = 10 # Timeout for database connection in seconds
22+
username = "db_user" # DB Username
23+
password = "db_pass" # DB Password. Only applicable when KMS is disabled.
24+
host = "localhost" # DB Host
25+
port = 5432 # DB Port
26+
dbname = "hyperswitch_db" # Name of Database
27+
pool_size = 5 # Number of connections to keep open
28+
connection_timeout = 10 # Timeout for database connection in seconds
29+
kms_encrypted_password = "" # Base64-encoded (KMS encrypted) ciphertext of the database password. Only applicable when KMS is enabled.
2930

3031
# Replica SQL data store credentials
3132
[replica_database]
32-
username = "replica_user" # DB Username
33-
password = "replica_pass" # DB Password
34-
host = "localhost" # DB Host
35-
port = 5432 # DB Port
36-
dbname = "hyperswitch_db" # Name of Database
37-
pool_size = 5 # Number of connections to keep open
38-
connection_timeout = 10 # Timeout for database connection in seconds
33+
username = "replica_user" # DB Username
34+
password = "replica_pass" # DB Password. Only applicable when KMS is disabled.
35+
host = "localhost" # DB Host
36+
port = 5432 # DB Port
37+
dbname = "hyperswitch_db" # Name of Database
38+
pool_size = 5 # Number of connections to keep open
39+
connection_timeout = 10 # Timeout for database connection in seconds
40+
kms_encrypted_password = "" # Base64-encoded (KMS encrypted) ciphertext of the database password. Only applicable when KMS is enabled.
3941

4042
# Redis credentials
4143
[redis]

crates/drainer/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ readme = "README.md"
88
license = "Apache-2.0"
99

1010
[features]
11-
vergen = [ "router_env/vergen" ]
11+
kms = ["external_services/kms"]
12+
vergen = ["router_env/vergen"]
1213

1314
[dependencies]
1415
async-bb8-diesel = { git = "https://github.com/juspay/async-bb8-diesel", rev = "9a71d142726dbc33f41c1fd935ddaa79841c7be5" }
@@ -26,9 +27,10 @@ tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] }
2627

2728
# First Party Crates
2829
common_utils = { version = "0.1.0", path = "../common_utils" }
30+
external_services = { version = "0.1.0", path = "../external_services" }
2931
redis_interface = { version = "0.1.0", path = "../redis_interface" }
3032
router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] }
3133
storage_models = { version = "0.1.0", path = "../storage_models", features = ["kv_store"] }
3234

3335
[build-dependencies]
34-
router_env = { version = "0.1.0", path = "../router_env", default-features = false }
36+
router_env = { version = "0.1.0", path = "../router_env", default-features = false }

crates/drainer/src/connection.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use bb8::PooledConnection;
22
use diesel::PgConnection;
3+
#[cfg(feature = "kms")]
4+
use external_services::kms;
35

46
use crate::settings::Database;
57

@@ -15,13 +17,29 @@ pub async fn redis_connection(
1517
}
1618

1719
#[allow(clippy::expect_used)]
18-
pub async fn diesel_make_pg_pool(database: &Database, _test_transaction: bool) -> PgPool {
20+
pub async fn diesel_make_pg_pool(
21+
database: &Database,
22+
_test_transaction: bool,
23+
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
24+
) -> PgPool {
25+
#[cfg(feature = "kms")]
26+
let password = kms::get_kms_client(kms_config)
27+
.await
28+
.decrypt(&database.kms_encrypted_password)
29+
.await
30+
.expect("Failed to KMS decrypt database password");
31+
32+
#[cfg(not(feature = "kms"))]
33+
let password = &database.password;
34+
1935
let database_url = format!(
2036
"postgres://{}:{}@{}:{}/{}",
21-
database.username, database.password, database.host, database.port, database.dbname
37+
database.username, password, database.host, database.port, database.dbname
2238
);
2339
let manager = async_bb8_diesel::ConnectionManager::<PgConnection>::new(database_url);
24-
let pool = bb8::Pool::builder().max_size(database.pool_size);
40+
let pool = bb8::Pool::builder()
41+
.max_size(database.pool_size)
42+
.connection_timeout(std::time::Duration::from_secs(database.connection_timeout));
2543

2644
pool.build(manager)
2745
.await

crates/drainer/src/services.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ pub struct StoreConfig {
1818
impl Store {
1919
pub async fn new(config: &crate::settings::Settings, test_transaction: bool) -> Self {
2020
Self {
21-
master_pool: diesel_make_pg_pool(&config.master_database, test_transaction).await,
21+
master_pool: diesel_make_pg_pool(
22+
&config.master_database,
23+
test_transaction,
24+
#[cfg(feature = "kms")]
25+
&config.kms,
26+
)
27+
.await,
2228
redis_conn: Arc::new(crate::connection::redis_connection(config).await),
2329
config: StoreConfig {
2430
drainer_stream_name: config.drainer.stream_name.clone(),

crates/drainer/src/settings.rs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::path::PathBuf;
22

33
use common_utils::ext_traits::ConfigExt;
44
use config::{Environment, File};
5+
#[cfg(feature = "kms")]
6+
use external_services::kms;
57
use redis_interface as redis;
68
pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry};
79
use router_env::{env, logger};
@@ -25,17 +27,23 @@ pub struct Settings {
2527
pub redis: redis::RedisSettings,
2628
pub log: Log,
2729
pub drainer: DrainerSettings,
30+
#[cfg(feature = "kms")]
31+
pub kms: kms::KmsConfig,
2832
}
2933

3034
#[derive(Debug, Deserialize, Clone)]
3135
#[serde(default)]
3236
pub struct Database {
3337
pub username: String,
38+
#[cfg(not(feature = "kms"))]
3439
pub password: String,
3540
pub host: String,
3641
pub port: u16,
3742
pub dbname: String,
3843
pub pool_size: u32,
44+
pub connection_timeout: u64,
45+
#[cfg(feature = "kms")]
46+
pub kms_encrypted_password: String,
3947
}
4048

4149
#[derive(Debug, Clone, Deserialize)]
@@ -51,12 +59,16 @@ pub struct DrainerSettings {
5159
impl Default for Database {
5260
fn default() -> Self {
5361
Self {
54-
username: String::default(),
55-
password: String::default(),
62+
username: String::new(),
63+
#[cfg(not(feature = "kms"))]
64+
password: String::new(),
5665
host: "localhost".into(),
5766
port: 5432,
58-
dbname: String::default(),
67+
dbname: String::new(),
5968
pool_size: 5,
69+
connection_timeout: 10,
70+
#[cfg(feature = "kms")]
71+
kms_encrypted_password: String::new(),
6072
}
6173
}
6274
}
@@ -77,29 +89,41 @@ impl Database {
7789
fn validate(&self) -> Result<(), errors::DrainerError> {
7890
use common_utils::fp_utils::when;
7991

80-
when(self.username.is_default_or_empty(), || {
92+
when(self.host.is_default_or_empty(), || {
8193
Err(errors::DrainerError::ConfigParsingError(
82-
"database username must not be empty".into(),
94+
"database host must not be empty".into(),
8395
))
8496
})?;
8597

86-
when(self.password.is_default_or_empty(), || {
98+
when(self.dbname.is_default_or_empty(), || {
8799
Err(errors::DrainerError::ConfigParsingError(
88-
"database user password must not be empty".into(),
100+
"database name must not be empty".into(),
89101
))
90102
})?;
91103

92-
when(self.host.is_default_or_empty(), || {
104+
when(self.username.is_default_or_empty(), || {
93105
Err(errors::DrainerError::ConfigParsingError(
94-
"database host must not be empty".into(),
106+
"database user username must not be empty".into(),
95107
))
96108
})?;
97109

98-
when(self.dbname.is_default_or_empty(), || {
99-
Err(errors::DrainerError::ConfigParsingError(
100-
"database name must not be empty".into(),
101-
))
102-
})
110+
#[cfg(not(feature = "kms"))]
111+
{
112+
when(self.password.is_default_or_empty(), || {
113+
Err(errors::DrainerError::ConfigParsingError(
114+
"database user password must not be empty".into(),
115+
))
116+
})
117+
}
118+
119+
#[cfg(feature = "kms")]
120+
{
121+
when(self.kms_encrypted_password.is_default_or_empty(), || {
122+
Err(errors::DrainerError::ConfigParsingError(
123+
"database KMS encrypted password must not be empty".into(),
124+
))
125+
})
126+
}
103127
}
104128
}
105129

crates/router/src/configs/defaults.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ impl Default for super::settings::Database {
1515
fn default() -> Self {
1616
Self {
1717
username: String::new(),
18+
#[cfg(not(feature = "kms"))]
1819
password: String::new(),
1920
host: "localhost".into(),
2021
port: 5432,
2122
dbname: String::new(),
2223
pool_size: 5,
2324
connection_timeout: 10,
25+
#[cfg(feature = "kms")]
26+
kms_encrypted_password: String::new(),
2427
}
2528
}
2629
}

crates/router/src/configs/settings.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,15 @@ pub struct Server {
223223
#[serde(default)]
224224
pub struct Database {
225225
pub username: String,
226+
#[cfg(not(feature = "kms"))]
226227
pub password: String,
227228
pub host: String,
228229
pub port: u16,
229230
pub dbname: String,
230231
pub pool_size: u32,
231232
pub connection_timeout: u64,
233+
#[cfg(feature = "kms")]
234+
pub kms_encrypted_password: String,
232235
}
233236

234237
#[derive(Debug, Deserialize, Clone)]

crates/router/src/configs/validations.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,35 @@ impl super::settings::Database {
6161
))
6262
})?;
6363

64-
when(self.username.is_default_or_empty(), || {
64+
when(self.dbname.is_default_or_empty(), || {
6565
Err(ApplicationError::InvalidConfigurationValueError(
66-
"database user username must not be empty".into(),
66+
"database name must not be empty".into(),
6767
))
6868
})?;
6969

70-
when(self.password.is_default_or_empty(), || {
70+
when(self.username.is_default_or_empty(), || {
7171
Err(ApplicationError::InvalidConfigurationValueError(
72-
"database user password must not be empty".into(),
72+
"database user username must not be empty".into(),
7373
))
7474
})?;
7575

76-
when(self.dbname.is_default_or_empty(), || {
77-
Err(ApplicationError::InvalidConfigurationValueError(
78-
"database name must not be empty".into(),
79-
))
80-
})
76+
#[cfg(not(feature = "kms"))]
77+
{
78+
when(self.password.is_default_or_empty(), || {
79+
Err(ApplicationError::InvalidConfigurationValueError(
80+
"database user password must not be empty".into(),
81+
))
82+
})
83+
}
84+
85+
#[cfg(feature = "kms")]
86+
{
87+
when(self.kms_encrypted_password.is_default_or_empty(), || {
88+
Err(ApplicationError::InvalidConfigurationValueError(
89+
"database KMS encrypted password must not be empty".into(),
90+
))
91+
})
92+
}
8193
}
8294
}
8395

crates/router/src/connection.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use async_bb8_diesel::{AsyncConnection, ConnectionError};
22
use bb8::{CustomizeConnection, PooledConnection};
33
use diesel::PgConnection;
44
use error_stack::{IntoReport, ResultExt};
5+
#[cfg(feature = "kms")]
6+
use external_services::kms;
57

68
use crate::{configs::settings::Database, errors};
79

@@ -37,10 +39,24 @@ pub async fn redis_connection(
3739
}
3840

3941
#[allow(clippy::expect_used)]
40-
pub async fn diesel_make_pg_pool(database: &Database, test_transaction: bool) -> PgPool {
42+
pub async fn diesel_make_pg_pool(
43+
database: &Database,
44+
test_transaction: bool,
45+
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
46+
) -> PgPool {
47+
#[cfg(feature = "kms")]
48+
let password = kms::get_kms_client(kms_config)
49+
.await
50+
.decrypt(&database.kms_encrypted_password)
51+
.await
52+
.expect("Failed to KMS decrypt database password");
53+
54+
#[cfg(not(feature = "kms"))]
55+
let password = &database.password;
56+
4157
let database_url = format!(
4258
"postgres://{}:{}@{}:{}/{}",
43-
database.username, database.password, database.host, database.port, database.dbname
59+
database.username, password, database.host, database.port, database.dbname
4460
);
4561
let manager = async_bb8_diesel::ConnectionManager::<PgConnection>::new(database_url);
4662
let mut pool = bb8::Pool::builder()

0 commit comments

Comments
 (0)