Skip to content

Commit 6f768a8

Browse files
authored
Auth passthrough (auth_query) (#266)
* Add a new exec_simple_query method This adds a new `exec_simple_query` method so we can make 'out of band' queries to servers that don't interfere with pools at all. In order to reuse startup code for making these simple queries, we need to set the stats (`Reporter`) optional, so using these simple queries wont interfere with stats. * Add auth passthough (auth_query) Adds a feature that allows setting auth passthrough for md5 auth. It adds 3 new (general and pool) config parameters: - `auth_query`: An string containing a query that will be executed on boot to obtain the hash of a given user. This query have to use a placeholder `$1`, so pgcat can replace it with the user its trying to fetch the hash from. - `auth_query_user`: The user to use for connecting to the server and executing the auth_query. - `auth_query_password`: The password to use for connecting to the server and executing the auth_query. The configuration can be done either on the general config (so pools share them) or in a per-pool basis. The behavior is, at boot time, when validating server connections, a hash is fetched per server and stored in the pool. When new server connections are created, and no cleartext password is specified, the obtained hash is used for creating them, if the hash could not be obtained for whatever reason, it retries it. When client authentication is tried, it uses cleartext passwords if specified, it not, it checks whether we have query_auth set up, if so, it tries to use the obtained hash for making client auth. If there is no hash (we could not obtain one when validating the connection), a new fetch is tried. Once we have a hash, we authenticate using it against whathever the client has sent us, if there is a failure we refetch the hash and retry auth (so password changes can be done). The idea with this 'retrial' mechanism is to make it fault tolerant, so if for whatever reason hash could not be obtained during connection validation, or the password has change, we can still connect later. * Add documentation for Auth passthrough
1 parent 0757d7f commit 6f768a8

24 files changed

+1026
-31
lines changed

.circleci/config.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ jobs:
4646
POSTGRES_PASSWORD: postgres
4747
POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256
4848

49+
- image: postgres:14
50+
command: ["postgres", "-p", "10432", "-c", "shared_preload_libraries=pg_stat_statements"]
51+
environment:
52+
POSTGRES_USER: postgres
53+
POSTGRES_DB: postgres
54+
POSTGRES_PASSWORD: postgres
55+
POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5
56+
4957
# Add steps to the job
5058
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
5159
steps:

.circleci/run_tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 5432 -U postgres -f tests/sharding/q
1919
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 7432 -U postgres -f tests/sharding/query_routing_setup.sql
2020
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 8432 -U postgres -f tests/sharding/query_routing_setup.sql
2121
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 9432 -U postgres -f tests/sharding/query_routing_setup.sql
22+
PGPASSWORD=postgres psql -e -h 127.0.0.1 -p 10432 -U postgres -f tests/sharding/query_routing_setup.sql
2223

2324
PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard0 -i
2425
PGPASSWORD=sharding_user pgbench -h 127.0.0.1 -U sharding_user shard1 -i

.rustfmt.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
edition = "2021"
2+
hard_tabs = false

CONFIG.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,41 @@ Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DAT
175175
### admin_password
176176
```
177177
path: general.admin_password
178-
default: "admin_pass"
178+
default: <UNSET>
179179
```
180180

181181
Password to access the virtual administrative database
182182

183+
### auth_query (experimental)
184+
```
185+
path: general.auth_query
186+
default: <UNSET>
187+
```
188+
189+
Query to be sent to servers to obtain the hash used for md5 authentication. The connection will be
190+
established using the database configured in the pool. This parameter is inherited by every pool
191+
and can be redefined in pool configuration.
192+
193+
### auth_query_user (experimental)
194+
```
195+
path: general.auth_query_user
196+
default: <UNSET>
197+
```
198+
199+
User to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
200+
specified in `auth_query_user`. The connection will be established using the database configured in the pool.
201+
This parameter is inherited by every pool and can be redefined in pool configuration.
202+
203+
### auth_query_password (experimental)
204+
```
205+
path: general.auth_query_password
206+
default: <UNSET>
207+
```
208+
209+
Password to be used for connecting to servers to obtain the hash used for md5 authentication by sending the query
210+
specified in `auth_query_user`. The connection will be established using the database configured in the pool.
211+
This parameter is inherited by every pool and can be redefined in pool configuration.
212+
183213
## `pools.<pool_name>` Section
184214

185215
### pool_mode
@@ -281,6 +311,30 @@ default: 3000
281311

282312
Connect timeout can be overwritten in the pool
283313

314+
### auth_query (experimental)
315+
```
316+
path: general.auth_query
317+
default: <UNSET>
318+
```
319+
320+
Auth query can be overwritten in the pool
321+
322+
### auth_query_user (experimental)
323+
```
324+
path: general.auth_query_user
325+
default: <UNSET>
326+
```
327+
328+
Auth query user can be overwritten in the pool
329+
330+
### auth_query_password (experimental)
331+
```
332+
path: general.auth_query_password
333+
default: <UNSET>
334+
```
335+
336+
Auth query password can be overwritten in the pool
337+
284338
## `pools.<pool_name>.users.<user_index>` Section
285339

286340
### username

Cargo.lock

Lines changed: 41 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ version = "1.0.0"
44
edition = "2021"
55

66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7-
87
[dependencies]
98
tokio = { version = "1", features = ["full"] }
109
bytes = "1"
@@ -38,6 +37,8 @@ futures = "0.3"
3837
socket2 = { version = "0.4.7", features = ["all"] }
3938
nix = "0.26.2"
4039
atomic_enum = "0.2.0"
40+
postgres-protocol = "0.6.4"
41+
fallible-iterator = "0.2"
4142

4243
[target.'cfg(not(target_env = "msvc"))'.dependencies]
4344
jemallocator = "0.5.0"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ PostgreSQL pooler and proxy (like PgBouncer) with support for sharding, load bal
2525
| Sharding using comments parsing/Regex | **Experimental** | Clients can include shard information (sharding key, shard ID) in the query comments. |
2626
| Automatic sharding | **Experimental** | PgCat can parse queries, detect sharding keys automatically, and route queries to the correct shard. |
2727
| Mirroring | **Experimental** | Mirror queries between multiple databases in order to test servers with realistic production traffic. |
28+
| Auth passthrough | **Experimental** | MD5 password authentication can be configured to use an `auth_query` so no cleartext passwords are needed in the config file. |
2829

2930

3031
## Status

dev/docker-compose.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ services:
5858
POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256 --auth=scram-sha-256
5959
PGPORT: 9432
6060
command: ["postgres", "-p", "9432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"]
61+
pg5:
62+
<<: *common-definition-pg
63+
environment:
64+
<<: *common-env-pg
65+
POSTGRES_INITDB_ARGS: --auth-local=md5 --auth-host=md5 --auth=md5
66+
PGPORT: 10432
67+
command: ["postgres", "-p", "5432", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all", "-c", "pg_stat_statements.max=100000"]
6168

6269
toxiproxy:
6370
build: .
@@ -71,6 +78,7 @@ services:
7178
- pg2
7279
- pg3
7380
- pg4
81+
- pg5
7482

7583
pgcat-shell:
7684
stdin_open: true

src/auth_passthrough.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use crate::errors::Error;
2+
use crate::server::Server;
3+
use log::debug;
4+
5+
#[derive(Clone, Debug)]
6+
pub struct AuthPassthrough {
7+
password: String,
8+
query: String,
9+
user: String,
10+
}
11+
12+
impl AuthPassthrough {
13+
/// Initializes an AuthPassthrough.
14+
pub fn new(query: &str, user: &str, password: &str) -> Self {
15+
AuthPassthrough {
16+
password: password.to_string(),
17+
query: query.to_string(),
18+
user: user.to_string(),
19+
}
20+
}
21+
22+
/// Returns an AuthPassthrough given the pool configuration.
23+
/// If any of required values is not set, None is returned.
24+
pub fn from_pool_config(pool_config: &crate::config::Pool) -> Option<Self> {
25+
if pool_config.is_auth_query_configured() {
26+
return Some(AuthPassthrough::new(
27+
pool_config.auth_query.as_ref().unwrap(),
28+
pool_config.auth_query_user.as_ref().unwrap(),
29+
pool_config.auth_query_password.as_ref().unwrap(),
30+
));
31+
}
32+
33+
None
34+
}
35+
36+
/// Returns an AuthPassthrough given the pool settings.
37+
/// If any of required values is not set, None is returned.
38+
pub fn from_pool_settings(pool_settings: &crate::pool::PoolSettings) -> Option<Self> {
39+
let pool_config = crate::config::Pool {
40+
auth_query: pool_settings.auth_query.clone(),
41+
auth_query_password: pool_settings.auth_query_password.clone(),
42+
auth_query_user: pool_settings.auth_query_user.clone(),
43+
..Default::default()
44+
};
45+
46+
AuthPassthrough::from_pool_config(&pool_config)
47+
}
48+
49+
/// Connects to server and executes auth_query for the specified address.
50+
/// If the response is a row with two columns containing the username set in the address.
51+
/// and its MD5 hash, the MD5 hash returned.
52+
///
53+
/// Note that the query is executed, changing $1 with the name of the user
54+
/// this is so we only hold in memory (and transfer) the least amount of 'sensitive' data.
55+
/// Also, it is compatible with pgbouncer.
56+
///
57+
/// # Arguments
58+
///
59+
/// * `address` - An Address of the server we want to connect to. The username for the hash will be obtained from this value.
60+
///
61+
/// # Examples
62+
///
63+
/// ```
64+
/// use pgcat::auth_passthrough::AuthPassthrough;
65+
/// use pgcat::config::Address;
66+
/// let auth_passthrough = AuthPassthrough::new("SELECT * FROM public.user_lookup('$1');", "postgres", "postgres");
67+
/// auth_passthrough.fetch_hash(&Address::default());
68+
/// ```
69+
///
70+
pub async fn fetch_hash(&self, address: &crate::config::Address) -> Result<String, Error> {
71+
let auth_user = crate::config::User {
72+
username: self.user.clone(),
73+
password: Some(self.password.clone()),
74+
pool_size: 1,
75+
statement_timeout: 0,
76+
};
77+
78+
let user = &address.username;
79+
80+
debug!("Connecting to server to obtain auth hashes.");
81+
let auth_query = self.query.replace("$1", user);
82+
match Server::exec_simple_query(address, &auth_user, &auth_query).await {
83+
Ok(password_data) => {
84+
if password_data.len() == 2 && password_data.first().unwrap() == user {
85+
if let Some(stripped_hash) = password_data.last().unwrap().to_string().strip_prefix("md5") {
86+
Ok(stripped_hash.to_string())
87+
}
88+
else {
89+
Err(Error::AuthPassthroughError(
90+
"Obtained hash from auth_query does not seem to be in md5 format.".to_string(),
91+
))
92+
}
93+
} else {
94+
Err(Error::AuthPassthroughError(
95+
"Data obtained from query does not follow the scheme 'user','hash'."
96+
.to_string(),
97+
))
98+
}
99+
}
100+
Err(err) => {
101+
Err(Error::AuthPassthroughError(
102+
format!("Error trying to obtain password from auth_query, ignoring hash for user '{}'. Error: {:?}",
103+
user, err)))
104+
}
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)