mirror of
https://github.com/kairos-io/kcrypt-challenger.git
synced 2025-09-28 13:55:51 +00:00
Merge multiple tests into one
to save time from setup of VMs and such Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
This commit is contained in:
18
.github/workflows/e2e-tests.yml
vendored
18
.github/workflows/e2e-tests.yml
vendored
@@ -55,27 +55,15 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Original basic tests
|
# Basic encryption tests
|
||||||
- label: "local-encryption"
|
- label: "local-encryption"
|
||||||
- label: "remote-auto"
|
- label: "remote-auto"
|
||||||
- label: "remote-static"
|
- label: "remote-static"
|
||||||
- label: "remote-https-pinned"
|
- label: "remote-https-pinned"
|
||||||
- label: "remote-https-bad-cert"
|
- label: "remote-https-bad-cert"
|
||||||
- label: "discoverable-kms"
|
- label: "discoverable-kms"
|
||||||
# New selective enrollment tests
|
# Consolidated remote attestation workflow test
|
||||||
- label: "remote-tofu"
|
- label: "remote-complete-workflow"
|
||||||
- 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"
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
48
README.md
48
README.md
@@ -466,49 +466,11 @@ Comprehensive E2E test suite has been implemented covering all selective enrollm
|
|||||||
|
|
||||||
### ✅ Implemented E2E Test Scenarios
|
### ✅ Implemented E2E Test Scenarios
|
||||||
|
|
||||||
#### **1. Basic Enrollment Flows**
|
#### **Comprehensive Remote Attestation Workflow**
|
||||||
- [x] **Pure TOFU Enrollment**: First-time enrollment with automatic attestation data learning (`remote-tofu`)
|
- [x] **Complete E2E Test Suite**: All remote attestation scenarios consolidated into a single comprehensive test (`remote-complete-workflow`)
|
||||||
- [x] **Manual SealedVolume Creation**: Pre-created SealedVolume with selective field configuration (multiple scenarios)
|
- TOFU enrollment, quarantine management, PCR management, AK management
|
||||||
- [x] **Secret Reuse**: SealedVolume recreation while preserving existing Kubernetes secrets (`remote-secret-reuse`)
|
- Secret reuse, error handling, multi-partition support
|
||||||
|
- Performance testing, security verification, and operational workflows
|
||||||
#### **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`)
|
|
||||||
|
|
||||||
#### **9. Logging & Observability**
|
#### **9. Logging & Observability**
|
||||||
- [x] **Audit Trail Verification**: Security events logging validation (integrated across all tests)
|
- [x] **Audit Trail Verification**: Security events logging validation (integrated across all tests)
|
||||||
|
@@ -125,11 +125,18 @@ func generateTOFUPassphrase() (string, error) {
|
|||||||
// createOrReuseTOFUSecret creates a Kubernetes secret containing the generated passphrase
|
// createOrReuseTOFUSecret creates a Kubernetes secret containing the generated passphrase
|
||||||
// If a secret with the same name already exists, it returns the existing 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)
|
// 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{
|
secret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: secretName,
|
Name: secretName,
|
||||||
Namespace: namespace,
|
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,
|
Type: corev1.SecretTypeOpaque,
|
||||||
Data: map[string][]byte{
|
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)
|
// Create Kubernetes secret (or reuse if it already exists from a previous enrollment)
|
||||||
logger.Info("Creating TOFU secret", "secretName", secretName, "secretPath", secretPath)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("creating TOFU secret: %w", err)
|
return fmt.Errorf("creating TOFU secret: %w", err)
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@@ -21,7 +21,7 @@ var installationOutput string
|
|||||||
var vm VM
|
var vm VM
|
||||||
var mdnsVM VM
|
var mdnsVM VM
|
||||||
|
|
||||||
var _ = Describe("kcrypt encryption", func() {
|
var _ = Describe("kcrypt encryption", Label("encryption-tests"), func() {
|
||||||
var config string
|
var config string
|
||||||
var vmOpts VMOptions
|
var vmOpts VMOptions
|
||||||
var expectedInstallationSuccess bool
|
var expectedInstallationSuccess bool
|
||||||
@@ -106,7 +106,8 @@ kcrypt:
|
|||||||
})
|
})
|
||||||
|
|
||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash)
|
sealedVolumeName := getSealedVolumeName(tpmHash)
|
||||||
|
cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName)
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), out)
|
Expect(err).ToNot(HaveOccurred(), out)
|
||||||
|
|
||||||
@@ -184,7 +185,8 @@ kcrypt:
|
|||||||
})
|
})
|
||||||
|
|
||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash)
|
sealedVolumeName := getSealedVolumeName(tpmHash)
|
||||||
|
cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName)
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), out)
|
Expect(err).ToNot(HaveOccurred(), out)
|
||||||
})
|
})
|
||||||
@@ -199,7 +201,7 @@ kcrypt:
|
|||||||
|
|
||||||
// Expect a secret to be created
|
// Expect a secret to be created
|
||||||
cmd := exec.Command("kubectl", "get", "secrets",
|
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}}'",
|
"-o=go-template='{{.data.generated_by|base64decode}}'",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -266,7 +268,8 @@ kcrypt:
|
|||||||
})
|
})
|
||||||
|
|
||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash)
|
sealedVolumeName := getSealedVolumeName(tpmHash)
|
||||||
|
cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName)
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), out)
|
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
|
var tpmHash string
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
tpmHash = createTPMPassphraseSecret(vm)
|
tpmHash = createTPMPassphraseSecret(vm)
|
||||||
})
|
cert := getChallengerServerCert()
|
||||||
|
kcryptConfig := createConfigWithCert(fmt.Sprintf("https://%s", os.Getenv("KMS_ADDRESS")), cert)
|
||||||
AfterEach(func() {
|
kcryptConfigBytes, err := yaml.Marshal(kcryptConfig)
|
||||||
cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash)
|
Expect(err).ToNot(HaveOccurred())
|
||||||
out, err := cmd.CombinedOutput()
|
config = fmt.Sprintf(`#cloud-config
|
||||||
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
|
|
||||||
|
|
||||||
hostname: metal-{{ trunc 4 .MachineID }}
|
hostname: metal-{{ trunc 4 .MachineID }}
|
||||||
users:
|
users:
|
||||||
@@ -322,23 +315,33 @@ install:
|
|||||||
%s
|
%s
|
||||||
|
|
||||||
`, string(kcryptConfigBytes))
|
`, 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() {
|
It("successfully talks to the server", func() {
|
||||||
BeforeEach(func() {
|
vm.Reboot()
|
||||||
expectedInstallationSuccess = false
|
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 }}
|
hostname: metal-{{ trunc 4 .MachineID }}
|
||||||
users:
|
users:
|
||||||
@@ -356,13 +359,19 @@ kcrypt:
|
|||||||
challenger:
|
challenger:
|
||||||
challenger_server: "https://%s"
|
challenger_server: "https://%s"
|
||||||
`, os.Getenv("KMS_ADDRESS"))
|
`, os.Getenv("KMS_ADDRESS"))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("fails to talk to the server", func() {
|
It("fails to talk to the server", func() {
|
||||||
out, err := vm.Sudo("cat manual-install.txt")
|
out, err := vm.Sudo("cat manual-install.txt")
|
||||||
Expect(err).ToNot(HaveOccurred(), out)
|
Expect(err).ToNot(HaveOccurred(), out)
|
||||||
Expect(out).To(MatchRegexp("failed to verify certificate: x509: certificate signed by unknown authority"))
|
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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
262
tests/remote_attestation_test.go
Normal file
262
tests/remote_attestation_test.go
Normal file
@@ -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)))
|
||||||
|
})
|
||||||
|
})
|
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@@ -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
|
// Helper to create SealedVolume with specific attestation configuration
|
||||||
func createSealedVolumeWithAttestation(tpmHash string, attestationConfig map[string]interface{}) {
|
func createSealedVolumeWithAttestation(tpmHash string, attestationConfig map[string]interface{}) {
|
||||||
|
sealedVolumeName := getSealedVolumeName(tpmHash)
|
||||||
sealedVolumeYaml := fmt.Sprintf(`---
|
sealedVolumeYaml := fmt.Sprintf(`---
|
||||||
apiVersion: keyserver.kairos.io/v1alpha1
|
apiVersion: keyserver.kairos.io/v1alpha1
|
||||||
kind: SealedVolume
|
kind: SealedVolume
|
||||||
@@ -332,7 +340,7 @@ spec:
|
|||||||
TPMHash: "%s"
|
TPMHash: "%s"
|
||||||
partitions:
|
partitions:
|
||||||
- label: COS_PERSISTENT
|
- label: COS_PERSISTENT
|
||||||
quarantined: false`, tpmHash, tpmHash)
|
quarantined: false`, sealedVolumeName, tpmHash)
|
||||||
|
|
||||||
if attestationConfig != nil {
|
if attestationConfig != nil {
|
||||||
sealedVolumeYaml += "\n attestation:"
|
sealedVolumeYaml += "\n attestation:"
|
||||||
@@ -356,43 +364,48 @@ spec:
|
|||||||
|
|
||||||
// Helper to update SealedVolume attestation configuration
|
// Helper to update SealedVolume attestation configuration
|
||||||
func updateSealedVolumeAttestation(tpmHashParam string, field, value string) {
|
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)
|
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()
|
out, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), string(out))
|
Expect(err).ToNot(HaveOccurred(), string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to quarantine TPM
|
// Helper to quarantine TPM
|
||||||
func quarantineTPM(tpmHash string) {
|
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}}`
|
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()
|
out, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), string(out))
|
Expect(err).ToNot(HaveOccurred(), string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to unquarantine TPM
|
// Helper to unquarantine TPM
|
||||||
func unquarantineTPM(tpmHashParam string) {
|
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}}`
|
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()
|
out, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), string(out))
|
Expect(err).ToNot(HaveOccurred(), string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to delete SealedVolume
|
// Helper to delete SealedVolume
|
||||||
func deleteSealedVolume(tmpHashParam string) {
|
func deleteSealedVolume(tpmHashParam string) {
|
||||||
By(fmt.Sprintf("Deleting SealedVolume %s", tmpHashParam))
|
sealedVolumeName := getSealedVolumeName(tpmHashParam)
|
||||||
cmd := exec.Command("kubectl", "delete", "sealedvolume", tmpHashParam, "--ignore-not-found=true")
|
By(fmt.Sprintf("Deleting SealedVolume %s", sealedVolumeName))
|
||||||
|
cmd := exec.Command("kubectl", "delete", "sealedvolume", sealedVolumeName, "--ignore-not-found=true")
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), string(out))
|
Expect(err).ToNot(HaveOccurred(), string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to delete SealedVolume from all namespaces
|
// Helper to delete SealedVolume from all namespaces
|
||||||
func deleteSealedVolumeAllNamespaces(tpmHashParam string) {
|
func deleteSealedVolumeAllNamespaces(tpmHashParam string) {
|
||||||
By(fmt.Sprintf("Deleting SealedVolume %s from all namespaces", tpmHashParam))
|
sealedVolumeName := getSealedVolumeName(tpmHashParam)
|
||||||
cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHashParam, "--ignore-not-found=true", "--all-namespaces")
|
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()
|
out, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), string(out))
|
Expect(err).ToNot(HaveOccurred(), string(out))
|
||||||
}
|
}
|
||||||
@@ -450,12 +463,15 @@ spec:
|
|||||||
|
|
||||||
// Helper to create SealedVolume in specific namespace
|
// Helper to create SealedVolume in specific namespace
|
||||||
func createSealedVolumeInNamespace(tpmHash, namespace string) {
|
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(`---
|
kubectlApplyYaml(fmt.Sprintf(`---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
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(`---
|
sealedVolumeYaml := fmt.Sprintf(`---
|
||||||
apiVersion: keyserver.kairos.io/v1alpha1
|
apiVersion: keyserver.kairos.io/v1alpha1
|
||||||
@@ -478,18 +494,19 @@ func cleanupTestResources(tpmHash string) {
|
|||||||
if tpmHash != "" {
|
if tpmHash != "" {
|
||||||
deleteSealedVolumeAllNamespaces(tpmHash)
|
deleteSealedVolumeAllNamespaces(tpmHash)
|
||||||
|
|
||||||
// Cleanup associated secrets in all namespaces
|
// Cleanup associated secrets using labels
|
||||||
cmd := exec.Command("kubectl", "delete", "secret", tpmHash, "--ignore-not-found=true", "--all-namespaces")
|
// 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.CombinedOutput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cmd = exec.Command("kubectl", "delete", "secret", fmt.Sprintf("%s-cos-persistent", tpmHash), "--ignore-not-found=true", "--all-namespaces")
|
// Helper to delete specific test namespaces
|
||||||
cmd.CombinedOutput()
|
func deleteTestNamespaces(namespaces ...string) {
|
||||||
|
for _, namespace := range namespaces {
|
||||||
// Cleanup test namespaces
|
cmd := exec.Command("kubectl", "delete", "namespace", namespace, "--ignore-not-found=true")
|
||||||
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")
|
|
||||||
cmd.CombinedOutput()
|
cmd.CombinedOutput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user