How to Connect Securely to Amazon RDS for PostgreSQL using Tokio and Rustls
Recently I ran into a curious problem while working on a piece of asynchronous database code for a micro-service written in Rust -- it looked something like this:
DEBUG rustls::client::tls12 > Server DNS name is DNSName("database-1.xq7f5vzbpq1x.ca-central-1.rds.amazonaws.com")
WARN rustls::session > Sending fatal alert BadCertificate
Error: Backend(Error { kind: Tls, cause: Some(Kind(InvalidInput)) })
This particular module used Tokio Postgres along with Deadpool Postgres -- part of my favorite Rust stack -- to interact with an AWS RDS for PostgreSQL database. Of course, everything worked great when I ran it against a Postgres instance deployed in my local Docker container -- the Deadpool documentation contains easy-to-follow examples that can get you going in minutes.
I knew I’d have to support connecting to the database securely in order to deploy the code to production -- the deployment policy requires (as it should!) that all connections to RDS instances use TLS. By default, the Tokio Postgres crate supports plain-text connections to help you get started, but luckily also outlines steps to enable TLS.
To be honest, I was a bit crestfallen when I didn’t see Rustls listed as one of the supported methods; only (effectively) OpenSSL. After all, there are many good reasons to use Rustls, in addition to the fact that it was already part of my stack (e.g., both Reqwest and Warp support it) and would allow me to crank out statically linked binaries more easily.
Never fear -- the amazing Rust community has your back! A quick search through crates.io produced the tokio-postgres-rustls crate, which was exactly what I needed; after a small addition to my configuration and database connection pool setup, I was on my way:
use config::{
Config,
Environment,
};
use deadpool_postgres::{
Config as PoolConfig,
Pool,
};
use rustls::ClientConfig as RustlsClientConfig;
use serde::Deserialize;
use tokio_postgres::NoTls;
use tokio_postgres_rustls::MakeRustlsConnect;
#[derive(Debug, Deserialize)]
struct Settings {
pg: PoolConfig,
#[serde(default)]
use_tls: bool,
}
#[tokio::main]
async fn run(pool: Pool) -> Result<(), Box<dyn std::error::Error>> {
let client = pool.get().await?;
...
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = Config::new();
config.merge(Environment::new())?;
let settings: Settings = config.try_into()?;
let pool = if settings.use_tls {
let tls_config = RustlsClientConfig::new();
let tls = MakeRustlsConnect::new(tls_config);
settings.pg.create_pool(tls)?
} else {
settings.pg.create_pool(NoTls)?
};
run(pool)
}
Reality Bites
As luck would have it, after pointing my Postgres client configuration to the RDS instance, I was met with the unpleasant surprise I had mentioned earlier -— BadCertificate error! Wha… wha… whad’ya MEAN??
I must admit -- in all my excitement I only skimmed through tokio-postgres-rustls documentation, and didn’t even peek at rustls, since the basic configuration example happened to compile just fine (yikes!). After a stern mental self-admonishment I carefully scanned Rustls’s Getting Started example and sure enough, I discovered that I may have missed a step:
tls_config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
Of course! Rustls (probably) doesn’t come with any pre-configured root certificates! That would make sense -— Rust libraries tend to defer use-case-specific decisions to the user -- why include files/bytes that may not even be needed?
Take Two
Ok -- new dependency included, Mozilla’s trusted root certificates added. That must have been the problem! Alas, the same error:
DEBUG rustls::client::tls12 > Server DNS name is DNSName("database-1.xq7f5vzbpq1x.ca-central-1.rds.amazonaws.com")
WARN rustls::session > Sending fatal alert BadCertificate
Error: Backend(Error { kind: Tls, cause: Some(Kind(InvalidInput)) })
“This means war!”, I thought to myself. Rather, it meant that I had to read the documentation more carefully -— a little bit of sleuthing revealed that AWS utilized their own/separate root certificates for their RDS instances. This, it turns out, is also helpfully indicated in the database instance details in RDS console -— if you know what to look for!
Third Time’s a Charm
After downloading the requisite root certificate and adding it to my database pool’s TLS configuration:
use config::{
Config,
Environment,
};
use deadpool_postgres::{
Config as PoolConfig,
Pool,
};
use rustls::ClientConfig as RustlsClientConfig;
use serde::Deserialize;
use std::{
fs::File,
io::BufReader,
};
use tokio_postgres::NoTls;
use tokio_postgres_rustls::MakeRustlsConnect;
#[derive(Debug, Deserialize)]
struct Settings {
pg: PoolConfig,
db_ca_cert: Option<String>,
}
#[tokio::main]
async fn run(pool: Pool) -> Result<(), Box<dyn std::error::Error>> {
let client = pool.get().await?;
...
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = Config::new();
config.merge(Environment::new())?;
let settings: Settings = config.try_into()?;
let pool = if let Some(ca_cert) = settings.db_ca_cert {
let mut tls_config = RustlsClientConfig::new();
let cert_file = File::open(&ca_cert)?;
let mut buf = BufReader::new(cert_file);
tls_config.root_store.add_pem_file(&mut buf).map_err(|_| {
anyhow::anyhow!("failed to read database root certificate: {}", ca_cert)
})?;
let tls = MakeRustlsConnect::new(tls_config);
settings.pg.create_pool(tls)?
} else {
settings.pg.create_pool(NoTls)?
};
run(pool)
}
Success!
DEBUG rustls::anchors > add_pem_file processed 1 valid and 0 invalid certs
DEBUG rustls::client::hs > No cached session for DNSNameRef("database-1.xq7f5vzbpq1x.ca-central-1.rds.amazonaws.com")
DEBUG rustls::client::hs > Not resuming any session
DEBUG rustls::client::hs > ALPN protocol is None
DEBUG rustls::client::hs > Using ciphersuite TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
DEBUG rustls::client::tls12 > ECDHE curve is ECParameters { curve_type: NamedCurve, named_group: secp256r1 }
DEBUG rustls::client::tls12 > Got CertificateRequest CertificateRequestPayload { certtypes: [RSASign, DSSSign, ECDSASign], sigschemes: [RSA_PKCS1_SHA512, Unknown(1538), ECDSA_NISTP521_SHA512, RSA_PKCS1_SHA384, Unknown(1282), ECDSA_NISTP384_SHA384, RSA_PKCS1_SHA256, Unknown(1026), ECDSA_NISTP256_SHA256, Unknown(769), Unknown(770), Unknown(771), RSA_PKCS1_SHA1, Unknown(514), ECDSA_SHA1_Legacy], canames: [PayloadU16([48, 129, 151, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 19, 48, 17, 6, 3, 85, 4, 8, 12, 10, 87, 97, 115, 104, 105, 110, 103, 116, 111, 110, 49, 16, 48, 14, 6, 3, 85, 4, 7, 12, 7, 83, 101, 97, 116, 116, 108, 101, 49, 34, 48, 32, 6, 3, 85, 4, 10, 12, 25, 65, 109, 97, 122, 111, 110, 32, 87, 101, 98, 32, 83, 101, 114, 118, 105, 99, 101, 115, 44, 32, 73, 110, 99, 46, 49, 19, 48, 17, 6, 3, 85, 4, 11, 12, 10, 65, 109, 97, 122, 111, 110, 32, 82, 68, 83, 49, 40, 48, 38, 6, 3, 85, 4, 3, 12, 31, 65, 109, 97, 122, 111, 110, 32, 82, 68, 83, 32, 99, 97, 45, 99, 101, 110, 116, 114, 97, 108, 45, 49, 32, 50, 48, 49, 57, 32, 67, 65])] }
DEBUG rustls::client::tls12 > Client auth requested but no cert/sigscheme available
DEBUG rustls::client::tls12 > Server cert is [Certificate(b"0\x82\x04\xe30\x82\x03\xcb\xa0\x03\x02\x01\x02\x02\x10\0\xc9\x0b^\x92\x04V\xa9\xd4#b*yh ;
...
\xcc\xf5\xb8\tu\xef\x84\xb9\x84\xd3d\xc0\xf7\xf1\xde\x0b\r\xca\x10r0\x89\xc3n\x11\xfc")]
DEBUG rustls::client::tls12 > Server DNS name is DNSName("database-1.xq7f5vzbpq1x.ca-central-1.rds.amazonaws.com")
DEBUG rustls::client::tls12 > Session not saved: server didn't allocate id or ticket
DEBUG tokio_postgres::prepare > preparing query s0: SELECT * FROM information_schema.information_schema_catalog_name
DEBUG tokio_postgres::query > executing statement s0 with parameters: []
INFO tokio_postgres_rustls_rds_demo > postgres
Conclusion
There are many great libraries that help you build modern micro-services and native cloud applications in Rust. If you ever find yourself working with PostgreSQL, use Tokio Postgres with Deadpool to build responsive, scalable applications with Rust’s powerful asynchronous constructs.
To secure your client connection, use Rustls; however, make sure it is configured properly either with Mozilla’s trusted root certificates or your service provider’s own root certificates!
To try the approach outlined in this article, check out the demo project hosted on GitHub.
Splash photo by Paweł Czerwiński on Unsplash.
Top comments (0)