From 645dad152ad015980d89603b4dfc2b64618ac12e Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 14:47:35 +0100 Subject: [PATCH 01/11] api: add registration url for the AK The AK registration endpoint is now part of the clevis pin configuration and it needs to be make configurable via the TEC CR. Signed-off-by: Alice Frosi --- api/v1alpha1/crds.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/v1alpha1/crds.go b/api/v1alpha1/crds.go index 53419df2..75bf37f1 100644 --- a/api/v1alpha1/crds.go +++ b/api/v1alpha1/crds.go @@ -34,6 +34,7 @@ var ( // +kubebuilder:rbac:groups=trusted-execution-clusters.io,resources=trustedexecutionclusters/status;machines/status;approvedimages/status;attestationkeys/status,verbs=get;patch;update // TrustedExecutionClusterSpec defines the desired state of TrustedExecutionCluster +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.publicAttestationKeyRegisterAddr) || has(self.publicAttestationKeyRegisterAddr)", message="Value is required once set" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.publicTrusteeAddr) || has(self.publicTrusteeAddr)", message="Value is required once set" type TrustedExecutionClusterSpec struct { // Image reference to Trustee all-in-one image @@ -52,6 +53,11 @@ type TrustedExecutionClusterSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" AttestationKeyRegisterImage *string `json:"attestationKeyRegisterImage"` + // Address where attester can connect to Attestation Key Register + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + PublicAttestationKeyRegisterAddr *string `json:"publicAttestationKeyRegisterAddr,omitempty"` + // Address where attester can connect to Trustee // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" From 345cfcf0e0bc9d6ca65c684d473eb331dabf6c75 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 15:07:15 +0100 Subject: [PATCH 02/11] api: add uuid in the AK spec We match the attestation keys with the machine using the uuid and not the IP anymore. Therefore, there is a new field uuid in the AK spec and we removed the registration ip from the machine spec. Signed-off-by: Alice Frosi --- api/v1alpha1/crds.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/crds.go b/api/v1alpha1/crds.go index 75bf37f1..8db22efc 100644 --- a/api/v1alpha1/crds.go +++ b/api/v1alpha1/crds.go @@ -121,8 +121,6 @@ type MachineSpec struct { // Machine ID, typically a UUID // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" Id string `json:"id"` - // Machine IP address at registration time - RegistrationAddress *string `json:"registrationAddress"` } // MachineStatus defines the observed state of Machine. @@ -214,9 +212,9 @@ type AttestationKeySpec struct { // +required PublicKey string `json:"publicKey"` - // Address defines the address of the machine associated to the attestation key. - // +optional - Address *string `json:"address,omitempty"` + // Uuid define the identifier to which the registration key is registered with. It needs + // to match with the id of the machine for the key to be approved. + Uuid *string `json:"uuid,omitempty"` } // AttestationKeyStatus defines the observed state of AttestationKey. From 30d3d85a5db8f2c563ce7f76fc7e1c0ca738cb2e Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 14:50:11 +0100 Subject: [PATCH 03/11] Configure the public address for the AK registration Signed-off-by: Alice Frosi --- Makefile | 5 +++++ api/trusted-cluster-gen.go | 17 +++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 472ffb17..3c4b4d67 100644 --- a/Makefile +++ b/Makefile @@ -143,10 +143,15 @@ push-all: push push-bundle ## Pushes all operator and bundle images install: $(YQ) ifndef TRUSTEE_ADDR $(error TRUSTEE_ADDR is undefined) +endif +ifndef AK_REGISTRATION_ADDR + $(error AK_REGISTRATION_ADDR is undefined) endif scripts/clean-cluster-kind.sh $(OPERATOR_IMAGE) $(COMPUTE_PCRS_IMAGE) $(REG_SERVER_IMAGE) $(ATTESTATION_KEY_REGISTER_IMAGE) $(YQ) '.spec.publicTrusteeAddr = "$(TRUSTEE_ADDR):8080"' \ -i $(DEPLOY_PATH)/trusted_execution_cluster_cr.yaml + $(YQ) '.spec.publicAttestationKeyRegisterAddr = "http://$(AK_REGISTRATION_ADDR):8001/register-ak"' \ + -i $(DEPLOY_PATH)/trusted_execution_cluster_cr.yaml sed "s/NAMESPACE/$(NAMESPACE)/g" config/rbac/kustomization.yaml.in > config/rbac/kustomization.yaml $(KUBECTL) apply -f $(DEPLOY_PATH)/operator.yaml $(KUBECTL) apply -f config/crd diff --git a/api/trusted-cluster-gen.go b/api/trusted-cluster-gen.go index 2b590b51..45e347d9 100644 --- a/api/trusted-cluster-gen.go +++ b/api/trusted-cluster-gen.go @@ -140,14 +140,15 @@ func generateTrustedExecutionClusterCR(args *Args) error { Namespace: args.namespace, }, Spec: v1alpha1.TrustedExecutionClusterSpec{ - TrusteeImage: args.trusteeImage, - PcrsComputeImage: args.pcrsComputeImage, - RegisterServerImage: args.registerServerImage, - AttestationKeyRegisterImage: &args.attestationKeyRegisterImage, - PublicTrusteeAddr: nil, - TrusteeKbsPort: 0, - RegisterServerPort: 0, - AttestationKeyRegisterPort: 0, + TrusteeImage: args.trusteeImage, + PcrsComputeImage: args.pcrsComputeImage, + RegisterServerImage: args.registerServerImage, + AttestationKeyRegisterImage: &args.attestationKeyRegisterImage, + PublicAttestationKeyRegisterAddr: nil, + PublicTrusteeAddr: nil, + TrusteeKbsPort: 0, + RegisterServerPort: 0, + AttestationKeyRegisterPort: 0, }, } From 1382a229237bd2ef4383f34843c3cad53c9a1827 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 14:51:02 +0100 Subject: [PATCH 04/11] The AK approval with the uuid The attesation key is now approved if there is an existing machine with the same uuid. Signed-off-by: Alice Frosi --- operator/src/attestation_key_register.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/operator/src/attestation_key_register.rs b/operator/src/attestation_key_register.rs index c920bd85..5263a4a5 100644 --- a/operator/src/attestation_key_register.rs +++ b/operator/src/attestation_key_register.rs @@ -143,7 +143,7 @@ async fn ak_reconcile( ControllerError::Anyhow(e.into()) })?; for machine in &machine_list.items { - if ak.spec.address.as_ref() == Some(&machine.spec.registration_address) { + if ak.spec.uuid.as_ref() == Some(&machine.spec.id) { approve_ak(&ak, machine, client.clone()).await?; return Ok(Action::await_change()); } @@ -170,13 +170,6 @@ async fn machine_reconcile( return Ok(Action::await_change()); } - let machine_address = machine.spec.registration_address.clone(); - - if machine_address.is_empty() { - info!("Machine IP not set, skipping reconciliation"); - return Ok(Action::await_change()); - } - let aks: Api = Api::default_namespaced(client.clone()); let lp = ListParams::default(); let ak_list: ObjectList = aks.list(&lp).await.map_err(|e| { @@ -184,8 +177,8 @@ async fn machine_reconcile( ControllerError::Anyhow(e.into()) })?; for ak in ak_list.items { - if let Some(ak_address) = &ak.spec.address - && *ak_address == machine_address + if let Some(ak_uuid) = &ak.spec.uuid + && *ak_uuid == machine.spec.id { approve_ak(&ak, &machine, client.clone()).await?; return Ok(Action::await_change()); From f82825ec002b64fb7e99565812f62ef7c67120c6 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 14:52:39 +0100 Subject: [PATCH 05/11] ak-reg: update the AK protocol The AK is now registered with the uuid and the public key content. Signed-off-by: Alice Frosi --- attestation-key-register/src/main.rs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/attestation-key-register/src/main.rs b/attestation-key-register/src/main.rs index ebe58f6a..92693e18 100644 --- a/attestation-key-register/src/main.rs +++ b/attestation-key-register/src/main.rs @@ -29,20 +29,17 @@ struct Args { #[derive(Debug, Deserialize, Serialize)] struct AttestationKeyRegistration { /// Public attestation key + #[serde(alias = "attestation_key")] public_key: String, - /// Optional address of the machine. If not provided, the request IP will be used. - #[serde(skip_serializing_if = "Option::is_none")] - address: Option, - /// Optional platform for the machine corresponding to the attestation key. + /// Optional uuid used for the machine registration #[serde(skip_serializing_if = "Option::is_none")] - platform: Option, + uuid: Option, } async fn handle_registration( registration: AttestationKeyRegistration, client: Client, - addr: Option, ) -> Result { info!("Received registration request: {registration:?}"); @@ -107,10 +104,6 @@ async fn handle_registration( } } - let address = registration - .address - .or_else(|| addr.map(|socket_addr| socket_addr.ip().to_string())); - let name = format!("ak-{}", Uuid::new_v4()); let attestation_key = AttestationKey { metadata: ObjectMeta { @@ -120,7 +113,7 @@ async fn handle_registration( }, spec: AttestationKeySpec { public_key: registration.public_key, - address, + uuid: registration.uuid, }, status: None, }; @@ -174,12 +167,9 @@ async fn main() -> anyhow::Result<()> { .and(warp::path(ATTESTATION_KEY_REGISTER_RESOURCE)) .and(warp::body::json()) .and(with_client(client)) - .and(warp::addr::remote()) .and_then(handle_registration); - let addr = SocketAddr::from(([0, 0, 0, 0], args.port)); - info!("Listening on {addr}"); - + let addr: SocketAddr = ([0, 0, 0, 0], args.port).into(); warp::serve(register).run(addr).await; Ok(()) From cf4b92354b352ebac3f73cf943c0c3fb6eeb4891 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 14:54:56 +0100 Subject: [PATCH 06/11] reg-server: generation of the clevis pin config Now, the clevis pin configuration includea the registration of the attestation key. Signed-off-by: Alice Frosi --- Cargo.lock | 2 +- register-server/src/main.rs | 79 +++++++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fad31301..8b49dc7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,7 +433,7 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clevis-pin-trustee-lib" version = "0.1.0" -source = "git+https://github.com/latchset/clevis-pin-trustee#ea95afd215ae7bd85fa2b0834298949ffce5a7f7" +source = "git+https://github.com/latchset/clevis-pin-trustee#75015a580a738fe153b5592e16e920a7595c90c6" dependencies = [ "serde", ] diff --git a/register-server/src/main.rs b/register-server/src/main.rs index 14e14208..4d138ac1 100644 --- a/register-server/src/main.rs +++ b/register-server/src/main.rs @@ -5,7 +5,9 @@ use anyhow::Context; use clap::Parser; -use clevis_pin_trustee_lib::{Config as ClevisConfig, Server as ClevisServer}; +use clevis_pin_trustee_lib::{ + AttestationKey, Config as ClevisConfig, Registration, Server as ClevisServer, +}; use env_logger::Env; use ignition_config::v3_5::{ Clevis, ClevisCustom, Config as IgnitionConfig, Filesystem, Luks, Storage, @@ -14,7 +16,6 @@ use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference} use kube::{Api, Client}; use log::{error, info}; use std::convert::Infallible; -use std::net::SocketAddr; use uuid::Uuid; use warp::{http::StatusCode, reply, Filter}; @@ -29,9 +30,15 @@ use trusted_cluster_operator_lib::{ struct Args { #[arg(short, long, default_value = "8000")] port: u16, + + #[arg( + long, + default_value = "http://attestation-key-register:8001/register-ak" + )] + attestation_key_registration_url: Option, } -fn generate_ignition(id: &str, public_addr: &str) -> IgnitionConfig { +fn generate_ignition(id: &str, public_addr: &str, ak_registration_url: &str) -> IgnitionConfig { let clevis_conf = ClevisConfig { servers: vec![ClevisServer { url: format!("http://{public_addr}"), @@ -49,7 +56,13 @@ fn generate_ignition(id: &str, public_addr: &str) -> IgnitionConfig { // uuid: id.to_string(), // }; // ... initdata: serde_json::to_string(&initdata)?, - // depending on ultimate design decision + attestation_key: Some(AttestationKey { + registration: Registration { + url: ak_registration_url.to_string(), + uuid: id.to_string(), + cert: "".to_string(), + }, + }), }; let luks_root = "root"; @@ -91,14 +104,10 @@ async fn get_public_trustee_addr(client: Client) -> anyhow::Result { )) } -async fn register_handler(remote_addr: Option) -> Result { +async fn register_handler( + ak_registration_url: Option, +) -> Result { let id = Uuid::new_v4().to_string(); - let client_ip = remote_addr - .map(|addr| addr.ip().to_string()) - .unwrap_or_else(|| "unknown".to_string()); - - info!("Registration request from IP: {client_ip}"); - let internal_error = |e: anyhow::Error| { let code = StatusCode::INTERNAL_SERVER_ERROR; error!("{e:?}"); @@ -125,7 +134,7 @@ async fn register_handler(remote_addr: Option) -> Result return internal_error(e.context("Failed to generate owner reference")), }; - match create_machine(kube_client.clone(), &id, &client_ip, owner_reference).await { + match create_machine(kube_client.clone(), &id, owner_reference).await { Ok(_) => info!("Machine created successfully: machine-{id}"), Err(e) => return internal_error(e.context("Failed to create machine")), } @@ -134,8 +143,25 @@ async fn register_handler(remote_addr: Option) -> Result return internal_error(e.context("Failed to get Trustee address")), }; + let ak_reg_url = ak_registration_url + .as_deref() + .unwrap_or("http://attestation-key-register:8001/register-ak"); + let ignition_config = generate_ignition(&id, &public_addr, ak_reg_url); + let mut ignition_json = match serde_json::to_value(&ignition_config) { + Ok(json) => json, + Err(e) => return internal_error(e.into()), + }; + + // Overwrite ignition version to 3.6-experimental + if let Some(obj) = ignition_json.as_object_mut() { + obj.insert( + "ignition".to_string(), + serde_json::json!({"version": "3.6.0-experimental"}), + ); + } + Ok(reply::with_status( - reply::json(&generate_ignition(&id, &public_addr)), + reply::json(&ignition_json), StatusCode::OK, )) } @@ -143,7 +169,6 @@ async fn register_handler(remote_addr: Option) -> Result anyhow::Result<()> { let machine_name = format!("machine-{uuid}"); @@ -155,17 +180,22 @@ async fn create_machine( }, spec: MachineSpec { id: uuid.to_string(), - registration_address: client_ip.to_string(), }, status: None, }; let machines: Api = Api::default_namespaced(client); machines.create(&Default::default(), &machine).await?; - info!("Created Machine: {machine_name} with IP: {client_ip}"); + info!("Created Machine: {machine_name} with UUID: {uuid}"); Ok(()) } +fn with_ak_registration_url( + url: Option, +) -> impl Filter,), Error = Infallible> + Clone { + warp::any().map(move || url.clone()) +} + #[tokio::main] async fn main() { env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); @@ -174,7 +204,9 @@ async fn main() { let register_route = warp::path(REGISTER_SERVER_RESOURCE) .and(warp::get()) - .and(warp::addr::remote()) + .and(with_ak_registration_url( + args.attestation_key_registration_url.clone(), + )) .and_then(register_handler); let routes = register_route; @@ -190,8 +222,6 @@ mod tests { use trusted_cluster_operator_lib::TrustedExecutionCluster; use trusted_cluster_operator_test_utils::mock_client::*; - const TEST_IP: &str = "12.34.56.78"; - fn dummy_clusters() -> ObjectList { ObjectList { types: Default::default(), @@ -262,7 +292,6 @@ mod tests { }, spec: MachineSpec { id: "test".to_string(), - registration_address: TEST_IP.to_string(), }, status: None, } @@ -283,18 +312,16 @@ mod tests { async fn test_create_machine() { let clos = async |_, _| Ok(serde_json::to_string(&dummy_machine()).unwrap()); count_check!(1, clos, |client| { - assert!( - create_machine(client, "test", "::", dummy_owner_reference()) - .await - .is_ok() - ); + assert!(create_machine(client, "test", dummy_owner_reference()) + .await + .is_ok()); }); } #[tokio::test] async fn test_create_machine_error() { test_create_error(async |c| { - create_machine(c, "test", TEST_IP, dummy_owner_reference()) + create_machine(c, "test", dummy_owner_reference()) .await .map(|_| ()) }) From a15c62e17a6c32544b1bf6c8c15d0fc9efec7ba8 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 14:56:54 +0100 Subject: [PATCH 07/11] operator: configure reg-server with AK registration URL Signed-off-by: Alice Frosi --- operator/src/main.rs | 1 + operator/src/register_server.rs | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/operator/src/main.rs b/operator/src/main.rs index 09c79d43..caf2315b 100644 --- a/operator/src/main.rs +++ b/operator/src/main.rs @@ -183,6 +183,7 @@ async fn install_register_server(client: Client, cluster: &TrustedExecutionClust client.clone(), owner_reference.clone(), &cluster.spec.register_server_image, + cluster.spec.public_attestation_key_register_addr.as_deref(), ) .await { diff --git a/operator/src/register_server.rs b/operator/src/register_server.rs index c4e7ccc8..823b1cb8 100644 --- a/operator/src/register_server.rs +++ b/operator/src/register_server.rs @@ -37,6 +37,7 @@ pub async fn create_register_server_deployment( client: Client, owner_reference: OwnerReference, image: &str, + attestation_key_register_addr: Option<&str>, ) -> Result<()> { let app_label = "register-server"; let labels = BTreeMap::from([("app".to_string(), app_label.to_string())]); @@ -67,7 +68,15 @@ pub async fn create_register_server_deployment( container_port: REGISTER_SERVER_PORT, ..Default::default() }]), - args: Some(vec!["--port".to_string(), REGISTER_SERVER_PORT.to_string()]), + args: { + let mut args = + vec!["--port".to_string(), REGISTER_SERVER_PORT.to_string()]; + if let Some(addr) = attestation_key_register_addr { + args.push("--attestation-key-registration-url".to_string()); + args.push(addr.to_string()); + } + Some(args) + }, ..Default::default() }], ..Default::default() @@ -205,13 +214,15 @@ mod tests { #[tokio::test] async fn test_create_reg_server_depl_success() { - let clos = |client| create_register_server_deployment(client, Default::default(), "image"); + let clos = + |client| create_register_server_deployment(client, Default::default(), "image", None); test_create_success::<_, _, Deployment>(clos).await; } #[tokio::test] async fn test_create_reg_server_depl_error() { - let clos = |client| create_register_server_deployment(client, Default::default(), "image"); + let clos = + |client| create_register_server_deployment(client, Default::default(), "image", None); test_create_error(clos).await; } From 96cac688430fc91b9c3cd70be8738fd03b1922c0 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 14:58:20 +0100 Subject: [PATCH 08/11] test_utils: add missing ak registration url We added a new field in the TEC CRD. Signed-off-by: Alice Frosi --- test_utils/src/mock_client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/test_utils/src/mock_client.rs b/test_utils/src/mock_client.rs index 068eeb92..a74411ae 100644 --- a/test_utils/src/mock_client.rs +++ b/test_utils/src/mock_client.rs @@ -193,6 +193,7 @@ pub fn dummy_cluster() -> TrustedExecutionCluster { trustee_kbs_port: None, attestation_key_register_image: "".to_string(), attestation_key_register_port: None, + public_attestation_key_register_addr: Some("::".to_string()), }, } } From 586c8def469d1afac302970cee5e4e542d669994 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 14:59:21 +0100 Subject: [PATCH 09/11] tests: match the AK and the machine with the 3297de76-592e-47d8-800e-8e1bf40a0c3e The AK is approved if there is a matching machine with the same uuid. Update the behavior of the tests using the uuid instead of the ip. Signed-off-by: Alice Frosi --- tests/trusted_execution_cluster.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/trusted_execution_cluster.rs b/tests/trusted_execution_cluster.rs index 4bdeeaf0..a9056d55 100644 --- a/tests/trusted_execution_cluster.rs +++ b/tests/trusted_execution_cluster.rs @@ -32,7 +32,6 @@ named_test!( // Create a test Machine with TEC as owner reference. We need to set the owner reference // manually since the machine is not created directly by the operator. let machine_uuid = uuid::Uuid::new_v4().to_string(); - let machine_ip = "192.168.100.50"; let machine_name = format!("test-machine-{}", &machine_uuid[..8]); let machines: Api = Api::namespaced(client.clone(), namespace); @@ -45,7 +44,6 @@ named_test!( }, spec: trusted_cluster_operator_lib::MachineSpec { id: machine_uuid.clone(), - registration_address: machine_ip.to_string(), }, status: None, }; @@ -53,7 +51,7 @@ named_test!( machines.create(&Default::default(), &machine).await?; test_ctx.info(format!("Created test Machine: {machine_name}")); - // Create an AttestationKey with the same IP as the Machine + // Create an AttestationKey with the same uuid as the Machine let ak_name = format!("test-ak-{}", &machine_uuid[..8]); let public_key = "test-public-key-data"; @@ -65,8 +63,8 @@ named_test!( ..Default::default() }, spec: trusted_cluster_operator_lib::AttestationKeySpec { - address: Some(machine_ip.to_string()), public_key: public_key.to_string(), + uuid: Some(machine_uuid.clone()), }, status: None, }; @@ -75,7 +73,7 @@ named_test!( .create(&Default::default(), &attestation_key) .await?; test_ctx.info(format!( - "Created test AttestationKey: {ak_name} with IP: {machine_ip}", + "Created test AttestationKey: {ak_name} with uuid: {machine_uuid}", )); // Wait for the AttestationKey to be approved (operator should match Machine IP and approve it) @@ -292,7 +290,6 @@ async fn test_attestation_key_lifecycle() -> anyhow::Result<()> { let tec = tec_api.get(tec_name).await?; let owner_reference = generate_owner_reference(&tec)?; - let machine_ip = "1.2.3.4"; let machine_uuid = uuid::Uuid::new_v4().to_string(); let ak_name = format!("test-ak-{}", &machine_uuid[..8]); @@ -307,8 +304,8 @@ async fn test_attestation_key_lifecycle() -> anyhow::Result<()> { ..Default::default() }, spec: trusted_cluster_operator_lib::AttestationKeySpec { - address: Some(machine_ip.to_string()), public_key: random_public_key, + uuid: Some(machine_uuid.clone()), }, status: None, }; @@ -317,7 +314,7 @@ async fn test_attestation_key_lifecycle() -> anyhow::Result<()> { .create(&Default::default(), &attestation_key) .await?; test_ctx.info(format!( - "Created test AttestationKey: {ak_name} with IP: {machine_ip}", + "Created test AttestationKey: {ak_name} with uuid: {machine_uuid}", )); let machine_name = format!("test-machine-{}", &machine_uuid[..8]); @@ -331,14 +328,13 @@ async fn test_attestation_key_lifecycle() -> anyhow::Result<()> { }, spec: trusted_cluster_operator_lib::MachineSpec { id: machine_uuid.clone(), - registration_address: machine_ip.to_string(), }, status: None, }; machines.create(&Default::default(), &machine).await?; test_ctx.info(format!( - "Created test Machine: {machine_name} with IP: {machine_ip}", + "Created test Machine: {machine_name} with uuid: {machine_uuid}", )); // Poll for the AttestationKey to be approved, have owner reference, and have a Secret created From 99b8d8deee33282a173d96ac67fe64bfe98092f7 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 14:27:38 +0100 Subject: [PATCH 10/11] Refactor get_root_key We cannot rely anymore on the IP since the machine is now only identified with the uuid. Instead, we now ssh into the VM, fetch the uuid from the clevis header and match the machine with the uuid. Signed-off-by: Alice Frosi Assisted-by: Claude --- test_utils/src/virt/kubevirt.rs | 33 ++++++++++++++++++++++----------- test_utils/src/virt/mod.rs | 19 ++----------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/test_utils/src/virt/kubevirt.rs b/test_utils/src/virt/kubevirt.rs index f542c8c1..e0718d47 100644 --- a/test_utils/src/virt/kubevirt.rs +++ b/test_utils/src/virt/kubevirt.rs @@ -3,15 +3,13 @@ // // SPDX-License-Identifier: MIT -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use k8s_openapi::{api::core::v1::Secret, apimachinery::pkg::util::intstr::IntOrString}; use kube::{Api, api::ObjectMeta}; use std::{collections::BTreeMap, time::Duration}; -use trusted_cluster_operator_lib::{ - virtualmachineinstances::VirtualMachineInstance, virtualmachines::*, -}; +use trusted_cluster_operator_lib::virtualmachines::*; -use super::{VmBackend, VmConfig, generate_ignition, get_root_key, ssh_exec}; +use super::{VmBackend, VmConfig, generate_ignition, ssh_exec}; use crate::{Poller, ensure_command}; pub struct KubevirtBackend(pub VmConfig); @@ -180,12 +178,25 @@ impl VmBackend for KubevirtBackend { } async fn get_root_key(&self) -> Result>> { - let vmis: Api = - Api::namespaced(self.0.client.clone(), &self.0.namespace); - let vmi = vmis.get(&self.0.vm_name).await?; - let interfaces = vmi.status.unwrap().interfaces.unwrap(); - let ip = interfaces.first().unwrap().ip_address.clone().unwrap(); - get_root_key(&self.0, &ip).await.map(Some) + // Extract the UUID from the Clevis token in the LUKS header + let uuid_cmd = "sudo cryptsetup token export --token-id 0 /dev/vda4 | jq -r \".jwe.protected\" | base64 -d | jq -r \".clevis.path\" | cut -d/ -f2"; + let uuid_output = self + .ssh_exec(uuid_cmd) + .await + .context("Failed to extract UUID from VM")?; + let uuid = uuid_output.trim(); + + if uuid.is_empty() { + return Err(anyhow!("Retrieved empty UUID from VM")); + } + + // Use the UUID to get the secret (secrets are named with just the UUID) + let secrets: Api = Api::namespaced(self.0.client.clone(), &self.0.namespace); + let secret = secrets + .get(uuid) + .await + .context(format!("Failed to get secret for UUID {uuid}"))?; + Ok(Some(secret.data.unwrap().get("root").unwrap().0.clone())) } async fn cleanup(&self) -> Result<()> { diff --git a/test_utils/src/virt/mod.rs b/test_utils/src/virt/mod.rs index d3e109c0..bfff5576 100644 --- a/test_utils/src/virt/mod.rs +++ b/test_utils/src/virt/mod.rs @@ -6,10 +6,9 @@ pub mod azure; pub mod kubevirt; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Result, anyhow}; use clevis_pin_trustee_lib::Key as ClevisKey; -use k8s_openapi::api::core::v1::Secret; -use kube::{Api, Client}; +use kube::Client; use std::{env, path::PathBuf, time::Duration}; use tokio::process::Command; @@ -162,20 +161,6 @@ pub async fn ssh_exec(command: &str) -> Result { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } -pub async fn get_root_key(config: &VmConfig, ip: &str) -> Result> { - let machines: Api = Api::namespaced(config.client.clone(), &config.namespace); - let list = machines.list(&Default::default()).await?; - let retrieval = |m: &&Machine| m.spec.registration_address == ip; - let err = format!("No machine found with registration IP {ip}"); - let machine = list.items.iter().find(retrieval).context(err)?; - let machine_name = machine.metadata.name.clone().unwrap(); - let secret_name = machine_name.strip_prefix("machine-").unwrap(); - - let secrets: Api = Api::namespaced(config.client.clone(), &config.namespace); - let secret = secrets.get(secret_name).await?; - Ok(secret.data.unwrap().get("root").unwrap().0.clone()) -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum VirtProvider { #[default] From 16532b2d43db97174ae0413428de4a0681d628c3 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 18 Feb 2026 15:14:50 +0100 Subject: [PATCH 11/11] Update kubevirt image The new kubevirt image includes the clevis pin with the attestation key registration. Signed-off-by: Alice Frosi --- Makefile | 2 +- examples/vm-coreos-ign.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 3c4b4d67..e599ef49 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ COMPUTE_PCRS_IMAGE=$(REGISTRY)/compute-pcrs:$(TAG) REG_SERVER_IMAGE=$(REGISTRY)/registration-server:$(TAG) ATTESTATION_KEY_REGISTER_IMAGE=$(REGISTRY)/attestation-key-register:$(TAG) TRUSTEE_IMAGE ?= quay.io/trusted-execution-clusters/key-broker-service:v0.17.0 -TEST_IMAGE ?= quay.io/trusted-execution-clusters/fedora-coreos-kubevirt:20260129 +TEST_IMAGE ?= quay.io/trusted-execution-clusters/fedora-coreos-kubevirt:clevis-attest-reg # tagged as 42.20251012.2.0 APPROVED_IMAGE ?= quay.io/trusted-execution-clusters/fedora-coreos@sha256:6997f51fd27d1be1b5fc2e6cc3ebf16c17eb94d819b5d44ea8d6cf5f826ee773 diff --git a/examples/vm-coreos-ign.yaml b/examples/vm-coreos-ign.yaml index dbb5e8f7..8e216440 100644 --- a/examples/vm-coreos-ign.yaml +++ b/examples/vm-coreos-ign.yaml @@ -31,7 +31,7 @@ spec: volumes: - name: containerdisk containerDisk: - image: "quay.io/trusted-execution-clusters/fedora-coreos-kubevirt:20260129" + image: "quay.io/trusted-execution-clusters/fedora-coreos-kubevirt:clevis-attest-reg" imagePullPolicy: IfNotPresent - name: cloudinitdisk cloudInitConfigDrive: