5.2 Enforce and review the signature policy
In this module you prove that policy can fail an unsigned image and pass the application digest you signed.
This is the second module in Track 5, which signs the Java application image and checks that policy accepts the signature. Konflux calls this gate Conforma, the Enterprise Contract. This cluster maps it to Task conforma-policy in namespace lightwell-tasks. The Task is not in the seeded pipeline. The seeded ConfigMap is too permissive: the identity is .*, the workshop rules are excluded, and the CVE ceiling is 999. Its skip-image-sig-check: "true" value does not disable image-signature verification in ec 0.7. Argo CD owns that object and reverts edits to it. You copy the ConfigMap into lw-poc-build and tighten the copy.
You will demonstrate two outcomes with the policy you configure: the unsigned builder image fails the image-signature check, and your signed application image passes it. This establishes that the selected policy checks the expected image signature. It does not demonstrate that changing skip-image-sig-check turned verification on: the Task always invokes ec validate image, and image-signature violations fail with either value of that setting. Build-provenance verification is outside this exercise: the cluster does not run Tekton Chains or create attestations with cosign attest.
Keep skip-att-sig-check set to true in your policy ConfigMap. The Showroom terminal uses ec version 0.7 (ec-rhel9:0.7), which has no --skip-att-sig-check command-line flag. The workshop handles that limitation when interpreting the report: set aside only builtin.attestation.signature_check, and investigate every other violation. Leave --ignore-rekor off so verification can use Rekor’s timestamp evidence to establish that signing occurred while the short-lived Fulcio certificate was valid.
For how these workshop controls map to Konflux and SLSA terminology, see Appendix: Konflux mapping.
Why this control exists
During Trusted Software Supply Chain (TSSC) delivery, a successful build produces an image, but the delivery process also needs a basis for accepting that image for promotion. Module 5.1 established who signed the application digest. Conforma turns that verification into a repeatable policy decision in the pipeline: does this image carry a valid signature from the identity the delivery team expects?
That decision depends on the policy’s requirements. A policy that accepts any signer or excludes all its rules provides little assurance about the application’s origin. The upcoming exercise replaces the workshop’s permissive defaults with explicit expectations for the pipeline identity, token issuer, and internal registry. This demonstrates why a delivery team must review the policy itself as well as a passing result. The workshop’s CVE-threshold setting illustrates another permissive default; changing it does not scan the image or establish that vulnerabilities are fixed.
Policy ownership matters because those requirements must remain under the control of the team responsible for delivery. The shared workshop policy is managed by Argo CD, which reconciles it back to its declared configuration. A separate policy in the build namespace gives you an independent place to define and inspect the application’s acceptance criteria. This models the distinction between a platform-provided starting point and the policy selected for a particular delivery workflow.
The policy check belongs after signing because it evaluates evidence that the signing task produces. Testing both an unsigned image and the signed application then provides complementary evidence: rejection shows that a missing signature is detected, while acceptance shows that the expected signing path works. Together, these results support a delivery handoff more clearly than a successful pipeline run alone. Track 6 applies the next control at promotion and admission.
This control addresses the application image and its signer. Lightwell dependencies and their vulnerability information address components inside the image, so they cannot establish who signed the assembled application. Track 7 examines that dependency evidence through VEX. This module’s narrower result is an image-signature policy decision; it does not establish build provenance or SLSA assurance. The Konflux mapping appendix explains how these workshop controls relate to the broader delivery model.
Learning objectives
By the end of this module, you will be able to:
-
Copy ConfigMap
conforma-policyinto the build namespace and tighten the copy -
Wire Task
conforma-policyaftercosign-sign-keyless, pointed at that copy -
Show
ec validatereportingbuiltin.image.signature_checkonopenjdk-21-builder -
Show the signed application digest passing that same check, with only the attestation builtin left over
The seed you copy
The output should show the Task in lightwell-tasks, the seed policy with skip-image-sig-check still set to "true" and identity still set to .*, and the Task’s parameter declarations.
oc -n lightwell-tasks get task conforma-policy
oc -n lightwell-tasks get configmap conforma-policy \
-o jsonpath='{.data.skip-image-sig-check}{"\n"}{.data.certificate-identity-regexp}{"\n"}'
oc -n lightwell-tasks get task conforma-policy -o yaml
Exercise
The exercise follows three stages: create your policy copy, tighten its requirements, and connect it to the pipeline after signing.
Create your own policy copy
Copy ConfigMap conforma-policy from lightwell-tasks into the build namespace. The command copies only the ConfigMap’s data into a new object, leaving Argo ownership and API metadata behind. Success is a separate ConfigMap in your build namespace.
BUILD_NS="$(oc -n gitea get configmap lab-gitea -o jsonpath='{.data.student_build_namespace}')"
oc -n lightwell-tasks get configmap conforma-policy -o json > /tmp/conforma-seed.json \
|| { echo "Could not read policy seed"; exit 1; }
python3 - "$BUILD_NS" <<'PYTHON' > /tmp/conforma-copy.json
import json, sys
with open('/tmp/conforma-seed.json') as stream:
seed = json.load(stream)
assert sys.argv[1], "Build namespace is empty"
print(json.dumps({"apiVersion": "v1", "kind": "ConfigMap",
"metadata": {"name": "conforma-policy", "namespace": sys.argv[1]},
"data": seed["data"]}))
PYTHON
oc apply -f /tmp/conforma-copy.json
oc -n "$BUILD_NS" get configmap conforma-policy
On a return visit, inspect your existing copy before running this block: applying the seed again resets your edits.
Tighten the copy and inspect the difference
Open your copy with oc -n "$BUILD_NS" edit configmap conforma-policy. Keep rules.rego and the local /tmp/ec/ file references. Make these changes, substituting the discovered build namespace into the identity.
| Location | Required change |
|---|---|
|
Set the string to |
|
Keep |
|
Use |
|
Use |
Embedded |
Set |
Embedded |
Set |
In ec 0.7, the exclude list does not control built-in signature checks. Adding attestation_signature_check to that list will not suppress the attestation result; the report comparison later in this exercise handles that expected exception.
Read the comparison as a change in accepted identity and selected policy, not as enabling a skipped signature check:
| Control | Seed | Your configured copy |
|---|---|---|
Image-signature verification |
|
The same verification runs with |
Certificate identity and issuer |
Wildcard matching does not restrict verification to this pipeline’s signer. |
The Task passes your anchored expressions to |
Workshop policy rules |
The catch-all exclusion omits the local rules. |
Removing that exclusion evaluates the local configuration rules. The CVE and registry values are configuration checks, not a vulnerability scan or a complete image-content policy. |
Report interpretation |
With |
Keep that same exception because the lab produces no provenance attestation. With |
The pipeline uses your policy only after you wire the Task to your ConfigMap in lw-poc-build. A signed image can pass the image-signature check with the seed’s wildcard identity, so a pass alone does not prove signer restriction. Inspect the selected policy and its anchored identity alongside the two reports. Do not describe the unsigned comparison as proof of the skip-image-sig-check flag’s effect.
Use single-quoted YAML strings for regular expressions containing backslashes. These workshop rules detect permissive configuration; changing the CVE threshold does not perform a vulnerability scan or prove remediation. That evidence belongs to Track 7.
oc -n "$BUILD_NS" get configmap conforma-policy -o yaml
Read back the saved object. The signature switch must be false, the identity must name your pipeline service account, and the wildcard registry and exclude-all entries must be gone.
Connect the policy Task after signing
In .tekton/pipeline.yaml on the student remote, add Task conforma-policy. Resolve it from the cluster, namespace lightwell-tasks, with runAfter: [cosign-sign-keyless]. Point policy-namespace and policy-configmap at your copy in lw-poc-build. Pass tuf-url: $(params.cosign-tuf-url) and rekor-url: $(params.cosign-rekor-url). Those parameters are the in-cluster *.trusted-artifact-signer.svc URLs from 4.3 and 5.1. Commit, push, and apply the Pipeline.
Add the following entry under spec.tasks, aligned with the existing tasks. Replace BUILD_NAMESPACE with the namespace printed above. image is required: the Task must receive the same application reference as the signing task. Keep the pipeline’s existing image-url parameter and avoid overlapping runs that could replace its tag.
- name: conforma-policy
runAfter: [cosign-sign-keyless]
taskRef:
resolver: cluster
params:
- name: kind
value: task
- name: name
value: conforma-policy
- name: namespace
value: lightwell-tasks
params:
- name: image
value: $(params.image-url)
- name: policy-configmap
value: conforma-policy
- name: policy-namespace
value: BUILD_NAMESPACE
- name: tuf-url
value: $(params.cosign-tuf-url)
- name: rekor-url
value: $(params.cosign-rekor-url)
cd /home/lab-user/lightwell-workspace/spring-boot-lw-poc
git diff -- .tekton/pipeline.yaml
git add .tekton/pipeline.yaml
git diff --cached
git commit -m "Enforce learner-owned signature policy after signing"
git push origin HEAD
git fetch origin
git rev-parse HEAD origin/main
Run and inspect the policy Task
Create a PipelineRun from the filled .tekton/pipelinerun.yaml after that apply. metadata.generateName starts a new run and leaves the 4.4 run in place. The Check reads a succeeded TaskRun for conforma-policy. That task runs after cosign-sign-keyless, so the sign task has to succeed first. Wait until both tasks show Succeeded. A Fulcio error on the sign task is still 5.1 Sign, and this module stays red until that signature exists.
BUILD_NS="$(oc -n gitea get configmap lab-gitea -o jsonpath='{.data.student_build_namespace}')"
cd /home/lab-user/lightwell-workspace/spring-boot-lw-poc
oc -n "${BUILD_NS}" apply -f .tekton/pipeline.yaml
RUN="$(oc -n "${BUILD_NS}" create -f .tekton/pipelinerun.yaml -o jsonpath='{.metadata.name}')"
echo "waiting on ${RUN}"
oc -n "${BUILD_NS}" wait --for=condition=Succeeded "pipelinerun/${RUN}" --timeout=20m || true
oc -n "${BUILD_NS}" get taskrun -l "tekton.dev/pipelineRun=${RUN}" \
-o 'custom-columns=TASK:.metadata.labels.tekton\.dev/pipelineTask,SUCCEEDED:.status.conditions[0].status,REASON:.status.conditions[0].reason'
Compare the unsigned and signed images
The fail image is ImageStream openjdk-21-builder in the build namespace, the image you imported on 4.1. It is not signed by the pipeline service account, and the internal registry is allowed to pull it. The pass image is the application produced and signed by the run above. Resolve both tags to digests before comparing them. If the application digest differs from your saved 5.1 reference, repeat 5.1 verification for this run and refresh Track 4’s image and SBOM evidence before continuing.
ec is on the Showroom PATH. Initialize TUF and log in to the internal registry the same way as 5.1. Leave --ignore-rekor and --skip-att-sig-check off the command. --strict=false writes a report even when the attestation builtin fails. You judge the files, not the exit code.
Check the following five results. The builder report contains builtin.image.signature_check as a [Violation]. The application report has no [Violation] left once builtin.attestation.signature_check is set aside. The live Pipeline includes conforma-policy after cosign-sign-keyless, with in-cluster tuf-url and rekor-url. policy-namespace is lw-poc-build. The conforma-policy TaskRun from the run above is Succeeded. If the overall PipelineRun fails because a different task fails, inspect that task separately; do not describe the whole pipeline as successful.
BUILD_NS="$(oc -n gitea get configmap lab-gitea -o jsonpath='{.data.student_build_namespace}')"
TUF="$(oc -n trusted-artifact-signer get configmap lab-rhtas -o jsonpath='{.data.tuf_url_hint}')"
REKOR="$(oc -n trusted-artifact-signer get configmap lab-rhtas -o jsonpath='{.data.rekor_url_hint}')"
mkdir -p /tmp/ec /tmp/lab-home
export HOME=/tmp/lab-home
export SSL_CERT_FILE=/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
cosign initialize --mirror="${TUF}" --root="${TUF}/root.json"
cosign login -u unused -p "$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
image-registry.openshift-image-registry.svc:5000
oc -n "${BUILD_NS}" extract configmap/conforma-policy --to=/tmp/ec --confirm
FAIL_IMG="$(oc -n "$BUILD_NS" get istag openjdk-21-builder:latest -o jsonpath='{.image.dockerImageReference}')"
PASS_IMG="$(oc -n "$BUILD_NS" get istag spring-boot-lw-poc:latest -o jsonpath='{.image.dockerImageReference}')"
[[ "$FAIL_IMG" == *@sha256:* && "$PASS_IMG" == *@sha256:* ]] || { echo "Missing digest"; exit 1; }
rm -f /tmp/ec-fail.txt /tmp/ec-pass.txt
IDENTITY="$(tr -d '[:space:]' < /tmp/ec/certificate-identity-regexp)"
ISSUER="$(tr -d '[:space:]' < /tmp/ec/certificate-oidc-issuer-regexp)"
# Fail path: unsigned builder must report builtin.image.signature_check
ec validate image --image="${FAIL_IMG}" --policy=/tmp/ec/policy.yaml --strict=false \
--show-successes --info --output=text=/tmp/ec-fail.txt \
--certificate-identity-regexp="${IDENTITY}" \
--certificate-oidc-issuer-regexp="${ISSUER}" \
--rekor-url="${REKOR}" || true
test -s /tmp/ec-fail.txt || { echo 'No fresh builder report'; exit 1; }
grep '\[Violation\]' /tmp/ec-fail.txt | grep -q 'builtin.image.signature_check' \
|| { echo 'FAIL PATH UNEXPECTEDLY PASSED'; cat /tmp/ec-fail.txt; exit 1; }
# Pass path: ignore only the attestation builtin (no cosign attest on this cluster)
ec validate image --image="${PASS_IMG}" --policy=/tmp/ec/policy.yaml --strict=false \
--show-successes --info --output=text=/tmp/ec-pass.txt \
--certificate-identity-regexp="${IDENTITY}" \
--certificate-oidc-issuer-regexp="${ISSUER}" \
--rekor-url="${REKOR}" || true
test -s /tmp/ec-pass.txt || { echo 'No fresh application report'; exit 1; }
grep -q 'builtin.image.signature_check' /tmp/ec-pass.txt || { echo 'Image signature result missing'; exit 1; }
grep '\[Violation\]' /tmp/ec-pass.txt | grep -q 'builtin.image.signature_check' \
&& { echo 'PASS PATH: image signature still failing'; cat /tmp/ec-pass.txt; exit 1; }
leftover="$(grep '\[Violation\]' /tmp/ec-pass.txt | grep -v 'builtin.attestation.signature_check' || true)"
[[ -z "${leftover}" ]] || { echo "PASS PATH leftover: ${leftover}"; cat /tmp/ec-pass.txt; exit 1; }
oc -n "${BUILD_NS}" get pipeline spring-boot-lw-poc-build-sign -o yaml \
| grep -A30 'name: conforma-policy' | head -40
As the figure shows, Conforma reports a violation on openjdk-21-builder, then the same signature check succeeds on the signed application digest. The builder lists builtin.image.signature_check as a violation. The application lists that same check as a success. Both reports can still show builtin.attestation.signature_check as a violation, and the pass report can still say Success: false, because this cluster has no cosign attest. The script above treats that attestation line as expected and fails the module on any other leftover violation.
Review
Use the two fresh reports and the selected run’s TaskRun list. The unsigned builder must have an image-signature violation; the application must contain the image-signature result with no violation other than the expected attestation-signature check. A missing report, registry error, or timeout is not a successful negative test. Explain why accepting the attestation exception here does not establish build provenance, and why the unsigned image would fail even if skip-image-sig-check remained true. Identify the anchored identity/issuer and policy selection as the effective changes.
Save both reports and the policy snapshot outside your application Git clone. These are evidence of this attempt, not files to add to the application repository.
mkdir -p /home/lab-user/lightwell-signing-evidence
cp /tmp/ec-fail.txt /tmp/ec-pass.txt /home/lab-user/lightwell-signing-evidence/
oc -n "$BUILD_NS" get configmap conforma-policy -o yaml > /home/lab-user/lightwell-signing-evidence/policy.yaml
printf '%s\n' "$PASS_IMG" > /home/lab-user/lightwell-signing-evidence/policy-image.txt
printf '%s\n' "$RUN" > /home/lab-user/lightwell-signing-evidence/policy-run.txt
export HOME=/home/lab-user
Record which policy the pipeline uses
The workshop stores your short answer to “Which policy does your pipeline use?” in ConfigMap report-13-attest-conforma, in namespace lw-poc-validate. This ConfigMap is separate from the Conforma fail/pass reports you saved above. Its policy_owner field records that the pipeline uses the policy copy you configured in the build namespace, rather than the shared seed managed by Argo CD.
After confirming that the pipeline uses your copy, record the required answer student-copy. The following command updates only that field in the existing ConfigMap. It does not change the policy or the pipeline.
oc -n lw-poc-validate patch configmap report-13-attest-conforma \
--type=merge -p '{"data":{"policy_owner":"student-copy"}}'
oc -n lw-poc-validate get configmap report-13-attest-conforma \
-o jsonpath='{.data.policy_owner}{"\n"}'
The output should be student-copy. The validation Job below checks this answer as well as the configured policy, Task order, and successful policy TaskRun. Recording the answer alone does not satisfy those checks; leaving it empty or set to REPLACE_ME leaves the short-answer requirement incomplete. validate-docs contains reference examples and is not the ConfigMap where your answer belongs.
Run the module validation
The scored Check is Job validate-13-attest-conforma in lw-poc-validate. Direct nav URLs still work (honor system). Unlimited retries. There is no Solve.
oc -n lw-poc-validate delete job validate-13-attest-conforma --ignore-not-found
TPL='job-13.yaml'
oc -n lw-poc-validate get configmap validate-job-templates \
-o go-template="{{ index .data \"${TPL}\" }}" | oc create -f -
oc -n lw-poc-validate logs -f job/validate-13-attest-conforma
Expect CHECK PASSED: before you continue. CHECK FAILED: names the object to fix. Delete the Job and create it again as often as you need.
The Validate Job grades the tightened copy, the Task order, and a succeeded conforma-policy TaskRun. 5.3 Disconnected verify and Track 6 still open if you skip this module.
Carry the result into module 5.3
Keep the final verified application digest, fresh SBOM, policy copy, and fail/pass reports. Next you add a key-based signature to that same digest and publish only its public key for admission.
Official documentation
-
Trusted Artifact Signer deployment guide (Conforma /
ecagainst Trusted Artifact Signer signatures) -
Konflux mapping (Conforma maps to Task
conforma-policy)
Key takeaways
-
The policy that can fail is the copy you tightened in
lw-poc-build. -
The unsigned builder fails
builtin.image.signature_check. The signed application digest passes it. -
Leave
skip-att-sig-check=true, and ignorebuiltin.attestation.signature_checkin the report. -
Lightwell Network and Clearinghouse do not skip this gate.