Почему Jenkins groovy sandbox держит твой код в клетке
Pipeline в Jenkins - это не конфиг, а живой Groovy-скрипт, который исполняется прямо на контроллере. А контроллер - это JVM с полным доступом к файловой системе, к credentials всех проектов, к API самого Jenkins. Если бы любой Jenkinsfile мог звать произвольные Java-методы, то строчка вроде
Код: Выделить всё
Jenkins.instance.securityRealm = nullМеханика простая и жёсткая. Когда конвейер исполняется в песочнице (по умолчанию для всех declarative pipeline и для scripted из SCM), движок перехватывает КАЖДЫЙ вызов метода и обращение к полю и сверяет его с белым списком разрешённых сигнатур. Безопасное подмножество - работа со строками, списками, числами, стандартные pipeline-шаги - проходит молча. А вот попытка дёрнуть
Код: Выделить всё
new File('/etc/passwd').textКод: Выделить всё
Jenkins.instance
Jenkins script approval: как утверждать методы и не выстрелить себе в ногу
Когда твой код упёрся в незнакомую сигнатуру, она не пропадает - она попадает в очередь. Открой Manage Jenkins -> In-process Script Approval, и увидишь список ожидающих утверждения вызовов. Администратор может нажать "Approve" - и сигнатура добавится в глобальный белый список, после чего ЛЮБОЙ конвейер сможет её использовать. Вот тут и кроется главная опасность jenkins script approval: ты утверждаешь не "этому проекту такой вызов", а "всем и навсегда".
Поэтому к каждой записи относись как к запросу на повышение привилегий. Утверждение
Код: Выделить всё
staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods execute java.lang.StringОтдельная ловушка - @NonCPS. Этот аннотированный метод исполняется не пошаговым CPS-движком, а как обычный Groovy. Он нужен, когда внутри логика, которую CPS не умеет сериализовать (итерация по сложным структурам, регулярки, работа со списками). Но запомни: @NonCPS не выключает песочницу. Сигнатуры внутри такого метода всё равно проверяются. Зато @NonCPS приносит свои грабли - внутри нельзя вызывать pipeline-шаги вроде sh или echo, потому что они требуют CPS. Метод просто молча отработает не так, как ты ждёшь.
Код: Выделить всё
// scripted-фрагмент: @NonCPS для несериализуемой логики
@NonCPS
def parseVersions(String json) {
// тут можно тяжёлый Groovy, но НЕЛЬЗЯ sh/echo
def data = new groovy.json.JsonSlurper().parseText(json)
return data.versions.findAll { it.startsWith('2.') }.join(', ')
}
Дублировать Groovy по всем Jenkinsfile - боль, поэтому общий код выносят в Shared Library. И вот ключевой для jenkins безопасность сценариев момент: библиотеки бывают двух сортов, и это определяет, исполняются они в песочнице или нет.
- Глобальные (Global) Shared Libraries - настраиваются на уровне Manage Jenkins администратором. Они считаются ДОВЕРЕННЫМИ и выполняются ВНЕ песочницы, с полным доступом к Jenkins API. Логика проста: их правит только админ, через защищённый репозиторий, значит, им можно доверять как самому Jenkins.
- Папочные (Folder-level) Shared Libraries - привязаны к папке проекта и могут редактироваться обычными пользователями. Они НЕДОВЕРЕННЫЕ и крутятся внутри песочницы с теми же ограничениями, что и пользовательский Jenkinsfile.
Код: Выделить всё
// Jenkinsfile: подключение доверенной глобальной библиотеки
@Library('platform-ci@v3') _
pipeline {
agent any
stages {
stage('Build') {
steps {
// шаг из глобальной библиотеки, исполняется вне песочницы
buildDockerImage(name: 'web', tag: env.GIT_COMMIT)
}
}
}
}
Теперь про секреты. У Jenkins есть свой credentials store, и для пары токенов его хватает. Но как единственное хранилище паролей он плох: секреты лежат статично, шифрованы ключом, который валяется рядом на том же диске; ротация - ручная; кто имел права в момент кражи бэкапа, тот и получил всё разом. Современный подход на 2026 год - секреты снаружи, Jenkins только тянет их в момент сборки и тут же забывает. Реализуется это связкой jenkins vault через плагин HashiCorp Vault.
Идея в том, что Vault - центральный сейф, а Jenkins аутентифицируется в нём (по AppRole или Kubernetes-токену агента) и забирает значения на лету. Плагин даёт build wrapper и pipeline-шаг withVault: он читает путь в Vault, подставляет значения в переменные окружения на время блока и маскирует их в логе сборки, чтобы случайный echo не слил пароль в консоль.
Код: Выделить всё
pipeline {
agent any
stages {
stage('Deploy') {
steps {
withVault(configuration: [
vaultUrl: 'https://vault.cyberlake.internal:8200',
vaultCredentialId: 'vault-approle'
], vaultSecrets: [[
path: 'secret/data/ci/registry',
secretValues: [
[envVar: 'REG_USER', vaultKey: 'username'],
[envVar: 'REG_PASS', vaultKey: 'password']
]
]]) {
sh 'echo "$REG_PASS" | docker login -u "$REG_USER" --password-stdin registry.cyberlake.ru'
}
}
}
}
}
Код: Выделить всё
unclassified:
hashicorpVault:
configuration:
vaultUrl: "https://vault.cyberlake.internal:8200"
vaultCredentialId: "vault-approle"
engineVersion: 2
Типичные грабли
- Сняли галочку песочницы "чтобы заработало". Это эквивалент chmod 777 на проде: любой Jenkinsfile получает полный доступ к контроллеру.
- Утвердили опасную сигнатуру (execute, getClass, ClassLoader) не глядя. Откати лишнее в In-process Script Approval - там же есть кнопка удаления одобренных записей.
- Положили код в глобальную библиотеку, думая что он в песочнице. Он НЕ в песочнице - он доверенный и всемогущий.
- Захардкодили пароль в Jenkinsfile или в переменную окружения job. Он попадёт в git и в логи. Только Vault или хотя бы credentials с маскировкой.
- Забыли, что внутри @NonCPS не работают sh/echo - метод "молча" ведёт себя не так.
- Сравнение с соседями: в GitLab CI и GitHub Actions нет исполняемого Groovy на сервере, поверхность атаки уже, но проблема секретов та же - там тоже выносят их во внешний Vault через OIDC. Концепция "секреты снаружи" универсальна.
- Напиши scripted-шаг с заведомо запрещённым вызовом (например, new File(...)), запусти и поймай ошибку. Зайди в In-process Script Approval, посмотри запись - и НЕ утверждай её.
- Создай папочную и глобальную Shared Library с одинаковым кодом, который пробует Jenkins.instance. Убедись, что папочная падает в песочнице, а глобальная проходит.
- Подними dev-инстанс Vault в режиме -dev, заведи AppRole, добавь credential в Jenkins и прочитай секрет через withVault. Проверь, что в логе значение замаскировано звёздочками.
- Почему отключать Groovy sandbox опасно и что именно проверяет песочница при исполнении конвейера?
- В чём разница между глобальной и папочной Shared Library с точки зрения доверия и песочницы?
- Что делает шаг withVault и почему хранить секреты во внешнем Vault безопаснее, чем в Jenkins credentials?
- Почему @NonCPS не является способом обойти проверку безопасности и какие у него ограничения?
Безопасность сценариев Jenkins держится на двух китах. Первый - изоляция кода: песочница и script approval не дают чужому Jenkinsfile дотянуться до контроллера, а граница доверия проходит ровно по тому, кто может править библиотеку. Второй - изоляция секретов: их место снаружи, в Vault, а Jenkins лишь тянет нужное на время сборки и сразу забывает. Не снимай галочку песочницы, не одобряй сигнатуры вслепую и не держи пароли внутри Jenkins - и твой CI перестанет быть самым лакомым куском инфраструктуры для атакующего.