Compare commits

..

4 Commits

Author SHA1 Message Date
vasyansk f980b651d9 fix foreach 2026-05-19 17:16:39 +07:00
vasyansk e995770695 add test pols 2026-05-19 16:37:43 +07:00
vasyansk ffa61ab646 fix pols for 1.18 2026-05-14 18:55:39 +07:00
vasyansk 5578400e7f init 2026-04-22 21:25:23 +07:00
27 changed files with 400 additions and 176 deletions
+22
View File
@@ -0,0 +1,22 @@
{
"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\")",
"Bash(~/.local/bin/kyverno apply *)",
"Bash(rtk ls *)",
"Bash(/bin/ls *)",
"Bash(export PATH=\"$HOME/.local/bin:$PATH\")",
"Bash(kyverno apply *)",
"Read(//private/tmp/**)",
"Bash(mkdir -p polsdir1 polsdir2)",
"Bash(cp t1.yaml polsdir1/)",
"Bash(cp t2.yaml polsdir2/)"
]
}
}
@@ -27,16 +27,8 @@ spec:
- kyverno - kyverno
validate: validate:
message: >- message: >-
Контейнер '{{ element.name }}' в поде '{{ request.object.metadata.name }}' Все контейнеры в поде '{{ request.object.metadata.name }}'
(namespace: {{ request.object.metadata.namespace }}) не имеет resource limits. обязаны иметь resources.limits.memory и resources.limits.cpu.
Добавьте в манифест:
resources:
limits:
memory: "256Mi"
cpu: "500m"
Документация: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
foreach: foreach:
- list: >- - list: >-
request.object.spec.containers[] | request.object.spec.containers[] |
@@ -26,19 +26,19 @@ spec:
- kube-system - kube-system
validate: validate:
message: >- message: >-
Образ '{{ element.image }}' использует тег :latest или не имеет тега. Один из образов в поде '{{ request.object.metadata.name }}' использует
Используйте конкретный тег (например, nginx:1.25.3) или digest тег :latest или не имеет тега. Используйте конкретный тег или digest.
(nginx@sha256:abc123...) для воспроизводимых деплойментов.
foreach: foreach:
- list: >- - list: >-
request.object.spec.containers[] | request.object.spec.containers[] |
merge(request.object.spec.initContainers[] || `[]`, @) merge(request.object.spec.initContainers[] || `[]`, @) |
merge(request.object.spec.ephemeralContainers[] || `[]`, @)
deny: deny:
conditions: conditions:
any: any:
- key: "{{ element.image }}" - key: "{{ regex_match(':latest', element.image) }}"
operator: Contains operator: Equals
value: ":latest" value: true
- key: "{{ element.image }}" - key: "{{ regex_match('^(([^/]+/)*[^/:]+)$', element.image) }}"
operator: NotContains operator: Equals
value: ":" value: true
@@ -28,24 +28,21 @@ spec:
- kyverno - kyverno
validate: validate:
message: >- message: >-
Образ '{{ element.image }}' из недоверенного реестра. Один из образов в поде '{{ request.object.metadata.name }}' из недоверенного реестра.
Разрешены только: Разрешены: registry.company.com/, gcr.io/company-project/.
- registry.company.com/
- gcr.io/company-project/
Загрузите образ в внутренний реестр и обновите манифест.
foreach: foreach:
- list: >- - list: >-
request.object.spec.containers[] | request.object.spec.containers[] |
merge(request.object.spec.initContainers[] || `[]`, @) merge(request.object.spec.initContainers[] || `[]`, @) |
merge(request.object.spec.ephemeralContainers[] || `[]`, @)
deny: deny:
conditions: conditions:
all: all:
# Образ НЕ из первого доверенного реестра # regex_match: false = образ НЕ начинается с доверенного реестра
- key: "{{ element.image }}" - key: "{{ regex_match('^registry\\.company\\.com/', element.image) }}"
operator: NotStartsWith operator: Equals
value: "registry.company.com/" value: false
# И НЕ из второго доверенного реестра - key: "{{ regex_match('^gcr\\.io/company-project/', element.image) }}"
- key: "{{ element.image }}" operator: Equals
operator: NotStartsWith value: false
value: "gcr.io/company-project/"
# Добавьте дополнительные условия по аналогии # Добавьте дополнительные условия по аналогии
@@ -27,14 +27,13 @@ spec:
- kube-system - kube-system
validate: validate:
message: >- message: >-
Контейнер '{{ element.name }}' добавляет запрещённые capabilities: Один из контейнеров в поде '{{ request.object.metadata.name }}' добавляет
{{ element.securityContext.capabilities.add }}. запрещённые capabilities. Разрешена только NET_BIND_SERVICE.
Разрешена только NET_BIND_SERVICE.
Пересмотрите необходимость этих привилегий.
foreach: foreach:
- list: >- - list: >-
request.object.spec.containers[] | request.object.spec.containers[] |
merge(request.object.spec.initContainers[] || `[]`, @) merge(request.object.spec.initContainers[] || `[]`, @) |
merge(request.object.spec.ephemeralContainers[] || `[]`, @)
deny: deny:
conditions: conditions:
any: any:
@@ -52,6 +52,6 @@ spec:
deny: deny:
conditions: conditions:
any: any:
- key: "{{ request.object.spec.volumes[].hostPath | length(@) }}" - key: "{{ request.object.spec.volumes[?hostPath] | length(@) }}"
operator: GreaterThan operator: GreaterThan
value: "0" value: "0"
@@ -27,9 +27,8 @@ spec:
- kube-system - kube-system
validate: validate:
message: >- message: >-
Контейнер '{{ element.name }}' имеет securityContext.privileged: true. Один из контейнеров в поде '{{ request.object.metadata.name }}'
Привилегированные контейнеры запрещены — они получают полный доступ к хосту. имеет securityContext.privileged: true. Запрещено.
Удалите поле securityContext.privileged или установите значение false.
foreach: foreach:
- list: >- - list: >-
request.object.spec.containers[] | request.object.spec.containers[] |
@@ -26,13 +26,13 @@ spec:
- kube-system - kube-system
validate: validate:
message: >- message: >-
Контейнер '{{ element.name }}' не сбрасывает все capabilities. Каждый контейнер в поде '{{ request.object.metadata.name }}' должен
Добавьте в securityContext: сбрасывать все capabilities: securityContext.capabilities.drop: [ALL].
capabilities:
drop:
- ALL
foreach: foreach:
- list: "request.object.spec.containers" - list: >-
request.object.spec.containers[] |
merge(request.object.spec.initContainers[] || `[]`, @) |
merge(request.object.spec.ephemeralContainers[] || `[]`, @)
deny: deny:
conditions: conditions:
all: all:
@@ -46,10 +46,12 @@ spec:
- kube-system - kube-system
validate: validate:
message: >- message: >-
Контейнер '{{ element.name }}' использует runAsUser: 0 (root). Один из контейнеров в поде '{{ request.object.metadata.name }}'
Установите runAsUser >= 1000. использует runAsUser: 0 (root). Установите runAsUser >= 1000.
foreach: foreach:
- list: "request.object.spec.containers" - list: >-
request.object.spec.containers[] |
merge(request.object.spec.initContainers[] || `[]`, @)
deny: deny:
conditions: conditions:
any: any:
@@ -30,11 +30,15 @@ spec:
Добавьте в spec.securityContext: Добавьте в spec.securityContext:
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
pattern: anyPattern:
spec: - spec:
securityContext: securityContext:
seccompProfile: seccompProfile:
type: "RuntimeDefault | Localhost" type: RuntimeDefault
- spec:
securityContext:
seccompProfile:
type: Localhost
- name: disallow-unconfined-seccomp - name: disallow-unconfined-seccomp
match: match:
@@ -5,6 +5,7 @@ metadata:
namespace: kyverno namespace: kyverno
labels: labels:
prometheus: kube-prometheus prometheus: kube-prometheus
release: monitoring-monitoring
role: alert-rules role: alert-rules
spec: spec:
groups: groups:
@@ -5,10 +5,11 @@ metadata:
namespace: kyverno namespace: kyverno
labels: labels:
prometheus: kube-prometheus prometheus: kube-prometheus
release: monitoring-monitoring
spec: spec:
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: kyverno app.kubernetes.io/instance: kyverno
app.kubernetes.io/component: admission-controller app.kubernetes.io/component: admission-controller
endpoints: endpoints:
- port: metrics-port - port: metrics-port
+1 -2
View File
@@ -28,5 +28,4 @@ spec:
spec: spec:
containers: containers:
- name: "{{ element.name }}" - name: "{{ element.name }}"
image: >- image: "{{ replace_all(element.image, ':latest', ':stable') }}"
{{ replace_all('{{ element.image }}', ':latest', ':stable') }}
+6 -6
View File
@@ -41,12 +41,12 @@ spec:
- list: "request.object.spec.containers" - list: "request.object.spec.containers"
preconditions: preconditions:
any: any:
- key: "{{ element.image }}" - key: "{{ regex_match('openjdk-v1', element.image) }}"
operator: Contains operator: Equals
value: "openjdk-v1" value: true
- key: "{{ element.image }}" - key: "{{ regex_match('eclipse-v1', element.image) }}"
operator: Contains operator: Equals
value: "eclipse-v1" value: true
patchStrategicMerge: patchStrategicMerge:
spec: spec:
containers: containers:
@@ -31,7 +31,7 @@ spec:
audit.company.com/created-by: "{{ request.userInfo.username }}" audit.company.com/created-by: "{{ request.userInfo.username }}"
audit.company.com/created-at: "{{ time_now_utc() }}" audit.company.com/created-at: "{{ time_now_utc() }}"
audit.company.com/user-groups: >- audit.company.com/user-groups: >-
{{ request.userInfo.groups | join(', ', @) }} {{ join(', ', request.userInfo.groups) }}
- name: set-environment-labels - name: set-environment-labels
match: match:
@@ -29,6 +29,9 @@ spec:
configMap: configMap:
name: kyverno-global-config name: kyverno-global-config
namespace: kyverno namespace: kyverno
- name: serviceType
variable:
value: "{{ request.object.metadata.labels.\"service-type\" || 'default' }}"
mutate: mutate:
foreach: foreach:
- list: "request.object.spec.containers" - list: "request.object.spec.containers"
@@ -38,7 +41,5 @@ spec:
- name: "{{ element.name }}" - name: "{{ element.name }}"
resources: resources:
limits: limits:
+(memory): >- +(memory): "{{ globalConfig.data.\"{{ serviceType }}_memory\" || '256Mi' }}"
{{ globalConfig.data.\"{{ request.object.metadata.labels.\"service-type\" || 'default' }}_memory\" || '256Mi' }} +(cpu): "{{ globalConfig.data.\"{{ serviceType }}_cpu\" || '250m' }}"
+(cpu): >-
{{ globalConfig.data.\"{{ request.object.metadata.labels.\"service-type\" || 'default' }}_cpu\" || '250m' }}
@@ -32,7 +32,6 @@ spec:
namespace: "{{ request.object.metadata.name }}" namespace: "{{ request.object.metadata.name }}"
synchronize: true synchronize: true
data: data:
kind: ConfigMap
metadata: metadata:
labels: labels:
generated-by: kyverno generated-by: kyverno
@@ -24,6 +24,6 @@ spec:
purpose: debug purpose: debug
conditions: conditions:
any: any:
- key: "{{ time_since('', request.object.metadata.creationTimestamp, '') }}" - key: "{{ time_since('', target.metadata.creationTimestamp, '') }}"
operator: GreaterThan operator: GreaterThan
value: "4h" value: "4h0s"
+79 -22
View File
@@ -1,9 +1,20 @@
# Другие варианты проверок apiVersion: kyverno.io/v1
# kind: ClusterPolicy
# проверить, что PVC использует StorageClass из одобренного списка: metadata:
# name: check-storage-class-approval
rules: annotations:
- name: check-storage-class 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: match:
resources: resources:
kinds: kinds:
@@ -16,42 +27,88 @@ rules:
validate: validate:
message: >- message: >-
StorageClass '{{ request.object.spec.storageClassName }}' не одобрена для production. StorageClass '{{ request.object.spec.storageClassName }}' не одобрена для production.
Используйте StorageClass с лейблом approved-for-production: "true" Используйте StorageClass с лейблом approved-for-production: "true".
deny: deny:
conditions: conditions:
any:
- key: "{{ storageClassInfo }}" - key: "{{ storageClassInfo }}"
operator: NotEquals operator: NotEquals
value: "true" value: "true"
---
# проверить, что количество реплик не превышает кворум с учётом текущей нагрузки: apiVersion: kyverno.io/v1
# kind: ClusterPolicy
context: metadata:
- name: existingDeployments 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: apiCall:
urlPath: >- urlPath: >-
/apis/apps/v1/namespaces/{{ request.object.metadata.namespace }}/deployments /apis/apps/v1/namespaces/{{ request.object.metadata.namespace }}/deployments
jmesPath: "items[?metadata.name != '{{ request.object.metadata.name }}'] | length(@)" jmesPath: "items[?metadata.name != '{{ request.object.metadata.name }}'] | length(@)"
validate: validate:
message: >- message: >-
В namespace уже {{ existingDeployments }} деплойментов. В namespace '{{ request.object.metadata.namespace }}' уже {{ existingDeployments }} деплойментов.
Максимум разрешено 20. Максимум разрешено 20.
deny: deny:
conditions: conditions:
any:
- key: "{{ existingDeployments }}" - key: "{{ existingDeployments }}"
operator: GreaterThanOrEquals operator: GreaterThanOrEquals
value: "20" value: "20"
---
# принимать решения на основе состояния нод apiVersion: kyverno.io/v1
# kind: ClusterPolicy
context: metadata:
- name: nodesInfo 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: apiCall:
urlPath: "/api/v1/nodes" urlPath: "/api/v1/nodes"
jmesPath: "items[?metadata.labels.\"node-type\" == 'gpu'].metadata.name" jmesPath: "items[?metadata.labels.\"node-type\" == 'gpu'].metadata.name"
validate: validate:
message: "GPU workloads требуют минимум 2 GPU-ноды в кластере" message: >-
GPU workloads требуют минимум 2 GPU-ноды в кластере.
Текущее количество GPU-нод: {{ length(gpuNodes) }}.
deny: deny:
conditions: conditions:
- key: "{{ length(nodesInfo) }}" any:
- key: "{{ length(gpuNodes) }}"
operator: LessThan operator: LessThan
value: "2" value: "2"
@@ -1,7 +1,21 @@
# Полезный паттерн — разные правила для CREATE и UPDATE apiVersion: kyverno.io/v1
kind: ClusterPolicy
rules: metadata:
- name: validate-on-create-only 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: match:
resources: resources:
kinds: kinds:
@@ -12,9 +26,15 @@ rules:
operator: Equals operator: Equals
value: CREATE value: CREATE
validate: validate:
# применяется только при создании message: >-
Новый Deployment '{{ request.object.metadata.name }}' должен иметь лейблы app и team.
pattern:
metadata:
labels:
app: "?*"
team: "?*"
- name: validate-image-on-update - name: validate-image-on-update
match: match:
resources: resources:
kinds: kinds:
@@ -24,10 +44,17 @@ rules:
- key: "{{ request.operation }}" - key: "{{ request.operation }}"
operator: Equals operator: Equals
value: UPDATE value: UPDATE
- key: >- - key: "{{ request.object.spec.template.spec.containers[0].image }}"
{{ request.object.spec.template.spec.containers[0].image }}
operator: NotEquals operator: NotEquals
value: >- value: "{{ request.oldObject.spec.template.spec.containers[0].image }}"
{{ request.oldObject.spec.template.spec.containers[0].image }}
validate: validate:
# применяется только при UPDATE с изменением образа 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"
+2 -2
View File
@@ -5,8 +5,8 @@
| Файл | Описание | | Файл | Описание |
|------|----------| |------|----------|
| `grafana-dashboard.json` | Готовый Grafana dashboard | | `grafana-dashboard.json` | Готовый Grafana dashboard |
| `../03-reporting/prometheus-alert-rules.yaml` | Alerting правила | | `../02-validation/03-reporting/prometheus-alert-rules.yaml` | Alerting правила |
| `../03-reporting/service-monitor.yaml` | ServiceMonitor | | `../02-validation/03-reporting/service-monitor.yaml` | ServiceMonitor |
## Настройка уровня логирования ## Настройка уровня логирования
@@ -0,0 +1,30 @@
# chart: kube-prometheus-stack
# version: 83.7.0
alertmanager:
enabled: false
prometheus:
ingress:
enabled: false
prometheusSpec:
replicas: 1
retention: 4h
retentionSize: "4GB"
storageSpec:
volumeClaimTemplate:
spec:
resources:
requests:
storage: 5Gi
grafana:
enabled: true
kubeControllerManager:
enabled: false
kubeEtcd:
enabled: false
kubeScheduler:
enabled: false
kubeProxy:
enabled: false
kubeApiServer:
enabled: false
+2 -2
View File
@@ -26,8 +26,8 @@ kubectl get clusterpolicy my-policy -o yaml | grep -A 10 "status:"
```bash ```bash
# Kyverno CLI — самый быстрый способ проверить # Kyverno CLI — самый быстрый способ проверить
kyverno apply my-policy.yaml \ kyverno apply test-pols/ \
--resource my-resource.yaml \ --resource test-deployment.yaml \
--detailed-results --detailed-results
# Вывод: # Вывод:
@@ -0,0 +1,42 @@
# Тестовые поды для демонстрации kyverno apply --detailed-results.
# Под good-pod — проходит обе политики.
# Под bad-pod — нарушает обе политики (нет limits, нет label 'owner').
---
apiVersion: v1
kind: Pod
metadata:
name: good-pod
namespace: default
labels:
app: demo
owner: team-platform
spec:
containers:
- name: app
image: nginx:1.25.3
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
---
apiVersion: v1
kind: Pod
metadata:
name: bad-pod
namespace: default
labels:
app: demo
spec:
containers:
- name: app
image: nginx:1.25.3
- name: sidecar
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: 10m
memory: 16Mi
@@ -0,0 +1,26 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels-demo
annotations:
policies.kyverno.io/title: "DEMO: Требовать обязательные labels"
policies.kyverno.io/description: >-
Демонстрационная политика для урока 6.2.
Проверяет наличие labels 'app' и 'owner' у Pod.
spec:
validationFailureAction: Audit
background: true
rules:
- name: check-required-labels
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Pod must have labels 'app' and 'owner'."
pattern:
metadata:
labels:
app: "?*"
owner: "?*"
@@ -0,0 +1,28 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits-demo
annotations:
policies.kyverno.io/title: "DEMO: Требовать resources.limits"
policies.kyverno.io/description: >-
Демонстрационная политика для урока 6.2.
Проверяет, что у всех контейнеров заданы CPU и memory limits.
spec:
validationFailureAction: Audit
background: true
rules:
- name: check-container-limits
match:
any:
- resources:
kinds:
- Pod
validate:
message: "All containers must have resources.limits.cpu and resources.limits.memory."
foreach:
- list: "request.object.spec.containers"
pattern:
resources:
limits:
cpu: "?*"
memory: "?*"
@@ -31,10 +31,14 @@ spec:
configMap: configMap:
name: external-data-cache name: external-data-cache
namespace: kyverno namespace: kyverno
- name: allowedPattern
variable:
value: >-
{{ join('', ['^(', join('|', split(allowedRegistries.data.\"allowed-registries\", '\n')[?@ != '']), ')']) }}
validate: validate:
message: >- message: >-
Образ '{{ element.image }}' из недоверенного реестра. Образ в поде '{{ request.object.metadata.name }}' из недоверенного реестра.
Список разрешённых реестров (обновлён {{ allowedRegistries.data.\"last-updated\" }}): Список разрешённых (обновлён {{ allowedRegistries.data.\"last-updated\" }}):
{{ allowedRegistries.data.\"allowed-registries\" }} {{ allowedRegistries.data.\"allowed-registries\" }}
foreach: foreach:
- list: >- - list: >-
@@ -43,12 +47,6 @@ spec:
deny: deny:
conditions: conditions:
all: all:
- key: "{{ element.image }}" - key: "{{ regex_match(allowedPattern, element.image) }}"
operator: NotStartsWith operator: Equals
value: "registry.company.com/" value: false
- key: "{{ element.image }}"
operator: NotStartsWith
value: "gcr.io/company-project/"
- key: "{{ element.image }}"
operator: NotStartsWith
value: "public.ecr.aws/company/"