IP allowlist on the proxy side (#5906)

## Problem

Per-project IP allowlist:
https://github.com/neondatabase/cloud/issues/8116

## Summary of changes

Implemented IP filtering on the proxy side. 

To retrieve ip allowlist for all scenarios, added `get_auth_info` call
to the control plane for:
* sql-over-http
* password_hack
* cleartext_hack

Added cache with ttl for sql-over-http path

This might slow down a bit, consider using redis in the future.

---------

Co-authored-by: Conrad Ludgate <conrad@neon.tech>
This commit is contained in:
Anna Khanova
2023-11-30 14:14:33 +01:00
committed by GitHub
parent 1e57ddaabc
commit e12e2681e9
23 changed files with 601 additions and 115 deletions
+23 -11
View File
@@ -8,7 +8,7 @@ use pbkdf2::{
Params, Pbkdf2,
};
use pq_proto::StartupMessageParams;
use std::{collections::HashMap, sync::Arc};
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
use std::{
fmt,
task::{ready, Poll},
@@ -21,7 +21,8 @@ use tokio::time;
use tokio_postgres::{AsyncMessage, ReadyForQueryStatus};
use crate::{
auth, console,
auth::{self, check_peer_addr_is_in_list},
console,
proxy::{
neon_options, LatencyTimer, NUM_DB_CONNECTIONS_CLOSED_COUNTER,
NUM_DB_CONNECTIONS_OPENED_COUNTER,
@@ -144,6 +145,7 @@ impl GlobalConnPool {
conn_info: &ConnInfo,
force_new: bool,
session_id: uuid::Uuid,
peer_addr: SocketAddr,
) -> anyhow::Result<Client> {
let mut client: Option<ClientInner> = None;
let mut latency_timer = LatencyTimer::new("http");
@@ -203,6 +205,7 @@ impl GlobalConnPool {
conn_id,
session_id,
latency_timer,
peer_addr,
)
.await
} else {
@@ -225,6 +228,7 @@ impl GlobalConnPool {
conn_id,
session_id,
latency_timer,
peer_addr,
)
.await
};
@@ -401,6 +405,7 @@ async fn connect_to_compute(
conn_id: uuid::Uuid,
session_id: uuid::Uuid,
latency_timer: LatencyTimer,
peer_addr: SocketAddr,
) -> anyhow::Result<ClientInner> {
let tls = config.tls_config.as_ref();
let common_names = tls.and_then(|tls| tls.common_names.clone());
@@ -411,12 +416,13 @@ async fn connect_to_compute(
("application_name", APP_NAME),
("options", conn_info.options.as_deref().unwrap_or("")),
]);
let creds = config
.auth_backend
.as_ref()
.map(|_| auth::ClientCredentials::parse(&params, Some(&conn_info.hostname), common_names))
.transpose()?;
let creds = auth::ClientCredentials::parse(
&params,
Some(&conn_info.hostname),
common_names,
peer_addr,
)?;
let backend = config.auth_backend.as_ref().map(|_| creds);
let console_options = neon_options(&params);
@@ -425,8 +431,14 @@ async fn connect_to_compute(
application_name: Some(APP_NAME),
options: console_options.as_deref(),
};
let node_info = creds
// TODO(anna): this is a bit hacky way, consider using console notification listener.
if !config.disable_ip_check_for_http {
let allowed_ips = backend.get_allowed_ips(&extra).await?;
if !check_peer_addr_is_in_list(&peer_addr.ip(), &allowed_ips) {
return Err(auth::AuthError::ip_address_not_allowed().into());
}
}
let node_info = backend
.wake_compute(&extra)
.await?
.context("missing cache entry from wake_compute")?;
@@ -439,7 +451,7 @@ async fn connect_to_compute(
},
node_info,
&extra,
&creds,
&backend,
latency_timer,
)
.await
+14 -2
View File
@@ -1,3 +1,4 @@
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::bail;
@@ -201,11 +202,19 @@ pub async fn handle(
sni_hostname: Option<String>,
conn_pool: Arc<GlobalConnPool>,
session_id: uuid::Uuid,
peer_addr: SocketAddr,
config: &'static HttpConfig,
) -> Result<Response<Body>, ApiError> {
let result = tokio::time::timeout(
config.timeout,
handle_inner(config, request, sni_hostname, conn_pool, session_id),
handle_inner(
config,
request,
sni_hostname,
conn_pool,
session_id,
peer_addr,
),
)
.await;
let mut response = match result {
@@ -292,6 +301,7 @@ async fn handle_inner(
sni_hostname: Option<String>,
conn_pool: Arc<GlobalConnPool>,
session_id: uuid::Uuid,
peer_addr: SocketAddr,
) -> anyhow::Result<Response<Body>> {
NUM_CONNECTIONS_ACCEPTED_COUNTER
.with_label_values(&["http"])
@@ -351,7 +361,9 @@ async fn handle_inner(
let body = hyper::body::to_bytes(request.into_body()).await?;
let payload: Payload = serde_json::from_slice(&body)?;
let mut client = conn_pool.get(&conn_info, !allow_pool, session_id).await?;
let mut client = conn_pool
.get(&conn_info, !allow_pool, session_id, peer_addr)
.await?;
let mut response = Response::builder()
.status(StatusCode::OK)
+3
View File
@@ -11,6 +11,7 @@ use hyper_tungstenite::{tungstenite::Message, HyperWebsocket, WebSocketStream};
use pin_project_lite::pin_project;
use std::{
net::SocketAddr,
pin::Pin,
task::{ready, Context, Poll},
};
@@ -132,6 +133,7 @@ pub async fn serve_websocket(
cancel_map: &CancelMap,
session_id: uuid::Uuid,
hostname: Option<String>,
peer_addr: SocketAddr,
) -> anyhow::Result<()> {
let websocket = websocket.await?;
handle_client(
@@ -140,6 +142,7 @@ pub async fn serve_websocket(
session_id,
WebSocketRw::new(websocket),
ClientMode::Websockets { hostname },
peer_addr,
)
.await?;
Ok(())