- Por Valeria Di Francesco
- ·
- Publicado 03 Jun 2024
Estrategias de testing aplicadas a negocio
Cuando se trata de desarrollar software, el testing es una pieza fundamental que garantiza la calidad y la confiabilidad de un producto y puede tener..
Si en vez de leer prefieres escuchar, dale al play.
La mayoría de los pipelines requieren claves secretas que deben ser autenticadas con algunos recursos externos. Para administrar adecuadamente estas claves, será crucial que estén almacenados fuera de nuestro repositorio de código y se proporcionen directamente al pipeline. Jenkins ofrece un almacén de credenciales que nos permite mantener y acceder a estas claves secretas de diversas formas, y es precisamente este tema el que trataremos en este blog.
Jenkins es una elección fácil cuando se trata de recopilación de inteligencia.
Con el fin de brindar el mejor servicio como consultores, en general necesitamos toda la información que un cliente pueda proporcionarnos. Generalmente, este último nos otorga acceso total al código fuente y la infraestructura.
Sin embargo, en algunas ocasiones, las cosas que nos gustaría verificar están, digamos, temporalmente fuera de nuestro alcance. Por supuesto, podríamos solicitar permiso para acceder a ello, pero esto puede llevar bastante tiempo e, incluso, podría darse el caso de que nadie sepa cómo acceder al recurso.
Para agilizar un poco este proceso, solemos explorar Jenkins para nosotros mismos encontrar la manera de obtener acceso. Si se nos ha concedido acceso total por escrito, entonces no surge ningún un dilema ético.
Para dar una buena primera impresión, pídele a Jenkins una confesión.
Al pedir acceso, podríamos encontrarnos con preguntas como "¿por qué necesitas eso?" o "tendré que hablar primero con mi supervisor". Sin embargo, hacer esto no es necesario; ya contamos con las aprobaciones correspondientes y hay que internalizar que nosotros somos los Consultores.
Conceder acceso a un Jenkins equivale a una licencia para ver todos los secretos almacenados allí. Si no quieres que la gente husmee, no les otorgues NINGÚN acceso a tu CI.
Las respuestas que busques, Jenkins las filtrará.
A veces nos encontramos con entidades renuentes a compartir. Podrían haber diferentes razones para ello, por lo que, frente a esta situación, no emitimos algún juicio; las cosas suceden, se deben cumplir plazos, entendemos. Lo que buscamos es tener una visión completa de la situación.
No los conocemos; ellos no nos conocen; sin embargo, Jenkins es imparcial y no elige bandos.
¿Qué haces cuando te unes a un proyecto y la persona con el conocimiento clave se fue hace mucho tiempo, y nadie sabe cómo acceder a esa máquina con Windows 98 en producción?
Jenkins tiene la respuesta.
Ahora tú sabes.
Conviértete en el héroe.
Cifrado, descifrado. Jenkins proporciona suscripción de texto plano.
Incluso si no necesitas obtener sigilosamente las credenciales de tu propia empresa, es bueno conocer sus vulnerabilidades al utilizar Jenkins.
Para implementar estas buenas prácticas de manera eficiente, considera seguir una metodología de Desarrollo Guiado por Pruebas (TDD). Consulta nuestra guía de TDD para obtener más detalles sobre cómo comenzar
No utilicé la palabra "seguro" en la introducción debido a que la forma en que cualquier servidor de CI almacena credenciales es inherentemente insegura.
Los servidores de CI no pueden usar funciones hash unidireccionales (como bcrypt) para codificar secretos, ya que, al ser solicitados por el pipeline, estos deben restaurarse a su forma original. Por lo que las funciones hash unidireccionales quedan descartadas, y lo que permanece es la encriptación bidireccional. Esto implica dos cosas:
Es posible que te estés preguntando por qué Jenkins se molesta en cifrar los secretos si pueden recuperarse en su forma original simplemente preguntando. La verdad es que no tengo una buena respuesta a esta pregunta. No obstante, utilizar el almacén de credenciales de Jenkins es infinitamente mejor que mantener secretos en el repositorio del proyecto.
Más abajo, hablo sobre lo que se puede hacer para minimizar la filtración de secretos desde Jenkins.
Si deseas seguir las instrucciones que aquí te proporciono, y ejecutar los ejemplos tú mismo, puedes poner en marcha una instancia pre-configurada de Jenkins desde mi repositorio jenkinsfile-examples en menos de un minuto (dependiendo el ancho de banda de tu conexión a Internet):
git clone https://github.com/hoto/jenkinsfile-examples.git
docker-compose pull
docker-compose up
Abre localhost:8080
, donde podrás ver un Jenkins con algunos jobs
Para navegar y añadir secretos, haz clic en Credentials.
Mi instancia de Jenkins ya tiene algunas credenciales pre-configuradas creadas por mí.
Para añadir secretos, coloca el cursor sobre (global) para mostrar un signo ▼ y haz clic en él. Selecciona Add credentials, para finalmente poder agregar secretos.
Si quieres, puedes agregar más secretos, pero para este blog estaré utilizando los secretos existentes.
Mi consejo es proporcionar un ID significativo y utilizar el mismo valor para la
Description
.user
no es un buen ID,github-rw-user
es mejor.
Ahora que hemos cubierto la creación de credenciales, pasemos a acceder a ellas desde un Jenkinsfile.
Ejecuta el trabajo 130-accessing-credentials
.
Trabajo 130-accessing-credentials
tiene el siguiente Jenkinsfile:
pipeline {
agent any
stages {
stage('usernamePassword') {
steps {
script {
withCredentials([
usernamePassword(credentialsId: 'gitlab',
usernameVariable: 'username',
passwordVariable: 'password')
]) {
print 'username=' + username + 'password=' + password
print 'username.collect { it }=' + username.collect { it }
print 'password.collect { it }=' + password.collect { it }
}
}
}
}
stage('usernameColonPassword') {
steps {
script {
withCredentials([
usernameColonPassword(
credentialsId: 'gitlab',
variable: 'userpass')
]) {
print 'userpass=' + userpass
print 'userpass.collect { it }=' + userpass.collect { it }
}
}
}
}
stage('string (secret text)') {
steps {
script {
withCredentials([
string(
credentialsId: 'joke-of-the-day',
variable: 'joke')
]) {
print 'joke=' + joke
print 'joke.collect { it }=' + joke.collect { it }
}
}
}
}
stage('sshUserPrivateKey') {
steps {
script {
withCredentials([
sshUserPrivateKey(
credentialsId: 'production-bastion',
keyFileVariable: 'keyFile',
passphraseVariable: 'passphrase',
usernameVariable: 'username')
]) {
print 'keyFile=' + keyFile
print 'passphrase=' + passphrase
print 'username=' + username
print 'keyFile.collect { it }=' + keyFile.collect { it }
print 'passphrase.collect { it }=' + passphrase.collect { it }
print 'username.collect { it }=' + username.collect { it }
print 'keyFileContent=' + readFile(keyFile)
}
}
}
}
stage('dockerCert') {
steps {
script {
withCredentials([
dockerCert(
credentialsId: 'production-docker-ee-certificate',
variable: 'DOCKER_CERT_PATH')
]) {
print 'DOCKER_CERT_PATH=' + DOCKER_CERT_PATH
print 'DOCKER_CERT_PATH.collect { it }=' + DOCKER_CERT_PATH.collect { it }
print 'DOCKER_CERT_PATH/ca.pem=' + readFile("$DOCKER_CERT_PATH/ca.pem")
print 'DOCKER_CERT_PATH/cert.pem=' + readFile("$DOCKER_CERT_PATH/cert.pem")
print 'DOCKER_CERT_PATH/key.pem=' + readFile("$DOCKER_CERT_PATH/key.pem")
}
}
}
}
stage('list credentials ids') {
steps {
script {
sh 'cat $JENKINS_HOME/credentials.xml | grep "<id>"'
}
}
}
}
}
Encuentra más ejemplos para diferentes tipos de secretos en la documentación oficial de Jenkins.
Al ejecutar el trabajo y revisar los registros, descubrimos que Jenkins intenta redactar los secretos del registro de construcción buscando los valores de los secretos y reemplazándolos con asteriscos ****.
Podemos ver los valores reales de los secretos si los imprimimos de tal manera que un simple match-and-replace no funcione. De esta manera, cada carácter se imprime por separado y Jenkins no redacta los valores.
Código de ejemplo:
print 'username.collect { it }=' + username.collect { it }
Output del registro:
username.collect { it }=[g, i, t, l, a, b, a, d, m, i, n]
Cualquier persona con acceso de edición a un repositorio construido en Jenkins puede revelar todas las credenciales globales modificando un Jenkinsfile en ese repositorio.
Así como, cualquier persona capaz de crear jobs en Jenkins puede revelar todos los secretos globales creando un job de canalización (pipeline).
Antes de pedirle a Jenkins una credencial, necesitas conocer su ID.
Puedes listar todos los ID's de credenciales leyendo el archivo $JENKINS_HOME/credentials.xml
file.
Código:
stage('list credentials ids') {
steps {
script {
sh 'cat $JENKINS_HOME/credentials.xml | grep "<id>"'
}
}
}
Output del registros:
<id>gitlab</id>
<id>production-bastion</id>
<id>joke-of-the-day</id>
<id>production-docker-ee-certificate</id>
System
y otros valores de credenciales de la UIJenkins tiene dos tipos de credenciales: System
y Global
.
Las credenciales de
sistema
son únicamente accesibles desde una configuración de Jenkins (por ejemplo, desde plugins).Las credenciales
globales
son iguales a las desistema
pero también se puede acceder a ellas desde trabajos de Jenkins.
Por definición, las credenciales de System
no son accesibles desde los jobs, pero podemos descifrarlas desde la interfaz de usuario (UI) de Jenkins. Para hacerlo, necesitas permiso de administrador.
Jenkins envía el valor cifrado de cada secreto a la UI. Esto representa una gran falla de seguridad.
Para obtener el secreto cifrado:
http://localhost:8080/credentials/
value
.En mi caso, el secreto encriptado es: {AQAAABAAAAAgPT7JbBVgyWiivobt0CJEduLyP0lB3uyTj+D5WBvVk6jyG6BQFPYGN4Z3VJN2JLDm}
.
Para descifrar cualquier credencial, podemos utilizar la consola de Jenkins, la cual requiere permiso de administrador para acceder.
Si no tienes privilegios de administrador, intenta elevar tus permisos buscando un usuario administrador en las credenciales
Global
primero.
Para abrir la Consola de Script
, ve a http://localhost:8080/script
.
Indícale a Jenkins que descifre e imprima el valor secreto:
println hudson.util.Secret.decrypt("{AQAAABAAAAAgPT7JbBVgyWiivobt0CJEduLyP0lB3uyTj+D5WBvVk6jyG6BQFPYGN4Z3VJN2JLDm}")
Ahí lo tienes; ahora puedes descifrar cualquier secreto de Jenkins (repito: si es que tienes privilegios de administrador).
Nota: si intentas ejecutar este código desde un trabajo de Jenkinsfile, obtendrás un mensaje de error:
"No se permite que los Scripts utilicen un método estático hudson.util.Secret decrypt java.lang.String. Los administradores pueden decidir si aprobar o rechazar esta firma
"
Aunque la mayoría de las credenciales se almacenan en la vista http://localhost:8080/credentials/, puedes encontrar secretos adicionales en:
http://localhost:8080/configure
- algunos plugins crean campos de tipo contraseña en esta vista
http://localhost:8080/configureSecurity/
- busca cosas como las credenciales AD
Otra manera de hacer esto es enlistando todas las credenciales para luego descifrarlas desde la consola:
def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class,
Jenkins.instance,
null,
null
)
for(c in creds) {
if(c instanceof com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey){
println(String.format("id=%s desc=%s key=%s\n", c.id, c.description, c.privateKeySource.getPrivateKeys()))
}
if (c instanceof com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl){
println(String.format("id=%s desc=%s user=%s pass=%s\n", c.id, c.description, c.username, c.password))
}
}
Output:
id=gitlab desc=gitlabadmin user=gitlabadmin pass=Drmhze6EPcv0fN_81Bj
id=production-bastion desc=production-bastion key=[-----BEGIN RSA PRIVATE KEY...
Considera que este script no está terminado. Puedes buscar todos class names de credenciales directamente en el código fuente de Jenkins.
Para acceder y descifrar las credenciales de Jenkins, necesitas tres archivos.
credentials.xml
- almacena credenciales cifradashudson.util.Secret
- decrifra entradas credentials.xml
, este archivo de por sí está cifradomaster.key
- decifra hudson.util.Secret
Los tres archivos se encuentran dentro del directorio principal de Jenkins:$JENKINS_HOME/credentials.xml
$JENKINS_HOME/secrets/master.key
$JENKINS_HOME/secrets/hudson.util.Secret
Debido a que Jenkins es open source, alguien ya realizó ingeniería inversa del procedimiento de cifrado y descifrado.
Los secretos están cifrados en credentials.xml
using AES-128
con hudson.util.Secret
como clave, y luego se codifican en base64
.
El archivo binario de hudson.util.Secret
está cifrado con master.key
. y master.key
se almacena en texto plano.
credentials.xml
almacena tanto credencialesGlobales
como deSistema
.
Para acceder directamente y descifrar ese archivo, NO necesitas privilegios de administrador.
Existen distintas herramientas para descifrar secretos de Jenkins. Las que encontré, como esta, están en Python. Incluiría el código fuente aquí, pero lamentablemente, ese repositorio no tiene una licencia.
El módulo de criptografía de Python no está incluido en la biblioteca estándar de Python, debe instalarse como una dependencia. Dado que no quise lidiar con el tiempo de ejecución de Python y las dependencias externas, escribí mi propio descifrador en Go. Los binarios de Go son auto contenidos y solo requieren el kernel para ejecutarse.
Nota: Jenkins está utilizando el algoritmo AES-128-ECB
, que no está incluido en la biblioteca estándar de Go. Ese algoritmo fue excluido deliberadamente en 2009 para desalentar a las personas de usarlo.
Puedes encontrar el repositorio de GitHub para mi herramienta jenkins-credentials-decryptor
aquí. Para ver el binario en acción, ejecuta el trabajo 131-dumping-credentials
, que utiliza el siguiente Jenkinsfile:
pipeline {
agent any
stages {
stage('Dump credentials') {
steps {
script {
sh '''
curl -L \ "https://github.com/hoto/jenkins-credentials-decryptor/releases/download/0.0.5-alpha/jenkins-credentials-decryptor_0.0.5-alpha_$(uname -s)_$(uname -m)" \ -o jenkins-credentials-decryptor
chmod +x jenkins-credentials-decryptor
./jenkins-credentials-decryptor \ -m $JENKINS_HOME/secrets/master.key \ -s $JENKINS_HOME/secrets/hudson.util.Secret \ -c $JENKINS_HOME/credentials.xml
'''
}
}
}
}
}
Esta herramienta también se puede ejecutar en el host de Jenkins a través de SSH. Solo ocupa ~6 MB y funcionará en cualquier distribución de Linux.
Al descifrar
credentials.xml
conjenkins-credentials-decryptor
, podemos imprimir los valores tanto de las credencialesGlobal como
System
sin necesidad de privilegios de administrador.
Realmente no creo que haya una forma de mitigar completamente las vulnerabilidades de seguridad al usar una CI. Solo podemos dificultar un poco más el proceso de obtener nuestros secretos, configurando capas y creando un objetivo móvil.
Esta es una elección sencilla y mi consejo #1 para cualquier persona que utilice Jenkins. Sé que las VPN pueden ser molestas, pero hoy en día la conexión a Internet es tan rápida y receptiva que ni siquiera deberías notarlo. Además, ocultar tu Jenkins del Internet público te permitirá evitar la mayoría de los ataques básicos.
A menudo, se descuida la actualización de los servidores Jenkins durante meses e incluso años. Las versiones antiguas presentan numerosas vulnerabilidades y brechas de seguridad conocidas. Lo mismo ocurre con los plugins y el sistema operativo; no dudes en mantenerlos actualizados también. Si te preocupa la actualización, configura una copia de seguridad automática del disco de Jenkins cada 24 horas.
Si solo se necesita acceso de lectura, entonces no uses credenciales con acceso de lectura y escritura.
Cuando un pipeline solo necesita acceso a un subconjunto de recursos, entonces crea credenciales solo para esos recursos y nada más.
Los secretos no se filtrarán si nunca los creamos en primer lugar.
Algunos proveedores de cloud permiten el acceso a un recurso asignando un rol a una máquina (por ejemplo, el rol IAM de AWS).
En lugar de almacenar un secreto en Jenkins, guárdalo en un almacén con rotación automática de contraseñas (por ejemplo, Hashicorp Vault, AWS Secrets Manager). Haz que tu pipeline llame al almacén para acceder a un secreto cada vez que lo necesite. Esto facilita la configuración de la rotación automática de contraseñas, ya que el almacén será la única fuente de verdad.
Aunque la rotación de contraseñas no evita que el secreto se filtre, el valor del secreto solo será válido por un tiempo limitado. Es por eso que lo llamamos "crear un objetivo en movimiento".
Considera a todos aquellos que tienen acceso a Jenkins como usuarios administradores y estarás seguro. Una vez otorgas acceso a alguien, incluso si es solo de lectura, en Jenkins, la situación se volverá critica. Todos los desarrolladores en un proyecto deberían conocer todos los secretos, sin excepción.
Si tienes algo que valga la pena robar, alguien intentará robarlo.
Puede que pienses que si alguien robó tu código fuente, ganando así acceso a tus bases de datos, será el fin del mundo, pero eso no es necesariamente cierto. Por ejemplo, si tu base de datos de producción fue extraído pero las contraseñas de los clientes dentro de ella están adecuadamente hasheadas de manera unidireccional, el daño puede reducirse enormemente. Lo único que perdería tu empresa en tal escenario es su credibilidad.
Cuando se trata de desarrollar software, el testing es una pieza fundamental que garantiza la calidad y la confiabilidad de un producto y puede tener..
Los equipos de desarrollo de muchas organizaciones han adoptado las revisiones de código como una de sus prácticas básicas. Aunque parece algo muy..
¿Qué es la productividad de los equipos de desarrollo? ¿Existe un equilibrio entre velocidad y calidad? ¿Qué estrategias pueden emplear las..
Suscríbete a nuestra newsletter para que podamos hacerte llegar recomendaciones de expertos y casos prácticos inspiradores