CKS — Kubernetes Security Specialist
Pinning Images and Controlling Provenance
Goal
You build the standard configuration that pins images entering the cluster by digest, narrows their origin to an allowlist, and handles private registry authentication at the ServiceAccount level.
Why it matters
A tag is a mutable pointer. A different image can be pushed to the same v1.2 tag, and then
the image you verified yesterday and the image running today may differ. A digest is a hash of the content, so it
eliminates this problem at the root. Whether you can instantly answer "what exactly is running right now" when an incident
occurs determines your response time.
Origin control comes next. The allowed registry list goes straight into the policy engine as a parameter,
and imagePullSecrets is the credential for connecting to that registry. If you attach it to a ServiceAccount rather than writing it on each Pod,
new workloads inherit it automatically, so omissions decrease.
This environment has no internet, so it can't actually pull images or install policy engine CRDs like Kyverno. So the policy manifest is written as a file and graded, and the rest is graded as real objects.
Steps
- Create the namespace
cks-supplyand create a Podpinnedin it. Pin the image by digest with no tag (registry.cks.local/shop/api@sha256:followed by 64 hex digits).imagePullPolicyisIfNotPresent. - In
/root/cks-supply-chain/allowed-registries.txt, write the allowed registry prefixes, one per line, 4 lines in exactly the order below.registry.cks.local/,harbor.cks.local/library/,registry.k8s.io/,ghcr.io/cks-lab/ - Create a Secret
harbor-pullof typekubernetes.io/dockerconfigjsonincks-supply. The server isharbor.cks.local, the user isrobot$cks, and the password iscks-lab-token. - Create a ServiceAccount
deployerincks-supplyand putharbor-pullin itsimagePullSecrets. - Write a signature verification policy manifest in
/root/cks-supply-chain/verify-images.yaml.apiVersion: kyverno.io/v1,kind: ClusterPolicy, the name isverify-image-signature,spec.validationFailureActionisEnforce, and one rule (spec.rules[0].nameischeck-signature) targetsregistry.cks.local/*withverifyImages[0].imageReferences[0]. - Create a Pod
legacy-web(imagenginx:latest) incks-supplyto reproduce an unpinned workload. Then write a detection script in/root/cks-supply-chain/find-unpinned.shand save its output to/root/cks-supply-chain/unpinned.txt. The output holds the Pods in thecks-supplynamespace that have a container whose image is not pinned with@sha256:, in the formatcks-supply/<파드이름>(the placeholder is the Pod name), sorted alphabetically, one per line. - Create a Deployment
checkoutincks-supply. Replicas 2, the selector and Pod label areapp=checkout,serviceAccountNameisdeployer, the container name isapp, the image is one that starts withregistry.cks.local/and is pinned with@sha256:, andimagePullPolicyisIfNotPresent.
Notes
- If you need a 64-digit digest, instead of
head -c 32 /dev/urandom | sha256sumyou can use the hash of any string, likeecho cks | sha256sum. Only the format needs to match. kubectl create secret docker-registry harbor-pull --docker-server=... --docker-username=... --docker-password=... -n cks-supplykubectl patch serviceaccount deployer -n cks-supply -p '{"imagePullSecrets":[{"name":"harbor-pull"}]}'- Common mistake 1: writing both a tag and a digest on the image (
nginx:1.27@sha256:...). In this lab you keep only the digest. - Common mistake 2: if you leave out the slash after an allowlist prefix, a name like
registry.cks.local.evil.com/passes.
Pin by digest instead of tag
Create the namespace cks-supply and create a Pod pinned in it. Pin the image by
digest with no tag (registry.cks.local/shop/api@sha256: followed by 64 hex digits).
imagePullPolicy is IfNotPresent.
An image reference has the form <레지스트리>/<이름>@sha256:<64자리 16진수> (the placeholders are the registry, the name, and the 64 hex digits). Don't use a tag and a digest together; keep only the digest.
The allowed registry list
In /root/cks-supply-chain/allowed-registries.txt, write the allowed registry prefixes, one per line,
4 lines in exactly the order below.
registry.cks.local/, harbor.cks.local/library/, registry.k8s.io/, ghcr.io/cks-lab/
This is a list that will be passed straight through as a policy engine parameter. Write it in prefix form, and add the trailing slash so that other registries with similar names don't pass.
A registry authentication Secret
Create a Secret harbor-pull of type kubernetes.io/dockerconfigjson in cks-supply.
The server is harbor.cks.local, the user is robot$cks, and the password is cks-lab-token.
One line of kubectl create secret docker-registry is enough. The type name is kubernetes.io/dockerconfigjson and the data key is .dockerconfigjson.
Attach the pull Secret to a ServiceAccount
Create a ServiceAccount deployer in cks-supply and put harbor-pull in its
imagePullSecrets.
If you put it in the ServiceAccount's imagePullSecrets, every Pod that uses that SA authenticates automatically. It is better than writing it on each Pod.
A signature and SBOM verification policy manifest
Write a signature verification policy manifest in /root/cks-supply-chain/verify-images.yaml.
apiVersion: kyverno.io/v1, kind: ClusterPolicy, the name is verify-image-signature,
spec.validationFailureAction is Enforce, and one rule (spec.rules[0].name is
check-signature) targets registry.cks.local/* with
verifyImages[0].imageReferences[0].
This cluster has no policy engine CRDs, so you write it only as a file. The structure is that imageReferences selects the target registry and you put the public key in attestors.
Detect Pods not pinned by digest
Create a Pod legacy-web (image nginx:latest) in cks-supply to reproduce an unpinned workload.
Then write a detection script in /root/cks-supply-chain/find-unpinned.sh and
save its output to /root/cks-supply-chain/unpinned.txt. The output holds the Pods in the cks-supply
namespace that have a container whose image is not pinned with @sha256:, in the format
cks-supply/<파드이름> (the placeholder is the Pod name), sorted alphabetically, one per line.
If the image string has no @sha256:, it is unpinned. You can filter with jq's contains or test, and you must look at init containers as well.
Putting it together: a deployment with pinning, authentication, and origin
Create a Deployment checkout in cks-supply. Replicas 2, the selector and Pod label are
app=checkout, serviceAccountName is deployer, the container name is app, the image is one that
starts with registry.cks.local/ and is pinned with @sha256:, and imagePullPolicy is
IfNotPresent.
Use the ServiceAccount you created earlier and the first prefix of the allowed registry list as they are. Pin the image by digest and specify imagePullPolicy as well.