feat: complete Epic 1 production foundation
This commit is contained in:
@@ -13,7 +13,9 @@ use crank_observability::{
|
||||
CriticalErrorCategory, MetricsConfig, ObservabilityConfig, ObservabilityLifecycle,
|
||||
OtlpTraceConfig, RedactionLimits, SentryConfig, ServiceIdentity, capture_critical_error,
|
||||
};
|
||||
use crank_registry::{PostgresPoolConfig, PostgresRegistry};
|
||||
use crank_registry::{
|
||||
MASTER_KEY_CIPHER_CONTRACT, MasterKeyIdentityCandidate, PostgresPoolConfig, PostgresRegistry,
|
||||
};
|
||||
use crank_runtime::{
|
||||
RequestRateLimitConfig, RequestRateLimiter, RuntimeCacheConfig, RuntimeCacheStores,
|
||||
RuntimeLimits, SecretCrypto,
|
||||
@@ -62,6 +64,36 @@ fn safe_startup_diagnostic(error: &(dyn std::error::Error + 'static)) -> String
|
||||
})
|
||||
.to_string();
|
||||
}
|
||||
if let Some(crank_registry::RegistryError::MasterKeyIdentityMismatch { epoch }) =
|
||||
cause.downcast_ref::<crank_registry::RegistryError>()
|
||||
{
|
||||
return serde_json::json!({
|
||||
"status": "error",
|
||||
"code": "master_key_identity_mismatch",
|
||||
"stage": "startup.master_key_identity",
|
||||
"version": epoch,
|
||||
"recovery": "configure_same_master_key",
|
||||
})
|
||||
.to_string();
|
||||
}
|
||||
if cause
|
||||
.downcast_ref::<crank_registry::RegistryError>()
|
||||
.is_some_and(|error| {
|
||||
matches!(
|
||||
error,
|
||||
crank_registry::RegistryError::InvalidMasterKeyIdentity
|
||||
)
|
||||
})
|
||||
{
|
||||
return serde_json::json!({
|
||||
"status": "error",
|
||||
"code": "master_key_identity_invalid",
|
||||
"stage": "startup.master_key_identity",
|
||||
"version": null,
|
||||
"recovery": "contact_operator",
|
||||
})
|
||||
.to_string();
|
||||
}
|
||||
current = cause.source();
|
||||
}
|
||||
serde_json::json!({
|
||||
@@ -145,10 +177,13 @@ async fn run(
|
||||
spawn_postgres_pool_metrics(registry.pool().clone());
|
||||
}
|
||||
let session_store = PostgresTransportSessionStore::from_pool(registry.pool().clone()).await?;
|
||||
let secret_crypto = SecretCrypto::new(config.runtime.master_key.expose_secret())?;
|
||||
let outbound_http_policy = crank_runtime::OutboundHttpPolicy::try_new(
|
||||
let secret_crypto =
|
||||
verified_startup_secret_crypto(®istry, config.runtime.master_key.expose_secret())
|
||||
.await?;
|
||||
let outbound_http_policy = crank_runtime::OutboundHttpPolicy::try_new_with_limits(
|
||||
config.runtime.outbound.allowed_hosts.clone(),
|
||||
config.runtime.outbound.denied_hosts.clone(),
|
||||
config.runtime.outbound.max_request_bytes,
|
||||
config.runtime.outbound.max_response_bytes,
|
||||
)?;
|
||||
let runtime = crank_runtime::community_with_outbound_policy(outbound_http_policy)
|
||||
@@ -275,9 +310,10 @@ fn preflight_config(config: &McpProcessConfig) -> Result<(), crank_config::Confi
|
||||
.map_err(|_| invalid("mcp.rate_limit"))?;
|
||||
SecretCrypto::new(config.runtime.master_key.expose_secret())
|
||||
.map_err(|_| invalid("runtime.master_key"))?;
|
||||
crank_runtime::OutboundHttpPolicy::try_new(
|
||||
crank_runtime::OutboundHttpPolicy::try_new_with_limits(
|
||||
config.runtime.outbound.allowed_hosts.clone(),
|
||||
config.runtime.outbound.denied_hosts.clone(),
|
||||
config.runtime.outbound.max_request_bytes,
|
||||
config.runtime.outbound.max_response_bytes,
|
||||
)
|
||||
.map_err(|_| invalid("runtime.outbound"))?;
|
||||
@@ -326,6 +362,53 @@ fn preflight_config(config: &McpProcessConfig) -> Result<(), crank_config::Confi
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn verified_startup_secret_crypto(
|
||||
registry: &PostgresRegistry,
|
||||
master_key: &str,
|
||||
) -> Result<SecretCrypto, Box<dyn std::error::Error>> {
|
||||
let active = registry.active_master_key_identity().await?;
|
||||
let secret_crypto = if let Some(identity) = active {
|
||||
SecretCrypto::with_epoch(master_key, identity.epoch)?
|
||||
} else {
|
||||
let crypto = SecretCrypto::new(master_key)?;
|
||||
let mut after_secret_id: Option<String> = None;
|
||||
let mut after_version: Option<u32> = None;
|
||||
loop {
|
||||
let versions = registry
|
||||
.list_secret_versions_for_master_key_epoch_page(
|
||||
1,
|
||||
after_secret_id.as_deref(),
|
||||
after_version,
|
||||
1_000,
|
||||
)
|
||||
.await?;
|
||||
if versions.is_empty() {
|
||||
break;
|
||||
}
|
||||
for version in versions {
|
||||
after_secret_id = Some(version.secret_version.secret_id.as_str().to_owned());
|
||||
after_version = Some(version.secret_version.version);
|
||||
crypto.decrypt_for_epoch(
|
||||
&version.secret_version.key_version,
|
||||
version.master_key_epoch,
|
||||
&version.secret_version.ciphertext,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
crypto
|
||||
};
|
||||
let master_key_observed_at = time::OffsetDateTime::now_utc();
|
||||
registry
|
||||
.verify_or_register_master_key_identity(MasterKeyIdentityCandidate {
|
||||
epoch: secret_crypto.master_key_epoch(),
|
||||
fingerprint: secret_crypto.master_key_fingerprint(),
|
||||
cipher_contract: MASTER_KEY_CIPHER_CONTRACT,
|
||||
observed_at: &master_key_observed_at,
|
||||
})
|
||||
.await?;
|
||||
Ok(secret_crypto)
|
||||
}
|
||||
|
||||
fn postgres_pool_config(
|
||||
config: &DatabaseSettings,
|
||||
) -> Result<PostgresPoolConfig, crank_registry::PostgresPoolConfigError> {
|
||||
|
||||
Reference in New Issue
Block a user