Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions apps/api/src/knotree_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Sha256>;

/// 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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions apps/api/src/registry_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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());
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -518,7 +518,7 @@ fn registry_unavailable() -> AppError {

async fn registry_get(path: &str, username: &str, secret: &str) -> Result<serde_json::Value, AppError> {
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
Expand Down
14 changes: 10 additions & 4 deletions apps/api/src/registry_consent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ pub(crate) fn session_hash(state: &AppState, headers: &HeaderMap) -> Result<Vec<
security::get_cookie(headers, state.config.session_cookie_name()).ok_or_else(invalid)?;
Ok(security::token_hash(&token))
}
pub(crate) fn registry_consent_unreachable(error: reqwest::Error) -> AppError {
tracing::warn!(error = %error, "could not reach Knotree Registry for consent");
invalid()
}
pub(crate) fn client() -> Result<reqwest::Client, AppError> {
reqwest::Client::builder()
.timeout(Duration::from_secs(10))
Expand All @@ -41,6 +45,8 @@ pub(crate) async fn bounded_json<T: serde::de::DeserializeOwned>(
mut response: reqwest::Response,
) -> Result<T, AppError> {
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();
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion deploy/ci/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
3 changes: 3 additions & 0 deletions deploy/ci/contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,8 @@
},
"external_secrets": [
"KNOTREE_REGISTRY_WEBHOOK_SECRET"
],
"optional_config": [
"KNOTREE_REGISTRY_SERVICE_ORIGIN"
]
}
Loading