diff --git a/compute_tools/src/http/extract/mod.rs b/compute_tools/src/http/extract/mod.rs
index 589681cfe2..93319c36c8 100644
--- a/compute_tools/src/http/extract/mod.rs
+++ b/compute_tools/src/http/extract/mod.rs
@@ -6,4 +6,5 @@ pub(crate) mod request_id;
pub(crate) use json::Json;
pub(crate) use path::Path;
pub(crate) use query::Query;
+#[allow(unused)]
pub(crate) use request_id::RequestId;
diff --git a/compute_tools/src/http/middleware/authorize.rs b/compute_tools/src/http/middleware/authorize.rs
index e6c3269b15..2d0f411d7a 100644
--- a/compute_tools/src/http/middleware/authorize.rs
+++ b/compute_tools/src/http/middleware/authorize.rs
@@ -13,7 +13,7 @@ use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation, jwk::JwkSet};
use tower_http::auth::AsyncAuthorizeRequest;
use tracing::{debug, warn};
-use crate::http::{JsonResponse, extract::RequestId};
+use crate::http::JsonResponse;
#[derive(Clone, Debug)]
pub(in crate::http) struct Authorize {
@@ -52,18 +52,6 @@ impl AsyncAuthorizeRequest
for Authorize {
let validation = self.validation.clone();
Box::pin(async move {
- let request_id = request.extract_parts::().await.unwrap();
-
- // TODO(tristan957): Remove this stanza after teaching neon_local
- // and the regression tests to use a JWT + JWKS.
- //
- // https://github.com/neondatabase/neon/issues/11316
- if cfg!(feature = "testing") {
- warn!(%request_id, "Skipping compute_ctl authorization check");
-
- return Ok(request);
- }
-
let TypedHeader(Authorization(bearer)) = request
.extract_parts::>>()
.await
diff --git a/control_plane/src/bin/neon_local.rs b/control_plane/src/bin/neon_local.rs
index 950b264163..1ff4295438 100644
--- a/control_plane/src/bin/neon_local.rs
+++ b/control_plane/src/bin/neon_local.rs
@@ -1544,7 +1544,7 @@ async fn handle_endpoint(subcmd: &EndpointCmd, env: &local_env::LocalEnv) -> Res
.with_context(|| format!("postgres endpoint {endpoint_id} is not found"))?;
let jwt = endpoint.generate_jwt()?;
- println!("{jwt}");
+ print!("{jwt}");
}
}
diff --git a/docker-compose/README.md b/docker-compose/README.md
index 648e4ca030..8232027adc 100644
--- a/docker-compose/README.md
+++ b/docker-compose/README.md
@@ -1,4 +1,3 @@
-
# Example docker compose configuration
The configuration in this directory is used for testing Neon docker images: it is
@@ -8,3 +7,13 @@ you can experiment with a miniature Neon system, use `cargo neon` rather than co
This configuration does not start the storage controller, because the controller
needs a way to reconfigure running computes, and no such thing exists in this setup.
+## Generating the JWKS for a compute
+
+```shell
+openssl genpkey -algorithm Ed25519 -out private-key.pem
+openssl pkey -in private-key.pem -pubout -out public-key.pem
+openssl pkey -pubin -inform pem -in public-key.pem -pubout -outform der -out public-key.der
+key="$(xxd -plain -cols 32 -s -32 public-key.der)"
+key_id="$(printf '%s' "$key" | sha256sum | awk '{ print $1 }' | basenc --base64url --wrap=0)"
+x="$(printf '%s' "$key" | basenc --base64url --wrap=0)"
+```
diff --git a/docker-compose/compute_wrapper/private-key.pem b/docker-compose/compute_wrapper/private-key.pem
new file mode 100644
index 0000000000..9bfbfebe27
--- /dev/null
+++ b/docker-compose/compute_wrapper/private-key.pem
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIOmnRbzt2AJ0d+S3aU1hiYOl/tXpvz1FmWBfwHYBgOma
+-----END PRIVATE KEY-----
diff --git a/docker-compose/compute_wrapper/public-key.der b/docker-compose/compute_wrapper/public-key.der
new file mode 100644
index 0000000000..1b25e50055
Binary files /dev/null and b/docker-compose/compute_wrapper/public-key.der differ
diff --git a/docker-compose/compute_wrapper/public-key.pem b/docker-compose/compute_wrapper/public-key.pem
new file mode 100644
index 0000000000..344450cb3d
--- /dev/null
+++ b/docker-compose/compute_wrapper/public-key.pem
@@ -0,0 +1,3 @@
+-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VwAyEADY0al/U0bgB3+9fUGk+3PKWnsck9OyxN5DjHIN6Xep0=
+-----END PUBLIC KEY-----
diff --git a/docker-compose/compute_wrapper/var/db/postgres/configs/config.json b/docker-compose/compute_wrapper/var/db/postgres/configs/config.json
index 3ddf96512a..21caf3800c 100644
--- a/docker-compose/compute_wrapper/var/db/postgres/configs/config.json
+++ b/docker-compose/compute_wrapper/var/db/postgres/configs/config.json
@@ -142,7 +142,19 @@
},
"compute_ctl_config": {
"jwks": {
- "keys": []
+ "keys": [
+ {
+ "use": "sig",
+ "key_ops": [
+ "verify"
+ ],
+ "alg": "EdDSA",
+ "kid": "ZGIxMzAzOGY0YWQwODk2ODU1MTk1NzMxMDFkYmUyOWU2NzZkOWNjNjMyMGRkZGJjOWY0MjdjYWVmNzE1MjUyOAo=",
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "x": "MGQ4ZDFhOTdmNTM0NmUwMDc3ZmJkN2Q0MWE0ZmI3M2NhNWE3YjFjOTNkM2IyYzRkZTQzOGM3MjBkZTk3N2E5ZAo="
+ }
+ ]
}
}
}
diff --git a/test_runner/fixtures/endpoint/http.py b/test_runner/fixtures/endpoint/http.py
index 4073ebc3b9..652c38f5c3 100644
--- a/test_runner/fixtures/endpoint/http.py
+++ b/test_runner/fixtures/endpoint/http.py
@@ -1,33 +1,59 @@
from __future__ import annotations
import urllib.parse
+from typing import TYPE_CHECKING, final
import requests
from requests.adapters import HTTPAdapter
+from requests.auth import AuthBase
+from typing_extensions import override
from fixtures.log_helper import log
+if TYPE_CHECKING:
+ from requests import PreparedRequest
+
+@final
+class BearerAuth(AuthBase):
+ """
+ Auth implementation for bearer authorization in HTTP requests through the
+ requests HTTP client library.
+ """
+
+ def __init__(self, jwt: str):
+ self.__jwt = jwt
+
+ @override
+ def __call__(self, request: PreparedRequest) -> PreparedRequest:
+ request.headers["Authorization"] = "Bearer " + self.__jwt
+ return request
+
+
+@final
class EndpointHttpClient(requests.Session):
def __init__(
self,
external_port: int,
internal_port: int,
+ jwt: str,
):
super().__init__()
self.external_port: int = external_port
self.internal_port: int = internal_port
+ self.auth = BearerAuth(jwt)
self.mount("http://", HTTPAdapter())
def dbs_and_roles(self):
- res = self.get(f"http://localhost:{self.external_port}/dbs_and_roles")
+ res = self.get(f"http://localhost:{self.external_port}/dbs_and_roles", auth=self.auth)
res.raise_for_status()
return res.json()
def database_schema(self, database: str):
res = self.get(
- f"http://localhost:{self.external_port}/database_schema?database={urllib.parse.quote(database, safe='')}"
+ f"http://localhost:{self.external_port}/database_schema?database={urllib.parse.quote(database, safe='')}",
+ auth=self.auth,
)
res.raise_for_status()
return res.text
@@ -58,13 +84,13 @@ class EndpointHttpClient(requests.Session):
# Current compute status.
def status(self):
- res = self.get(f"http://localhost:{self.external_port}/status")
+ res = self.get(f"http://localhost:{self.external_port}/status", auth=self.auth)
res.raise_for_status()
return res.json()
# Compute startup-related metrics.
def metrics_json(self):
- res = self.get(f"http://localhost:{self.external_port}/metrics.json")
+ res = self.get(f"http://localhost:{self.external_port}/metrics.json", auth=self.auth)
res.raise_for_status()
return res.json()
diff --git a/test_runner/fixtures/neon_cli.py b/test_runner/fixtures/neon_cli.py
index 5f5626fb98..80852b610b 100644
--- a/test_runner/fixtures/neon_cli.py
+++ b/test_runner/fixtures/neon_cli.py
@@ -535,6 +535,18 @@ class NeonLocalCli(AbstractNeonCli):
res.check_returncode()
return res
+ def endpoint_generate_jwt(self, endpoint_id: str) -> str:
+ """
+ Generate a JWT for making requests to the endpoint's external HTTP
+ server.
+ """
+ args = ["endpoint", "generate-jwt", endpoint_id]
+
+ cmd = self.raw_cli(args)
+ cmd.check_returncode()
+
+ return cmd.stdout
+
def endpoint_start(
self,
endpoint_id: str,
diff --git a/test_runner/fixtures/neon_fixtures.py b/test_runner/fixtures/neon_fixtures.py
index 13bd74e05d..e70ddc8e66 100644
--- a/test_runner/fixtures/neon_fixtures.py
+++ b/test_runner/fixtures/neon_fixtures.py
@@ -4110,13 +4110,14 @@ class Endpoint(PgProtocol, LogUtils):
# try and stop the same process twice, as stop() is called by test teardown and
# potentially by some __del__ chains in other threads.
self._running = threading.Semaphore(0)
+ self.__jwt: str | None = None
- def http_client(
- self, auth_token: str | None = None, retries: Retry | None = None
- ) -> EndpointHttpClient:
+ def http_client(self, retries: Retry | None = None) -> EndpointHttpClient:
+ assert self.__jwt is not None
return EndpointHttpClient(
external_port=self.external_http_port,
internal_port=self.internal_http_port,
+ jwt=self.__jwt,
)
def create(
@@ -4200,6 +4201,8 @@ class Endpoint(PgProtocol, LogUtils):
self.config(config_lines)
+ self.__jwt = self.env.neon_cli.endpoint_generate_jwt(self.endpoint_id)
+
return self
def start(