Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 38 additions & 20 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,6 @@ gen-mirror-helm-secret: ## Generate mirror-registry Helm OCI password file from
enable-airgap-secrets: ## Uncomment disconnected-mirror secrets (registryCaCert, bootstrap_secrets) in the generated values-secret file
@scripts/enable-airgap-secrets.sh

.PHONY: pck-register
pck-register: ## Register PCK certificates with Intel PCS (requires INTEL_PCS_API_KEY)
@if [ -z "$(INTEL_PCS_API_KEY)" ]; then \
echo "ERROR: Set INTEL_PCS_API_KEY environment variable"; \
echo " Usage: make pck-register INTEL_PCS_API_KEY=<key>"; \
exit 1; \
fi
@PCS_TOOL="$(HOME)/confidential-computing.tee.dcap/tools/PcsClientTool/pcsclient.py"; \
if [ ! -f "$$PCS_TOOL" ]; then \
echo "ERROR: PCS Client Tool not found at $$PCS_TOOL"; \
echo " Clone: git clone https://github.com/intel/confidential-computing.tee.dcap ~/confidential-computing.tee.dcap"; \
exit 1; \
fi; \
cd "$$(dirname $$PCS_TOOL)" && python3 pcsclient.py -t register -k "$(INTEL_PCS_API_KEY)"

##@ Reference Value Collection

.PHONY: collect-firmware-refvals
Expand All @@ -99,13 +84,46 @@ collect-azure-refvals: ## Collect PCR reference values (Azure)
@scripts/collect_firmware_refvals.py --platform azure

.PHONY: collect-dcap-collateral
collect-dcap-collateral: ## Collect TDX DCAP collateral from Intel PCS (API key via OS keyring)
collect-dcap-collateral: dcap-tools ## Collect platform-independent TDX DCAP verification collateral (connected low side)
@scripts/collect-dcap-collateral.sh

.PHONY: dcap-offline-provision
dcap-offline-provision: ## Full DCAP offline provisioning workflow (collect collateral + load secrets)
$(MAKE) collect-dcap-collateral
$(MAKE) load-secrets
##@ Intel TDX PCK Lifecycle
DCAP_PCSCLIENT_REPO ?= $(HOME)/.coco-pattern/intel-dcap
DCAP_PCSCLIENT_DIR ?= $(DCAP_PCSCLIENT_REPO)/tools/PcsClientTool
DCAP_PCSCLIENT_REF ?= 64b78f3766e7196d3d2c60e401540f0f853b2deb
DCAP_PCK_DIR ?= $(HOME)/.coco-pattern/dcap-pck
DCAP_REQUEST_BUNDLE ?= $(DCAP_PCK_DIR)/platform-request
DCAP_RESPONSE_BUNDLE ?= $(DCAP_PCK_DIR)/pck-response
DCAP_NAMESPACE ?= intel-dcap-operator-system
DCAP_QGS_DAEMONSET ?=
DCAP_PCK_EXPIRE_HOURS ?= 8760
DCAP_QGS_TIMEOUT ?= 10m
export DCAP_PCSCLIENT_REPO DCAP_PCSCLIENT_DIR DCAP_PCSCLIENT_REF DCAP_PCK_DIR DCAP_REQUEST_BUNDLE DCAP_RESPONSE_BUNDLE DCAP_NAMESPACE DCAP_QGS_DAEMONSET DCAP_PCK_EXPIRE_HOURS DCAP_QGS_TIMEOUT

.PHONY: dcap-tools
dcap-tools: ## Clone the pinned Intel PcsClientTool and install its Python dependencies (connected low side)
@python3 scripts/dcap-pck.py tools

.PHONY: dcap-platform-export
dcap-platform-export: ## Export QGS platform data as a transfer bundle (disconnected high side; requires oc)
@python3 scripts/dcap-pck.py export

.PHONY: dcap-pck-generate
dcap-pck-generate: dcap-tools ## Generate a PCK response bundle (connected low side; requires Intel PCS access)
@python3 scripts/dcap-pck.py generate

.PHONY: dcap-pck-import
dcap-pck-import: ## Import a PCK response bundle and restart QGS (disconnected high side; requires oc)
@python3 scripts/dcap-pck.py import

.PHONY: dcap-pck-provision
dcap-pck-provision: dcap-tools ## Resume export, generation, and import PCK cache (bastion connected to both Intel PCS and cluster)
@python3 scripts/dcap-pck.py provision

.PHONY: dcap-status
dcap-status: ## Check PCK and collateral expiry (requires oc)
@$(MAKE) check-pck-expiry
@$(MAKE) check-collateral-expiry

.PHONY: check-pck-expiry
check-pck-expiry: ## Check expiry of PCK cache secrets and platform data (requires oc login)
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Breaking change from v5. Upgrades to OSC 1.13 / Trustee 1.2, adds full airgap (d
- **Chart architecture:** trustee-chart moved to OCI Helm artifact (`quay.io/validatedpatterns/trustee:0.10.0`). Kyverno chart vendored under `charts/vendor/`. External charts (sandboxed-containers, sandboxed-policies) remain OCI.
- **TDX MachineConfig:** QGS socket port `socket_port=0` now deployed via MachineConfig drop-in (replaces manual `sed` workaround).
- **DCAP collateral:** `collect-dcap-collateral.sh` uses `pcsclient.py fetch -p E5` with `jq` fixup for QeIdentity (Red Hat OSC 1.13 disconnected TDX procedure).
- **TDX PCK lifecycle:** Make targets export platform data, generate PCK cache material on a connected low-side host, and import it into the disconnected high-side cluster. See [the air-gap runbook](airgap/DEPLOY-RUNBOOK.md#phase-e-dcap-and-tdx-attestation).
- **Experimental:** KubeVirt TDX confidential VMs (`charts/all/kubevirtconfidential/`, `charts/all/kubevirtvm/`). Disabled by default. Requires Intel TDX hardware and KubeVirt post-v1.8.4 for full QGS attestation. See chart readmes for details.

### Previous versions
Expand All @@ -64,7 +65,7 @@ For air-gapped bare metal environments, see [`airgap/DEPLOY-RUNBOOK.md`](airgap/

**Prerequisites for airgap:**

- A jump host with internet access (for `oc-mirror`) and network access to the target cluster
- A low-side host with Intel PCS access for mirroring, collateral collection, and PCK generation. A connected bastion with access to both Intel PCS and the target cluster can run the combined PCK target.
- `docker.io/library/registry:2` container running as the mirror registry (setup documented in Phase 0)
- `scripts/git-http-server.py` serving pattern repos over smart HTTP (required by the patterns-operator's go-git client)
- All operator images, OCI Helm charts, and workload images mirrored via `airgap/imageset-config-4.22.yaml`
Expand Down Expand Up @@ -104,6 +105,16 @@ These scripts generate the cryptographic material and attestation reference valu
- See [docs/firmware-reference-values.md](docs/firmware-reference-values.md) for detailed workflow and options.
3. Review and customise `~/values-secret-coco-pattern.yaml` — this file is loaded into Vault and provides secrets to the pattern.

### Intel TDX PCK lifecycle

All lifecycle actions are invoked with Make. `collect-dcap-collateral` prepares the pinned Intel tool automatically, then retrieves public verification collateral on the connected low side; transfer `~/.coco-pattern/dcap-offline/platform_collaterals.json` to the high side, then use the existing `make load-secrets` to load it into Vault.

PCK certificates are platform-specific and use separate request and response bundles. On a disconnected high-side cluster, run `make dcap-platform-export`, transfer `~/.coco-pattern/dcap-pck/platform-request` to the connected low side, then run `make dcap-tools dcap-pck-generate`. Transfer `~/.coco-pattern/dcap-pck/pck-response` back to the high side and run `make dcap-pck-import`.

When one bastion can reach both Intel PCS and the cluster, run `make dcap-pck-provision`. It resumes safely after interruption: matching request and unexpired response bundles are reused, while changed platform data replaces both bundles. The generator accepts `INTEL_PCS_API_KEY` from its environment for automation, or requests it through a hidden terminal prompt. Do not pass the key as a Make variable or command-line argument.

The importer discovers the QGS DaemonSet from the `pck-certs-watcher` pod owner reference. Set `DCAP_QGS_DAEMONSET` only when that discovery cannot identify a single DaemonSet.

> **Note:** `gen-secrets.sh` will not overwrite existing secrets. Delete `~/.coco-pattern/` if you need to regenerate.

### Single cluster deployment (Azure)
Expand Down
86 changes: 24 additions & 62 deletions airgap/DEPLOY-RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
| B | Full Re-Mirror (oc-mirror v2) | ~3-4 hours |
| C | Cluster Install | ~35 min |
| D | Bootstrap and Pattern Deploy | ~25 min |
| E | DCAP and TDX Attestation | ~15 min (+ pcsclient time if QE ID mismatch) |
| E | DCAP and TDX Attestation | ~15 min (+ connected low-side PCK generation) |
| F | Verification and Pass/Fail | ~10 min |
| **Total** | | **~5-6 hours** (first-time add ~30 min) |

Expand Down Expand Up @@ -1127,74 +1127,36 @@ echo "E-2: QGS socket port verification complete at $(date)" 2>&1 | tee -a "$LOG
> `oc get mc | grep kata-tdx` shows a MachineConfig with the drop-in content.
> See Appendix Section A.3 for the temporary sed fallback. Record in DEVIATIONS.md.

### E-3: PCK Registration — pcsclient.py cache (ALWAYS REQUIRED, INTERACTIVE)
### E-3: PCK Lifecycle

> **This step is ALWAYS required** — even when QE IDs match and cached PCK cert files exist.
> `pcsclient.py cache` does two things: (1) provisions the PCK certificate secret so QGS can
> generate quotes, and (2) populates the local cache with **QeIdentity and TcbInfo** that
> `collect-dcap-collateral.sh` reads in E-4. Skipping this step leaves `qeidentity` empty in
> `platform_collaterals.json`, causing KBS to fail attestation with
> `"collateral JSON error: invalid type: string "", expected struct QeIdentity"`.
>
> **This step must be run interactively** — `pcsclient.py` prompts for the Intel PCS API key
> via `getpass` and cannot be automated via SSH pipe (see `UPSTREAM-ISSUES-INTEL-DCAP.md` Issue 3).
PCK certificates are specific to each QGS platform. The Make targets create checksummed request and response bundles, validate the complete QE-ID/PCE-ID set, and restart QGS only after all matching PCK Secrets are applied.

The importer discovers the QGS DaemonSet from the `pck-certs-watcher` pod owner reference, avoiding a version-specific DaemonSet label or name. Set `DCAP_QGS_DAEMONSET` only if the namespace has more than one candidate.

If this bastion can reach **both** the disconnected cluster and Intel PCS, run the combined workflow. The API key is read from `INTEL_PCS_API_KEY` if exported, otherwise Make prompts without echoing it. Never put the key on the Make command line.

```bash
echo "=== E-3: PCK Registration (INTERACTIVE — run in tmux) ===" 2>&1 | tee -a "$LOG"
cd ~/pck-registration

# Step 1: Generate platform_list.json from cluster platform-data secrets
echo "Generating platform_list.json..." 2>&1 | tee -a "$LOG"
oc get secrets -o json -n intel-dcap-operator-system -l 'type=platform-data' \
| jq '[.items[] | .data | map_values(@base64d)]' > platform_list.json
echo "platform_list.json: $(wc -l < platform_list.json) lines" 2>&1 | tee -a "$LOG"

# Step 2: Compare QE IDs — cache reuse check (informational only)
CLUSTER_QE_ID=$(oc get secrets -n intel-dcap-operator-system -l type=platform-data \
--no-headers -o custom-columns=NAME:.metadata.name | head -1)
CACHED_QE_ID=$(ls cache/*_0000 2>/dev/null | head -1 | xargs basename 2>/dev/null | sed 's/_0000//')
echo "Cluster QE ID: $CLUSTER_QE_ID" 2>&1 | tee -a "$LOG"
echo "Cached QE ID: $CACHED_QE_ID" 2>&1 | tee -a "$LOG"
if [ "$CLUSTER_QE_ID" = "$CACHED_QE_ID" ] && [ -n "$CACHED_QE_ID" ]; then
echo "QE ID MATCH — PCK cert cache reusable. Still must run pcsclient.py cache for QeIdentity." \
2>&1 | tee -a "$LOG"
else
echo "QE ID MISMATCH — record as DEV-2 in DEVIATIONS.md" 2>&1 | tee -a "$LOG"
fi
echo "=== E-3: Combined PCK provisioning ===" 2>&1 | tee -a "$LOG"
cd ~/coco-pattern
make dcap-pck-provision 2>&1 | tee -a "$LOG"
```

# Step 3: Run pcsclient.py cache INTERACTIVELY
# Will prompt: "Please input ApiKey for Intel PCS:" — paste your key.
# Populates cache/ with PCK certs AND QeIdentity/TcbInfo for E-4.
echo "STOP: Run the following command interactively in this tmux window:" 2>&1 | tee -a "$LOG"
echo ""
echo " python3 ~/confidential-computing.tee.dcap/tools/PcsClientTool/pcsclient.py cache \\"
echo " -i platform_list.json \\"
echo " -e 8760 \\"
echo " -t early"
echo ""
echo "Enter API key when prompted, then press Enter."
echo "When complete, continue to Step 4 below."

# Step 4: Apply PCK cert secret to cluster and restart QGS
# (Run after pcsclient.py cache completes)
for f in cache/*_0000; do
qe_id=$(basename "$f" _0000)
echo "Applying PCK cert for QE ID: $qe_id" 2>&1 | tee -a "$LOG"
oc create secret generic "${qe_id}-pck" --from-file=certificate="$f" \
-n intel-dcap-operator-system --dry-run=client -o yaml | oc apply -f - \
2>&1 | tee -a "$LOG"
done
oc delete pod -n intel-dcap-operator-system -l app=intel-tdx-qgs \
2>&1 | tee -a "$LOG"
If the high-side cluster cannot reach the connected low-side host, use the split workflow:

```bash
# High side: create ~/.coco-pattern/dcap-pck/platform-request and transfer it high -> low.
make dcap-platform-export

# Low side: copy platform-request to ~/.coco-pattern/dcap-pck/, then run these commands.
make dcap-tools dcap-pck-generate

echo "E-3: PCK registration complete at $(date)" 2>&1 | tee -a "$LOG"
# Transfer ~/.coco-pattern/dcap-pck/pck-response low -> high, then import it on the high side.
make dcap-pck-import
```

### E-4: Collect DCAP Collateral and Firmware Reference Values

> **Must run AFTER E-3** — `collect-dcap-collateral.sh` calls `pcsclient.py fetch` which reads
> from the local cache populated by `pcsclient.py cache` in E-3. Running E-4 before E-3
> produces `platform_collaterals.json` with `qeidentity: ""` — KBS will fail attestation.
`collect-dcap-collateral` is independent of PCK provisioning. It collects public Intel TDX verification collateral on the connected low side and does not require an Intel PCS API key. Transfer `~/.coco-pattern/dcap-offline/platform_collaterals.json` to the high side before E-5.

```bash
echo "=== E-4: Collect DCAP Collateral + Firmware Refvals ===" 2>&1 | tee -a "$LOG"
Expand All @@ -1206,8 +1168,8 @@ make collect-dcap-collateral 2>&1 | tee -a "$LOG"
python3 -c "
import json, os
c = json.load(open(os.path.expanduser('~/.coco-pattern/dcap-offline/platform_collaterals.json')))
qi = c.get('collaterals', {}).get('qeidentity', '')
print('qeidentity length:', len(qi), '— PASS' if len(qi) > 100 else '— FAIL: re-run E-3 first')
qi = c.get('collaterals', {}).get('qeidentity_early') or c.get('collaterals', {}).get('qeidentity')
print('qeidentity present:', isinstance(qi, dict), '— PASS' if isinstance(qi, dict) else '— FAIL: recollect collateral')
" 2>&1 | tee -a "$LOG"

make collect-firmware-refvals 2>&1 | tee -a "$LOG"
Expand Down
32 changes: 15 additions & 17 deletions scripts/check-collateral-expiry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ if ! oc get secret "$SECRET" -n "$NS" &>/dev/null; then
exit 1
fi

oc get secret "$SECRET" -n "$NS" \
while IFS='|' read -r label next_update; do
check_date "$label" "$next_update"
done < <(oc get secret "$SECRET" -n "$NS" \
-o jsonpath='{.data.platform_collaterals\.json}' | base64 -d | python3 -c "
import json, sys
data = json.load(sys.stdin)
Expand All @@ -63,22 +65,18 @@ for ti in col.get('tcbinfos', []):
if nu:
print(f'{key} FMSPC={fmspc}|{nu}')

for qi in col.get('qeidentities', []):
for key in ['qe_identity_early', 'qe_identity']:
ei = qi.get(key, {})
if isinstance(ei, str):
try:
ei = json.loads(ei)
except json.JSONDecodeError:
continue
if isinstance(ei, dict):
info = ei.get('enclaveIdentity', {})
nu = info.get('nextUpdate')
if nu:
print(f'{key}|{nu}')
" | while IFS='|' read -r label next_update; do
check_date "$label" "$next_update"
done
for key in ['qeidentity_early', 'qeidentity', 'tdqeidentity_early', 'tdqeidentity', 'qveidentity_early', 'qveidentity']:
identity = col.get(key, {})
if isinstance(identity, str):
try:
identity = json.loads(identity)
except json.JSONDecodeError:
continue
if isinstance(identity, dict):
next_update = identity.get('enclaveIdentity', {}).get('nextUpdate')
if next_update:
print(f'{key}|{next_update}')
")

echo ""
if [ "$fail" -ne 0 ]; then
Expand Down
50 changes: 28 additions & 22 deletions scripts/check-pck-expiry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,33 @@ else
echo " PCK secret: ${secret_name} (QE ID: ${qe_id})"

# The PCK cache blob contains URL-encoded PEM X.509 certs + embedded JSON TCB info
oc get secret "$secret_name" -n "$NS" -o jsonpath='{.data.certificate}' | \
while IFS='|' read -r rtype val1 val2; do
case "$rtype" in
NONE)
echo -e " ${YELLOW}UNKNOWN${NC} No expiry data found in PCK cache blob"
fail=1
;;
CERT)
iso=$(python3 -c "
from datetime import datetime
try:
dt = datetime.strptime('$val2', '%b %d %H:%M:%S %Y %Z')
except ValueError:
dt = datetime.strptime('$val2', '%b %d %H:%M:%S %Y %Z')
print(dt.strftime('%Y-%m-%dT%H:%M:%SZ'))
" 2>/dev/null)
if [ -z "$iso" ]; then
echo -e " ${YELLOW}UNKNOWN${NC} Cannot parse certificate expiry: $val2"
fail=1
else
check_date " cert: $val1" "$iso"
fi
;;
TCB)
check_date " TCB nextUpdate" "$val1"
;;
esac
done < <(oc get secret "$secret_name" -n "$NS" -o jsonpath='{.data.certificate}' | \
base64 -d | python3 -c "
import sys, re, subprocess, urllib.parse
data = sys.stdin.buffer.read()
Expand Down Expand Up @@ -82,27 +108,7 @@ for m in re.findall(r'\"nextUpdate\":\"([^\"]+)\"', text):

if not certs and not re.search(r'nextUpdate', text):
print('NONE')
" | while IFS='|' read -r rtype val1 val2; do
case "$rtype" in
NONE)
echo -e " ${YELLOW}UNKNOWN${NC} No expiry data found in PCK cache blob"
;;
CERT)
iso=$(python3 -c "
from datetime import datetime
try:
dt = datetime.strptime('$val2', '%b %d %H:%M:%S %Y %Z')
except ValueError:
dt = datetime.strptime('$val2', '%b %d %H:%M:%S %Y %Z')
print(dt.strftime('%Y-%m-%dT%H:%M:%SZ'))
" 2>/dev/null)
[ -n "$iso" ] && check_date " cert: $val1" "$iso"
;;
TCB)
check_date " TCB nextUpdate" "$val1"
;;
esac
done
")
done
fi

Expand Down
7 changes: 3 additions & 4 deletions scripts/collect-dcap-collateral.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
# the Trustee dcap_verifier in file:// mode. This is platform-level data that
# does NOT change per-cluster — only per CPU family (identified by FMSPC).
#
# IMPORTANT: Run AFTER pcsclient.py cache (E-3). The cache step provisions PCK
# certs for the specific platform. This fetch step gets the verification collateral
# (QeIdentity, TcbInfo, CRLs) needed by KBS to verify attestation reports.
# This collection is independent of platform registration and PCK cache generation.
# It gets public verification collateral (QeIdentity, TcbInfo, CRLs) needed by KBS.
#
# Usage:
# ./scripts/collect-dcap-collateral.sh [OPTIONS]
Expand All @@ -34,7 +33,7 @@
set -euo pipefail

# Defaults
PCSCLIENT_DIR="${HOME}/confidential-computing.tee.dcap/tools/PcsClientTool"
PCSCLIENT_DIR="${PCSCLIENT_DIR:-${DCAP_PCSCLIENT_DIR:-${HOME}/confidential-computing.tee.dcap/tools/PcsClientTool}}"
OUTPUT_DIR="${HOME}/.coco-pattern/dcap-offline"

# Parse arguments
Expand Down
Loading
Loading