diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..eeffd17 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(kyverno version *)", + "Bash(rtk curl *)", + "Bash(tar -xzf /tmp/kyverno.tar.gz -C /tmp kyverno)", + "Bash(sudo mv /tmp/kyverno /usr/local/bin/kyverno)", + "Bash(mv /tmp/kyverno ~/bin/kyverno)", + "Bash(mv /tmp/kyverno ~/.local/bin/kyverno)", + "Bash(export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\")" + ] + } +} diff --git a/02-validation/01-resource-validation/1. require-resource-limits.yaml b/02-validation/01-resource-validation/1. require-resource-limits.yaml index f4cccbc..65b1e4d 100644 --- a/02-validation/01-resource-validation/1. require-resource-limits.yaml +++ b/02-validation/01-resource-validation/1. require-resource-limits.yaml @@ -26,22 +26,14 @@ spec: - kube-system - kyverno validate: - message: >- - Контейнер '{{ element.name }}' в поде '{{ request.object.metadata.name }}' - (namespace: {{ request.object.metadata.namespace }}) не имеет resource limits. - - Добавьте в манифест: - resources: - limits: - memory: "256Mi" - cpu: "500m" - - Документация: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ foreach: - list: >- request.object.spec.containers[] | merge(request.object.spec.initContainers[] || `[]`, @) | merge(request.object.spec.ephemeralContainers[] || `[]`, @) + message: >- + Контейнер '{{ element.name }}' в поде '{{ request.object.metadata.name }}' + не имеет resource limits. Добавьте resources.limits.memory и resources.limits.cpu. pattern: resources: limits: diff --git a/02-validation/01-resource-validation/3. disallow-latest-tag.yaml b/02-validation/01-resource-validation/3. disallow-latest-tag.yaml index d04ba4b..f6d7274 100644 --- a/02-validation/01-resource-validation/3. disallow-latest-tag.yaml +++ b/02-validation/01-resource-validation/3. disallow-latest-tag.yaml @@ -25,20 +25,20 @@ spec: namespaces: - kube-system validate: - message: >- - Образ '{{ element.image }}' использует тег :latest или не имеет тега. - Используйте конкретный тег (например, nginx:1.25.3) или digest - (nginx@sha256:abc123...) для воспроизводимых деплойментов. foreach: - list: >- request.object.spec.containers[] | - merge(request.object.spec.initContainers[] || `[]`, @) + merge(request.object.spec.initContainers[] || `[]`, @) | + merge(request.object.spec.ephemeralContainers[] || `[]`, @) + message: >- + Образ '{{ element.image }}' использует тег :latest или не имеет тега. + Используйте конкретный тег (nginx:1.25.3) или digest (nginx@sha256:...). deny: conditions: any: - - key: "{{ element.image }}" - operator: Contains - value: ":latest" - - key: "{{ element.image }}" - operator: NotContains - value: ":" + - key: "{{ regex_match(':latest', element.image) }}" + operator: Equals + value: true + - key: "{{ regex_match('^(([^/]+/)*[^/:]+)$', element.image) }}" + operator: Equals + value: true diff --git a/02-validation/01-resource-validation/4. allow-only-trusted-registries.yaml b/02-validation/01-resource-validation/4. allow-only-trusted-registries.yaml index 78909be..1554e88 100644 --- a/02-validation/01-resource-validation/4. allow-only-trusted-registries.yaml +++ b/02-validation/01-resource-validation/4. allow-only-trusted-registries.yaml @@ -27,25 +27,23 @@ spec: - kube-system - kyverno validate: - message: >- - Образ '{{ element.image }}' из недоверенного реестра. - Разрешены только: - - registry.company.com/ - - gcr.io/company-project/ - Загрузите образ в внутренний реестр и обновите манифест. foreach: - list: >- request.object.spec.containers[] | - merge(request.object.spec.initContainers[] || `[]`, @) + merge(request.object.spec.initContainers[] || `[]`, @) | + merge(request.object.spec.ephemeralContainers[] || `[]`, @) + message: >- + Образ '{{ element.image }}' из недоверенного реестра. + Разрешены: registry.company.com/, gcr.io/company-project/. + Загрузите образ в внутренний реестр и обновите манифест. deny: conditions: all: - # Образ НЕ из первого доверенного реестра - - key: "{{ element.image }}" - operator: NotStartsWith - value: "registry.company.com/" - # И НЕ из второго доверенного реестра - - key: "{{ element.image }}" - operator: NotStartsWith - value: "gcr.io/company-project/" + # regex_match: false = образ НЕ начинается с доверенного реестра + - key: "{{ regex_match('^registry\\.company\\.com/', element.image) }}" + operator: Equals + value: false + - key: "{{ regex_match('^gcr\\.io/company-project/', element.image) }}" + operator: Equals + value: false # Добавьте дополнительные условия по аналогии diff --git a/02-validation/02-security/disallow-dangerous-capabilities.yaml b/02-validation/02-security/disallow-dangerous-capabilities.yaml index 1fcd548..8765e8e 100644 --- a/02-validation/02-security/disallow-dangerous-capabilities.yaml +++ b/02-validation/02-security/disallow-dangerous-capabilities.yaml @@ -26,15 +26,14 @@ spec: namespaces: - kube-system validate: - message: >- - Контейнер '{{ element.name }}' добавляет запрещённые capabilities: - {{ element.securityContext.capabilities.add }}. - Разрешена только NET_BIND_SERVICE. - Пересмотрите необходимость этих привилегий. foreach: - list: >- request.object.spec.containers[] | - merge(request.object.spec.initContainers[] || `[]`, @) + merge(request.object.spec.initContainers[] || `[]`, @) | + merge(request.object.spec.ephemeralContainers[] || `[]`, @) + message: >- + Контейнер '{{ element.name }}' добавляет запрещённые capabilities. + Разрешена только NET_BIND_SERVICE. Пересмотрите необходимость привилегий. deny: conditions: any: diff --git a/02-validation/02-security/disallow-host-namespaces.yaml b/02-validation/02-security/disallow-host-namespaces.yaml index 0e76dbd..bbd874b 100644 --- a/02-validation/02-security/disallow-host-namespaces.yaml +++ b/02-validation/02-security/disallow-host-namespaces.yaml @@ -52,6 +52,6 @@ spec: deny: conditions: any: - - key: "{{ request.object.spec.volumes[].hostPath | length(@) }}" + - key: "{{ request.object.spec.volumes[?hostPath] | length(@) }}" operator: GreaterThan value: "0" diff --git a/02-validation/02-security/disallow-privileged-containers.yaml b/02-validation/02-security/disallow-privileged-containers.yaml index 1095251..099d42b 100644 --- a/02-validation/02-security/disallow-privileged-containers.yaml +++ b/02-validation/02-security/disallow-privileged-containers.yaml @@ -26,15 +26,14 @@ spec: namespaces: - kube-system validate: - message: >- - Контейнер '{{ element.name }}' имеет securityContext.privileged: true. - Привилегированные контейнеры запрещены — они получают полный доступ к хосту. - Удалите поле securityContext.privileged или установите значение false. foreach: - list: >- request.object.spec.containers[] | merge(request.object.spec.initContainers[] || `[]`, @) | merge(request.object.spec.ephemeralContainers[] || `[]`, @) + message: >- + Контейнер '{{ element.name }}' имеет securityContext.privileged: true. + Привилегированные контейнеры запрещены. Удалите поле или установите false. deny: conditions: any: diff --git a/02-validation/02-security/require-drop-all-capabilities.yaml b/02-validation/02-security/require-drop-all-capabilities.yaml index 3dd9299..d8ae2d3 100644 --- a/02-validation/02-security/require-drop-all-capabilities.yaml +++ b/02-validation/02-security/require-drop-all-capabilities.yaml @@ -25,14 +25,14 @@ spec: namespaces: - kube-system validate: - message: >- - Контейнер '{{ element.name }}' не сбрасывает все capabilities. - Добавьте в securityContext: - capabilities: - drop: - - ALL foreach: - - list: "request.object.spec.containers" + - list: >- + request.object.spec.containers[] | + merge(request.object.spec.initContainers[] || `[]`, @) | + merge(request.object.spec.ephemeralContainers[] || `[]`, @) + message: >- + Контейнер '{{ element.name }}' не сбрасывает все capabilities. + Добавьте securityContext.capabilities.drop: [ALL]. deny: conditions: all: diff --git a/02-validation/02-security/require-run-as-non-root.yaml b/02-validation/02-security/require-run-as-non-root.yaml index fe5c5d3..97436a5 100644 --- a/02-validation/02-security/require-run-as-non-root.yaml +++ b/02-validation/02-security/require-run-as-non-root.yaml @@ -45,11 +45,11 @@ spec: namespaces: - kube-system validate: - message: >- - Контейнер '{{ element.name }}' использует runAsUser: 0 (root). - Установите runAsUser >= 1000. foreach: - - list: "request.object.spec.containers" + - list: >- + request.object.spec.containers[] | + merge(request.object.spec.initContainers[] || `[]`, @) + message: "Контейнер '{{ element.name }}' использует runAsUser: 0 (root). Установите runAsUser >= 1000." deny: conditions: any: diff --git a/02-validation/02-security/require-seccomp-profile.yaml b/02-validation/02-security/require-seccomp-profile.yaml index d1b1483..a087ab5 100644 --- a/02-validation/02-security/require-seccomp-profile.yaml +++ b/02-validation/02-security/require-seccomp-profile.yaml @@ -30,11 +30,15 @@ spec: Добавьте в spec.securityContext: seccompProfile: type: RuntimeDefault - pattern: - spec: + anyPattern: + - spec: securityContext: seccompProfile: - type: "RuntimeDefault | Localhost" + type: RuntimeDefault + - spec: + securityContext: + seccompProfile: + type: Localhost - name: disallow-unconfined-seccomp match: diff --git a/03-mutation/01-basics/normalize.yaml b/03-mutation/01-basics/normalize.yaml index bb2e9ce..a33391c 100644 --- a/03-mutation/01-basics/normalize.yaml +++ b/03-mutation/01-basics/normalize.yaml @@ -28,5 +28,4 @@ spec: spec: containers: - name: "{{ element.name }}" - image: >- - {{ replace_all('{{ element.image }}', ':latest', ':stable') }} \ No newline at end of file + image: "{{ replace_all(element.image, ':latest', ':stable') }}" \ No newline at end of file diff --git a/03-mutation/01-basics/pre-conditions.yaml b/03-mutation/01-basics/pre-conditions.yaml index 396e6ad..5d0e029 100644 --- a/03-mutation/01-basics/pre-conditions.yaml +++ b/03-mutation/01-basics/pre-conditions.yaml @@ -41,12 +41,12 @@ spec: - list: "request.object.spec.containers" preconditions: any: - - key: "{{ element.image }}" - operator: Contains - value: "openjdk-v1" - - key: "{{ element.image }}" - operator: Contains - value: "eclipse-v1" + - key: "{{ regex_match('openjdk-v1', element.image) }}" + operator: Equals + value: true + - key: "{{ regex_match('eclipse-v1', element.image) }}" + operator: Equals + value: true patchStrategicMerge: spec: containers: diff --git a/03-mutation/03-advanced/add-creator-audit-annotation.yaml b/03-mutation/03-advanced/add-creator-audit-annotation.yaml index 96061db..c218b9b 100644 --- a/03-mutation/03-advanced/add-creator-audit-annotation.yaml +++ b/03-mutation/03-advanced/add-creator-audit-annotation.yaml @@ -31,7 +31,7 @@ spec: audit.company.com/created-by: "{{ request.userInfo.username }}" audit.company.com/created-at: "{{ time_now_utc() }}" audit.company.com/user-groups: >- - {{ request.userInfo.groups | join(', ', @) }} + {{ join(', ', request.userInfo.groups) }} - name: set-environment-labels match: diff --git a/03-mutation/03-advanced/set-dynamic-resource-limits.yaml b/03-mutation/03-advanced/set-dynamic-resource-limits.yaml index b7ea406..56998bf 100644 --- a/03-mutation/03-advanced/set-dynamic-resource-limits.yaml +++ b/03-mutation/03-advanced/set-dynamic-resource-limits.yaml @@ -29,6 +29,9 @@ spec: configMap: name: kyverno-global-config namespace: kyverno + - name: serviceType + variable: + value: "{{ request.object.metadata.labels.\"service-type\" || 'default' }}" mutate: foreach: - list: "request.object.spec.containers" @@ -38,7 +41,5 @@ spec: - name: "{{ element.name }}" resources: limits: - +(memory): >- - {{ globalConfig.data.\"{{ request.object.metadata.labels.\"service-type\" || 'default' }}_memory\" || '256Mi' }} - +(cpu): >- - {{ globalConfig.data.\"{{ request.object.metadata.labels.\"service-type\" || 'default' }}_cpu\" || '250m' }} + +(memory): "{{ globalConfig.data.\"{{ serviceType }}_memory\" || '256Mi' }}" + +(cpu): "{{ globalConfig.data.\"{{ serviceType }}_cpu\" || '250m' }}" diff --git a/04-generation/01-configmaps-secrets/generate-namespace-config.yaml b/04-generation/01-configmaps-secrets/generate-namespace-config.yaml index d0d28cb..7f0f9e5 100644 --- a/04-generation/01-configmaps-secrets/generate-namespace-config.yaml +++ b/04-generation/01-configmaps-secrets/generate-namespace-config.yaml @@ -32,7 +32,6 @@ spec: namespace: "{{ request.object.metadata.name }}" synchronize: true data: - kind: ConfigMap metadata: labels: generated-by: kyverno diff --git a/04-generation/02-lifecycle/cleanup-debug-pods.yaml b/04-generation/02-lifecycle/cleanup-debug-pods.yaml index c4e36a9..e9d4e4e 100644 --- a/04-generation/02-lifecycle/cleanup-debug-pods.yaml +++ b/04-generation/02-lifecycle/cleanup-debug-pods.yaml @@ -24,6 +24,6 @@ spec: purpose: debug conditions: any: - - key: "{{ time_since('', request.object.metadata.creationTimestamp, '') }}" + - key: "{{ time_since('', target.metadata.creationTimestamp, '') }}" operator: GreaterThan - value: "4h" + value: "4h0s" diff --git a/05-variables/02-context/another-examples.yaml b/05-variables/02-context/another-examples.yaml index f2ee1db..6a4d0f8 100644 --- a/05-variables/02-context/another-examples.yaml +++ b/05-variables/02-context/another-examples.yaml @@ -1,57 +1,114 @@ -# Другие варианты проверок -# -# проверить, что PVC использует StorageClass из одобренного списка: -# -rules: -- name: check-storage-class - match: - resources: - kinds: - - PersistentVolumeClaim - context: - - name: storageClassInfo - apiCall: - urlPath: "/apis/storage.k8s.io/v1/storageclasses/{{ request.object.spec.storageClassName }}" - jmesPath: "metadata.labels.\"approved-for-production\"" - validate: - message: >- - StorageClass '{{ request.object.spec.storageClassName }}' не одобрена для production. - Используйте StorageClass с лейблом approved-for-production: "true" - deny: - conditions: - - key: "{{ storageClassInfo }}" - operator: NotEquals - value: "true" - -# проверить, что количество реплик не превышает кворум с учётом текущей нагрузки: -# -context: -- name: existingDeployments - apiCall: - urlPath: >- - /apis/apps/v1/namespaces/{{ request.object.metadata.namespace }}/deployments - jmesPath: "items[?metadata.name != '{{ request.object.metadata.name }}'] | length(@)" -validate: - message: >- - В namespace уже {{ existingDeployments }} деплойментов. - Максимум разрешено 20. - deny: - conditions: - - key: "{{ existingDeployments }}" - operator: GreaterThanOrEquals - value: "20" - -# принимать решения на основе состояния нод -# -context: -- name: nodesInfo - apiCall: - urlPath: "/api/v1/nodes" - jmesPath: "items[?metadata.labels.\"node-type\" == 'gpu'].metadata.name" -validate: - message: "GPU workloads требуют минимум 2 GPU-ноды в кластере" - deny: - conditions: - - key: "{{ length(nodesInfo) }}" - operator: LessThan - value: "2" \ No newline at end of file +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: check-storage-class-approval + annotations: + policies.kyverno.io/title: "Проверка одобрения StorageClass" + policies.kyverno.io/category: Governance + policies.kyverno.io/severity: high + policies.kyverno.io/subject: PersistentVolumeClaim + policies.kyverno.io/description: >- + Проверяет, что PVC использует StorageClass из одобренного списка. + StorageClass считается одобренной, если имеет лейбл approved-for-production: "true". +spec: + validationFailureAction: Enforce + background: false + rules: + - name: check-storage-class + match: + resources: + kinds: + - PersistentVolumeClaim + context: + - name: storageClassInfo + apiCall: + urlPath: "/apis/storage.k8s.io/v1/storageclasses/{{ request.object.spec.storageClassName }}" + jmesPath: "metadata.labels.\"approved-for-production\"" + validate: + message: >- + StorageClass '{{ request.object.spec.storageClassName }}' не одобрена для production. + Используйте StorageClass с лейблом approved-for-production: "true". + deny: + conditions: + any: + - key: "{{ storageClassInfo }}" + operator: NotEquals + value: "true" +--- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: limit-deployments-per-namespace + annotations: + policies.kyverno.io/title: "Лимит Deployment в namespace" + policies.kyverno.io/category: Governance + policies.kyverno.io/severity: medium + policies.kyverno.io/subject: Deployment + policies.kyverno.io/description: >- + Ограничивает количество Deployment в одном namespace до 20. + Используется apiCall для подсчёта существующих деплойментов. +spec: + validationFailureAction: Enforce + background: false + rules: + - name: check-deployment-count + match: + resources: + kinds: + - Deployment + context: + - name: existingDeployments + apiCall: + urlPath: >- + /apis/apps/v1/namespaces/{{ request.object.metadata.namespace }}/deployments + jmesPath: "items[?metadata.name != '{{ request.object.metadata.name }}'] | length(@)" + validate: + message: >- + В namespace '{{ request.object.metadata.namespace }}' уже {{ existingDeployments }} деплойментов. + Максимум разрешено 20. + deny: + conditions: + any: + - key: "{{ existingDeployments }}" + operator: GreaterThanOrEquals + value: "20" +--- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-gpu-nodes-for-gpu-workloads + annotations: + policies.kyverno.io/title: "GPU workloads требуют GPU-нод" + policies.kyverno.io/category: Resources + policies.kyverno.io/severity: high + policies.kyverno.io/subject: Pod + policies.kyverno.io/description: >- + Запрещает запускать GPU-workloads если в кластере меньше 2 GPU-нод. + GPU-workload определяется лейблом workload-type: gpu на поде. +spec: + validationFailureAction: Enforce + background: false + rules: + - name: check-gpu-nodes + match: + resources: + kinds: + - Pod + selector: + matchLabels: + workload-type: gpu + context: + - name: gpuNodes + apiCall: + urlPath: "/api/v1/nodes" + jmesPath: "items[?metadata.labels.\"node-type\" == 'gpu'].metadata.name" + validate: + message: >- + GPU workloads требуют минимум 2 GPU-ноды в кластере. + Текущее количество GPU-нод: {{ length(gpuNodes) }}. + deny: + conditions: + any: + - key: "{{ length(gpuNodes) }}" + operator: LessThan + value: "2" diff --git a/05-variables/02-context/validate-on-create-only.yaml b/05-variables/02-context/validate-on-create-only.yaml index 9536830..f7537db 100644 --- a/05-variables/02-context/validate-on-create-only.yaml +++ b/05-variables/02-context/validate-on-create-only.yaml @@ -1,33 +1,60 @@ -# Полезный паттерн — разные правила для CREATE и UPDATE +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: validate-deployment-operations + annotations: + policies.kyverno.io/title: "Разные проверки для CREATE и UPDATE" + policies.kyverno.io/category: Governance + policies.kyverno.io/severity: medium + policies.kyverno.io/subject: Deployment + policies.kyverno.io/description: >- + Демонстрирует паттерн: разные правила для CREATE и UPDATE операций. + При создании — обязательные лейблы app и team. + При обновлении — запрет смены образа на тег latest. +spec: + validationFailureAction: Enforce + background: false + rules: + - name: validate-on-create-only + match: + resources: + kinds: + - Deployment + preconditions: + any: + - key: "{{ request.operation }}" + operator: Equals + value: CREATE + validate: + message: >- + Новый Deployment '{{ request.object.metadata.name }}' должен иметь лейблы app и team. + pattern: + metadata: + labels: + app: "?*" + team: "?*" -rules: -- name: validate-on-create-only - match: - resources: - kinds: - - Deployment - preconditions: - any: - - key: "{{ request.operation }}" - operator: Equals - value: CREATE - validate: - # применяется только при создании - -- name: validate-image-on-update - match: - resources: - kinds: - - Deployment - preconditions: - all: - - key: "{{ request.operation }}" - operator: Equals - value: UPDATE - - key: >- - {{ request.object.spec.template.spec.containers[0].image }} - operator: NotEquals - value: >- - {{ request.oldObject.spec.template.spec.containers[0].image }} - validate: - # применяется только при UPDATE с изменением образа \ No newline at end of file + - name: validate-image-on-update + match: + resources: + kinds: + - Deployment + preconditions: + all: + - key: "{{ request.operation }}" + operator: Equals + value: UPDATE + - key: "{{ request.object.spec.template.spec.containers[0].image }}" + operator: NotEquals + value: "{{ request.oldObject.spec.template.spec.containers[0].image }}" + validate: + message: >- + Образ изменён с '{{ request.oldObject.spec.template.spec.containers[0].image }}' + на '{{ request.object.spec.template.spec.containers[0].image }}'. + Запрещено использовать тег latest при обновлении образа. + deny: + conditions: + any: + - key: "{{ request.object.spec.template.spec.containers[0].image }}" + operator: EndsWith + value: ":latest" diff --git a/07-advanced/02-external-data/validate-registry-from-cache.yaml b/07-advanced/02-external-data/validate-registry-from-cache.yaml index 058b10a..918d775 100644 --- a/07-advanced/02-external-data/validate-registry-from-cache.yaml +++ b/07-advanced/02-external-data/validate-registry-from-cache.yaml @@ -31,24 +31,22 @@ spec: configMap: name: external-data-cache namespace: kyverno + - name: allowedPattern + variable: + value: >- + {{ join('', ['^(', join('|', split(allowedRegistries.data.\"allowed-registries\", '\n')[?@ != '']), ')']) }} validate: - message: >- - Образ '{{ element.image }}' из недоверенного реестра. - Список разрешённых реестров (обновлён {{ allowedRegistries.data.\"last-updated\" }}): - {{ allowedRegistries.data.\"allowed-registries\" }} foreach: - list: >- request.object.spec.containers[] | merge(request.object.spec.initContainers[] || `[]`, @) + message: >- + Образ '{{ element.image }}' из недоверенного реестра. + Список разрешённых реестров (обновлён {{ allowedRegistries.data.\"last-updated\" }}): + {{ allowedRegistries.data.\"allowed-registries\" }} deny: conditions: all: - - key: "{{ element.image }}" - operator: NotStartsWith - value: "registry.company.com/" - - key: "{{ element.image }}" - operator: NotStartsWith - value: "gcr.io/company-project/" - - key: "{{ element.image }}" - operator: NotStartsWith - value: "public.ecr.aws/company/" + - key: "{{ regex_match(allowedPattern, element.image) }}" + operator: Equals + value: false