From 3eb11553ebff8e8ac0511a06554fa342e506ce95 Mon Sep 17 00:00:00 2001 From: Van Tan Minh Date: Sat, 3 Oct 2026 20:42:08 +0700 Subject: [PATCH] fix: call Knotree Registry through the in-cluster service, not Cloudflare Co-Authored-By: Claude Opus 5.5 --- apps/api/src/knotree_registry.rs | 39 +++++++++++++++++++++++++++++-- apps/api/src/registry_accounts.rs | 10 ++++---- apps/api/src/registry_consent.rs | 14 +++++++---- deploy/ci/config.example.json | 3 ++- deploy/ci/contract.json | 3 +++ 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/apps/api/src/knotree_registry.rs b/apps/api/src/knotree_registry.rs index eafc578..e6ce34c 100644 --- a/apps/api/src/knotree_registry.rs +++ b/apps/api/src/knotree_registry.rs @@ -20,12 +20,35 @@ use crate::{auth, cluster_kubernetes, error::AppError, projects, security, state pub const REGISTRY_HOST: &str = "registry.knotree.com"; const REGISTRY_URL: &str = "https://registry.knotree.com"; +const REGISTRY_SERVICE_ORIGIN: &str = "http://registry.knotree-registry.svc.cluster.local"; const REGISTRY_TOKEN_SERVICE: &str = "knotree-registry"; const WEBHOOK_CLOCK_SKEW_SECONDS: i64 = 300; const MAX_REGISTRY_DELIVERY_BYTES: usize = 64 * 1024; type HmacSha256 = Hmac; +/// Origin for Cloud's server-to-server Registry calls. In production these +/// stay inside the cluster (`KNOTREE_REGISTRY_SERVICE_ORIGIN`) so Cloudflare +/// cannot challenge them; browsers always use the public registry URL. +pub(crate) fn registry_api_origin() -> &'static str { + static ORIGIN: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new(); + ORIGIN.get_or_init(|| { + api_origin_from(std::env::var("KNOTREE_REGISTRY_SERVICE_ORIGIN").ok().as_deref()) + }) +} + +fn api_origin_from(value: Option<&str>) -> &'static str { + match value.map(|value| value.trim().trim_end_matches('/')) { + None | Some("") => REGISTRY_URL, + Some(REGISTRY_SERVICE_ORIGIN) => REGISTRY_SERVICE_ORIGIN, + Some(other) => { + tracing::warn!(origin = other, + "ignoring KNOTREE_REGISTRY_SERVICE_ORIGIN; only the in-cluster Registry service is allowed"); + REGISTRY_URL + } + } +} + #[derive(Clone)] pub(crate) struct RegistryDockerCredentials { pub username: String, @@ -554,7 +577,7 @@ pub(crate) async fn resolve_tag_digest( .timeout(Duration::from_secs(15)) .build() .ok()?; - let url = format!("{REGISTRY_URL}/v2/{repository}/manifests/{tag}"); + let url = format!("{}/v2/{repository}/manifests/{tag}", registry_api_origin()); let response = client .get(url) .bearer_auth(bearer) @@ -593,7 +616,8 @@ async fn request_registry_bearer( return None; }; let token_url = format!( - "{REGISTRY_URL}/auth/token?service={REGISTRY_TOKEN_SERVICE}&scope=repository:{repository}:pull" + "{}/auth/token?service={REGISTRY_TOKEN_SERVICE}&scope=repository:{repository}:pull", + registry_api_origin() ); let response = client .get(token_url) @@ -918,6 +942,17 @@ mod tests { )); } + #[test] + fn server_calls_only_use_the_in_cluster_registry_service() { + assert_eq!(api_origin_from(None), REGISTRY_URL); + assert_eq!(api_origin_from(Some("")), REGISTRY_URL); + assert_eq!( + api_origin_from(Some("http://registry.knotree-registry.svc.cluster.local/")), + REGISTRY_SERVICE_ORIGIN + ); + assert_eq!(api_origin_from(Some("http://attacker.example")), REGISTRY_URL); + } + #[test] fn account_connections_deploy_only_for_matching_owner() { let metadata = |issuer: Option<&str>, subject: Option<&str>| RegistryEventMetadata { diff --git a/apps/api/src/registry_accounts.rs b/apps/api/src/registry_accounts.rs index cfe84c9..5ddd3dc 100644 --- a/apps/api/src/registry_accounts.rs +++ b/apps/api/src/registry_accounts.rs @@ -102,7 +102,7 @@ pub async fn start( let verifier = security::random_token(); let challenge = URL_SAFE_NO_PAD.encode(security::token_hash(&verifier)); let response = registry_consent::client()? - .post(format!("{REGISTRY}/api/v1/cloud-grants/requests")) + .post(format!("{}/api/v1/cloud-grants/requests", crate::knotree_registry::registry_api_origin())) .json(&serde_json::json!({ "client_id": registry_consent::CLIENT, "redirect_uri": registry_consent::CALLBACK, @@ -115,7 +115,7 @@ pub async fn start( })) .send() .await - .map_err(|_| invalid())?; + .map_err(registry_consent::registry_consent_unreachable)?; let started: Started = registry_consent::bounded_json(response).await?; if started.authorization_url != format!("{REGISTRY}/cloud/authorize/{}", started.request_id) { return Err(invalid()); @@ -228,7 +228,7 @@ pub(crate) async fn complete_callback( &state.config.database_credentials_encryption_key, )?; let response = registry_consent::client()? - .post(format!("{REGISTRY}/api/v1/cloud-grants/exchange")) + .post(format!("{}/api/v1/cloud-grants/exchange", crate::knotree_registry::registry_api_origin())) .json(&serde_json::json!({ "client_id": registry_consent::CLIENT, "redirect_uri": registry_consent::CALLBACK, @@ -237,7 +237,7 @@ pub(crate) async fn complete_callback( })) .send() .await - .map_err(|_| invalid())?; + .map_err(registry_consent::registry_consent_unreachable)?; let grant: Grant = registry_consent::bounded_json(response).await?; validate_grant(&grant, &attempt)?; let encrypted = security::encrypt_secret( @@ -518,7 +518,7 @@ fn registry_unavailable() -> AppError { async fn registry_get(path: &str, username: &str, secret: &str) -> Result { let mut response = registry_consent::client()? - .get(format!("{REGISTRY}{path}")) + .get(format!("{}{path}", knotree_registry::registry_api_origin())) .basic_auth(username, Some(secret)) .send() .await diff --git a/apps/api/src/registry_consent.rs b/apps/api/src/registry_consent.rs index 4bb82bf..90e9177 100644 --- a/apps/api/src/registry_consent.rs +++ b/apps/api/src/registry_consent.rs @@ -30,6 +30,10 @@ pub(crate) fn session_hash(state: &AppState, headers: &HeaderMap) -> Result AppError { + tracing::warn!(error = %error, "could not reach Knotree Registry for consent"); + invalid() +} pub(crate) fn client() -> Result { reqwest::Client::builder() .timeout(Duration::from_secs(10)) @@ -41,6 +45,8 @@ pub(crate) async fn bounded_json( mut response: reqwest::Response, ) -> Result { if !response.status().is_success() { + tracing::warn!(status = %response.status(), url = %response.url(), + "Knotree Registry rejected a consent request"); return Err(invalid()); } let mut bytes = Vec::new(); @@ -87,9 +93,9 @@ pub async fn start( let state_token = security::random_token(); let verifier = security::random_token(); let challenge = URL_SAFE_NO_PAD.encode(security::token_hash(&verifier)); - let response = client()?.post(format!("{REGISTRY}/api/v1/cloud-grants/requests")) + let response = client()?.post(format!("{}/api/v1/cloud-grants/requests", crate::knotree_registry::registry_api_origin())) .json(&serde_json::json!({"client_id":CLIENT,"redirect_uri":CALLBACK,"state":state_token,"repository":repository,"code_challenge":challenge,"code_challenge_method":"S256","expected_issuer":config.issuer,"expected_subject":subject})) - .send().await.map_err(|_| invalid())?; + .send().await.map_err(registry_consent_unreachable)?; let started: Started = bounded_json(response).await?; if started.authorization_url != format!("{REGISTRY}/cloud/authorize/{}", started.request_id) { return Err(invalid()); @@ -213,9 +219,9 @@ pub async fn callback( &attempt.verifier_ciphertext, &state.config.database_credentials_encryption_key, )?; - let response = client()?.post(format!("{REGISTRY}/api/v1/cloud-grants/exchange")) + let response = client()?.post(format!("{}/api/v1/cloud-grants/exchange", crate::knotree_registry::registry_api_origin())) .json(&serde_json::json!({"client_id":CLIENT,"redirect_uri":CALLBACK,"code":code,"code_verifier":verifier})) - .send().await.map_err(|_| invalid())?; + .send().await.map_err(registry_consent_unreachable)?; let grant: Grant = bounded_json(response).await?; validate_grant(&grant, &attempt)?; if !knotree_registry::verify_pull_access( diff --git a/deploy/ci/config.example.json b/deploy/ci/config.example.json index 8894d9b..ac52d0a 100644 --- a/deploy/ci/config.example.json +++ b/deploy/ci/config.example.json @@ -49,5 +49,6 @@ "POSTGRES_USER": "knotree", "POSTGRES_DB": "knotree_cloud", "DATABASE_CLUSTER_DOCKER_BINARY": "docker", - "DATABASE_CLUSTER_BIND_ADDRESS": "127.0.0.1" + "DATABASE_CLUSTER_BIND_ADDRESS": "127.0.0.1", + "KNOTREE_REGISTRY_SERVICE_ORIGIN": "http://registry.knotree-registry.svc.cluster.local" } diff --git a/deploy/ci/contract.json b/deploy/ci/contract.json index 9cd035f..0fb4c8c 100644 --- a/deploy/ci/contract.json +++ b/deploy/ci/contract.json @@ -169,5 +169,8 @@ }, "external_secrets": [ "KNOTREE_REGISTRY_WEBHOOK_SECRET" + ], + "optional_config": [ + "KNOTREE_REGISTRY_SERVICE_ORIGIN" ] }