feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+108 -10
View File
@@ -17,7 +17,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,
@@ -65,6 +67,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!({
@@ -149,7 +181,11 @@ async fn run(
cookie_secure: base_url.starts_with("https://"),
bootstrap_admin: BootstrapAdminConfig {
email: config.bootstrap_email.clone(),
password: config.bootstrap_password.expose_secret().to_owned(),
password: config
.bootstrap_password
.as_ref()
.map(|password| password.expose_secret().to_owned())
.unwrap_or_default(),
display_name: config.bootstrap_display_name.clone(),
},
};
@@ -163,10 +199,13 @@ async fn run(
config.rate_limit.requests_per_second,
config.rate_limit.burst,
)?;
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(&registry, 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.clone())
@@ -183,10 +222,10 @@ async fn run(
secret_crypto,
runtime,
)
.with_public_base_url(base_url)
.with_outbound_http_policy(outbound_http_policy)
.with_identity_provider(std::sync::Arc::new(identity_provider))
.build();
service.bootstrap_admin_user().await?;
if config.demo_seed {
service.seed_demo_assets().await?;
}
@@ -198,7 +237,7 @@ async fn run(
} else {
RequestRateLimiter::new(api_rate_limit)
},
trust_forwarded_headers: config.trust_forwarded_headers,
trusted_proxy_ips: config.trusted_proxy_ips.clone(),
};
let app = build_app(state);
let listener = TcpListener::bind(config.bind_addr).await?;
@@ -285,9 +324,10 @@ fn preflight_config(config: &AdminProcessConfig) -> Result<(), crank_config::Con
.map_err(|_| invalid("admin.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"))?;
@@ -315,6 +355,53 @@ fn preflight_config(config: &AdminProcessConfig) -> Result<(), crank_config::Con
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 otlp_config(
config: &ObservabilitySettings,
) -> Result<OtlpTraceConfig, crank_observability::OtlpTraceConfigError> {
@@ -395,12 +482,23 @@ fn spawn_invocation_log_cleanup(service: admin_api::service::AdminService, reten
interval.tick().await;
let cutoff = time::OffsetDateTime::now_utc() - time::Duration::days(retention_days);
match service.cleanup_invocation_logs_before(cutoff).await {
Ok(removed) if removed > 0 => info!(
Ok(outcome) if outcome.deleted_records > 0 => info!(
name: "admin.invocation_log_cleanup.completed",
removed,
status = ?outcome.status,
removed = outcome.deleted_records,
requested_cutoff = %outcome.policy.requested_cutoff,
effective_cutoff = %outcome.policy.effective_cutoff,
preserved_usage_window_days = outcome.policy.preserved_usage_window_days,
"expired invocation logs removed"
),
Ok(_) => {}
Ok(outcome) => info!(
name: "admin.invocation_log_cleanup.noop",
status = ?outcome.status,
requested_cutoff = %outcome.policy.requested_cutoff,
effective_cutoff = %outcome.policy.effective_cutoff,
preserved_usage_window_days = outcome.policy.preserved_usage_window_days,
"no expired invocation logs removed"
),
Err(_) => warn!(
name: "admin.invocation_log_cleanup.failed",
error_category = "registry_cleanup",