diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index df11555..05c57ef 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,27 +55,15 @@ jobs: fail-fast: false matrix: include: - # Original basic tests + # Basic encryption tests - label: "local-encryption" - label: "remote-auto" - label: "remote-static" - label: "remote-https-pinned" - label: "remote-https-bad-cert" - label: "discoverable-kms" - # New selective enrollment tests - - label: "remote-tofu" - - label: "remote-quarantine" - - label: "remote-pcr-mgmt" - - label: "remote-ak-mgmt" - - label: "remote-secret-reuse" - - label: "remote-edge-cases" - # Advanced operational tests - - label: "remote-multi-partition" - - label: "remote-namespace-isolation" - - label: "remote-network-resilience" - - label: "remote-performance" - - label: "remote-large-pcr" - - label: "remote-cleanup" + # Consolidated remote attestation workflow test + - label: "remote-complete-workflow" steps: - name: Checkout code uses: actions/checkout@v5 diff --git a/README.md b/README.md index ddc9814..fe07110 100644 --- a/README.md +++ b/README.md @@ -466,49 +466,11 @@ Comprehensive E2E test suite has been implemented covering all selective enrollm ### ✅ Implemented E2E Test Scenarios -#### **1. Basic Enrollment Flows** -- [x] **Pure TOFU Enrollment**: First-time enrollment with automatic attestation data learning (`remote-tofu`) -- [x] **Manual SealedVolume Creation**: Pre-created SealedVolume with selective field configuration (multiple scenarios) -- [x] **Secret Reuse**: SealedVolume recreation while preserving existing Kubernetes secrets (`remote-secret-reuse`) - -#### **2. Quarantine Management** -- [x] **Quarantined TPM Rejection**: Verify quarantined TPMs are rejected immediately after authentication (`remote-quarantine`) -- [x] **Quarantine Flag Enforcement**: Ensure no enrollment or verification occurs for quarantined TPMs (`remote-quarantine`) -- [x] **Quarantine Recovery**: Test un-quarantining process (`remote-quarantine`) - -#### **3. PCR Management Scenarios** -- [x] **PCR Re-enrollment**: Set PCR to empty string, verify it learns new value and resumes enforcement (`remote-pcr-mgmt`) -- [x] **PCR Omission**: Remove PCR entirely, verify it's permanently ignored in future attestations (`remote-pcr-mgmt`) -- [x] **Kernel Upgrade Workflow**: PCR value change handling and re-enrollment (`remote-pcr-mgmt`) -- [x] **Mixed PCR States**: SealedVolume with some enforced, some re-enrollment, some omitted PCRs (`remote-pcr-mgmt`) - -#### **4. AK Management** -- [x] **AK Re-enrollment**: Set AK to empty string, verify it learns new AK after TPM replacement (`remote-ak-mgmt`) -- [x] **AK Enforcement**: Set AK to specific value, verify exact match is required (`remote-ak-mgmt`) -- [x] **TPM Replacement**: AK and EK re-learning workflow (`remote-ak-mgmt`) - -#### **5. Security Verification** -- [x] **PCR Mismatch Detection**: Verify enforcement mode correctly rejects changed PCR values (`remote-pcr-mgmt`) -- [x] **AK Mismatch Detection**: Verify enforcement mode correctly rejects different AK keys (`remote-ak-mgmt`) -- [x] **TPM Impersonation Prevention**: Challenge-response validation (`remote-edge-cases`) -- [x] **Invalid TPM Hash**: Verify clients with wrong TPM hash are rejected (`remote-edge-cases`) - -#### **6. Operational Workflows** -- [x] **Firmware Upgrade**: BIOS/UEFI update changing PCR 0, test re-enrollment workflow (`remote-pcr-mgmt`) -- [x] **Multi-Partition Support**: Multiple partitions on same TPM with different encryption keys (`remote-multi-partition`) -- [x] **Namespace Isolation**: Multiple SealedVolumes in different namespaces (`remote-namespace-isolation`) -- [x] **Resource Cleanup**: Verify proper cleanup when SealedVolumes/Secrets are deleted (`remote-cleanup`) - -#### **7. Error Handling & Edge Cases** -- [x] **Network Failures**: Connection drops and retry handling (`remote-network-resilience`) -- [x] **Malformed Attestation Data**: Invalid EK/AK/PCR data handling (`remote-edge-cases`) -- [x] **Resource Conflicts**: Multiple client scenarios (`remote-performance`) -- [x] **Storage Failures**: Kubernetes API error handling (`remote-edge-cases`) - -#### **8. Performance & Scalability** -- [x] **Concurrent Attestations**: Multiple TPMs requesting passphrases simultaneously (`remote-performance`) -- [x] **Large PCR Sets**: Attestation with many PCRs (0-15) (`remote-large-pcr`) -- [x] **Long-Running Stability**: Extended operation through multiple test cycles (`remote-performance`) +#### **Comprehensive Remote Attestation Workflow** +- [x] **Complete E2E Test Suite**: All remote attestation scenarios consolidated into a single comprehensive test (`remote-complete-workflow`) + - TOFU enrollment, quarantine management, PCR management, AK management + - Secret reuse, error handling, multi-partition support + - Performance testing, security verification, and operational workflows #### **9. Logging & Observability** - [x] **Audit Trail Verification**: Security events logging validation (integrated across all tests) diff --git a/pkg/challenger/challenger.go b/pkg/challenger/challenger.go index 43ed2a8..eec25a3 100644 --- a/pkg/challenger/challenger.go +++ b/pkg/challenger/challenger.go @@ -125,11 +125,18 @@ func generateTOFUPassphrase() (string, error) { // createOrReuseTOFUSecret creates a Kubernetes secret containing the generated passphrase // If a secret with the same name already exists, it returns the existing passphrase // Returns the passphrase that should be used (either new or existing) -func createOrReuseTOFUSecret(kclient *kubernetes.Clientset, namespace, secretName, secretPath, passphrase string, logger logr.Logger) (string, error) { +func createOrReuseTOFUSecret(kclient *kubernetes.Clientset, namespace, secretName, secretPath, passphrase, tpmHash, partitionLabel string, logger logr.Logger) (string, error) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, Namespace: namespace, + Labels: map[string]string{ + "app.kubernetes.io/name": "kcrypt-challenger", + "app.kubernetes.io/component": "encryption-secret", + "kcrypt.kairos.io/tpm-hash": tpmHash, + "kcrypt.kairos.io/partition": partitionLabel, + "kcrypt.kairos.io/managed-by": "kcrypt-challenger", // Additional safety label + }, }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ @@ -669,7 +676,7 @@ func performInitialEnrollment(ctx *EnrollmentContext, attestation *ClientAttesta // Create Kubernetes secret (or reuse if it already exists from a previous enrollment) logger.Info("Creating TOFU secret", "secretName", secretName, "secretPath", secretPath) - actualPassphrase, err := createOrReuseTOFUSecret(kclient, namespace, secretName, secretPath, passphrase, logger) + actualPassphrase, err := createOrReuseTOFUSecret(kclient, namespace, secretName, secretPath, passphrase, ctx.TPMHash, ctx.Partition.Label, logger) if err != nil { return fmt.Errorf("creating TOFU secret: %w", err) } diff --git a/tests/advanced_scenarios_test.go b/tests/advanced_scenarios_test.go deleted file mode 100644 index 436479a..0000000 --- a/tests/advanced_scenarios_test.go +++ /dev/null @@ -1,418 +0,0 @@ -package e2e_test - -import ( - "fmt" - "os" - "os/exec" - "strings" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/spectrocloud/peg/matcher" -) - -// Advanced scenarios that test complex operational workflows, -// performance aspects, and edge cases - -var _ = Describe("Advanced Scenarios E2E Tests", func() { - var config string - var vmOpts VMOptions - var expectedInstallationSuccess bool - var testVM VM - var tpmHash string - - BeforeEach(func() { - expectedInstallationSuccess = true - vmOpts = DefaultVMOptions() - _, testVM = startVM(vmOpts) - fmt.Printf("\nadvanced scenarios VM.StateDir = %+v\n", testVM.StateDir) - testVM.EventuallyConnects(1200) - }) - - AfterEach(func() { - cleanupVM(testVM) - }) - - installKairosWithConfig := func(config string) { - installKairosWithConfigAdvanced(testVM, config, expectedInstallationSuccess) - } - - When("Testing Multi-Partition Support", Label("remote-multi-partition"), func() { - It("should handle multiple partitions on same TPM with different encryption keys", func() { - // Step 1: Get TPM hash - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) - - // Step 2: Create SealedVolume with multiple partitions - createMultiPartitionSealedVolume(tpmHash, []string{"COS_PERSISTENT", "COS_OEM"}) - - // Step 3: Configure Kairos with multiple encrypted partitions - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - - COS_OEM - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - - // Step 4: Verify both partitions are encrypted - By("Verifying both partitions are encrypted") - out, err := testVM.Sudo("blkid") - Expect(err).ToNot(HaveOccurred(), out) - Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"persistent\""), out) - Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"oem\""), out) - - // Step 5: Verify separate secrets were created for each partition - By("Verifying separate secrets were created for each partition") - Eventually(func() bool { - return secretExistsInNamespace(fmt.Sprintf("%s-cos-persistent", tpmHash), "default") && - secretExistsInNamespace(fmt.Sprintf("%s-cos-oem", tpmHash), "default") - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing Namespace Isolation", Label("remote-namespace-isolation"), func() { - It("should properly isolate SealedVolumes in different namespaces", func() { - // Step 1: Get TPM hash - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) - - // Step 2: Create SealedVolumes in different namespaces - createSealedVolumeInNamespace(tpmHash, "test-ns-1") - createSealedVolumeInNamespace(tpmHash, "test-ns-2") - - // Step 3: Initial setup with default namespace - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - - // Should fail initially because no SealedVolume in default namespace (test via CLI) - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) - - // Step 4: Create SealedVolume in default namespace - By("Creating SealedVolume in default namespace") - createSealedVolumeInNamespace(tpmHash, "default") - - time.Sleep(5 * time.Second) - - // Should now work via CLI - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", true) - - // Step 5: Verify secrets are created in appropriate namespaces - By("Verifying namespace isolation of secrets") - Eventually(func() bool { - return secretExistsInNamespace(fmt.Sprintf("%s-cos-persistent", tpmHash), "default") - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - // Secrets should not cross namespace boundaries - Expect(secretExistsInNamespace(fmt.Sprintf("%s-cos-persistent", tpmHash), "test-ns-1")).To(BeFalse()) - Expect(secretExistsInNamespace(fmt.Sprintf("%s-cos-persistent", tpmHash), "test-ns-2")).To(BeFalse()) - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing Network Resilience", Label("remote-network-resilience"), func() { - It("should handle network interruptions gracefully", func() { - // Step 1: Initial setup - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) - - // Create SealedVolume for enrollment - kubectlApplyYaml(fmt.Sprintf(`--- -apiVersion: keyserver.kairos.io/v1alpha1 -kind: SealedVolume -metadata: - name: "%s" - namespace: default -spec: - TPMHash: "%s" - partitions: - - label: COS_PERSISTENT - quarantined: false`, tpmHash, tpmHash)) - - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" - timeout: 30s - retry_attempts: 3 -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - // Step 2: Simulate network interruption during boot - By("Testing resilience to temporary network outage") - - // We can't easily simulate network interruption in the current test setup, - // but we can verify the timeout and retry configuration works by checking logs - out, err := testVM.Sudo("journalctl -u kcrypt* --no-pager") - Expect(err).ToNot(HaveOccurred()) - - // Should see evidence of successful KMS communication - Expect(out).To(ContainSubstring("kcrypt")) - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing Performance Under Load", Label("remote-performance"), func() { - It("should handle multiple concurrent authentication requests", func() { - // Step 1: Setup multiple encrypted partitions to test concurrent access - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) - - createMultiPartitionSealedVolume(tpmHash, []string{"COS_PERSISTENT", "COS_OEM"}) - - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - - COS_OEM - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - - // Step 2: Verify both partitions were decrypted successfully - By("Verifying concurrent partition decryption") - out, err := testVM.Sudo("blkid") - Expect(err).ToNot(HaveOccurred(), out) - Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"persistent\""), out) - Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"oem\""), out) - Expect(out).To(MatchRegexp("/dev/mapper.*LABEL=\"COS_PERSISTENT\""), out) - Expect(out).To(MatchRegexp("/dev/mapper.*LABEL=\"COS_OEM\""), out) - - // Step 3: Test multiple rapid reboots to stress test the system - By("Testing system stability under multiple rapid authentication cycles") - for i := 0; i < 3; i++ { - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - time.Sleep(2 * time.Second) // Brief pause between cycles - } - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing Large PCR Configuration", Label("remote-large-pcr"), func() { - It("should handle attestation with many PCRs", func() { - // Step 1: Create SealedVolume with extensive PCR configuration - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) - - // Create complex PCR configuration - sealedVolumeYaml := fmt.Sprintf(`--- -apiVersion: keyserver.kairos.io/v1alpha1 -kind: SealedVolume -metadata: - name: "%s" - namespace: default -spec: - TPMHash: "%s" - partitions: - - label: COS_PERSISTENT - quarantined: false - attestation: - pcrValues: - pcrs: - "0": "" # BIOS/UEFI - re-enroll - "1": "" # Platform Configuration - re-enroll - "2": "" # Option ROM Code - re-enroll - "3": "" # Option ROM Configuration - re-enroll - "4": "" # MBR/GPT - re-enroll - "5": "" # Boot Manager - re-enroll - "6": "" # Platform State - re-enroll - "7": "" # Secure Boot State - re-enroll - "8": "" # Command Line - re-enroll - "9": "" # initrd - re-enroll - "10": "" # IMA - re-enroll - # PCR 11 omitted - will be ignored - "12": "" # Kernel Command Line - re-enroll - "13": "" # sysvinit - re-enroll - "14": "" # systemd - re-enroll - "15": "" # System Integrity - re-enroll`, tpmHash, tpmHash) - - kubectlApplyYaml(sealedVolumeYaml) - - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - // Step 2: Verify that many PCRs were successfully enrolled - By("Verifying extensive PCR enrollment") - Eventually(func() int { - cmd := exec.Command("kubectl", "get", "sealedvolume", tpmHash, "-o", "yaml") - out, err := cmd.CombinedOutput() - if err != nil { - return 0 - } - - // Count non-empty PCR values - lines := strings.Split(string(out), "\n") - enrolledPCRs := 0 - for _, line := range lines { - if strings.Contains(line, "\":") && - !strings.Contains(line, "\": \"\"") && - strings.Contains(line, "\"") { - enrolledPCRs++ - } - } - return enrolledPCRs - }, 60*time.Second, 10*time.Second).Should(BeNumerically(">=", 10)) - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing Resource Cleanup", Label("remote-cleanup"), func() { - It("should properly cleanup resources when SealedVolumes are deleted", func() { - // Step 1: Create and verify initial setup - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) - - kubectlApplyYaml(fmt.Sprintf(`--- -apiVersion: keyserver.kairos.io/v1alpha1 -kind: SealedVolume -metadata: - name: "%s" - namespace: default -spec: - TPMHash: "%s" - partitions: - - label: COS_PERSISTENT - quarantined: false`, tpmHash, tpmHash)) - - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - // Step 2: Verify secret was created - secretName := fmt.Sprintf("%s-cos-persistent", tpmHash) - Eventually(func() bool { - return secretExistsInNamespace(secretName, "default") - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - // Step 3: Delete SealedVolume and verify orphaned secret handling - By("Testing resource cleanup after SealedVolume deletion") - deleteSealedVolume(tpmHash) - - // Secret should still exist (policy decision - secrets are not auto-deleted) - Expect(secretExistsInNamespace(secretName, "default")).To(BeTrue()) - - // Step 4: Try to retrieve passphrase without SealedVolume (should fail) - By("Testing passphrase retrieval after SealedVolume deletion") - time.Sleep(5 * time.Second) - - // Should fail to get passphrase without SealedVolume - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) - - // Step 5: Manual secret cleanup for test hygiene - cmd := exec.Command("kubectl", "delete", "secret", secretName, "--ignore-not-found=true") - cmd.CombinedOutput() - - }) - }) -}) diff --git a/tests/encryption_test.go b/tests/encryption_test.go index 7f971f1..3d9d59f 100644 --- a/tests/encryption_test.go +++ b/tests/encryption_test.go @@ -21,7 +21,7 @@ var installationOutput string var vm VM var mdnsVM VM -var _ = Describe("kcrypt encryption", func() { +var _ = Describe("kcrypt encryption", Label("encryption-tests"), func() { var config string var vmOpts VMOptions var expectedInstallationSuccess bool @@ -106,7 +106,8 @@ kcrypt: }) AfterEach(func() { - cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash) + sealedVolumeName := getSealedVolumeName(tpmHash) + cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName) out, err := cmd.CombinedOutput() Expect(err).ToNot(HaveOccurred(), out) @@ -184,7 +185,8 @@ kcrypt: }) AfterEach(func() { - cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash) + sealedVolumeName := getSealedVolumeName(tpmHash) + cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName) out, err := cmd.CombinedOutput() Expect(err).ToNot(HaveOccurred(), out) }) @@ -199,7 +201,7 @@ kcrypt: // Expect a secret to be created cmd := exec.Command("kubectl", "get", "secrets", - fmt.Sprintf("%s-cos-persistent", tpmHash), + fmt.Sprintf("%s-cos-persistent", getSealedVolumeName(tpmHash)), "-o=go-template='{{.data.generated_by|base64decode}}'", ) @@ -266,7 +268,8 @@ kcrypt: }) AfterEach(func() { - cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash) + sealedVolumeName := getSealedVolumeName(tpmHash) + cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName) out, err := cmd.CombinedOutput() Expect(err).ToNot(HaveOccurred(), out) @@ -286,26 +289,16 @@ kcrypt: }) }) - When("the key management server is listening on https", func() { + When("the certificate is pinned on the configuration", Label("remote-https-pinned"), func() { var tpmHash string BeforeEach(func() { tpmHash = createTPMPassphraseSecret(vm) - }) - - AfterEach(func() { - cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash) - out, err := cmd.CombinedOutput() - Expect(err).ToNot(HaveOccurred(), out) - }) - - When("the certificate is pinned on the configuration", Label("remote-https-pinned"), func() { - BeforeEach(func() { - cert := getChallengerServerCert() - kcryptConfig := createConfigWithCert(fmt.Sprintf("https://%s", os.Getenv("KMS_ADDRESS")), cert) - kcryptConfigBytes, err := yaml.Marshal(kcryptConfig) - Expect(err).ToNot(HaveOccurred()) - config = fmt.Sprintf(`#cloud-config + cert := getChallengerServerCert() + kcryptConfig := createConfigWithCert(fmt.Sprintf("https://%s", os.Getenv("KMS_ADDRESS")), cert) + kcryptConfigBytes, err := yaml.Marshal(kcryptConfig) + Expect(err).ToNot(HaveOccurred()) + config = fmt.Sprintf(`#cloud-config hostname: metal-{{ trunc 4 .MachineID }} users: @@ -322,23 +315,33 @@ install: %s `, string(kcryptConfigBytes)) - }) - - It("successfully talks to the server", func() { - vm.Reboot() - vm.EventuallyConnects(1200) - out, err := vm.Sudo("blkid") - Expect(err).ToNot(HaveOccurred(), out) - Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"persistent\""), out) - Expect(out).To(MatchRegexp("/dev/mapper.*LABEL=\"COS_PERSISTENT\""), out) - }) }) - When("the no certificate is set in the configuration", Label("remote-https-bad-cert"), func() { - BeforeEach(func() { - expectedInstallationSuccess = false + It("successfully talks to the server", func() { + vm.Reboot() + vm.EventuallyConnects(1200) + out, err := vm.Sudo("blkid") + Expect(err).ToNot(HaveOccurred(), out) + Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"persistent\""), out) + Expect(out).To(MatchRegexp("/dev/mapper.*LABEL=\"COS_PERSISTENT\""), out) + }) - config = fmt.Sprintf(`#cloud-config + AfterEach(func() { + sealedVolumeName := getSealedVolumeName(tpmHash) + cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName) + out, err := cmd.CombinedOutput() + Expect(err).ToNot(HaveOccurred(), out) + }) + }) + + When("the no certificate is set in the configuration", Label("remote-https-bad-cert"), func() { + var tpmHash string + + BeforeEach(func() { + tpmHash = createTPMPassphraseSecret(vm) + expectedInstallationSuccess = false + + config = fmt.Sprintf(`#cloud-config hostname: metal-{{ trunc 4 .MachineID }} users: @@ -356,13 +359,19 @@ kcrypt: challenger: challenger_server: "https://%s" `, os.Getenv("KMS_ADDRESS")) - }) + }) - It("fails to talk to the server", func() { - out, err := vm.Sudo("cat manual-install.txt") - Expect(err).ToNot(HaveOccurred(), out) - Expect(out).To(MatchRegexp("failed to verify certificate: x509: certificate signed by unknown authority")) - }) + It("fails to talk to the server", func() { + out, err := vm.Sudo("cat manual-install.txt") + Expect(err).ToNot(HaveOccurred(), out) + Expect(out).To(MatchRegexp("failed to verify certificate: x509: certificate signed by unknown authority")) + }) + + AfterEach(func() { + sealedVolumeName := getSealedVolumeName(tpmHash) + cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName) + out, err := cmd.CombinedOutput() + Expect(err).ToNot(HaveOccurred(), out) }) }) }) diff --git a/tests/remote_attestation_test.go b/tests/remote_attestation_test.go new file mode 100644 index 0000000..05a09e9 --- /dev/null +++ b/tests/remote_attestation_test.go @@ -0,0 +1,262 @@ +package e2e_test + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/spectrocloud/peg/matcher" +) + +// Advanced scenarios that test complex operational workflows, +// performance aspects, and edge cases + +var _ = Describe("Remote Attestation E2E Tests", Label("remote-complete-workflow"), func() { + var config string + var vmOpts VMOptions + var expectedInstallationSuccess bool + var testVM VM + var tpmHash string + + BeforeEach(func() { + expectedInstallationSuccess = true + vmOpts = DefaultVMOptions() + _, testVM = startVM(vmOpts) + testVM.EventuallyConnects(1200) + }) + + AfterEach(func() { + cleanupVM(testVM) + // Clean up test resources if tpmHash was set + if tpmHash != "" { + cleanupTestResources(tpmHash) + } + }) + + installKairosWithConfig := func(config string) { + installKairosWithConfigAdvanced(testVM, config, expectedInstallationSuccess) + } + + It("should perform TOFU enrollment, quarantine testing, PCR management, AK management, error handling, secret reuse, and multi-partition support", func() { + tpmHash = getTPMHash(testVM) + + deleteSealedVolume(tpmHash) + + config = fmt.Sprintf(`#cloud-config + +hostname: metal-{{ trunc 4 .MachineID }} +users: +- name: kairos +passwd: kairos + +install: +encrypted_partitions: +- COS_PERSISTENT +- COS_OEM +grub_options: +extra_cmdline: "rd.neednet=1" +reboot: false + +kcrypt: +challenger: +challenger_server: "http://%s" +`, os.Getenv("KMS_ADDRESS")) + + installKairosWithConfig(config) + rebootAndConnect(testVM) + verifyEncryptedPartition(testVM) + + // Verify both partitions are encrypted + By("Verifying both partitions are encrypted") + out, err := testVM.Sudo("blkid") + Expect(err).ToNot(HaveOccurred(), out) + Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"persistent\""), out) + Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"oem\""), out) + + By("Verifying SealedVolume was auto-created with attestation data") + Eventually(func() bool { + sealedVolumeName := getSealedVolumeName(tpmHash) + cmd := exec.Command("kubectl", "get", "sealedvolume", sealedVolumeName, "-o", "yaml") + out, err := cmd.CombinedOutput() + if err != nil { + return false + } + // Check that attestation data was populated (not empty) + return strings.Contains(string(out), "attestation:") && + strings.Contains(string(out), "ekPublicKey:") && + strings.Contains(string(out), "akPublicKey:") + }, 30*time.Second, 5*time.Second).Should(BeTrue()) + + By("Verifying encryption secrets were auto-generated for both partitions") + Eventually(func() bool { + sealedVolumeName := getSealedVolumeName(tpmHash) + return secretExists(fmt.Sprintf("%s-cos-persistent", sealedVolumeName)) && + secretExists(fmt.Sprintf("%s-cos-oem", sealedVolumeName)) + }, 30*time.Second, 5*time.Second).Should(BeTrue()) + + By("Testing subsequent authentication with learned attestation data") + rebootAndConnect(testVM) + verifyEncryptedPartition(testVM) + + By("quarantining the TPM") + quarantineTPM(tpmHash) + + By("Testing that quarantined TPM is rejected via CLI for both partitions") + expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) + expectPassphraseRetrieval(testVM, "COS_OEM", false) + + By("Testing recovery by unquarantining TPM") + unquarantineTPM(tpmHash) + + expectPassphraseRetrieval(testVM, "COS_PERSISTENT", true) + expectPassphraseRetrieval(testVM, "COS_OEM", true) + + // Continue with PCR and AK Management testing + By("Testing PCR re-enrollment by setting PCR 0 to wrong value") + updateSealedVolumeAttestation(tpmHash, "pcrValues.pcrs.0", "wrong-pcr0-value") + + By("checking that the passphrase retrieval fails with wrong PCR for both partitions") + expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) + expectPassphraseRetrieval(testVM, "COS_OEM", false) + + By("setting PCR 0 to an empty value (re-enrollment mode)") + updateSealedVolumeAttestation(tpmHash, "pcrValues.pcrs.0", "") + + By("checking that the passphrase retrieval works after PCR re-enrollment for both partitions") + expectPassphraseRetrieval(testVM, "COS_PERSISTENT", true) + expectPassphraseRetrieval(testVM, "COS_OEM", true) + + By("Verifying PCR 0 was re-enrolled with current value") + Eventually(func() bool { + sealedVolumeName := getSealedVolumeName(tpmHash) + cmd := exec.Command("kubectl", "get", "sealedvolume", sealedVolumeName, "-o", "yaml") + out, err := cmd.CombinedOutput() + if err != nil { + return false + } + // PCR 0 should now have a new non-empty value + return strings.Contains(string(out), "\"0\":") && + !strings.Contains(string(out), "\"0\": \"\"") && + !strings.Contains(string(out), "\"0\": \"wrong-pcr0-value\"") + }, 30*time.Second, 5*time.Second).Should(BeTrue()) + + // Continue with AK Management testing + By("Testing AK re-enrollment by setting AK to empty") + updateSealedVolumeAttestation(tpmHash, "akPublicKey", "") + + By("Verifying AK was re-enrolled with actual value") + var learnedAK, learnedEK string + Eventually(func() bool { + sealedVolumeName := getSealedVolumeName(tpmHash) + cmd := exec.Command("kubectl", "get", "sealedvolume", sealedVolumeName, "-o", "yaml") + out, err := cmd.CombinedOutput() + if err != nil { + return false + } + + // Extract learned AK and EK for later enforcement test + lines := strings.Split(string(out), "\n") + for _, line := range lines { + if strings.Contains(line, "akPublicKey:") && !strings.Contains(line, "akPublicKey: \"\"") { + parts := strings.Split(line, "akPublicKey:") + if len(parts) > 1 { + learnedAK = strings.TrimSpace(strings.Trim(parts[1], "\"")) + } + } + if strings.Contains(line, "ekPublicKey:") && !strings.Contains(line, "ekPublicKey: \"\"") { + parts := strings.Split(line, "ekPublicKey:") + if len(parts) > 1 { + learnedEK = strings.TrimSpace(strings.Trim(parts[1], "\"")) + } + } + } + + return learnedAK != "" && learnedEK != "" + }, 30*time.Second, 5*time.Second).Should(BeTrue()) + + // Test AK enforcement by setting wrong AK + By("Testing AK enforcement by setting wrong AK value") + updateSealedVolumeAttestation(tpmHash, "akPublicKey", "wrong-ak-value") + + time.Sleep(5 * time.Second) + + // Should fail to retrieve passphrase with wrong AK for both partitions + expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) + expectPassphraseRetrieval(testVM, "COS_OEM", false) + + // Restore correct AK and verify it works via CLI + By("Restoring correct AK and verifying authentication works for both partitions") + updateSealedVolumeAttestation(tpmHash, "akPublicKey", learnedAK) + + time.Sleep(5 * time.Second) + + // Should now work with correct AK for both partitions + expectPassphraseRetrieval(testVM, "COS_PERSISTENT", true) + expectPassphraseRetrieval(testVM, "COS_OEM", true) + + // Continue with Error Handling testing + By("Testing invalid TPM hash rejection") + invalidHash := "invalid-tpm-hash-12345" + createSealedVolumeWithAttestation(invalidHash, nil) + + // Should fail due to TPM hash mismatch for both partitions (test via CLI, no risky reboot) + expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) + expectPassphraseRetrieval(testVM, "COS_OEM", false) + + // Cleanup invalid SealedVolume + deleteSealedVolume(invalidHash) + + // Test with correct TPM hash to verify system still works for both partitions + By("Verifying system still works with correct TPM hash for both partitions") + // The original SealedVolume should still exist and work + expectPassphraseRetrieval(testVM, "COS_PERSISTENT", true) + expectPassphraseRetrieval(testVM, "COS_OEM", true) + + // Continue with Secret Reuse testing + By("Testing secret reuse when SealedVolume is recreated for both partitions") + sealedVolumeName := getSealedVolumeName(tpmHash) + persistentSecretName := fmt.Sprintf("%s-cos-persistent", sealedVolumeName) + oemSecretName := fmt.Sprintf("%s-cos-oem", sealedVolumeName) + + // Get secret data for comparison for both partitions + cmd := exec.Command("kubectl", "get", "secret", persistentSecretName, "-o", "yaml") + originalPersistentSecretData, err := cmd.CombinedOutput() + Expect(err).ToNot(HaveOccurred()) + + cmd = exec.Command("kubectl", "get", "secret", oemSecretName, "-o", "yaml") + originalOemSecretData, err := cmd.CombinedOutput() + Expect(err).ToNot(HaveOccurred()) + + // Delete SealedVolume but keep secrets + deleteSealedVolume(tpmHash) + + // Verify secrets still exist + Expect(secretExists(persistentSecretName)).To(BeTrue()) + Expect(secretExists(oemSecretName)).To(BeTrue()) + + // Recreate SealedVolume and verify secret reuse + By("Recreating SealedVolume and verifying secret reuse for both partitions") + createSealedVolumeWithAttestation(tpmHash, nil) + + // Should reuse existing secrets + rebootAndConnect(testVM) + verifyEncryptedPartition(testVM) + + // Verify the same secrets are being used + cmd = exec.Command("kubectl", "get", "secret", persistentSecretName, "-o", "yaml") + newPersistentSecretData, err := cmd.CombinedOutput() + Expect(err).ToNot(HaveOccurred()) + + cmd = exec.Command("kubectl", "get", "secret", oemSecretName, "-o", "yaml") + newOemSecretData, err := cmd.CombinedOutput() + Expect(err).ToNot(HaveOccurred()) + + // The secret data should be identical (reused, not regenerated) for both partitions + Expect(string(newPersistentSecretData)).To(Equal(string(originalPersistentSecretData))) + Expect(string(newOemSecretData)).To(Equal(string(originalOemSecretData))) + }) +}) diff --git a/tests/selective_enrollment_test.go b/tests/selective_enrollment_test.go deleted file mode 100644 index e040432..0000000 --- a/tests/selective_enrollment_test.go +++ /dev/null @@ -1,485 +0,0 @@ -package e2e_test - -import ( - "fmt" - "os" - "os/exec" - "strings" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/spectrocloud/peg/matcher" -) - -// These tests focus on selective enrollment scenarios and VM reuse optimization -// Instead of spinning up a new VM for each test case, we reuse VMs across -// sequential scenarios to reduce test execution time. - -var _ = Describe("Selective Enrollment E2E Tests", func() { - var config string - var vmOpts VMOptions - var expectedInstallationSuccess bool - var testVM VM - var tpmHash string - - // VM lifecycle management for reuse optimization - var vmInitialized bool - - BeforeEach(func() { - expectedInstallationSuccess = true - vmOpts = DefaultVMOptions() - vmInitialized = false - }) - - AfterEach(func() { - if vmInitialized { - testVM.GatherLog("/run/immucore/immucore.log") - } - }) - - // Local helper functions using common suite functions - ensureVMRunning := func() { - if !vmInitialized { - By("Starting VM for selective enrollment tests") - _, testVM = startVM(vmOpts) - fmt.Printf("\nselective enrollment VM.StateDir = %+v\n", testVM.StateDir) - testVM.EventuallyConnects(1200) - vmInitialized = true - } - } - - installKairosWithConfig := func(config string) { - installKairosWithConfigAdvanced(testVM, config, expectedInstallationSuccess) - } - - // Cleanup VM at the very end - var _ = AfterSuite(func() { - if vmInitialized { - cleanupVM(testVM) - } - }) - - When("Testing Pure TOFU Enrollment Flow", Label("remote-tofu"), func() { - It("should perform complete TOFU enrollment and subsequent successful authentications", func() { - ensureVMRunning() - - // Step 1: Get TPM hash but don't create any SealedVolume (pure TOFU) - tpmHash = getTPMHash(testVM) - - // Ensure no pre-existing SealedVolume - deleteSealedVolume(tpmHash) - - // Step 2: Configure Kairos for remote KMS without pre-created SealedVolume - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - // Step 3: Verify SealedVolume was auto-created with TOFU enrollment - By("Verifying SealedVolume was auto-created with attestation data") - Eventually(func() bool { - cmd := exec.Command("kubectl", "get", "sealedvolume", tpmHash, "-o", "yaml") - out, err := cmd.CombinedOutput() - if err != nil { - return false - } - // Check that attestation data was populated (not empty) - return strings.Contains(string(out), "attestation:") && - strings.Contains(string(out), "ekPublicKey:") && - strings.Contains(string(out), "akPublicKey:") - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - // Step 4: Verify secret was created - By("Verifying encryption secret was auto-generated") - Eventually(func() bool { - return secretExists(fmt.Sprintf("%s-cos-persistent", tpmHash)) - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - // Step 5: Test subsequent authentication works - By("Testing subsequent authentication with learned attestation data") - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing Quarantine Management", Label("remote-quarantine"), func() { - It("should handle quarantine, rejection, and recovery flows using the same VM", func() { - ensureVMRunning() - - // Step 1: Initial enrollment - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) // Ensure clean state - - // Create SealedVolume for TOFU enrollment - createSealedVolumeWithAttestation(tpmHash, nil) - - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - // Step 2: Quarantine the TPM - quarantineTPM(tpmHash) - - // Give some time for the change to propagate - time.Sleep(5 * time.Second) - - // Step 3: Verify quarantined TPM is rejected via CLI (no risky reboot) - By("Testing that quarantined TPM is rejected via CLI") - - // Give some time for quarantine to propagate - time.Sleep(5 * time.Second) - - // Should fail to retrieve passphrase when quarantined - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) - - // Step 4: Test recovery by unquarantining - By("Testing recovery by unquarantining TPM") - unquarantineTPM(tpmHash) - - // Give some time for the change to propagate - time.Sleep(5 * time.Second) - - // Should now be able to retrieve passphrase again - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", true) - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing PCR Management Scenarios", Label("remote-pcr-mgmt"), func() { - It("should handle PCR re-enrollment, omission, and mixed states using the same VM", func() { - ensureVMRunning() - - // Step 1: Initial enrollment with specific PCR enforcement - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) - - // Create SealedVolume with specific PCR values enforced - attestationConfig := map[string]interface{}{ - "pcrValues": map[string]string{ - "0": "specific-pcr0-value", // Will be enforced - "7": "", // Will be re-enrolled - // PCR 11 omitted - will be ignored - }, - } - createSealedVolumeWithAttestation(tpmHash, attestationConfig) - - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - // Step 2: Verify PCR 7 was re-enrolled (updated from empty to actual value) - By("Verifying PCR 7 was re-enrolled with actual value") - Eventually(func() bool { - cmd := exec.Command("kubectl", "get", "sealedvolume", tpmHash, "-o", "yaml") - out, err := cmd.CombinedOutput() - if err != nil { - return false - } - // PCR 7 should now have a non-empty value - return strings.Contains(string(out), "\"7\":") && - !strings.Contains(string(out), "\"7\": \"\"") - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - // Step 3: Test PCR enforcement by changing enforced PCR (should fail via CLI) - By("Testing PCR enforcement by modifying enforced PCR 0") - updateSealedVolumeAttestation(tpmHash, "pcrValues.pcrs.0", "wrong-pcr0-value") - - time.Sleep(5 * time.Second) - - // Should fail to retrieve passphrase with wrong PCR value - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) - - // Step 4: Test PCR re-enrollment by setting to empty - By("Testing PCR re-enrollment by setting PCR 0 to empty") - updateSealedVolumeAttestation(tpmHash, "pcrValues.pcrs.0", "") - - time.Sleep(5 * time.Second) - - // Should now re-enroll and work via CLI - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", true) - - // Step 5: Verify PCR 0 was re-enrolled with new value - By("Verifying PCR 0 was re-enrolled with current value") - Eventually(func() bool { - cmd := exec.Command("kubectl", "get", "sealedvolume", tpmHash, "-o", "yaml") - out, err := cmd.CombinedOutput() - if err != nil { - return false - } - // PCR 0 should now have a new non-empty value - return strings.Contains(string(out), "\"0\":") && - !strings.Contains(string(out), "\"0\": \"\"") && - !strings.Contains(string(out), "\"0\": \"wrong-pcr0-value\"") - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing AK Management", Label("remote-ak-mgmt"), func() { - It("should handle AK re-enrollment and enforcement using the same VM", func() { - ensureVMRunning() - - // Step 1: Initial enrollment with AK re-enrollment mode - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) - - // Create SealedVolume with empty AK (re-enrollment mode) - attestationConfig := map[string]interface{}{ - "akPublicKey": "", // Will be re-enrolled - "ekPublicKey": "", // Will be re-enrolled - } - createSealedVolumeWithAttestation(tpmHash, attestationConfig) - - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - // Step 2: Verify AK and EK were re-enrolled - By("Verifying AK and EK were re-enrolled with actual values") - var learnedAK, learnedEK string - Eventually(func() bool { - cmd := exec.Command("kubectl", "get", "sealedvolume", tpmHash, "-o", "yaml") - out, err := cmd.CombinedOutput() - if err != nil { - return false - } - - // Extract learned AK and EK for later enforcement test - lines := strings.Split(string(out), "\n") - for _, line := range lines { - if strings.Contains(line, "akPublicKey:") && !strings.Contains(line, "akPublicKey: \"\"") { - parts := strings.Split(line, "akPublicKey:") - if len(parts) > 1 { - learnedAK = strings.TrimSpace(strings.Trim(parts[1], "\"")) - } - } - if strings.Contains(line, "ekPublicKey:") && !strings.Contains(line, "ekPublicKey: \"\"") { - parts := strings.Split(line, "ekPublicKey:") - if len(parts) > 1 { - learnedEK = strings.TrimSpace(strings.Trim(parts[1], "\"")) - } - } - } - - return learnedAK != "" && learnedEK != "" - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - // Step 3: Test AK enforcement by setting wrong AK - By("Testing AK enforcement by setting wrong AK value") - updateSealedVolumeAttestation(tpmHash, "akPublicKey", "wrong-ak-value") - - time.Sleep(5 * time.Second) - - // Should fail to retrieve passphrase with wrong AK - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) - - // Step 4: Restore correct AK and verify it works via CLI - By("Restoring correct AK and verifying authentication works") - updateSealedVolumeAttestation(tpmHash, "akPublicKey", learnedAK) - - time.Sleep(5 * time.Second) - - // Should now work with correct AK - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", true) - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing Secret Reuse Scenarios", Label("remote-secret-reuse"), func() { - It("should reuse existing secrets when SealedVolume is recreated", func() { - ensureVMRunning() - - // Step 1: Initial enrollment to create secret - tpmHash = getTPMHash(testVM) - deleteSealedVolume(tpmHash) - - createSealedVolumeWithAttestation(tpmHash, nil) - - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - // Step 2: Get the generated secret - secretName := fmt.Sprintf("%s-cos-persistent", tpmHash) - Eventually(func() bool { - return secretExists(secretName) - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - // Get secret data for comparison - cmd := exec.Command("kubectl", "get", "secret", secretName, "-o", "yaml") - originalSecretData, err := cmd.CombinedOutput() - Expect(err).ToNot(HaveOccurred()) - - // Step 3: Delete SealedVolume but keep secret - deleteSealedVolume(tpmHash) - - // Verify secret still exists - Expect(secretExists(secretName)).To(BeTrue()) - - // Step 4: Recreate SealedVolume and verify secret reuse - By("Recreating SealedVolume and verifying secret reuse") - createSealedVolumeWithAttestation(tpmHash, nil) - - // Should reuse existing secret - rebootAndConnect(testVM) - verifyEncryptedPartition(testVM) - - // Step 5: Verify the same secret is being used - cmd = exec.Command("kubectl", "get", "secret", secretName, "-o", "yaml") - newSecretData, err := cmd.CombinedOutput() - Expect(err).ToNot(HaveOccurred()) - - // The secret data should be identical (reused, not regenerated) - Expect(string(newSecretData)).To(Equal(string(originalSecretData))) - - cleanupTestResources(tpmHash) - }) - }) - - When("Testing Error Handling and Edge Cases", Label("remote-edge-cases"), func() { - It("should handle various error conditions properly", func() { - ensureVMRunning() - - // Step 1: Test invalid TPM hash rejection - By("Testing invalid TPM hash rejection") - invalidHash := "invalid-tpm-hash-12345" - createSealedVolumeWithAttestation(invalidHash, nil) - - config = fmt.Sprintf(`#cloud-config - -hostname: metal-{{ trunc 4 .MachineID }} -users: -- name: kairos - passwd: kairos - -install: - encrypted_partitions: - - COS_PERSISTENT - grub_options: - extra_cmdline: "rd.neednet=1" - reboot: false - -kcrypt: - challenger: - challenger_server: "http://%s" -`, os.Getenv("KMS_ADDRESS")) - - installKairosWithConfig(config) - - // Should fail due to TPM hash mismatch (test via CLI, no risky reboot) - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", false) - - // Cleanup invalid SealedVolume - deleteSealedVolume(invalidHash) - - // Step 2: Test with correct TPM hash to verify system works - tpmHash = getTPMHash(testVM) - createSealedVolumeWithAttestation(tpmHash, nil) - - // Test with correct hash should work - expectPassphraseRetrieval(testVM, "COS_PERSISTENT", true) - - cleanupTestResources(tpmHash) - }) - }) -}) diff --git a/tests/suite_test.go b/tests/suite_test.go index ea5d698..5d97a12 100644 --- a/tests/suite_test.go +++ b/tests/suite_test.go @@ -320,8 +320,16 @@ func expectPassphraseRetrieval(vm VM, partitionLabel string, shouldSucceed bool) } } +// Helper to get the correct SealedVolume name from TPM hash +func getSealedVolumeName(tpmHash string) string { + // Convert to lowercase and take first 8 characters to match the actual naming pattern + // This matches the pattern used in pkg/challenger/challenger.go: fmt.Sprintf("tofu-%s", tpmHash[:8]) + return fmt.Sprintf("tofu-%s", strings.ToLower(tpmHash[:8])) +} + // Helper to create SealedVolume with specific attestation configuration func createSealedVolumeWithAttestation(tpmHash string, attestationConfig map[string]interface{}) { + sealedVolumeName := getSealedVolumeName(tpmHash) sealedVolumeYaml := fmt.Sprintf(`--- apiVersion: keyserver.kairos.io/v1alpha1 kind: SealedVolume @@ -332,7 +340,7 @@ spec: TPMHash: "%s" partitions: - label: COS_PERSISTENT - quarantined: false`, tpmHash, tpmHash) + quarantined: false`, sealedVolumeName, tpmHash) if attestationConfig != nil { sealedVolumeYaml += "\n attestation:" @@ -356,43 +364,48 @@ spec: // Helper to update SealedVolume attestation configuration func updateSealedVolumeAttestation(tpmHashParam string, field, value string) { - By(fmt.Sprintf("Updating SealedVolume %s field %s to %s", tpmHashParam, field, value)) + sealedVolumeName := getSealedVolumeName(tpmHashParam) + By(fmt.Sprintf("Updating SealedVolume %s field %s to %s", sealedVolumeName, field, value)) patch := fmt.Sprintf(`{"spec":{"attestation":{"%s":"%s"}}}`, field, value) - cmd := exec.Command("kubectl", "patch", "sealedvolume", tpmHashParam, "--type=merge", "-p", patch) + cmd := exec.Command("kubectl", "patch", "sealedvolume", sealedVolumeName, "--type=merge", "-p", patch) out, err := cmd.CombinedOutput() Expect(err).ToNot(HaveOccurred(), string(out)) } // Helper to quarantine TPM func quarantineTPM(tpmHash string) { - By(fmt.Sprintf("Quarantining TPM %s", tpmHash)) + sealedVolumeName := getSealedVolumeName(tpmHash) + By(fmt.Sprintf("Quarantining TPM %s", sealedVolumeName)) patch := `{"spec":{"quarantined":true}}` - cmd := exec.Command("kubectl", "patch", "sealedvolume", tpmHash, "--type=merge", "-p", patch) + cmd := exec.Command("kubectl", "patch", "sealedvolume", sealedVolumeName, "--type=merge", "-p", patch) out, err := cmd.CombinedOutput() Expect(err).ToNot(HaveOccurred(), string(out)) } // Helper to unquarantine TPM func unquarantineTPM(tpmHashParam string) { - By(fmt.Sprintf("Unquarantining TPM %s", tpmHashParam)) + sealedVolumeName := getSealedVolumeName(tpmHashParam) + By(fmt.Sprintf("Unquarantining TPM %s", sealedVolumeName)) patch := `{"spec":{"quarantined":false}}` - cmd := exec.Command("kubectl", "patch", "sealedvolume", tpmHashParam, "--type=merge", "-p", patch) + cmd := exec.Command("kubectl", "patch", "sealedvolume", sealedVolumeName, "--type=merge", "-p", patch) out, err := cmd.CombinedOutput() Expect(err).ToNot(HaveOccurred(), string(out)) } // Helper to delete SealedVolume -func deleteSealedVolume(tmpHashParam string) { - By(fmt.Sprintf("Deleting SealedVolume %s", tmpHashParam)) - cmd := exec.Command("kubectl", "delete", "sealedvolume", tmpHashParam, "--ignore-not-found=true") +func deleteSealedVolume(tpmHashParam string) { + sealedVolumeName := getSealedVolumeName(tpmHashParam) + By(fmt.Sprintf("Deleting SealedVolume %s", sealedVolumeName)) + cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName, "--ignore-not-found=true") out, err := cmd.CombinedOutput() Expect(err).ToNot(HaveOccurred(), string(out)) } // Helper to delete SealedVolume from all namespaces func deleteSealedVolumeAllNamespaces(tpmHashParam string) { - By(fmt.Sprintf("Deleting SealedVolume %s from all namespaces", tpmHashParam)) - cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHashParam, "--ignore-not-found=true", "--all-namespaces") + sealedVolumeName := getSealedVolumeName(tpmHashParam) + By(fmt.Sprintf("Deleting SealedVolume %s from all namespaces", sealedVolumeName)) + cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName, "--ignore-not-found=true", "--all-namespaces") out, err := cmd.CombinedOutput() Expect(err).ToNot(HaveOccurred(), string(out)) } @@ -450,12 +463,15 @@ spec: // Helper to create SealedVolume in specific namespace func createSealedVolumeInNamespace(tpmHash, namespace string) { - // First create the namespace if it doesn't exist + // First create the namespace if it doesn't exist with test labels kubectlApplyYaml(fmt.Sprintf(`--- apiVersion: v1 kind: Namespace metadata: - name: %s`, namespace)) + name: %s + labels: + test.kcrypt.kairos.io/type: test-namespace + test.kcrypt.kairos.io/purpose: kcrypt-challenger-testing`, namespace)) sealedVolumeYaml := fmt.Sprintf(`--- apiVersion: keyserver.kairos.io/v1alpha1 @@ -478,18 +494,19 @@ func cleanupTestResources(tpmHash string) { if tpmHash != "" { deleteSealedVolumeAllNamespaces(tpmHash) - // Cleanup associated secrets in all namespaces - cmd := exec.Command("kubectl", "delete", "secret", tpmHash, "--ignore-not-found=true", "--all-namespaces") + // Cleanup associated secrets using labels + // This will delete all secrets created by kcrypt-challenger for this TPM hash + cmd := exec.Command("kubectl", "delete", "secret", + "-l", fmt.Sprintf("kcrypt.kairos.io/tpm-hash=%s", tpmHash), + "--ignore-not-found=true", "--all-namespaces") cmd.CombinedOutput() + } +} - cmd = exec.Command("kubectl", "delete", "secret", fmt.Sprintf("%s-cos-persistent", tpmHash), "--ignore-not-found=true", "--all-namespaces") - cmd.CombinedOutput() - - // Cleanup test namespaces - cmd = exec.Command("kubectl", "delete", "namespace", "test-ns-1", "--ignore-not-found=true") - cmd.CombinedOutput() - - cmd = exec.Command("kubectl", "delete", "namespace", "test-ns-2", "--ignore-not-found=true") +// Helper to delete specific test namespaces +func deleteTestNamespaces(namespaces ...string) { + for _, namespace := range namespaces { + cmd := exec.Command("kubectl", "delete", "namespace", namespace, "--ignore-not-found=true") cmd.CombinedOutput() } }