Skip to content

Commit 2cbfd47

Browse files
butler54beraldolealclaude
committed
feat: add PCR measurement and attestation infrastructure
Add PCR measurement extraction script, update initdata gzipper and default toml template for attestation, and configure trustee and secret template values for PCR stash support. Co-authored-by: Beraldo Leal <bleal@redhat.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9c6f715 commit 2cbfd47

5 files changed

Lines changed: 265 additions & 37 deletions

File tree

ansible/init-data-gzipper.yaml

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
- name: Gzip initdata
1+
- name: Gzip initdata and register init data
22
become: false
33
connection: local
44
hosts: localhost
@@ -7,6 +7,7 @@
77
kubeconfig: "{{ lookup('env', 'KUBECONFIG') }}"
88
cluster_platform: "{{ global.clusterPlatform | default('none') | lower }}"
99
hub_domain: "{{ global.hubClusterDomain | default('none') | lower}}"
10+
security_policy_flavour: "{{ global.coco.securityPolicyFlavour | default('insecure') }}"
1011
template_src: "initdata-default.toml.tpl"
1112
tasks:
1213
- name: Create temporary working directory
@@ -36,7 +37,6 @@
3637
- name: Define temp file paths
3738
ansible.builtin.set_fact:
3839
rendered_path: "{{ tmpdir.path }}/rendered.toml"
39-
gz_path: "{{ tmpdir.path }}/rendered.toml.gz"
4040

4141
- name: Render template to temp file
4242
ansible.builtin.template:
@@ -45,15 +45,33 @@
4545
mode: "0600"
4646

4747

48-
- name: Gzip the rendered content
48+
- name: Gzip and base64 encode the rendered content
4949
ansible.builtin.shell: |
50-
gzip -c "{{ rendered_path }}" > "{{ gz_path }}"
51-
changed_when: true
50+
set -o pipefail
51+
cat "{{ rendered_path }}" | gzip | base64 -w0
52+
register: initdata_encoded
53+
changed_when: false
54+
55+
# This block runs a shell script that calculates a hash value (PCR8_HASH) derived from the contents of 'initdata.toml'.
56+
# The script performs the following steps:
57+
# 1. hash=$(sha256sum initdata.toml | cut -d' ' -f1): Computes the sha256 hash of 'initdata.toml' and assigns it to $hash.
58+
# 2. initial_pcr=0000000000000000000000000000000000000000000000000000000000000000: Initializes a string of zeros as the initial PCR value.
59+
# 3. PCR8_HASH=$(echo -n "$initial_pcr$hash" | xxd -r -p | sha256sum | cut -d' ' -f1):
60+
# Concatenates initial_pcr and $hash, converts from hex to binary,
61+
# computes its sha256 hash, and stores the result as PCR8_HASH.
62+
# 4. echo $PCR8_HASH: Outputs the PCR hash value.
63+
# The important part: The 'register: pcr8_hash' registers the **stdout of the command**,
64+
# which is the value output by 'echo $PCR8_HASH', as 'pcr8_hash.stdout' in Ansible.
65+
# It does NOT register an environment variable, but rather the value actually printed by 'echo'.
66+
- name: Register init data pcr into a var
67+
ansible.builtin.shell: |
68+
set -o pipefail
69+
hash=$(sha256sum "{{ rendered_path }}" | cut -d' ' -f1)
70+
initial_pcr=0000000000000000000000000000000000000000000000000000000000000000
71+
PCR8_HASH=$(echo -n "$initial_pcr$hash" | xxd -r -p | sha256sum | cut -d' ' -f1) && echo $PCR8_HASH
72+
register: pcr8_hash
73+
changed_when: false
5274

53-
- name: Read gzip as base64
54-
ansible.builtin.slurp:
55-
path: "{{ gz_path }}"
56-
register: gz_slurped
5775

5876
- name: Create/update ConfigMap with gzipped+base64 content
5977
kubernetes.core.k8s:
@@ -66,4 +84,5 @@
6684
name: "initdata"
6785
namespace: "imperative"
6886
data:
69-
INITDATA: "{{ gz_slurped.content }}"
87+
INITDATA: "{{ initdata_encoded.stdout }}"
88+
PCR8_HASH: "{{ pcr8_hash.stdout }}"

ansible/initdata-default.toml.tpl

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
algorithm = "sha384"
1+
algorithm = "sha256"
22
version = "0.1.0"
33

44
[data]
@@ -9,9 +9,7 @@ url = "https://kbs.{{ hub_domain }}"
99

1010
[token_configs.kbs]
1111
url = "https://kbs.{{ hub_domain }}"
12-
cert = """
13-
{{ trustee_cert }}
14-
"""
12+
cert = """{{ trustee_cert }}"""
1513
'''
1614

1715
"cdh.toml" = '''
@@ -21,22 +19,27 @@ credentials = []
2119
[kbc]
2220
name = "cc_kbc"
2321
url = "https://kbs.{{ hub_domain }}"
24-
kbs_cert = """
25-
{{ trustee_cert }}
26-
"""
22+
kbs_cert = """{{ trustee_cert }}"""
23+
24+
25+
[image]
26+
image_security_policy_uri = 'kbs:///default/security-policy/{{ security_policy_flavour }}'
2727
'''
2828

2929
"policy.rego" = '''
3030
package agent_policy
3131

32+
import future.keywords.in
33+
import future.keywords.if
34+
import future.keywords.every
35+
3236
default AddARPNeighborsRequest := true
3337
default AddSwapRequest := true
3438
default CloseStdinRequest := true
3539
default CopyFileRequest := true
3640
default CreateContainerRequest := true
3741
default CreateSandboxRequest := true
3842
default DestroySandboxRequest := true
39-
default ExecProcessRequest := false
4043
default GetMetricsRequest := true
4144
default GetOOMEventRequest := true
4245
default GuestDetailsRequest := true
@@ -52,7 +55,6 @@ default RemoveStaleVirtiofsShareMountsRequest := true
5255
default ReseedRandomDevRequest := true
5356
default ResumeContainerRequest := true
5457
default SetGuestDateTimeRequest := true
55-
default SetPolicyRequest := true
5658
default SignalProcessRequest := true
5759
default StartContainerRequest := true
5860
default StartTracingRequest := true
@@ -64,5 +66,20 @@ default UpdateEphemeralMountsRequest := true
6466
default UpdateInterfaceRequest := true
6567
default UpdateRoutesRequest := true
6668
default WaitProcessRequest := true
67-
default WriteStreamRequest := true
68-
'''
69+
default ExecProcessRequest := false
70+
default SetPolicyRequest := true
71+
default WriteStreamRequest := false
72+
73+
ExecProcessRequest if {
74+
input_command = concat(" ", input.process.Args)
75+
some allowed_command in policy_data.allowed_commands
76+
input_command == allowed_command
77+
}
78+
79+
policy_data := {
80+
"allowed_commands": [
81+
"curl http://127.0.0.1:8006/cdh/resource/default/attestation-status/status",
82+
"curl http://127.0.0.1:8006/cdh/resource/default/attestation-status/random"
83+
]
84+
}
85+
'''

overrides/values-trustee.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ kbs:
66
- name: "kbsres1" # name is the name of the k8s secret that will be presented to trustee and accessible via the CDH
77
key: "secret/data/hub/kbsres1" # this is the path to the secret in vault.
88
- name: "passphrase"
9-
key: "secret/data/hub/passphrase"
9+
key: "secret/data/hub/passphrase"
10+
# Override the default values for the coco pattern this is because when testing against a branch strange stuff happens
11+
# FIXME: Don't commit this to main
12+
global:
13+
coco:
14+
secured: true # true or false. If true, the cluster will be secured. If false, the cluster will be insecure.

scripts/get-pcr.sh

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# Script to retrieve the sandboxed container operator CSV for the current clusterGroup
5+
# using the pull secret for authentication if needed.
6+
7+
# 1. Locate pull secret
8+
PULL_SECRET_PATH="${HOME}/pull-secret.json"
9+
if [ ! -f "$PULL_SECRET_PATH" ]; then
10+
if [ -n "${PULL_SECRET}" ]; then
11+
PULL_SECRET_PATH="${PULL_SECRET}"
12+
if [ ! -f "$PULL_SECRET_PATH" ]; then
13+
echo "ERROR: Pull secret file not found at path specified in PULL_SECRET: $PULL_SECRET_PATH"
14+
exit 1
15+
fi
16+
else
17+
echo "ERROR: Pull secret not found at ~/pull-secret.json"
18+
echo "Please either place your pull secret at ~/pull-secret.json or set the PULL_SECRET environment variable"
19+
exit 1
20+
fi
21+
fi
22+
23+
echo "Using pull secret: $PULL_SECRET_PATH"
24+
25+
# 2. Check for required tools
26+
if ! command -v yq &> /dev/null; then
27+
echo "ERROR: yq is required but not installed"
28+
echo "Please install yq: https://github.com/mikefarah/yq#install"
29+
exit 1
30+
fi
31+
32+
if ! command -v skopeo &> /dev/null; then
33+
echo "ERROR: skopeo is required but not installed"
34+
echo "Please install skopeo: https://github.com/containers/skopeo/blob/main/install.md"
35+
exit 1
36+
fi
37+
38+
if ! command -v podman &> /dev/null; then
39+
echo "ERROR: podman is required but not installed"
40+
echo "Please install podman: https://podman.io/docs/installation"
41+
exit 1
42+
fi
43+
44+
# 3. Check values-global.yaml exists
45+
if [ ! -f "values-global.yaml" ]; then
46+
echo "ERROR: values-global.yaml not found in current directory"
47+
echo "Please run this script from the root directory of the project"
48+
exit 1
49+
fi
50+
51+
# 4. Get the active clusterGroupName from values-global.yaml
52+
CLUSTER_GROUP_NAME=$(yq eval '.main.clusterGroupName' values-global.yaml)
53+
54+
if [ -z "$CLUSTER_GROUP_NAME" ] || [ "$CLUSTER_GROUP_NAME" == "null" ]; then
55+
echo "ERROR: Could not determine clusterGroupName from values-global.yaml"
56+
echo "Expected: main.clusterGroupName to be set"
57+
exit 1
58+
fi
59+
60+
echo "Active clusterGroup: $CLUSTER_GROUP_NAME"
61+
62+
# 5. Locate the values file for the active clusterGroup
63+
VALUES_FILE="values-${CLUSTER_GROUP_NAME}.yaml"
64+
65+
if [ ! -f "$VALUES_FILE" ]; then
66+
echo "ERROR: Values file for clusterGroup not found: $VALUES_FILE"
67+
exit 1
68+
fi
69+
70+
# 6. Get the sandboxed container operator CSV from the clusterGroup values
71+
SANDBOX_CSV=$(yq eval '.clusterGroup.subscriptions.sandbox.csv' "$VALUES_FILE")
72+
73+
if [ -z "$SANDBOX_CSV" ] || [ "$SANDBOX_CSV" == "null" ]; then
74+
echo "WARNING: No sandboxed container operator CSV found in $VALUES_FILE"
75+
echo "The subscription clusterGroup.subscriptions.sandbox.csv is not defined"
76+
exit 0
77+
fi
78+
79+
# Extract version from CSV (e.g., "sandboxed-containers-operator.v1.11.0" -> "1.11.0")
80+
# Remove everything up to and including ".v"
81+
SANDBOX_VERSION="${SANDBOX_CSV##*.v}"
82+
83+
echo "Sandboxed container operator CSV: $SANDBOX_CSV"
84+
echo "Version: $SANDBOX_VERSION"
85+
# alternatively, use the operator-version tag.
86+
# OSC_VERSION=1.11.1
87+
VERITY_IMAGE=registry.redhat.io/openshift-sandboxed-containers/osc-dm-verity-image
88+
89+
TAG=$(skopeo inspect --authfile $PULL_SECRET_PATH docker://${VERITY_IMAGE}:${SANDBOX_VERSION} | jq -r .Digest)
90+
91+
IMAGE=${VERITY_IMAGE}@${TAG}
92+
93+
echo "IMAGE: $IMAGE"
94+
95+
curl -L https://tuf-default.apps.rosa.rekor-prod.2jng.p3.openshiftapps.com/targets/rekor.pub -o rekor.pub
96+
curl -L https://security.access.redhat.com/data/63405576.txt -o cosign-pub-key.pem
97+
# export REGISTRY_AUTH_FILE=${PULL_SECRET_PATH}
98+
# echo "REGISTRY_AUTH_FILE: $REGISTRY_AUTH_FILE"
99+
# export SIGSTORE_REKOR_PUBLIC_KEY=${PWD}/rekor.pub
100+
# echo "SIGSTORE_REKOR_PUBLIC_KEY: $SIGSTORE_REKOR_PUBLIC_KEY"
101+
# cosign verify --key cosign-pub-key.pem --output json --rekor-url=https://rekor-server-default.apps.rosa.rekor-prod.2jng.p3.openshiftapps.com $IMAGE > cosign_verify.log
102+
103+
104+
# Ensure output directory exists
105+
mkdir -p ~/.coco-pattern
106+
107+
# Clean up any existing measurement files
108+
rm -f ~/.coco-pattern/measurements-raw.json ~/.coco-pattern/measurements.json
109+
110+
# Download the measurements using podman cp (works on macOS with remote podman)
111+
podman pull --authfile $PULL_SECRET_PATH $IMAGE
112+
113+
cid=$(podman create --entrypoint /bin/true $IMAGE)
114+
echo "CID: ${cid}"
115+
podman cp $cid:/image/measurements.json ~/.coco-pattern/measurements-raw.json
116+
podman rm $cid
117+
118+
# Trim leading "0x" from all measurement values
119+
jq 'walk(if type == "string" and startswith("0x") then .[2:] else . end)' \
120+
~/.coco-pattern/measurements-raw.json > ~/.coco-pattern/measurements.json
121+
122+
echo "Measurements saved to ~/.coco-pattern/measurements.json (0x prefixes removed)"

0 commit comments

Comments
 (0)