В этом уроке разберём, как Jenkins хранит учётные данные и как правильно подсунуть их в конвейер так, чтобы пароль не утёк в лог. Тема jenkins credentials - одна из тех, где мелкая ошибка стоит дорого, поэтому пройдём её основательно.
Хранилище jenkins credentials: типы и области
Jenkins держит секреты в собственном зашифрованном хранилище (плагин Credentials Plugin, он в дефолтной поставке). На диске они лежат в credentials.xml, зашифрованные мастер-ключом из $JENKINS_HOME/secrets. В UI это раздел Manage Jenkins -> Credentials. Ключевая идея: в Jenkinsfile ты никогда не пишешь сам пароль - только его идентификатор (credentialsId). Сам секрет живёт в хранилище.
Типов учётных данных несколько, и под каждую задачу свой:
- Username with password - классическая пара логин/пароль. Реестры, Nexus, базы, basic-auth API.
- Secret text - один токен или ключ. GitHub PAT, токен облака, webhook-секрет.
- SSH Username with private key - приватный ключ плюс юзернейм. Деплой по SSH, git-доступ по ключу.
- Secret file - целый файл-секрет. kubeconfig, service-account JSON, сертификат как файл.
- Certificate - клиентский сертификат PKCS#12 для mTLS.
- System - доступен только самому Jenkins и его системным задачам (например, конфигурация облачного агента). Конвейеры эти креды не видят. Самая узкая область.
- Global - доступен любому пайплайну на этом контроллере. Удобно, но опасно: токен прода увидит и левая тестовая джоба.
- Folder - доступен только джобам внутри конкретной папки (нужен Folders Plugin). Это рабочая лошадка для разграничения: команда A не дотянется до секретов команды B.

jenkins withcredentials: привязка секрета внутри шага
Базовый и самый универсальный способ подтянуть jenkins учетные данные в конвейер - шаг withCredentials. Он берёт секрет из хранилища, кладёт его в переменные окружения только на время выполнения блока и - что критично - маскирует значение в логах. Если секрет случайно попадёт в вывод, Jenkins заменит его на звёздочки.
Вот рабочий declarative-конвейер, который логинится в Docker-реестр и выкатывает образ:
Код: Выделить всё
pipeline {
agent any
stages {
stage('Push image') {
steps {
withCredentials([usernamePassword(
credentialsId: 'registry-creds',
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS'
)]) {
sh '''
echo "$REG_PASS" | docker login registry.example.com -u "$REG_USER" --password-stdin
docker push registry.example.com/app:$BUILD_NUMBER
'''
}
}
}
}
}
Для SSH-деплоя тип другой, и withCredentials отдаёт путь к временному файлу с ключом:
Код: Выделить всё
stage('Deploy') {
steps {
withCredentials([sshUserPrivateKey(
credentialsId: 'prod-deploy-key',
keyFileVariable: 'SSH_KEY',
usernameVariable: 'SSH_USER'
)]) {
sh '''
scp -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new \
build/app.jar "$SSH_USER@prod.example.com:/opt/app/"
'''
}
}
}
Код: Выделить всё
withCredentials([string(credentialsId: 'cloud-token', variable: 'CLOUD_TOKEN')]) {
sh 'curl -H "Authorization: Bearer $CLOUD_TOKEN" https://api.example.com/deploy'
}
withCredentials([file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')]) {
sh 'kubectl --kubeconfig "$KUBECONFIG" rollout status deploy/app'
}
credentials() в environment: jenkins пароль на весь stage
Второй способ - хелпер credentials() прямо в блоке environment. Он короче, когда секрет нужен на весь stage или весь pipeline, а не на один шаг. Поведение зависит от типа:
- Для secret text переменная получает само значение токена.
- Для secret file переменная получает путь к временному файлу.
- Для username/password переменная получает строку username:password, и дополнительно автоматически создаются две переменные с суффиксами _USR и _PSW.
Код: Выделить всё
pipeline {
agent any
environment {
NEXUS = credentials('nexus-creds')
}
stages {
stage('Publish') {
steps {
sh 'mvn deploy -Dnexus.user="$NEXUS_USR" -Dnexus.pass="$NEXUS_PSW"'
}
}
}
}
Грабли: как jenkins пароль утекает в лог
Маскирование - не магия. Jenkins маскирует только точное совпадение значения секрета в выводе. Любое преобразование ломает защиту:
- echo секрета напрямую. sh 'echo $TOKEN' выведет токен. Маскирование сработает по точному значению, но не делай так в принципе - это плохая привычка, а при отладке легко забыть убрать.
- Преобразование значения. base64, urlencode, разбивка на части - результат уже не совпадёт с оригиналом, и Jenkins его не замаскирует. Закодировал токен в base64 и вывел - утёк.
- set -x в shell. При sh 'set -x; ...' shell печатает каждую команду с подставленными значениями. Маскирование может не угнаться. Не включай трассировку в блоках с секретами.
- Секрет в имени файла или URL с параметром. curl "https://api?token=$TOKEN" - токен уедет в access-логи целевого сервера, и это уже вне контроля Jenkins.
- Groovy-интерполяция двойными кавычками. sh "deploy --pass=${PASS}" подставляет пароль в команду на контроллере. Используй одинарные кавычки и передавай через переменную окружения - это прямо предупреждение самого Jenkins в логах.
Ещё про ротацию. Любой секрет имеет срок жизни. Заведи привычку обновлять токены и ключи по расписанию - например раз в квартал. В Jenkins это делается через Update в карточке credentials: меняешь значение, credentialsId остаётся прежним, и ни один Jenkinsfile править не нужно. Это и есть главный плюс того, что в коде живёт только ID. При компрометации - немедленно меняешь значение и отзываешь старый токен на стороне сервиса.
Отдельно: само хранилище credentials.xml неплохо для команды, но для серьёзного прода секреты лучше держать во внешнем хранилище (HashiCorp Vault, облачные Secret Manager), а Jenkins ходит туда за ними на лету. Это тема следующего урока - там разберём интеграцию и почему она снимает проблему ротации почти полностью.
Мини-лаба
Руками, на тестовом Jenkins:
- Заведи Username with password в Global scope с id registry-creds.
- Напиши конвейер с withCredentials и usernamePassword, выполни sh с echo "***" (не самим паролем) - убедись, что сборка зелёная.
- Теперь намеренно сделай sh 'echo $REG_PASS' и посмотри в лог: значение замаскировано звёздочками.
- Сломай маскирование: sh 'echo $REG_PASS | base64' - увидишь, что закодированный пароль утёк в открытую. Зафиксируй для себя этот эффект.
- Создай Folder, перенеси креды в folder-scope и проверь, что джоба вне папки их больше не видит (сборка падает с ошибкой про отсутствующий credentialsId).
- Чем отличаются области system, global и folder и в каком случае выбирать folder-scope?
- Какие переменные автоматически создаёт credentials() в environment для типа username/password?
- Почему sh 'echo $TOKEN | base64' сводит на нет маскирование, а прямой echo - нет?
- Почему пароль надо передавать через --password-stdin или переменную окружения, а не аргументом команды?
В Jenkinsfile живёт только идентификатор секрета, сам секрет - в зашифрованном хранилище. Точечный доступ дают через withCredentials (маскирование плюс короткое время жизни), на весь stage - через credentials() в environment. Folder-scope разграничивает доступ между командами, ротация делается сменой значения без правки кода. А любое преобразование секрета - base64, set -x, интерполяция двойными кавычками - ломает маскирование и сливает jenkins пароль в лог. Дальше - выносим секреты во внешнее хранилище.