惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
L
LINUX DO - 最新话题
Help Net Security
Help Net Security
N
News | PayPal Newsroom
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
W
WeLiveSecurity
C
CXSECURITY Database RSS Feed - CXSecurity.com
Webroot Blog
Webroot Blog
T
Troy Hunt's Blog
V
Vulnerabilities – Threatpost
Google Online Security Blog
Google Online Security Blog
N
News and Events Feed by Topic
T
Threat Research - Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tor Project blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
PCI Perspectives
PCI Perspectives
Google DeepMind News
Google DeepMind News
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
S
SegmentFault 最新的问题
J
Java Code Geeks
P
Privacy & Cybersecurity Law Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 【当耐特】
博客园_首页
H
Hacker News: Front Page
T
Threatpost
Jina AI
Jina AI
博客园 - Franky
月光博客
月光博客
L
LINUX DO - 热门话题
The Cloudflare Blog
H
Heimdal Security Blog
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
Cloudbric
Cloudbric
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
S
Secure Thoughts
T
Tenable Blog
I
Intezer
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Пайплайн не должен хранить секрет: безопасное хранение и доставка секретов для CI/CD с Deckhouse Code и Stronghold
an_shilov (Ф · 2026-05-21 · via Все публикации подряд на Хабре

Привет! Меня зовут Андрей Шилов, я старший инженер по практикам DevOps в компании «Флант». Мы занимаемся консалтингом и помогаем клиентам строить информационные системы, что почти всегда означает построение CI/CD-пайплайнов, которые должны быть безопасными, масштабируемыми и функциональными. И ключевое слово здесь — «безопасными». А безопасность пайплайна во многом определяется тем, как в нём хранятся секреты.

В статье разберёмся, как с этим обстоят дела на практике. Сначала я рассмотрю подходы к хранению секретов для пайплайнов и ограничения GitLab CI/CD Variables. Затем покажу три кейса самодельной интеграции GitLab CE с HashiCorp Vault и сложности, которые остаются даже при аккуратной её реализации. А в конце настрою нативную интеграцию Deckhouse Code и Deckhouse Stronghold, которая решает проблемы хранения секретов в пайплайнах «из коробки».

Дисклеймер: Deckhouse Code и Deckhouse Stronghold — это коммерческие продукты, для их использования нужно купить лицензию, хотя базовые возможности Stronghold доступны и в Open Source-платформе Deckhouse Kubernetes Platform Community Edition. В статье я разбираю проблемы существующих подходов и показываю, как наши продукты их решают. Итоговый выбор решения, как и всегда, остаётся за вами.

Способы хранения секретов для пайплайнов: от репозитория до хранилища

В CI/CD-пайплайнах секреты нужны практически на каждом этапе. Сборка и публикация образов — нужны credentials к registry. Деплой в Kubernetes-кластер — нужен kubeconfig или токен сервисного аккаунта. Интеграционные тесты — нужны реальные connection strings для подключения к базе данных. Нотификации и внешние API — токены Slack, Telegram, Kaiten, S3.

Если грубо прикинуть, способы хранения секретов для пайплайнов выстраиваются от «просто, но небезопасно» к «сложнее, но надёжнее».

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

CI/CD Variables. Секреты хранятся в зашифрованном виде внутри инструмента CI/CD, могут маскироваться, их видимость можно фильтровать по веткам и тегам. Звучит лучше, чем первый вариант. Правда, подводных камней, как мы разберём ниже, здесь значительно больше, чем кажется.

Сюда же примыкает промежуточный вариант: секреты лежат в репозитории, но в зашифрованном виде. Так работают SOPS, helm-secrets и werf: чувствительные значения шифруются ключом, а сам ключ передаётся в пайплайн через переменные CI/CD (реже — лежит в KMS). Прочитать секрет из репозитория без ключа нельзя, при этом история изменений сохраняется в Git. Но вся схема безопасна ровно настолько, насколько безопасен ключ в CI/CD Variables, а значит, наследует все их проблемы.

Выделенное хранилище секретов. Секреты хранятся в зашифрованном виде внутри специализированного решения и доставляются несколькими безопасными способами. Все секреты в одном месте, управление жизненным циклом и доступом строится на единых принципах, доставка безопасна на всех этапах и не требует написания каких-то особенных механизмов. Индустриальным стандартом здесь исторически является HashiCorp Vault.

Основным способом хранения секретов для CI/CD являются GitLab CI/CD Variables. Почему так? По данным нашего исследования State of DevOps Russia за 2025 год, львиная доля российского рынка пользуется GitLab и как системой хранения кода (70,2 %), и как платформой для CI/CD (64,5 % в вопросе с мультивыбором). И именно CI/CD Variables GitLab позиционирует как место для хранения секретов.

Давайте разберёмся, почему с этим есть серьёзные проблемы.

Почему GitLab CI/CD Variables — это не хранилище секретов

GitLab предоставляет три уровня хранения переменных: 

  1. Уровень инстанса — переменные доступны во всех проектах, управляются только администратором. 

  2. Уровень группы — доступны во всех проектах группы и подгрупп, наследуются автоматически. 

  3. Уровень проекта — доступны только в конкретном проекте, управляются maintainer'ом.

Каждая переменная может иметь тип Variable или File, несколько режимов видимости (Visible, Masked, Masked and hidden) и флаги Protected и Expand variable reference. Можно привязать переменную к конкретному окружению.

Звучит функционально. Но здесь кроется фундаментальная архитектурная проблема.

GitLab CI/CD Variables не разделяют «кто может запускать пайплайн» и «кто должен знать секрет».

Maintainer может добавить в Job printenv — и все секреты окажутся в логах. Но это ещё не всё. Секреты доступны через GitLab API при наличии токена. Все мы знаем привычку выдавать токенам максимально долгий lifecycle и максимально широкие скоупы, просто чтобы их не перевыпускать. Ротации нет: однажды прописанный секрет живёт годами без каких-либо алертов. Нет audit log: непонятно, кто и когда менял или читал секрет. Owner группы видит переменные всех дочерних проектов.

Кстати, ротация — это отдельная боль. Один секрет используется в 30 пайплайнах, разбросанных по разным группам, например тот же connection string к базе. При ротации нужно обновить его в каждом проекте вручную. Про часть, вероятно, забудут, что приведёт как минимум к падению пайплайна, как максимум — к инциденту на каком-то окружении.

Но самое неприятное то, что маскировка переменной не спасает. Представьте, что вы хотите дать разработчикам доступ к пайплайнам, но скрыть от них продовые секреты. С CI/CD Variables это архитектурно невозможно. Masked var не поможет, потому что любой разработчик может написать вот такое:

script: 
  - echo ${SUPER_SECRET::1} ${SUPER_SECRET:1}

GitLab маскирует в логах только точные совпадения значения переменной. Здесь же секрет разрезан Bash-подстрокой на первый символ и хвост — ни одна из частей не совпадает с маской целиком, поэтому в логах окажется значение в открытом виде, разделённое пробелом.

Если возиться с логами не хочется, можно сохранить секрет в артефакт и скачать через UI:

script:
  - echo $SUPER_SECRET > secret.txt
artifacts:
  paths:
    - secret.txt  # Скачать через UI.

Момент с артефактами можно закрыть точечно: например, через artifacts:access: 'maintainer', который запретит скачивание артефакта не-мейнтейнерам. Но это перекрывает один способ утечки из десятков: способ компрометации вообще ничем не ограничен, кроме изобретательности злоумышленника.

Более системное решение может дать механизм general pipelines. CI-конфиг выносится в отдельный репозиторий с read-only-доступом для разработчиков, импортируется в проектные репозитории целиком, и переопределить из локального .gitlab-ci.yml ничего нельзя.

Но в нашей консалтинговой практике такое решение встречается крайне редко — оно требует очень высокой инженерной культуры. Командам почти всегда нужны «крутилки»: параметры, кастомные задачи (Jobs), переопределения под конкретный проект. Сделать общий пайплайн, который одновременно покроет все потребности и при этом не даст ничего изменить локально, сложно. Поэтому большинство команд использует более мягкие схемы — общие шаблоны через include и extends, где переопределить можно всё. А значит, на практике остаётся то же правило: если у разработчика есть возможность менять .gitlab-ci.yml, он дотянется до значений переменных окружения.

Концепция решения: пайплайн не хранит секрет

Решение через HashiCorp Vault меняет саму концепцию: пайплайн не хранит секрет — он его получает. Делается это через JWT/OIDC flow.

Схема работает так. При каждом запуске пайплайна GitLab автоматически генерирует одноразовый JWT-токен, подписанный своим приватным ключом. Внутри токена — обширный набор метаданных о контексте запуска: проект, ветка, окружение и многое другое, включая то, является ли ветка protected. Этот токен уникален для каждой задачи.

Runner передаёт JWT на эндпоинт аутентификации Vault. Никаких статичных паролей или токенов не нужно. Vault скачивает публичный ключ GitLab по OIDC Discovery URL, верифицирует подпись и проверяет bound claims — например, что запрос приходит именно из вашего проекта и с protected-ветки.

Если проверка прошла, Vault возвращает временный токен доступа с ограниченным TTL — ровно на время выполнения задачи. С этим токеном runner читает нужный секрет из Vault. После завершения задачи токен автоматически аннулируется, ничего не сохраняется.

Элегантная схема. Но здесь начинаются практические сложности.

Три варианта самодельной интеграции GitLab CE и HashiCorp Vault

Большинство наших клиентов и респондентов State of DevOps Russia пользуются GitLab в Community Edition и «ванильным» HashiCorp Vault. Проблема в том, что в GitLab CE нет keyword secrets:, оно доступно только в Premium/Ultimate. Всё, что остаётся, — писать самодельную интеграцию. На примере трёх реальных кейсов разберём, как это выглядит на практике.

Вариант 1: Python hvac + AppRole

В GitLab CI/CD Variables хранятся AppRole Credentials — VAULT_ROLE_ID (логин роли) и VAULT_SECRET_ID (пароль роли). Это не ссылки на секреты, а учётные данные для входа в Vault.

Внимательный читатель заметит: это снова секреты, к которым должны предъявляться требования безопасного хранения. Клиент хранил их в GitLab CI Variables — а значит, просто добавил ещё один «хоп» на пути к компрометации.

Python-скрипт через библиотеку hvac авторизуется в Vault по AppRole, обходит все переменные с префиксом V_, читает для них значения из Vault и инжектит их в окружение задачи:

# .gitlab-ci.yml
variables:
  V_TRIVY_REGISTRY_PASSWORD: projects/project/TRIVY_REGISTRY_PASSWORD
script:
  - source <(get-vault-secrets-by-approle)
  - echo $TRIVY_REGISTRY_PASSWORD
# Ключевые функции скрипта.
def get_vault_vars(self):
    for k, v in os.environ.items():
        if k.startswith(self.var_prefix):  # var_prefix = 'V_'
            self.vault_vars[k] = v

def login_to_vault_using_approle(self):
    self.client = hvac.Client(namespace=os.environ.get("VAULT_NAMESPACE"))
    self.client.auth.approle.login(
        os.environ.get('VAULT_ROLE_ID'),
        os.environ.get('VAULT_SECRET_ID'))

def get_vault_secrets(self):
    for k, v in self.vault_vars.items():
        secret_var_name = re.sub(r'^V_', '', k)
        vault_path = re.fullmatch(
            r'^(?P<mount_point>[^/]+)/(?P<path>.+)/(?P<key>[^/]+)$', v
        ).groupdict()
        vault_secret = self.client.secrets.kv.v2.read_secret_version(
            vault_path['path'], 0, vault_path['mount_point'])
        self.secret_vars[secret_var_name] = \
            vault_secret['data']['data'][vault_path['key']]
    for k, v in self.secret_vars.items():
        print("export {}=\"{}\"".format(k, v))

Вариант 2: Bash-скрипт с префиксом vault:

Этот вариант уже использует JWT-аутентификацию и концептуально ближе к тому, что предлагает GitLab EE. Переменная с префиксом vault: содержит путь к секрету. before_script обходит env и подставляет значения из Vault через API:

variables:
  VAULT_ADDR: https://vault.example.com
  VAULT_ROLE: project_role

job_with_secrets:
  id_tokens:
    VAULT_ID_TOKEN:
      aud: vault
  variables:
    TRIVY_REGISTRY_PASSWORD: vault:projects/project#TRIVY_REGISTRY_PASSWORD
  before_script:
    - export VAULT_TOKEN=$(vault write -field=token \
        auth/jwt/login role=$VAULT_ROLE jwt=$VAULT_ID_TOKEN)
    - |
      while IFS='=' read -r name value; do
        if [[ $value == vault:* ]]; then
          stripped=${value#vault:}
          export "$name=$(vault kv get \
            -field=${stripped#*#} ${stripped%%#*})"
        fi
      done < <(env)
  script:
    - echo $TRIVY_REGISTRY_PASSWORD

before_script обычно выносится в отдельный шаблон и подключается через extends — чтобы не копировать «портянку» в каждую задачу. Фактически это аналог keyword secrets: из GitLab EE, но работающий в CE.

Вариант 3: Bash-скрипт в сборочном образе

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

Скрипт зашит в каждый сборочный образ. Роль и путь строятся строго по маске CLIENT-PROJECT-ENV — произвольный путь задать нельзя. Достаточно указать три параметра, и скрипт сам знает, куда идти в Vault:

deploy:
  variables:
    CI_VAULT_CLIENT: mycompany
    CI_VAULT_PROJECT: backend
    CI_ENV: production
  before_script:
    - source ExportVaultVars
  script:
    - echo $TRIVY_REGISTRY_PASSWORD
# Построение роли и пути.
VAULT_ROLE="$VAULT_CLIENT-${VAULT_PROJECT/./-}-$ENV"  # mycompany-backend-production
VAULT_PATH="$VAULT_CLIENT/data/$VAULT_PROJECT/$ENV"    # mycompany/data/backend/production

# Двойная аутентификация: JWT в CI, LDAP локально.
if [[ $CI_JOB_JWT != "" ]]; then
    VAULT_TOKEN=$(curl -s -X POST "$VAULT_URL/v1/auth/jwt/login" \
      --data "{\"jwt\":\"$CI_JOB_JWT\",\"role\":\"$VAULT_ROLE\"}" | jq -r .auth.client_token)
else
    VAULT_TOKEN=$(curl -s -X POST "$VAULT_URL/v1/auth/ldap/login/$username" \
      --data "{\"password\":\"$password\"}" | jq -r .auth.client_token)
fi

# Cleanup
unset VAULT_TOKEN VAULT_ROLE VAULT_PATH VAULT_CLIENT VAULT_PROJECT ENV

Интересная особенность этого варианта в том, что поддерживается двойная аутентификация — JWT в CI и LDAP локально. Разработчик может запустить скрипт руками с теми же правами. Есть три режима вывода: export в env, JSON-файл или .env-файл — это способы сохранения полученных секретов.

Но у этого клиента случился неприятный инцидент. Переменная CI_JOB_JWT в GitLab помечена как deprecated, и в один момент при обновлении до мажорной версии она просто перестала работать. Все автоматизации и деплои встали, пока не нашли выход из ситуации. 

Проблемы самодельной интеграции и общие проблемы с HashiCorp Vault

Подведём итог по самодельным интеграциям: 

  • Нет нативного keyword secrets:, эта возможность доступна только в GitLab Premium/Ultimate. 

  • Нет единого стандарта: кто-то пишет Bash, кто-то — Python, кто-то тащит Vault Agent. 

  • В ряде случаев приходится настраивать политики для «распечатывания» секретов. Каждый новый проект начинает с нуля. 

  • При любом обновлении GitLab или Vault — риск полной поломки. 

Но на этом список сложностей не ограничивается. Существуют проблемы любой интеграции с Vault, и они актуальны вне зависимости от того, CE или Premium у вас GitLab.

Во-первых, printenv внутри Job работает при любом подходе — выше я упоминал про это. Если секрет доставлен в env, процесс может его прочитать. Это фундаментальное ограничение доставки секретов через переменные окружения.

Во-вторых, дрифт конфигурации Vault. Роли и политики создаются руками. Есть риск, что через полгода никто не вспомнит, зачем нужна каждая роль. Даже если они создаются через Terraform, рано или поздно HCL-код может превратиться в нечитаемую портянку, внутри которой легко затеряется роль с wildcard-доступом.

В-третьих, операционная нагрузка. Vault как отдельный production-сервис требует HA, unseal, ротации сертификатов, мониторинга.

И наконец, HashiCorp Vault с 2023 года перешёл на лицензию BUSL 1.1 — коммерческое использование требует покупки лицензии. Существует альтернатива в виде OpenBao, но это молодой проект без enterprise-поддержки. Плюс оба продукта сложно использовать в серьёзных российских компаниях по довольно очевидным причинам.

Эти проблемы можно решить выбором продуктов, где интеграция уже сделана за вас и нет сложностей с покупкой лицензий.

Deckhouse Code + Deckhouse Stronghold: нативная интеграция для безопасных пайплайнов

Давайте познакомимся с действующими лицами.

Deckhouse Code — это полностью совместимая с GitLab CE и расширяющая его возможности платформа для хранения кода и CI/CD. И keyword secrets: в ней доступен. 

Deckhouse Stronghold — это Vault-совместимое решение для централизованного управления жизненным циклом секретов. Deckhouse Code умеет работать с ним нативно через JWT/OIDC flow.

А поподробнее?

Deckhouse Code — это решение для непрерывной разработки и управления жизненным циклом ПО с enterprise-возможностями. Позволяет безопасно хранить исходный код и автоматизировать CI/CD-конвейеры любой сложности. На Deckhouse Code просто мигрировать с любой версии GitLab благодаря полной совместимости.

Из ключевых возможностей: создание кастомных правил code review, зеркалирование репозиториев из внешних источников и стабильный CI/CD-конвейер, который обеспечивает предсказуемую работу пайплайнов, а также высокая доступность и автомасштабирование.  

Deckhouse Stronghold — это решение для централизованного управления жизненным циклом секретов с сертификатом ФСТЭК России. Он защищает пароли, ключи API, сертификаты, SSH-ключи, токены и другие конфиденциальные данные от утечек, обеспечивая при этом безопасную доставку секретов в приложения.

Мы не копируем HashiCorp Vault, а переосмысливаем хранилище секретов и создаём стандарт для российского рынка. При этом Deckhouse Stronghold полностью API-совместим с HashiCorp Vault — и через CLI, и через API.

Из ключевых возможностей: механизм namespaces для создания пространств имён и делегирования прав, auto unseal без использования внешних KMS, автоматическое резервное копирование, репликация KV1/KV2 на архитектуре master-slave, удобный веб-интерфейс, модуль secrets-store-integration для безопасной автоматизированной доставки секретов в приложения. И всё это — в единственном бинарном файле, что минимизирует векторы атаки.

Работает Deckhouse Stronghold по знакомой схеме: клиент подтверждает свою идентичность, Stronghold проверяет, есть ли у него доступ к запрошенному секрету, и либо разрешает операцию чтения/записи, либо отклоняет. Всё просто — и все ваши секреты под защитой.

Настройка интеграции состоит из трёх шагов на стороне Deckhouse Stronghold и минимального конфига на стороне Deckhouse Code.

Настройка интеграции шаг за шагом

Команды ниже работают через стандартный vault CLI — Deckhouse Stronghold с ним API-совместим. Те же команды можно выполнить через d8 stronghold — это часть d8, нашего «швейцарского ножа» для работы со всеми компонентами экосистемы Deckhouse. Например, вместо vault write auth/jwt/config будет d8 stronghold write auth/jwt/config. Для краткости в примерах оставлю vault.

Шаг 1: включить JWT-аутентификацию в Deckhouse Stronghold

Deckhouse Stronghold принимает ID-токены через метод JWT. Указываются OIDC Discovery URL и issuer — это URL вашего Deckhouse Code:

vault auth enable jwt

vault write auth/jwt/config \
  oidc_discovery_url="https://code.example.com" \
  bound_issuer="https://code.example.com"

Шаг 2: создать роль с bound_claims

bound_claims ограничивает доступ — только токены с указанными значениями смогут аутентифицироваться. Без этого доступ получит любой CI-токен.

vault write auth/jwt/role/gitlab-role - <<EOF
{
  "role_type": "jwt",
  "user_claim": "sub",
  "bound_audiences": ["vault"],
  "bound_claims": {
    "project_id": "23"
  },
  "policies": ["gitlab-policy"],
  "ttl": "1h"
}
EOF

В bound_claims можно использовать project_id, project_path, ref, ref_protected, environment, namespace_path. Подберите нужную комбинацию под конкретный сценарий, например разрешите доступ только с protected-веток конкретного проекта.

Шаг 3: написать ACL-политику

Политика задаёт конкретные пути в Deckhouse Stronghold, к которым разрешено обращаться. Здесь работает принцип минимальных прав: только read к нужным секретам.

vault policy write gitlab-policy - <<EOF
path "kv/data/code/vault-demo" {
  capabilities = ["read"]
}
EOF

Шаг 4. Конфигурация на стороне Deckhouse Code

В CI нужно указать обязательную переменную VAULT_SERVER_URL (URL Stronghold-сервера). Опционально — VAULT_AUTH_ROLE (название роли, по умолчанию берётся из конфигурации JWT auth), VAULT_AUTH_PATH (путь до метода аутентификации, по умолчанию jwt) и VAULT_NAMESPACE (для многоуровневой иерархии).

Использование в .gitlab-ci.yml:

stages:
  - test

vault-demo:
  stage: test
  image: alpine

  # 1. Запрашиваем JWT-токен с audience = наш Vault.
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.example.com

  # 2. Объявляем секрет: путь@движок.
  secrets:
    DATABASE_PASSWORD:
      vault: code/vault-demo/DATABASE_PASSWORD@kv
      token: $VAULT_ID_TOKEN
      file: false

  script:
    - echo $DATABASE_PASSWORD

Разберём анатомию пути к секрету в строке code/vault-demo/DATABASE_PASSWORD@kv:

  • code/vault-demo — путь до секрета в Vault;

  • DATABASE_PASSWORD — имя поля внутри секрета;

  • kv — точка монтирования Secret Engine (по умолчанию kv-v2).

Всё. Никакого before_script с Bash-портянкой, никакого Python-скрипта в образе. Просто декларативное описание того, какой секрет нужен и откуда его брать.

Что даёт связка Deckhouse Code и Deckhouse Stronghold

Помимо самой интеграции, у этой связки есть несколько приятных свойств, которые стоит упомянуть.

Один вендор — одна зона ответственности. Оба продукта разрабатываются «Флантом». Если возникает проблема на стыке Deckhouse Code и Deckhouse Stronghold — это один тикет и один инженер, который разберётся. Никаких «это не наша проблема» между двумя вендорами.

Защита самого пайплайна. Однострочник с маскировкой — частный случай. Пока кто-то может вписать в .gitlab-ci.yml произвольный код, никакое хранилище секретов не спасёт от компрометации. Безопасность пайплайна упирается в контроль над CI-конфигом — кто и при каких условиях может его менять.

В чистом GitLab CE такого контроля нет: обязательный апрув от владельца файла перед слиянием — функциональность платных редакций, Premium и Ultimate. В CE можно ограничить слияние в защищённую ветку только maintainer’ами или вынесением CI-конфига в отдельный репозиторий через general pipelines (что мы разобрали выше). 

В Deckhouse Code такая защита есть и она работает так же, как в GitLab Premium, — через файл CODEOWNERS. В нём указываются люди или группы, отвечающие за .gitlab-ci.yml, а ветка, в которую идёт слияние, защищается через protected branches. После этого merge request с изменениями в .gitlab-ci.yml нельзя смержить, пока его не одобрит хотя бы один из указанных владельцев. Maintainer репозитория здесь не имеет приоритета: если он не в списке владельцев .gitlab-ci.yml, его апрува недостаточно.

Deckhouse Stronghold и CODEOWNERS закрывают разные части задачи. Stronghold отвечает за то, кому и когда отдавать секрет — через JWT-аутентификацию, bound claims и ACL-политики. CODEOWNERS отвечает за то, какой код вообще может оказаться в пайплайне, читающем этот секрет. 

Получается двойная защита: Deckhouse Stronghold гарантирует, кто получит секрет, а CODEOWNERS — что в пайплайне не появится код для его компрометации.

Механизм CODEOWNERS в Deckhouse Code входит в стандартную поставку.

Единая Ops-модель в Deckhouse Kubernetes Platform. Если вы уже используете DKP, то и Deckhouse Code и Stronghold подключаются как модули — через ModuleConfig или d8 CLI. Оба модуля интегрированы с мониторингом, логированием и сетевыми политиками платформы.

Регуляторика и юридические вопросы. «Флант» — российский вендор, а это значит, что все контракты будут в рамках российского права, да и общение с поддержкой будет на русском языке. Кроме того Deckhouse Code и Deckhouse Stronghold входят в реестр российского ПО, а Stronghold недавно ещё и получил сертификат ФСТЭК России. Для компаний, которым важны такие моменты, это снимает отдельный пласт вопросов.

Итог: сравнение подходов

Давайте сравним четыре подхода к хранению секретов для пайплайнов по ключевым критериям. Для полноты картины к упомянутым в статье добавим вариант с коммерческой лицензией GItLab.

Критерий

GitLab CI/CD Variables

CE GitLab + HashiCorp Vault

EE GitLab + HashiCorp Vault

Deckhouse Code + Deckhouse Stronghold

Нативный secrets: keyword

Короткоживущие токены

Audit log

Единая точка ротации секрета

Prod-секреты недоступны разработчику

Нет лицензионных рисков

Один вендор / служба поддержки

CODEOWNERS: защита .gitlab-ci.yml

Реестр российского ПО

GitLab CI/CD Variables — нет разделения на «кто запускает пайплайн» и «кто знает секрет». Maintainer всегда потенциально имеет доступ ко всей чувствительной информации.

GitLab CE + Vault — концепция правильная, но с интеграцией придётся повозиться самостоятельно. При любом обновлении одного из решений интеграция может сломаться.

GitLab EE + Vault — всё работает нативно, но в случае проблем нужно будет общаться сразу с двумя вендорами. Ну и важно помнить про BUSL-лицензию, так что покупать нужно оба продукта.

Deckhouse Code + Deckhouse Stronghold — нативная интеграция, один вендор, без лицензионных рисков.

Узнать больше про наши продукты можно на их страницах — Deckhouse Code, Deckhouse Stronghold. Мы готовы помочь с пилотом: развернуть стенд и провалидировать решение под вашу инфраструктуру. Ну и, конечно, «Флант» проектирует и внедряет безопасные CI/CD-платформы — обращайтесь.

P. S. 

Читайте также в нашем блоге: