
Firmar una imagen con Cosign no impide que Kubernetes despliegue otra imagen sin firma. Para convertir la firma en un control efectivo necesitas una puerta de admisión que compruebe la imagen antes de aceptar el recurso.
Sigstore policy-controller realiza esa verificación mediante recursos ClusterImagePolicy y puede admitir, advertir o rechazar el despliegue según la autoridad configurada.
En esta guía vas a instalar policy-controller, definir una política keyless limitada a un repositorio, activarla en un namespace de ensayo y comprobar dos resultados: una imagen con la identidad autorizada se admite y una imagen sin firma válida se rechaza.
Resultado esperado. El API server solo aceptará, dentro del namespace protegido, imágenes del repositorio definido que tengan una firma verificable emitida para elissuery elsubjectautorizados.
Qué controla esta política
El pipeline firma una imagen almacenada en un registro OCI. Cuando alguien intenta crear un Deployment, Pod, Job u otro recurso con un PodSpec, el API server llama al webhook de policy-controller.
El controlador resuelve la referencia de la imagen a un digest, selecciona las ClusterImagePolicy cuyo patrón coincide y valida la firma contra las autoridades declaradas.
Todas las ClusterImagePolicy que coincidan deben cumplirse: entre políticas la relación es AND. Dentro de una misma política basta con que una de sus autoridades sea válida: entre autoridades la relación es OR.
Por tanto, una política adicional puede volver más restrictiva una imagen que ya satisface otra.
La firma se produce antes del despliegue; policy-controller la verifica durante la admisión.
Antes de comenzar
Necesitas:
- Un clúster Kubernetes de ensayo o un namespace donde puedas realizar pruebas sin afectar producción.
kubectlconfigurado con permisos para instalar CRD, webhooks y recursos de alcance de clúster.- Helm 3.
- Cosign para verificar la imagen antes de probar la admisión.
- Acceso del controlador al registro OCI y, para firmas keyless públicas, a los servicios de Sigstore.
- Una imagen firmada y otra imagen no firmada o firmada por una identidad distinta, preferentemente del mismo repositorio.
- El
issuery elsubjectexactos de la identidad autorizada.
Los comandos están escritos para Bash. Sustituye los valores ORGANIZACION, REPOSITORIO, las referencias de imágenes y la versión del chart antes de ejecutar.
Precaución. No habilites el namespace antes de instalar y validar la política. Una cobertura incompleta puede bloquear imágenes auxiliares, initContainers o sidecars inyectados. Conserva otra sesión con acceso administrativo al clúster.Compatibilidad que debes validar
Al 31 de agosto de 2026, la última versión publicada de policy-controller es v0.15.1. El proyecto continúa en desarrollo activo.
También existen incidencias abiertas sobre el descubrimiento de algunas firmas creadas con Cosign v3, tanto keyless como con clave.
Por eso, no asumas que cosign verify y el webhook interpretarán de la misma forma cualquier formato de firma.
La API distingue signatureFormat: legacy|bundle y utiliza legacy de forma predeterminada. La documentación actual indica limitaciones del formato bundle para firmas planas. Este ejemplo declara legacy, pero no debe publicarse como combinación recomendada hasta comprobar qué formato produce la versión de Cosign elegida.
Registra las versiones de Cosign, del chart y de la aplicación, verifica primero la imagen fuera del clúster y ejecuta este procedimiento con la misma combinación que utilizarás en CI.
Paso 1: registra el contexto y los permisos
Confirma que trabajas en el clúster correcto:
kubectl config current-context
kubectl version
helm version
cosign versionComprueba los permisos principales:
kubectl auth can-i create \
customresourcedefinitions.apiextensions.k8s.io
kubectl auth can-i create \
validatingwebhookconfigurations.admissionregistration.k8s.io
kubectl auth can-i create \
clusterimagepolicies.policy.sigstore.dev
kubectl auth can-i patch namespacesRegistra además qué namespaces ya tienen habilitado el controlador:
kubectl get namespaces \
-l policy.sigstore.dev/include=trueUna respuesta no en los permisos requiere intervención de quien administra el clúster. No sustituyas estos permisos por un rol más amplio sin revisar el alcance.
Paso 2: crea el namespace de ensayo sin activar la política
Prepara un namespace aislado, pero todavía sin el label opt-in:
kubectl create namespace firma-lab \
--dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace firma-lab \
policy.sigstore.dev/include- --overwrite
kubectl get namespace firma-lab --show-labelsEl resultado no debe incluir policy.sigstore.dev/include=true.
Si el label no existía, kubectl puede informar que el namespace no estaba etiquetado. No ocultes errores de conexión o RBAC: confirma siempre el estado final con kubectl get namespace.
Este orden conserva una vía de recuperación mientras instalas y validas el controlador.
Paso 3: instala policy-controller con una versión fijada
Añade el repositorio oficial y revisa las versiones disponibles:
helm repo add sigstore \
https://sigstore.github.io/helm-charts
helm repo update
helm search repo sigstore/policy-controller \
--versions | headSelecciona una versión que hayas validado con tu versión de Kubernetes y asígnala explícitamente. No dejes el marcador del ejemplo:
export POLICY_CONTROLLER_CHART_VERSION="VERSION_VALIDADA"
helm upgrade --install \
policy-controller \
sigstore/policy-controller \
--namespace cosign-system \
--create-namespace \
--version "$POLICY_CONTROLLER_CHART_VERSION" \
--atomic \
--wait--atomic revierte el release si la instalación no llega al estado esperado. Esto no sustituye la revisión de CRD y webhooks.
Comprueba la instalación:
helm list -n cosign-system
kubectl get deployments,pods \
-n cosign-system
kubectl get crd \
clusterimagepolicies.policy.sigstore.dev
kubectl get \
validatingwebhookconfigurations,mutatingwebhookconfigurations \
| grep -E 'policy.sigstore.dev|clusterimagepolicy'Los pods deben estar Running y disponibles. El CRD y las configuraciones de webhook deben existir.
Si el chart utiliza nombres diferentes, inspecciona los recursos del release:
helm get manifest policy-controller \
-n cosign-systemPaso 4: verifica la imagen fuera del clúster
Define las referencias por digest y la identidad exacta utilizada para firmar.
El siguiente ejemplo representa una firma keyless emitida desde GitHub Actions:
export SIGNED_IMAGE="ghcr.io/ORGANIZACION/APLICACION@sha256:DIGEST_FIRMADO"
export UNSIGNED_IMAGE="ghcr.io/ORGANIZACION/APLICACION@sha256:DIGEST_NO_AUTORIZADO"
export COSIGN_ISSUER="https://token.actions.githubusercontent.com"
export COSIGN_SUBJECT="https://github.com/ORGANIZACION/REPOSITORIO/.github/workflows/firmar.yml@refs/heads/main"Verifica la imagen que debería ser admitida:
cosign verify "$SIGNED_IMAGE" \
--certificate-oidc-issuer="$COSIGN_ISSUER" \
--certificate-identity="$COSIGN_SUBJECT"La verificación debe terminar con código 0 y mostrar que se comprobaron la firma, la identidad y los claims del digest.
Si falla aquí, no actives el namespace. Corrige primero la firma, la identidad, el registro o la compatibilidad de versiones.
La clave privada, si tu organización utiliza firmas con clave, debe permanecer en CI o en un KMS. Nunca la incluyas en una ClusterImagePolicy, un ConfigMap, una captura o el repositorio de manifiestos.
Paso 5: crea una ClusterImagePolicy en modo warn
Guarda este manifiesto como cip-firma-keyless.yaml y reemplaza todos los valores de ejemplo:
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: donweb-firma-keyless
spec:
mode: warn
images:
- glob: "ghcr.io/ORGANIZACION/APLICACION@sha256:*"
authorities:
- name: github-actions
signatureFormat: legacy
keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subject: https://github.com/ORGANIZACION/REPOSITORIO/.github/workflows/firmar.yml@refs/heads/main
ctlog:
url: https://rekor.sigstore.devEl patrón está limitado a un repositorio y a referencias por digest. No empieces con "**" en producción.
Usa un subject exacto cuando el flujo de firma sea estable. Una expresión regular demasiado amplia puede aceptar identidades no previstas.
mode: warn permite observar incumplimientos de esta política sin bloquearlos.
signatureFormat: legacy hace explícito el formato esperado por esta autoridad. Debe coincidir con el formato realmente publicado por el pipeline.
Una imagen que no coincida con ninguna política puede seguir siendo rechazada por la configuración global no-match-policy, cuyo comportamiento predeterminado es denegar cuando no se configura. Revisa todos los contenedores que aparecerán en el namespace.
Valida el manifiesto antes de persistirlo:
kubectl apply \
--dry-run=server \
-f cip-firma-keyless.yaml
kubectl diff \
-f cip-firma-keyless.yamlkubectl diff devuelve código 1 cuando encuentra diferencias; eso no representa necesariamente un error. Un código mayor que 1 sí requiere investigación.
Aplica y examina la política:
kubectl apply \
-f cip-firma-keyless.yaml
kubectl get clusterimagepolicy \
donweb-firma-keyless
kubectl describe clusterimagepolicy \
donweb-firma-keylessRevisa el YAML completo y los logs del controlador:
kubectl get clusterimagepolicy \
donweb-firma-keyless \
-o yaml
kubectl logs \
-n cosign-system \
-l app.kubernetes.io/name=policy-controller \
--all-containers \
--tail=200Algunas versiones exponen status o conditions. Si aparecen, no deben indicar errores de reconciliación. La ausencia de esas condiciones no prueba por sí sola un fallo.
No avances si el API rechazó el recurso o los logs muestran errores de sintaxis, confianza o acceso al registro.
Paso 6: inspecciona no-match-policy
Antes del opt-in, inspecciona el comportamiento global para imágenes sin política coincidente:
kubectl get configmap \
config-policy-controller \
--namespace cosign-system \
-o yamlBusca data.no-match-policy.
Los valores admitidos son:
warn: admite la imagen, pero muestra una advertencia.allow: admite la imagen sin exigir una política coincidente.deny: rechaza la imagen.- Ausente: la documentación indica que el comportamiento predeterminado es rechazar.
Registra el valor porque afecta a todas las imágenes de los namespaces incluidos, no solo al repositorio de este ejemplo.
Paso 7: activa solo el namespace de ensayo
Una vez validadas la política y la configuración global, habilita la admisión:
kubectl label namespace firma-lab \
policy.sigstore.dev/include=true \
--overwrite
kubectl get namespace firma-lab \
--show-labelsEl label debe aparecer exactamente como:
policy.sigstore.dev/include=trueEl comportamiento opt-in evita que la instalación afecte automáticamente a todos los namespaces.
La activación del namespace ocurre después de validar la política y antes de las pruebas controladas.
Paso 8: prueba el modo warn
Crea primero el recurso con la imagen firmada:
kubectl create deployment \
firma-valida \
--namespace firma-lab \
--image="$SIGNED_IMAGE"
kubectl rollout status \
deployment/firma-valida \
--namespace firma-lab \
--timeout=120sDespués prueba el digest no autorizado:
kubectl create deployment \
firma-invalida-warn \
--namespace firma-lab \
--image="$UNSIGNED_IMAGE"En warn, el segundo recurso puede admitirse, pero kubectl debe presentar una advertencia de la política.
Captura la salida y revisa los logs del controlador:
kubectl logs \
-n cosign-system \
-l app.kubernetes.io/name=policy-controller \
--all-containers \
--tail=200Las etiquetas exactas dependen de la versión del chart.
Si el selector no devuelve pods, obtén las etiquetas disponibles:
kubectl get pods \
-n cosign-system \
--show-labelsAjusta después el selector del comando de logs.
Paso 9: cambia a enforce y repite las pruebas
Elimina los recursos de prueba para que las siguientes solicitudes recorran nuevamente la admisión:
kubectl delete deployment \
firma-valida \
firma-invalida-warn \
--namespace firma-lab \
--ignore-not-foundCambia la política a enforce:
kubectl patch clusterimagepolicy \
donweb-firma-keyless \
--type=merge \
-p '{"spec":{"mode":"enforce"}}'Comprueba el cambio:
kubectl get clusterimagepolicy \
donweb-firma-keyless \
-o jsonpath='{.spec.mode}{"\n"}'La salida esperada es:
enforceRepite la prueba positiva:
kubectl create deployment \
firma-valida \
--namespace firma-lab \
--image="$SIGNED_IMAGE"
kubectl rollout status \
deployment/firma-valida \
--namespace firma-lab \
--timeout=120sLa solicitud debe admitirse y el pod debe quedar disponible.
Ejecuta ahora la prueba negativa:
kubectl create deployment \
firma-invalida \
--namespace firma-lab \
--image="$UNSIGNED_IMAGE"La respuesta esperada es un rechazo inmediato del API server atribuido al webhook policy.sigstore.dev.
El Deployment no debe crearse:
kubectl get deployment \
firma-invalida \
--namespace firma-labLa salida esperada debe indicar que el recurso no existe.
Un ImagePullBackOff no valida esta política: significa que el objeto ya fue admitido y que el fallo ocurrió después, al descargar la imagen.
Imagen relacionada: tercera imagen mostrada arriba.
La prueba negativa debe fallar durante la admisión, antes de crear el recurso.
Cómo comprobar el resultado
Conserva como evidencia:
- Contexto y versión de Kubernetes.
- Versión de Helm y Cosign.
- Versión del chart y de
policy-controller. - Formato de firma utilizado.
- Manifiesto exacto de la
ClusterImagePolicy. - Valor de
no-match-policy. - Label del namespace.
- Referencias por digest de ambas imágenes.
- Salida de
cosign verify. - Creación y estado del despliegue firmado.
- Rechazo de admisión del despliegue no autorizado.
- Eventos y logs relevantes del webhook.
- Fecha y hora UTC de las pruebas.
La aceptación mínima es:
- La imagen autorizada se admite.
- El pod autorizado queda
Ready. - La imagen no autorizada se rechaza durante la admisión.
- El
Deploymentinválido no se crea. - Ningún otro namespace resulta afectado.
Rollback seguro
Si la política bloquea cargas necesarias, retira primero el namespace del alcance del webhook:
kubectl label namespace firma-lab \
policy.sigstore.dev/include-
kubectl get namespace firma-lab \
--show-labelsConfirma que el label ya no aparezca.
Después puedes volver la política a warn:
kubectl patch clusterimagepolicy \
donweb-firma-keyless \
--type=merge \
-p '{"spec":{"mode":"warn"}}'Si ya no necesitas la política:
kubectl delete clusterimagepolicy \
donweb-firma-keylessAntes de desinstalar el controlador, confirma que no queden namespaces opt-in:
kubectl get namespaces \
-l policy.sigstore.dev/include=trueSi la lista está vacía y decidiste retirar completamente el componente:
helm uninstall policy-controller \
--namespace cosign-systemkubectl rollout undo no revierte una política de admisión.
El rollback correcto empieza retirando el label que incluye el namespace en el alcance del webhook.
Problemas frecuentes
La imagen pasa cosign verify, pero policy-controller la rechaza
Compara:
- El digest.
- El
issuer. - El
subject. - El patrón
glob. - El valor de
signatureFormat. - Las versiones de Cosign, del chart y de
policy-controller.
Revisa si la firma se almacenó con un formato que la versión del controlador puede descubrir.
Existen incidencias abiertas con determinadas firmas de Cosign v3, por lo que una verificación local correcta no garantiza automáticamente que el webhook encuentre el mismo objeto de firma.
El error indica no signatures found
Confirma que:
- La firma esté publicada en el registro.
- El registro soporte el método de almacenamiento utilizado.
- El webhook pueda acceder al registro.
- La firma se encuentre junto a la imagen o en el
sourceconfigurado. - La referencia corresponda al digest firmado.
- El formato coincida con
signatureFormat.
Verifica la misma referencia por digest con Cosign desde un entorno que utilice credenciales equivalentes.
Las imágenes sin firma se admiten
Comprueba:
- El label del namespace.
spec.mode.- El patrón de imagen.
- El registro del webhook.
- El valor de
no-match-policy. - Los logs de
policy-controller.
En warn, el recurso se admite deliberadamente con una advertencia.
Revisa también todas las CIP coincidentes. Una política con static: pass no anula otra política restrictiva que falle, porque las CIP coincidentes se combinan con lógica AND. Solo permite el paso cuando es la política aplicable que debe satisfacerse para esa imagen.
Se rechaza una imagen que no pertenece al repositorio de la política
El comportamiento predeterminado puede rechazar imágenes que no coinciden con ninguna política dentro de un namespace habilitado.
Revisa:
no-match-policy.- Las imágenes auxiliares.
- Los sidecars.
- Los
initContainers. - Los contenedores agregados por otros webhooks.
- Todos los patrones
globexistentes.
No cambies globalmente a allow sin evaluar todos los namespaces opt-in.
El registro es privado
El controlador necesita credenciales para descubrir firmas y attestations.
Por defecto puede utilizar los imagePullSecrets del PodSpec. Si las firmas están en otra ubicación, configura source y signaturePullSecrets según la documentación oficial.
El Secret debe existir en el namespace del workload y ser de tipo:
kubernetes.io/dockerconfigjsonUn sidecar inyectado bloquea el despliegue
La política evalúa las imágenes presentes en el recurso admitido, incluidas las que agregan otros webhooks.
Identifica todas las imágenes finales y define una política o autoridad explícita para ellas antes de activar enforce.
Buenas prácticas para producción
- Fija la versión del chart y conserva sus valores de instalación.
- Registra la versión real de la aplicación incluida en el chart.
- Usa imágenes por digest en despliegues y pruebas.
- Declara explícitamente el formato de firma esperado.
- Restringe
issuerysubject. - Evita expresiones regulares amplias.
- Mantén la clave privada fuera del clúster.
- Utiliza un KMS cuando corresponda.
- Empieza con
warn. - Mide incumplimientos antes de pasar a
enforce. - Amplía el alcance por namespace.
- Revisa todas las políticas coincidentes.
- Recuerda que varias
ClusterImagePolicyse combinan con lógica AND. - Incluye sidecars,
initContainersy jobs auxiliares en el inventario. - Supervisa disponibilidad, latencia y errores del webhook.
- Documenta excepciones con responsable, justificación y vencimiento.
- Prueba el rollback antes de ampliar el alcance.