Cómo crear y descifrar credenciales de Jenkins

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.

¿Por qué extraer credenciales de Jenkins?

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.

Almacenamiento de credenciales en Jenkins

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:

  1. Jenkins encripta secretos en estado de reposo pero guarda la clave de descifrado en algún lugar de su host.
  2. Cualquier persona que pueda crear jobs en Jenkins puede ver todos los secretos en texto plano.

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.


Cómo crear credenciales en Jenkins

Si deseas seguir las instrucciones que aquí te proporciono, y ejecutar los ejemplos tú mismo, puedes poner en marcha una instancia preconfigurada 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 preconfiguradas creadas por mí.

jenkins

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.

jenkins

Si quieres, puedes agregar más secretos, pero para est 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.

jenkins

jenkins

Ahora que hemos cubierto la creación de credenciales, pasemo a acceder a ellas desde un Jenkinsfile.

Cómo acceder a credenciales desde un Jenkinsfile

Ejecuta el trabajo 130-accessing-credentials.

jenkins

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.

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]

jenkins

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).

Listado de identificadores de secretos en Jenkins

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>

Cómo acceder al System y otros valores de credenciales de la UI

Jenkins 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 de sistema pero también se puede acceder a ellas desde trabajos de Jenkins.

jenkins

Obtén credenciales mediante una herramienta de inspección del navegador

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:

  1. Ve a http://localhost:8080/credentials/
  2. Actualiza cualquiera de las credenciales.
  3. Abre la consola de desarrollo (F12 en Chrome).
  4. Inspecciona el elemento con puntos.
  5. Copia el texto de value.

jenkins

jenkins

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}")

jenkins

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:

  1. http://localhost:8080/configure - algunos plugins crean campos de tipo contraseña en esta vista

  2. http://localhost:8080/configureSecurity/ - busca cosas como las credenciales AD

Itera y descifra credenciales desde la consola

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.


Cómo Jenkins almacena credenciales

Para acceder y descifrar las credenciales de Jenkins, necesitas tres archivos.

  • credentials.xml - almacena credenciales cifradas
  • hudson.util.Secret - decrifra entradas credentials.xml, este archivo de por sí está cifrado
  • master.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 credenciales Globales como de Sistema.
Para acceder directamente y descifrar ese archivo, NO necesitas privilegios de administrador.

Descifrando credenciales

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 
           '''
        }
      }
    }

  }
}

jenkins

jenkins

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 con jenkins-credentials-decryptor, podemos imprimir los valores tanto de las credenciales Global como  System sin necesidad de privilegios de administrador.


Prevención y mejores prácticas

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.

1. Oculta Jenkins detrás de una VPN

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.

2. Actualiza Jenkins regularmente

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.

3. Sigue el principio de privilegio mínimo

Si solo se necesita acceso de lectura, entonces no uses credenciales con acceso de lectura y escritura.

4. Limita el alcance del acceso

Cuando un pipeline solo necesita acceso a un subconjunto de recursos, entonces crea credenciales solo para esos recursos y nada más.

5. Evita el uso de secretos cuando sea posible

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).

6. Crea un objetivo en movimiento

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".

7. Trata todas las credenciales almacenadas en Jenkins como un texto plano

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.

8. Anticipa un hackeo 


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.