By default, any container image is admitted to your workload clusters. Add an admission check to require that images are cryptographically signed before pods can run.
> [!NOTE]
> Syself's node image has its own integrity story: dm-verity and a published SBOM. This page covers your workload images, not the node image. See [Verify node integrity](/docs/hetzner/apalla/security/verify-node-integrity) for the node side.
## Prerequisites
- `kubectl` access to the workload cluster
- Your images are signed with cosign or another sigstore-compatible tool
- Kyverno installed if you use Option A (see [Use Kyverno policies](/docs/hetzner/apalla/security/use-kyverno-policies))
## How image-signature checking works
When a pod is created, an admission controller (a plugin that inspects resources before Kubernetes accepts them) checks the container images and rejects the pod if a signature is missing or invalid.
On Syself, set `failurePolicy: Ignore` on any admission controller you deploy for this check. This is a recommendation, not something [webhook-guard](/docs/hetzner/apalla/security/control-admission) enforces: webhook-guard restricts which API groups and resources a webhook can target, it does not check a webhook's `failurePolicy`. With `failurePolicy: Ignore`, if the verifier is down or unreachable, the API server skips it and admits the pod. Image-signature checking is therefore best-effort. Run the verifier highly available to make the check reliable in practice.
## Why a native ValidatingAdmissionPolicy does not work here
A native ValidatingAdmissionPolicy (VAP) runs inside the API server and can hard-block pods without the `failurePolicy: Ignore` limitation. For many rules, a VAP is the right tool.
Signature verification is the exception. A VAP can inspect the image string: registry, repository, tag, or digest. It cannot fetch a signature from a registry and verify it against a key. That requires network calls and cryptography that a VAP does not support.
Use a VAP to require that images come from a specific registry or use a digest. Use a webhook-based tool (below) to require that images are signed.
## Restrict image sources and tags
Signature verification proves who built an image. It does not stop a pod from pulling `nginx:latest` from a public registry. Add a second, cheaper check that runs inside the API server with a ValidatingAdmissionPolicy (VAP): require every image to come from an allowed registry or be pinned to a digest, and reject the `:latest` tag. A VAP only reads the image string, so it needs no network calls and can hard-block without the `failurePolicy: Ignore` limitation.
```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: restrict-image-sources
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: >-
object.spec.containers.all(c,
c.image.contains('@sha256:') || c.image.startsWith('registry.example.com/'))
message: "images must come from registry.example.com or be pinned to a @sha256 digest"
- expression: "object.spec.containers.all(c, !c.image.endsWith(':latest'))"
message: "the :latest tag is not allowed; pin a version tag or a digest"
```
Bind it to your own namespaces so it does not touch platform workloads:
```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: restrict-image-sources
spec:
policyName: restrict-image-sources
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: team-a
```
The expressions above cover `containers` only. Extend them to `object.spec.initContainers` and `object.spec.ephemeralContainers` if your pods use them.
**Signing is not scanning.** A signature proves who built an image, not what is inside it. Scan your workload images for known CVEs as well, in CI before you push and again inside the cluster; [Scan workload images for vulnerabilities](/docs/hetzner/apalla/security/scan-workload-images) is the how. The platform SBOM covers the node image only, never the contents of your workload images, so that side is yours.
## Require signatures with a webhook verifier
Two webhook-based tools can require signatures on this platform. Both run with `failurePolicy: Ignore`, since Syself does not enforce this setting for you.
Kyverno's `verifyImages` rule performs cosign (a tool for signing and verifying container images) signature verification. It runs as a webhook. Set `failurePolicy: Ignore` on it, since Syself does not enforce this setting for you.
See [Use Kyverno policies](/docs/hetzner/apalla/security/use-kyverno-policies) for installation and high-availability configuration.
Example policy (keyless, using GitHub Actions as the signing identity):
```yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce # block pods that fail the rule when Kyverno is up
webhookConfiguration:
failurePolicy: Ignore # recommended so a verifier outage does not block the API server
rules:
- name: verify-signature
match:
any:
- resources:
kinds: [Pod]
namespaces: [team-a] # scope to your namespaces
verifyImages:
- imageReferences: ["registry.example.com/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/your-org/*"
issuer: "https://token.actions.githubusercontent.com"
```
The sigstore policy-controller is a purpose-built admission controller for image signatures and attestations. It is also webhook-based. Configure it with `failurePolicy: Ignore` and run it highly available.
## Roll out safely
Scope the policy to your own namespaces. Do not target `kube-system` or platform namespaces; Syself manages those images. First set `validationFailureAction: Audit`. This logs which pods would be blocked without actually blocking them. Once you confirm your signed images pass, switch to `Enforce`. Because the webhook uses `failurePolicy: Ignore`, it only protects you while it is running. Keep it running by deploying several replicas and adding a PodDisruptionBudget (a rule that keeps a minimum number of replicas up during node drains). See [Use Kyverno policies](/docs/hetzner/apalla/security/use-kyverno-policies) for a working example.
## Verify
```console
# A signed image should be admitted:
$ kubectl -n team-a run signed --image=registry.example.com/app@sha256:
# An unsigned image should be blocked by your policy:
$ kubectl -n team-a run unsigned --image=docker.io/library/busybox # expect: denied
```
## Known limits
**Best-effort under `Ignore`.** If the verifier is down when a pod is created, an unsigned image can be admitted. High-availability mode with alerting on verifier health is the mitigation.
**Workload images only.** This policy covers images you deploy. The node image integrity story is separate and handled by the platform.
## Related
- [Use Kyverno policies](/docs/hetzner/apalla/security/use-kyverno-policies): installation, `failurePolicy` wiring, and high-availability setup.
- [Control admission](/docs/hetzner/apalla/security/control-admission): the webhook-guard contract and when to use a native ValidatingAdmissionPolicy.
- [Verify node integrity](/docs/hetzner/apalla/security/verify-node-integrity): dm-verity and SBOM for the node image.
- [Security architecture](/docs/hetzner/apalla/security/security-architecture): the full workload security checklist.
- [Segment with network policies](/docs/hetzner/apalla/security/segment-with-network-policies): adding network controls alongside image-signature checks.