Идея подписи и проверки образов Docker крутилась у меня давно. Часто я не понимал на сколько это снижает риски и повышает реальную безопасность приложений. Но для себя таки понял одно - защищает она от одного но важного вектора - атака на ваш реестр образов (registry) и внедрение вредоносного кода в образы. От всего остального нужно защитится другими средствами. Если у вас все запускается по чексуммам, можете не париться.... Но если ваши образы в приложениях k8s описаны в виде тэгов, а не чексумм, kubernetes запускает образ из источника вслепую. А еще если установлен imagePullPolicy: always, тогда в случае компрометации реестра ваш кластер запустит вредоносное ПО уже при следующем рестарте.
 

containers:
 - image: registry.local/my-awesome-app:alpine
imagePullPolicy: always

Неоднократно исследовались различные решения от Notary до Cosign. Так или иначе все эти продукты были сложны для использования, требовали отдельного хранилища для криптографической информации + на клиентах также нужно было где то хранить кучу закрытых ключей. Cosign более прост в использовании но для него также требовалось где-то хранить закрытые ключи, менять их и все прелести этого. Мне же хотелось чтобы подпись работала по принципу обычных x509 сертификатов но без валидации срока действия. Подписал образ доверенным УЦ (выдал серт) — все, образ валиден. Эту мысль я и стал развивать далее и все оказалось очевидно но, не без нюансов…

Идея заключалась в следующем: взять чексумму образа, выдать сертификат с commonName эквивалентным чексумме (или чтобы чексумма входила в его часть). НО, сертификат нужно было куда то положить. Если положить его в образ тогда, чексумма изменится и сертификат станет уже неактуален. Потом я понял что можно положить сертификат например в LABEL образа затем, при верификации убирать LABEL и отсчитывать обратную чексумму. Все это слишком сложно… Затем я увидел как cosign хранит подпись образа и решил сделать также (да, я это позаимствовал). Cosign создает дополнительный тэг sig-<sha256sum> в котором и хранит публичную часть ключа. Я сделал аналогично, только префикс изменил на dia-<sha256sum> где sha256sum это чексумма образа в реестре. Пазл сложился, но опять, не все так просто….

В силу некоторого своего невежства я упустил что In the common name field of the DN of a X 509 certificate, as defined in ASN. 1 notation for OID "2.5. 4.3", the limit is up to 64 characters. Решил я эту проблему, невежливо) я его обрезал. Т.е. валидация может быть выполнена по части символов из чексуммы, этот параметр я назвал digestSlice.

openssl x509 -noout subject -in image.crt  
subject=C = RU, ST = Moscow, L = Moscow, O = company, OU = local, CN = dia-0addcc1de26ee0f660d21b01c1afdff9f59efb

Оставалось разработать вебхук для валидации в k8s ValidationWebhook и предоставить удобный инструмент для запроса сертификатов после сборки образа в CI. Архитектура получилась следующая:

Вебхук для k8s написал как полагается на go, также helm чарт для его установки: https://github.com/spanarek/dia/tree/master/chart. Чтобы в неймспейсе запускались только подписанные образы необходимо NS пометить так: diwah=enabled. Принцип работы заключается в следующем: чарт имеет параметр attestor_ca. В эту переменную необходимо положить сертификат УЦ (base64 строкой разумеется) который выдает образам сертификаты и будет доверенным. Во время создания pod вебхук берет ссылку до образа в registry, затем ищет слой с сертификатом (dia-<sha256sum>), проверяет CN, пренадлежность к доверенному УЦ (attestor_ca) и выдает заключение. Образы которые объявлены в pod по чексумме, а не по тэгу, валидации не подлежат и пропускаются (это не имеет смысла кмк).

В качестве инструмента для подписи я написал скрипт для ручного добавления сертификата образа, а также манифест для выпуска сертификата в gitalb-ci с помощью hashicorp vault. Поскольку уже был опыт и паттерны использования, vault в gitlab, hashicorp полностью решает вопрос автоматизированного выпуска сертификатов для ваших приложений. Итого для gitlab разработчикам нужно просто добавлять одну строку в script джобы сборки образа:

dia-sign.sh ${DOCKER_REG_IMAGE}

Job целиком в моем случае(с vault) выглядит так:

make-test-image:
  image:
    name: ghcr.io/spanarek/dia/dia-dind:hashicorp0.1.0
  stage: build
  variables:
    VAULT_AUTH_ROLE: any
    VAULT_AUTH_PATH: auth/jwt/gitlab
    VAULT_ADDR: https://vault.local:8200
    VAULT_CAPATH: vault.pem
    VAULT_PKI_PATH: pki/issue/by-gitlab-id
    DOCKER_REG_IMAGE: registry.local/test-app
  script:
    - docker login -u "{REGISTRY_PASSWORD}" "${REGISTRY_URL}"
    - docker build -t ${DOCKER_REG_IMAGE}:latest .
    - docker push ${DOCKER_REG_IMAGE}:latest
    - dia-sign.sh ${DOCKER_REG_IMAGE}

Что имеем в итоге:

  • подпись стандартом x509 и независимость от решения, я использовал pki vault для получения сертификатов, но это в силу специфики инфраструктуры компании, вы вольны использовать любую PKI

  • простота подписывания для разработчиков и devops (один дополнительный шаг, для записи сертификата в отдельный тэг)

  • отсутствие издержек на хранение закрытых ключей и т. п., ключи не хранятся нигде, вообще, мы про них забываем сразу после получения сертификата

    Очевидные и приемлемые на мой взгляд минусы:

  • не самая мощная криптографическая стойкость (следствие обрезанного хэша)

  • невозможность отозвать сертификат у образа (пока я это не реализовал, но возможно сделаю)

А что означает DIA вы прочтете здесь :-)

Комментарии (5)


  1. Busla
    00.00.0000 00:00

    хотелось чтобы подпись работала по принципу обычных x509 сертификатов но без валидации срока действия

    вроде бы подписывание кода/ПО/скриптов всегда так и работало, без валидации срока - на Windows, MacOS, древнее ПО и драйверы прекрасно продолжает устанавливаться и работать будучи подписанным давно "протухшими" сертификатами

    Идея заключалась в следующем: взять чексумму образа, выдать сертификат с commonName эквивалентным чексумме

    Не очень понял: как вы пришли к этой идее вместо традиционного подписывания?


  1. HunterXXI
    00.00.0000 00:00

    Я только не понял, а где сам выданный сертификат находиться раз в образ его решили не класть?

    P.S. Есть комментарий к imagePullPolicy: always - эта политика проверяет по хэш сумме и скачивает образ только и только тогда когда образа с такой хэш суммой нет на ноде, на которой запускается под. В любом cis по kubernetes явно указано что необходимо избегать лейблов и использовать хэш для контроля целостности.


    1. spanarek Автор
      00.00.0000 00:00
      +1

      // Я только не понял, а где сам выданный сертификат находиться раз в образ его решили не класть?

      создается дополнительный тэг dia-<sha256sum> в котором и хранит публичную часть ключа.
      т.е. в репозитории у вас кроме основного образа, образ с сертификатом

      registry.local/alpine:latest

      registry.local/alpine:dia-284dffdsf34829fdsffjsdlf38438

      В реестре по факту x2 тэгов. Во втором у вас только серт и он очень легкий(
      FROM scratch
      ADD cert.pem)


      1. HunterXXI
        00.00.0000 00:00

        хм, хитро :) вполне рабочая реализация


    1. spanarek Автор
      00.00.0000 00:00
      +1

      always - эта политика проверяет по хэш сумме и скачивает образ только и только тогда когда образа с такой хэш суммой нет на ноде, на которой запускается под.

      Да, я и написал:

      Если у вас все запускается по чексуммам, можете не париться.... 

      Однако, часто(а на моей практике как правило) наш брат хочет запускать все по тэгам, ввиду особенностей CI\CD, удобство и т.п..