Separate mgmt and libpq authentication configs in pageserver. (#3773)

This makes it possible to enable authentication only for the mgmt HTTP
API or the compute API. The HTTP API doesn't need to be directly
accessible from compute nodes, and it can be secured through network
policies. This also allows rolling out authentication in a piecemeal
fashion.
This commit is contained in:
Heikki Linnakangas
2023-03-15 13:52:29 +02:00
committed by GitHub
parent a7ab53c80c
commit 10a5d36af8
12 changed files with 107 additions and 44 deletions

View File

@@ -2,7 +2,8 @@
[pageserver] [pageserver]
listen_pg_addr = '127.0.0.1:64000' listen_pg_addr = '127.0.0.1:64000'
listen_http_addr = '127.0.0.1:9898' listen_http_addr = '127.0.0.1:9898'
auth_type = 'Trust' pg_auth_type = 'Trust'
http_auth_type = 'Trust'
[[safekeepers]] [[safekeepers]]
id = 1 id = 1

View File

@@ -3,7 +3,8 @@
[pageserver] [pageserver]
listen_pg_addr = '127.0.0.1:64000' listen_pg_addr = '127.0.0.1:64000'
listen_http_addr = '127.0.0.1:9898' listen_http_addr = '127.0.0.1:9898'
auth_type = 'Trust' pg_auth_type = 'Trust'
http_auth_type = 'Trust'
[[safekeepers]] [[safekeepers]]
id = 1 id = 1

View File

@@ -53,14 +53,15 @@ listen_addr = '{DEFAULT_BROKER_ADDR}'
id = {DEFAULT_PAGESERVER_ID} id = {DEFAULT_PAGESERVER_ID}
listen_pg_addr = '{DEFAULT_PAGESERVER_PG_ADDR}' listen_pg_addr = '{DEFAULT_PAGESERVER_PG_ADDR}'
listen_http_addr = '{DEFAULT_PAGESERVER_HTTP_ADDR}' listen_http_addr = '{DEFAULT_PAGESERVER_HTTP_ADDR}'
auth_type = '{pageserver_auth_type}' pg_auth_type = '{trust_auth}'
http_auth_type = '{trust_auth}'
[[safekeepers]] [[safekeepers]]
id = {DEFAULT_SAFEKEEPER_ID} id = {DEFAULT_SAFEKEEPER_ID}
pg_port = {DEFAULT_SAFEKEEPER_PG_PORT} pg_port = {DEFAULT_SAFEKEEPER_PG_PORT}
http_port = {DEFAULT_SAFEKEEPER_HTTP_PORT} http_port = {DEFAULT_SAFEKEEPER_HTTP_PORT}
"#, "#,
pageserver_auth_type = AuthType::Trust, trust_auth = AuthType::Trust,
) )
} }
@@ -627,7 +628,7 @@ fn handle_pg(pg_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> {
let node = cplane.nodes.get(&(tenant_id, node_name.to_string())); let node = cplane.nodes.get(&(tenant_id, node_name.to_string()));
let auth_token = if matches!(env.pageserver.auth_type, AuthType::NeonJWT) { let auth_token = if matches!(env.pageserver.pg_auth_type, AuthType::NeonJWT) {
let claims = Claims::new(Some(tenant_id), Scope::Tenant); let claims = Claims::new(Some(tenant_id), Scope::Tenant);
Some(env.generate_auth_token(&claims)?) Some(env.generate_auth_token(&claims)?)

View File

@@ -97,7 +97,7 @@ impl ComputeControlPlane {
}); });
node.create_pgdata()?; node.create_pgdata()?;
node.setup_pg_conf(self.env.pageserver.auth_type)?; node.setup_pg_conf(self.env.pageserver.pg_auth_type)?;
self.nodes self.nodes
.insert((tenant_id, node.name.clone()), Arc::clone(&node)); .insert((tenant_id, node.name.clone()), Arc::clone(&node));

View File

@@ -110,12 +110,14 @@ impl NeonBroker {
pub struct PageServerConf { pub struct PageServerConf {
// node id // node id
pub id: NodeId, pub id: NodeId,
// Pageserver connection settings // Pageserver connection settings
pub listen_pg_addr: String, pub listen_pg_addr: String,
pub listen_http_addr: String, pub listen_http_addr: String,
// used to determine which auth type is used // auth type used for the PG and HTTP ports
pub auth_type: AuthType, pub pg_auth_type: AuthType,
pub http_auth_type: AuthType,
// jwt auth token used for communication with pageserver // jwt auth token used for communication with pageserver
pub auth_token: String, pub auth_token: String,
@@ -127,7 +129,8 @@ impl Default for PageServerConf {
id: NodeId(0), id: NodeId(0),
listen_pg_addr: String::new(), listen_pg_addr: String::new(),
listen_http_addr: String::new(), listen_http_addr: String::new(),
auth_type: AuthType::Trust, pg_auth_type: AuthType::Trust,
http_auth_type: AuthType::Trust,
auth_token: String::new(), auth_token: String::new(),
} }
} }

View File

@@ -82,7 +82,7 @@ impl PageServerNode {
let (host, port) = parse_host_port(&env.pageserver.listen_pg_addr) let (host, port) = parse_host_port(&env.pageserver.listen_pg_addr)
.expect("Unable to parse listen_pg_addr"); .expect("Unable to parse listen_pg_addr");
let port = port.unwrap_or(5432); let port = port.unwrap_or(5432);
let password = if env.pageserver.auth_type == AuthType::NeonJWT { let password = if env.pageserver.pg_auth_type == AuthType::NeonJWT {
Some(env.pageserver.auth_token.clone()) Some(env.pageserver.auth_token.clone())
} else { } else {
None None
@@ -106,25 +106,32 @@ impl PageServerNode {
self.env.pg_distrib_dir_raw().display() self.env.pg_distrib_dir_raw().display()
); );
let authg_type_param = format!("auth_type='{}'", self.env.pageserver.auth_type); let http_auth_type_param =
format!("http_auth_type='{}'", self.env.pageserver.http_auth_type);
let listen_http_addr_param = format!( let listen_http_addr_param = format!(
"listen_http_addr='{}'", "listen_http_addr='{}'",
self.env.pageserver.listen_http_addr self.env.pageserver.listen_http_addr
); );
let pg_auth_type_param = format!("pg_auth_type='{}'", self.env.pageserver.pg_auth_type);
let listen_pg_addr_param = let listen_pg_addr_param =
format!("listen_pg_addr='{}'", self.env.pageserver.listen_pg_addr); format!("listen_pg_addr='{}'", self.env.pageserver.listen_pg_addr);
let broker_endpoint_param = format!("broker_endpoint='{}'", self.env.broker.client_url()); let broker_endpoint_param = format!("broker_endpoint='{}'", self.env.broker.client_url());
let mut overrides = vec![ let mut overrides = vec![
id, id,
pg_distrib_dir_param, pg_distrib_dir_param,
authg_type_param, http_auth_type_param,
pg_auth_type_param,
listen_http_addr_param, listen_http_addr_param,
listen_pg_addr_param, listen_pg_addr_param,
broker_endpoint_param, broker_endpoint_param,
]; ];
if self.env.pageserver.auth_type != AuthType::Trust { if self.env.pageserver.http_auth_type != AuthType::Trust
|| self.env.pageserver.pg_auth_type != AuthType::Trust
{
overrides.push("auth_validation_public_key_path='auth_public_key.pem'".to_owned()); overrides.push("auth_validation_public_key_path='auth_public_key.pem'".to_owned());
} }
overrides overrides
@@ -247,7 +254,10 @@ impl PageServerNode {
} }
fn pageserver_env_variables(&self) -> anyhow::Result<Vec<(String, String)>> { fn pageserver_env_variables(&self) -> anyhow::Result<Vec<(String, String)>> {
Ok(if self.env.pageserver.auth_type != AuthType::Trust { // FIXME: why is this tied to pageserver's auth type? Whether or not the safekeeper
// needs a token, and how to generate that token, seems independent to whether
// the pageserver requires a token in incoming requests.
Ok(if self.env.pageserver.http_auth_type != AuthType::Trust {
// Generate a token to connect from the pageserver to a safekeeper // Generate a token to connect from the pageserver to a safekeeper
let token = self let token = self
.env .env
@@ -283,7 +293,7 @@ impl PageServerNode {
fn http_request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder { fn http_request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
let mut builder = self.http_client.request(method, url); let mut builder = self.http_client.request(method, url);
if self.env.pageserver.auth_type == AuthType::NeonJWT { if self.env.pageserver.http_auth_type == AuthType::NeonJWT {
builder = builder.bearer_auth(&self.env.pageserver.auth_token) builder = builder.bearer_auth(&self.env.pageserver.auth_token)
} }
builder builder

View File

@@ -137,10 +137,12 @@ Each compute should present a token valid for the timeline's tenant.
Pageserver also has HTTP API: some parts are per-tenant, Pageserver also has HTTP API: some parts are per-tenant,
some parts are server-wide, these are different scopes. some parts are server-wide, these are different scopes.
The `auth_type` configuration variable in Pageserver's config may have Authentication can be enabled separately for the HTTP mgmt API, and
either of three values: for the libpq connections from compute. The `http_auth_type` and
`pg_auth_type` configuration variables in Pageserver's config may
have one of these values:
* `Trust` removes all authentication. The outdated `MD5` value does likewise * `Trust` removes all authentication.
* `NeonJWT` enables JWT validation. * `NeonJWT` enables JWT validation.
Tokens are validated using the public key which lies in a PEM file Tokens are validated using the public key which lies in a PEM file
specified in the `auth_validation_public_key_path` config. specified in the `auth_validation_public_key_path` config.

View File

@@ -270,15 +270,31 @@ fn start_pageserver(
WALRECEIVER_RUNTIME.block_on(pageserver::broker_client::init_broker_client(conf))?; WALRECEIVER_RUNTIME.block_on(pageserver::broker_client::init_broker_client(conf))?;
// Initialize authentication for incoming connections // Initialize authentication for incoming connections
let auth = match &conf.auth_type { let http_auth;
AuthType::Trust => None, let pg_auth;
AuthType::NeonJWT => { if conf.http_auth_type == AuthType::NeonJWT || conf.pg_auth_type == AuthType::NeonJWT {
// unwrap is ok because check is performed when creating config, so path is set and file exists // unwrap is ok because check is performed when creating config, so path is set and file exists
let key_path = conf.auth_validation_public_key_path.as_ref().unwrap(); let key_path = conf.auth_validation_public_key_path.as_ref().unwrap();
Some(JwtAuth::from_key_path(key_path)?.into()) info!(
} "Loading public key for verifying JWT tokens from {:#?}",
}; key_path
info!("Using auth: {:#?}", conf.auth_type); );
let auth: Arc<JwtAuth> = Arc::new(JwtAuth::from_key_path(key_path)?);
http_auth = match &conf.http_auth_type {
AuthType::Trust => None,
AuthType::NeonJWT => Some(auth.clone()),
};
pg_auth = match &conf.pg_auth_type {
AuthType::Trust => None,
AuthType::NeonJWT => Some(auth),
};
} else {
http_auth = None;
pg_auth = None;
}
info!("Using auth for http API: {:#?}", conf.http_auth_type);
info!("Using auth for pg connections: {:#?}", conf.pg_auth_type);
match var("NEON_AUTH_TOKEN") { match var("NEON_AUTH_TOKEN") {
Ok(v) => { Ok(v) => {
@@ -308,7 +324,7 @@ fn start_pageserver(
{ {
let _rt_guard = MGMT_REQUEST_RUNTIME.enter(); let _rt_guard = MGMT_REQUEST_RUNTIME.enter();
let router = http::make_router(conf, launch_ts, auth.clone(), remote_storage)? let router = http::make_router(conf, launch_ts, http_auth, remote_storage)?
.build() .build()
.map_err(|err| anyhow!(err))?; .map_err(|err| anyhow!(err))?;
let service = utils::http::RouterService::new(router).unwrap(); let service = utils::http::RouterService::new(router).unwrap();
@@ -382,9 +398,9 @@ fn start_pageserver(
async move { async move {
page_service::libpq_listener_main( page_service::libpq_listener_main(
conf, conf,
auth, pg_auth,
pageserver_listener, pageserver_listener,
conf.auth_type, conf.pg_auth_type,
libpq_ctx, libpq_ctx,
) )
.await .await

View File

@@ -138,9 +138,15 @@ pub struct PageServerConf {
pub pg_distrib_dir: PathBuf, pub pg_distrib_dir: PathBuf,
pub auth_type: AuthType, // Authentication
/// authentication method for the HTTP mgmt API
pub http_auth_type: AuthType,
/// authentication method for libpq connections from compute
pub pg_auth_type: AuthType,
/// Path to a file containing public key for verifying JWT tokens.
/// Used for both mgmt and compute auth, if enabled.
pub auth_validation_public_key_path: Option<PathBuf>, pub auth_validation_public_key_path: Option<PathBuf>,
pub remote_storage_config: Option<RemoteStorageConfig>, pub remote_storage_config: Option<RemoteStorageConfig>,
pub default_tenant_conf: TenantConf, pub default_tenant_conf: TenantConf,
@@ -208,7 +214,8 @@ struct PageServerConfigBuilder {
pg_distrib_dir: BuilderValue<PathBuf>, pg_distrib_dir: BuilderValue<PathBuf>,
auth_type: BuilderValue<AuthType>, http_auth_type: BuilderValue<AuthType>,
pg_auth_type: BuilderValue<AuthType>,
// //
auth_validation_public_key_path: BuilderValue<Option<PathBuf>>, auth_validation_public_key_path: BuilderValue<Option<PathBuf>>,
@@ -251,7 +258,8 @@ impl Default for PageServerConfigBuilder {
pg_distrib_dir: Set(env::current_dir() pg_distrib_dir: Set(env::current_dir()
.expect("cannot access current directory") .expect("cannot access current directory")
.join("pg_install")), .join("pg_install")),
auth_type: Set(AuthType::Trust), http_auth_type: Set(AuthType::Trust),
pg_auth_type: Set(AuthType::Trust),
auth_validation_public_key_path: Set(None), auth_validation_public_key_path: Set(None),
remote_storage_config: Set(None), remote_storage_config: Set(None),
id: NotSet, id: NotSet,
@@ -323,8 +331,12 @@ impl PageServerConfigBuilder {
self.pg_distrib_dir = BuilderValue::Set(pg_distrib_dir) self.pg_distrib_dir = BuilderValue::Set(pg_distrib_dir)
} }
pub fn auth_type(&mut self, auth_type: AuthType) { pub fn http_auth_type(&mut self, auth_type: AuthType) {
self.auth_type = BuilderValue::Set(auth_type) self.http_auth_type = BuilderValue::Set(auth_type)
}
pub fn pg_auth_type(&mut self, auth_type: AuthType) {
self.pg_auth_type = BuilderValue::Set(auth_type)
} }
pub fn auth_validation_public_key_path( pub fn auth_validation_public_key_path(
@@ -419,7 +431,10 @@ impl PageServerConfigBuilder {
pg_distrib_dir: self pg_distrib_dir: self
.pg_distrib_dir .pg_distrib_dir
.ok_or(anyhow!("missing pg_distrib_dir"))?, .ok_or(anyhow!("missing pg_distrib_dir"))?,
auth_type: self.auth_type.ok_or(anyhow!("missing auth_type"))?, http_auth_type: self
.http_auth_type
.ok_or(anyhow!("missing http_auth_type"))?,
pg_auth_type: self.pg_auth_type.ok_or(anyhow!("missing pg_auth_type"))?,
auth_validation_public_key_path: self auth_validation_public_key_path: self
.auth_validation_public_key_path .auth_validation_public_key_path
.ok_or(anyhow!("missing auth_validation_public_key_path"))?, .ok_or(anyhow!("missing auth_validation_public_key_path"))?,
@@ -612,7 +627,8 @@ impl PageServerConf {
"auth_validation_public_key_path" => builder.auth_validation_public_key_path(Some( "auth_validation_public_key_path" => builder.auth_validation_public_key_path(Some(
PathBuf::from(parse_toml_string(key, item)?), PathBuf::from(parse_toml_string(key, item)?),
)), )),
"auth_type" => builder.auth_type(parse_toml_from_str(key, item)?), "http_auth_type" => builder.http_auth_type(parse_toml_from_str(key, item)?),
"pg_auth_type" => builder.pg_auth_type(parse_toml_from_str(key, item)?),
"remote_storage" => { "remote_storage" => {
builder.remote_storage_config(RemoteStorageConfig::from_toml(item)?) builder.remote_storage_config(RemoteStorageConfig::from_toml(item)?)
} }
@@ -647,7 +663,7 @@ impl PageServerConf {
let mut conf = builder.build().context("invalid config")?; let mut conf = builder.build().context("invalid config")?;
if conf.auth_type == AuthType::NeonJWT { if conf.http_auth_type == AuthType::NeonJWT || conf.pg_auth_type == AuthType::NeonJWT {
let auth_validation_public_key_path = conf let auth_validation_public_key_path = conf
.auth_validation_public_key_path .auth_validation_public_key_path
.get_or_insert_with(|| workdir.join("auth_public_key.pem")); .get_or_insert_with(|| workdir.join("auth_public_key.pem"));
@@ -766,7 +782,8 @@ impl PageServerConf {
superuser: "cloud_admin".to_string(), superuser: "cloud_admin".to_string(),
workdir: repo_dir, workdir: repo_dir,
pg_distrib_dir, pg_distrib_dir,
auth_type: AuthType::Trust, http_auth_type: AuthType::Trust,
pg_auth_type: AuthType::Trust,
auth_validation_public_key_path: None, auth_validation_public_key_path: None,
remote_storage_config: None, remote_storage_config: None,
default_tenant_conf: TenantConf::default(), default_tenant_conf: TenantConf::default(),
@@ -951,7 +968,8 @@ log_format = 'json'
max_file_descriptors: defaults::DEFAULT_MAX_FILE_DESCRIPTORS, max_file_descriptors: defaults::DEFAULT_MAX_FILE_DESCRIPTORS,
workdir, workdir,
pg_distrib_dir, pg_distrib_dir,
auth_type: AuthType::Trust, http_auth_type: AuthType::Trust,
pg_auth_type: AuthType::Trust,
auth_validation_public_key_path: None, auth_validation_public_key_path: None,
remote_storage_config: None, remote_storage_config: None,
default_tenant_conf: TenantConf::default(), default_tenant_conf: TenantConf::default(),
@@ -1008,7 +1026,8 @@ log_format = 'json'
max_file_descriptors: 333, max_file_descriptors: 333,
workdir, workdir,
pg_distrib_dir, pg_distrib_dir,
auth_type: AuthType::Trust, http_auth_type: AuthType::Trust,
pg_auth_type: AuthType::Trust,
auth_validation_public_key_path: None, auth_validation_public_key_path: None,
remote_storage_config: None, remote_storage_config: None,
default_tenant_conf: TenantConf::default(), default_tenant_conf: TenantConf::default(),

View File

@@ -82,6 +82,7 @@ fn get_config(request: &Request<Body>) -> &'static PageServerConf {
get_state(request).conf get_state(request).conf
} }
/// Check that the requester is authorized to operate on given tenant
fn check_permission(request: &Request<Body>, tenant_id: Option<TenantId>) -> Result<(), ApiError> { fn check_permission(request: &Request<Body>, tenant_id: Option<TenantId>) -> Result<(), ApiError> {
check_permission_with(request, |claims| { check_permission_with(request, |claims| {
crate::auth::check_permission(claims, tenant_id) crate::auth::check_permission(claims, tenant_id)

View File

@@ -924,7 +924,8 @@ class NeonEnv:
pg=self.port_distributor.get_port(), pg=self.port_distributor.get_port(),
http=self.port_distributor.get_port(), http=self.port_distributor.get_port(),
) )
pageserver_auth_type = "NeonJWT" if config.auth_enabled else "Trust" http_auth_type = "NeonJWT" if config.auth_enabled else "Trust"
pg_auth_type = "NeonJWT" if config.auth_enabled else "Trust"
toml += textwrap.dedent( toml += textwrap.dedent(
f""" f"""
@@ -932,7 +933,8 @@ class NeonEnv:
id=1 id=1
listen_pg_addr = 'localhost:{pageserver_port.pg}' listen_pg_addr = 'localhost:{pageserver_port.pg}'
listen_http_addr = 'localhost:{pageserver_port.http}' listen_http_addr = 'localhost:{pageserver_port.http}'
auth_type = '{pageserver_auth_type}' pg_auth_type = '{pg_auth_type}'
http_auth_type = '{http_auth_type}'
""" """
) )

View File

@@ -246,6 +246,13 @@ def prepare_snapshot(
if get_neon_version(neon_binpath) == "49da498f651b9f3a53b56c7c0697636d880ddfe0": if get_neon_version(neon_binpath) == "49da498f651b9f3a53b56c7c0697636d880ddfe0":
pageserver_config["broker_endpoints"] = etcd_broker_endpoints # old etcd version pageserver_config["broker_endpoints"] = etcd_broker_endpoints # old etcd version
# Older pageserver versions had just one `auth_type` setting. Now there
# are separate settings for pg and http ports. We don't use authentication
# in compatibility tests so just remove authentication related settings.
pageserver_config.pop("auth_type", None)
pageserver_config.pop("pg_auth_type", None)
pageserver_config.pop("http_auth_type", None)
if pg_distrib_dir: if pg_distrib_dir:
pageserver_config["pg_distrib_dir"] = str(pg_distrib_dir) pageserver_config["pg_distrib_dir"] = str(pg_distrib_dir)