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.