feat(security): enforce declared cosign image signatures at the pull sites
New container::image_verify gates PodmanClient::pull_image and the dev-only DockerRuntime::pull_image. Signature claims classify three ways: absent/empty (pull unverified, logged), the literal 'cosign://...' placeholder every fleet manifest carries today (same — enforcement stays dormant until the signing ceremony ships real values), or a declared signature, which must verify via 'cosign verify --key /etc/archipelago/cosign.pub --insecure-ignore-tlog=true' (plus --allow-insecure-registry --allow-http-registry for the HTTP mirror; flags checked against cosign's own docs) before anything is fetched. Missing key, missing cosign binary, timeout, or verification failure all hard-fail the pull — a declared signature cannot be skipped on either runtime. Key path overridable via ARCHIPELAGO_COSIGN_PUBKEY for tests/staging. Deletes security::ImageVerifier: zero callers, blocking std::process::Command on would-be async paths, and a fantasy 'cosign verify --signature' invocation (that flag belongs to verify-blob). Activation ships with the Workstream B ceremony, in order: pin cosign.pub on nodes + install cosign, then publish real image_signature values in the catalog. Tests: archipelago-container 58/58 (5 new), archipelago container:: 159/159, security check clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2c8c99fd28
commit
eed830e1ee
@@ -1,111 +0,0 @@
|
||||
// Container image signature verification using Cosign
|
||||
// Verifies that container images are signed and trusted
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::process::Command;
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub struct ImageVerifier {
|
||||
cosign_public_key: Option<String>,
|
||||
require_signatures: bool,
|
||||
}
|
||||
|
||||
impl ImageVerifier {
|
||||
pub fn new(cosign_public_key: Option<String>) -> Self {
|
||||
Self {
|
||||
cosign_public_key,
|
||||
require_signatures: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a verifier that requires all images to be signed.
|
||||
pub fn new_strict(cosign_public_key: Option<String>) -> Self {
|
||||
Self {
|
||||
cosign_public_key,
|
||||
require_signatures: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify a container image signature
|
||||
pub async fn verify_image(&self, image: &str, signature: Option<&str>) -> Result<bool> {
|
||||
if signature.is_none() && self.cosign_public_key.is_none() {
|
||||
if self.require_signatures {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Image '{}' has no signature and no cosign key is configured. \
|
||||
All container images must be signed for production use.",
|
||||
image
|
||||
));
|
||||
}
|
||||
warn!("No signature provided for image: {}", image);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check if cosign is available
|
||||
let cosign_available = Command::new("cosign").arg("version").output().is_ok();
|
||||
|
||||
if !cosign_available {
|
||||
if self.require_signatures {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cosign binary not found. Install cosign to verify container image signatures."
|
||||
));
|
||||
}
|
||||
warn!("Cosign not available, skipping signature verification");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// If public key is provided, use it for verification
|
||||
if let Some(ref public_key) = self.cosign_public_key {
|
||||
let output = Command::new("cosign")
|
||||
.arg("verify")
|
||||
.arg("--key")
|
||||
.arg(public_key)
|
||||
.arg(image)
|
||||
.output()
|
||||
.context("Failed to run cosign verify")?;
|
||||
|
||||
if output.status.success() {
|
||||
info!("Image signature verified: {}", image);
|
||||
return Ok(true);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("Signature verification failed: {}", stderr));
|
||||
}
|
||||
}
|
||||
|
||||
// If signature URL is provided, verify using that
|
||||
if let Some(sig_url) = signature {
|
||||
if sig_url.starts_with("cosign://") {
|
||||
// Extract signature reference
|
||||
let sig_ref = sig_url.strip_prefix("cosign://").unwrap();
|
||||
let output = Command::new("cosign")
|
||||
.arg("verify")
|
||||
.arg("--signature")
|
||||
.arg(sig_ref)
|
||||
.arg(image)
|
||||
.output()
|
||||
.context("Failed to run cosign verify")?;
|
||||
|
||||
if output.status.success() {
|
||||
info!("Image signature verified: {}", image);
|
||||
return Ok(true);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("Signature verification failed: {}", stderr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Check if an image has a signature
|
||||
pub async fn has_signature(&self, image: &str) -> bool {
|
||||
// Try to find signature in registry
|
||||
let output = Command::new("cosign")
|
||||
.arg("triangulate")
|
||||
.arg(image)
|
||||
.output();
|
||||
|
||||
output.is_ok() && output.unwrap().status.success()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
pub mod container_policies;
|
||||
pub mod image_verifier;
|
||||
pub mod secrets_manager;
|
||||
|
||||
pub use container_policies::ContainerPolicyGenerator;
|
||||
pub use image_verifier::ImageVerifier;
|
||||
pub use secrets_manager::SecretsManager;
|
||||
|
||||
Reference in New Issue
Block a user