Стадия жизненного цикла модуля: General Availability

Как настроить альтернативные решения по управлению политиками безопасности?

Для корректной работы DKP необходимы расширенные привилегии на запуск и работу полезной нагрузки системных компонентов. Если вместо модуля admission-policy-engine используется альтернативное решение по управлению политиками безопасности (например, Kyverno), необходима настройка исключений для следующих неймспейсов:

  • kube-system;
  • все неймспейсы с префиксом d8-* (например, d8-system).

Как настроить селекторы политик?

В OperationPolicy и SecurityPolicy поле spec.match определяет, на какие именно объекты (поды) в кластере будет распространяться политика. Оно обязательно должно присутствовать в конфигурации. Фильтрация выполняется путём комбинирования двух основных критериев: селектора подов (labelSelector) и селектора неймспейсов (namespaceSelector).

Если указаны оба селектора, политика применяется только к тем подам, которые одновременно:

  • удовлетворяют условиям выбора подов;
  • находятся в неймспейсах, прошедших фильтрацию.

Если какой-либо из селекторов не указан, соответствующее проверка не производится (будут использоваться все поды или неймспейсы).

spec.match.labelSelector – выбор подов

С помощью labelSelector задаются критерии отбора подов по их лейблам. Поддерживаются два взаимоисключающих способа:

  • matchLabels – простая проверка на точное совпадение лейблов (ключ‑значение). Под должен иметь все указанные лейблы.
  • matchExpressions – гибкие выражения с операторами. Каждое выражение задаётся объектом с полями:
    • key (строка, обязательно) – имя лейбла.
    • operator (строка, обязательно) – одно из значений: In, NotIn, Exists, DoesNotExist.
    • values (массив строк) – список значений для операторов In / NotIn; для Exists / DoesNotExist не указывается.

Все элементы списка matchExpressions объединяются логическим И – под должен удовлетворять каждому выражению.

Примеры:

spec:
  match:
    labelSelector:
      matchLabels:
        app: nginx
        role: frontend
spec:
  match:
    labelSelector:
      matchExpressions:
        - key: tier
          operator: In
          values:
            - production
            - staging
        - key: monitoring
          operator: Exists

spec.match.namespaceSelector – выбор неймспейсов

Позволяет ограничить неймспейсы, в которых действует политика. Можно использовать комбинацию трёх фильтров:

  • matchNames – явный список разрешённых неймспейсов. Если задан, политика действует только в перечисленных неймспейсах.
  • excludeNames – список исключаемых неймспейсов. Политика будет действовать во всех неймспейсах, кроме указанных.
  • labelSelector – селектор по лейблам самого объекта Namespace. Синтаксис полностью аналогичен селектору подов (matchLabels / matchExpressions). Фильтруются именно метаданные неймспейса.

Все заданные условия внутри namespaceSelector также объединяются логическим И. Рекомендуется не смешивать matchNames и excludeNames без явной необходимости – если указаны оба, результирующий набор вычисляется как (matchNames ∩ все) \ excludeNames (сначала берутся разрешённые, потом из них вычитаются исключённые).

Примеры:

spec:
  match:
    namespaceSelector:
      matchNames:
        - production
        - staging
spec:
  match:
    namespaceSelector:
      excludeNames:
        - kube-system
        - gatekeeper-system
spec:
  match:
    namespaceSelector:
      labelSelector:
        matchLabels:
          team: backend
          environment: production
spec:
  match:
    namespaceSelector:
      labelSelector:
        matchExpressions:
          - key: compliance
            operator: In
            values:
              - pci
              - sox

Совместное использование селекторов

Наиболее типичный сценарий – ограничение и по подам, и по неймспейсам одновременно:

spec:
  match:
    labelSelector:
      matchLabels:
        app: payments
        version: v2
    namespaceSelector:
      labelSelector:
        matchLabels:
          env: prod
      excludeNames:
        - legacy-prod

Эта политика сработает для подов с лейблами app=payments и version=v2, которые находятся в неймспейсах, имеющих лейбл env=prod, за исключением неймспейса legacy-prod.

spec:
  match:
    labelSelector:
      matchExpressions:
        - key: security
          operator: NotIn
          values:
            - low
    namespaceSelector:
      matchNames:
        - frontend
        - backend

Здесь политика охватывает поды, у которых значение лейбла security не равно low, в неймспейсах frontend и backend.

Как расширить политики Pod Security Standards?

Pod Security Standards реагируют на label security.deckhouse.io/pod-policy: restricted или security.deckhouse.io/pod-policy: baseline.

Чтобы расширить политику Pod Security Standards, добавив к существующим проверкам политики свои собственные, необходимо:

  • создать шаблон проверки (ConstraintTemplate);
  • привязать его к политике restricted или baseline.

Пример шаблона для проверки адреса репозитория образа контейнера:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package d8.pod_security_standards.extended

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          satisfied := [good | repo = input.parameters.repos[_] ; good = startswith(container.image, repo)]
          not any(satisfied)
          msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          satisfied := [good | repo = input.parameters.repos[_] ; good = startswith(container.image, repo)]
          not any(satisfied)
          msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
        }

Пример привязки проверки к политике restricted:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: prod-repo
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaceSelector:
      matchLabels:
        security.deckhouse.io/pod-policy: restricted
  parameters:
    repos:
      - "mycompany.registry.com"

Пример демонстрирует настройку проверки адреса репозитория в поле image у всех подов, создающихся в неймспейсах, имеющих label security.deckhouse.io/pod-policy: restricted. Если адрес в поле image создаваемого пода начинается не с mycompany.registry.com, под создан не будет.

Подробнее о шаблонах и языке политик можно узнать в документации Gatekeeper.

Больше примеров описания проверок для расширения политики можно найти в библиотеке Gatekeeper.

Как включить одну или несколько политик Pod Security Standards, не отключая весь набор?

Чтобы применить только нужные политики безопасности, не отключая весь предустановленный набор:

  1. Добавьте в нужный неймспейс лейбл: security.deckhouse.io/pod-policy: privileged, чтобы отключить встроенный набор политик.
  2. Создайте ресурс SecurityPolicy, соответствующий уровню baseline или restricted. В секции policies укажите только необходимые вам настройки.
  3. Добавьте в неймспейс дополнительный лейбл, который будет соответствовать селектору namespaceSelector в SecurityPolicy. В примерах ниже это security-policy.deckhouse.io/baseline-enabled: "true" либо security-policy.deckhouse.io/restricted-enabled: "true"

SecurityPolicy, соответствующая baseline:

apiVersion: deckhouse.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: baseline
spec:
  enforcementAction: Deny
  policies:
    allowHostIPC: false
    allowHostNetwork: false
    allowHostPID: false
    allowPrivilegeEscalation: true
    allowPrivileged: false
    allowedAppArmor:
      - runtime/default
      - localhost/*
    allowedCapabilities:
      - AUDIT_WRITE
      - CHOWN
      - DAC_OVERRIDE
      - FOWNER
      - FSETID
      - KILL
      - MKNOD
      - NET_BIND_SERVICE
      - SETFCAP
      - SETGID
      - SETPCAP
      - SETUID
      - SYS_CHROOT
    allowedHostPaths: []
    allowedHostPorts:
      - max: 0
        min: 0
    allowedProcMount: Default
    allowedUnsafeSysctls:
      - kernel.shm_rmid_forced
      - net.ipv4.ip_local_port_range
      - net.ipv4.ip_unprivileged_port_start
      - net.ipv4.tcp_syncookies
      - net.ipv4.ping_group_range
      - net.ipv4.ip_local_reserved_ports
      - net.ipv4.tcp_keepalive_time
      - net.ipv4.tcp_fin_timeout
      - net.ipv4.tcp_keepalive_intvl
      - net.ipv4.tcp_keepalive_probes
    seLinux:
      - type: ""
      - type: container_t
      - type: container_init_t
      - type: container_kvm_t
      - type: container_engine_t
    seccompProfiles:
      allowedProfiles:
        - RuntimeDefault
        - Localhost
        - undefined
        - ''
      allowedLocalhostFiles:
        - '*'
  match:
    namespaceSelector:
      labelSelector:
        matchLabels:
          security-policy.deckhouse.io/baseline-enabled: "true"

SecurityPolicy, соответствующая restricted:

apiVersion: deckhouse.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: restricted
spec:
  enforcementAction: Deny
  policies:
    allowHostIPC: false
    allowHostNetwork: false
    allowHostPID: false
    allowPrivilegeEscalation: false
    allowPrivileged: false
    allowedAppArmor:
      - runtime/default
      - localhost/*
    allowedCapabilities:
      - NET_BIND_SERVICE
    allowedHostPaths: []
    allowedHostPorts:
      - max: 0
        min: 0
    allowedProcMount: Default
    allowedUnsafeSysctls:
      - kernel.shm_rmid_forced
      - net.ipv4.ip_local_port_range
      - net.ipv4.ip_unprivileged_port_start
      - net.ipv4.tcp_syncookies
      - net.ipv4.ping_group_range
      - net.ipv4.ip_local_reserved_ports
      - net.ipv4.tcp_keepalive_time
      - net.ipv4.tcp_fin_timeout
      - net.ipv4.tcp_keepalive_intvl
      - net.ipv4.tcp_keepalive_probes
    allowedVolumes:
      - configMap
      - csi
      - downwardAPI
      - emptyDir
      - ephemeral
      - persistentVolumeClaim
      - projected
      - secret
    requiredDropCapabilities:
      - ALL
    runAsUser:
      rule: MustRunAsNonRoot
    seLinux:
      - type: ""
      - type: container_t
      - type: container_init_t
      - type: container_kvm_t
      - type: container_engine_t
    seccompProfiles:
      allowedProfiles:
        - RuntimeDefault
        - Localhost
      allowedLocalhostFiles:
        - '*'
  match:
    namespaceSelector:
      labelSelector:
        matchLabels:
          security-policy.deckhouse.io/restricted-enabled: "true"

Что, если несколько политик (операционных или безопасности) применяются на один объект?

В этом случае необходимо, чтобы конфигурация объекта соответствовала всем политикам, которые на него распространяются.

Например, рассмотрим две следующие политики безопасности:

apiVersion: deckhouse.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: foo
spec:
  enforcementAction: Deny
  match:
    namespaceSelector:
      labelSelector:
        matchLabels:
          name: test
  policies:
    readOnlyRootFilesystem: true
    requiredDropCapabilities:
    - MKNOD
---
apiVersion: deckhouse.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: bar
spec:
  enforcementAction: Deny
  match:
    namespaceSelector:
      labelSelector:
        matchLabels:
          name: test
  policies:
    requiredDropCapabilities:
    - NET_BIND_SERVICE

Тогда для выполнения требований приведенных политик безопасности в спецификации контейнера нужно указать:

    securityContext:
      capabilities:
        drop:
          - MKNOD
          - NET_BIND_SERVICE
      readOnlyRootFilesystem: true

Проверка подписи образов

Доступно в следующих редакциях DKP: SE+, EE, CSE Lite, CSE Pro.

Поддерживается Cosign не выше v2. Версии v3 и выше не поддерживаются.

В модуле реализована функция проверки подписи образов контейнеров, подписанных с помощью инструмента Cosign. Подробнее о подписании и проверке образов контейнеров можно узнать в документации DKP.

Как запретить удаление узла без лейбла

Операции DELETE обрабатываются Gatekeeper по умолчанию.

Можно создать собственную политику Gatekeeper, запрещающую удаление узла без специального лейбла. Пример ниже использует oldObject для проверки лейблов удаляемого узла:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: d8customnodedeleteguard
spec:
  crd:
    spec:
      names:
        kind: D8CustomNodeDeleteGuard
      validation:
        openAPIV3Schema:
          type: object
          properties:
            requiredLabelKey:
              type: string
            requiredLabelValue:
              type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package d8.custom

        is_delete { input.review.operation == "DELETE" }
        is_node { input.review.kind.kind == "Node" }

        has_required_label {
          key := input.parameters.requiredLabelKey
          val := input.parameters.requiredLabelValue
          obj := input.review.oldObject
          obj.metadata.labels[key] == val
        }

        violation[{"msg": msg}] {
          is_delete
          is_node
          not has_required_label
          msg := sprintf("Удаление Node запрещено. Добавьте лейбл %q=%q.", [input.parameters.requiredLabelKey, input.parameters.requiredLabelValue])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: D8CustomNodeDeleteGuard
metadata:
  name: require-node-delete-label
spec:
  enforcementAction: warn
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Node"]
  parameters:
    requiredLabelKey: "admission.deckhouse.io/allow-delete"
    requiredLabelValue: "true"

Как запретить операции kubectl exec и kubectl attach в определённые поды?

Вебхук модуля admission-policy-engine направляет запросы CONNECT для pods/exec и pods/attach через Gatekeeper. Это позволяет создавать пользовательские политики для разрешения или запрета операций kubectl exec и kubectl attach.

Встроенная политика для подов с heritage: deckhouse

Для защиты системных компонентов под управлением Deckhouse в модуле admission-policy-engine предусмотрена встроенная политика D8DenyExecHeritage, которая запрещает выполнение операций kubectl exec и kubectl attach во все поды с лейблом heritage: deckhouse.

Политика не распространяется на следующих пользователей, которым разрешены операции kubectl exec и kubectl attach в поды с лейблом heritage: deckhouse:

  • system:sudouser;
  • сервисные аккаунты из неймспейсов d8-* (system:serviceaccount:d8-*);
  • сервисные аккаунты из неймспейсов kube-* (system:serviceaccount:kube-*).

Пример пользовательской политики

Вы можете создать собственную политику Gatekeeper для запрета операций kubectl exec и kubectl attach в определённых неймспейсах. В примере ниже используются input.review.operation и input.review.resource.resource для проверки операций CONNECT:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: d8customdenyexec
spec:
  crd:
    spec:
      names:
        kind: D8CustomDenyExec
      validation:
        openAPIV3Schema:
          type: object
          properties:
            forbiddenNamespaces:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package d8.custom

        is_connect {
          input.review.operation == "CONNECT"
        }

        # requestSubResource предпочтительнее, но на всякий случай падаем в subResource
        subresource_is(sub) {
          sr := object.get(input.review, "requestSubResource", input.review.subResource)
          sr == sub
        }

        is_exec_or_attach {
          input.review.resource.resource == "pods"
          subresource_is("exec")
        }

        is_exec_or_attach {
          input.review.resource.resource == "pods"
          subresource_is("attach")
        }

        is_forbidden_namespace {
          ns := input.review.namespace
          ns == input.parameters.forbiddenNamespaces[_]
        }

        violation[{"msg": msg}] {
          is_connect
          is_exec_or_attach
          is_forbidden_namespace
          msg := sprintf("Exec/attach запрещён в неймспейсе %q", [input.review.namespace])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: D8CustomDenyExec
metadata:
  name: deny-exec-in-namespaces
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ["*"]
        kinds: ["*"]
    scope: Namespaced
  parameters:
    forbiddenNamespaces:
      - production
      - staging

Ключевые данные и проверки, доступные при валидации операций CONNECT:

  • Используйте input.review.operation == "CONNECT" для проверки операций CONNECT.
  • Информация о пользователе доступна в input.review.userInfo.username и input.review.userInfo.groups.
  • Неймспейс доступен в input.review.namespace.