Removing Blue Ocean credentials security vulnerability

Issue

Credentials created with the Blue Ocean Pipeline Wizard are stored in the user’s store. The Blue Ocean Credential Provider can load these credentials from the user’s store for anyone, resulting in a security issue.

To avoid this issue, the Blue Ocean Credentials Provider will be disabled on the Blue Ocean plugin version 1.25.4 (and 1.25.0.1) that will be included on CloudBees CI 2.332.3.3. This can cause the jobs that use these credentials to fail.

Environment

Resolution

To avoid this impact on your instance, you must check if you have been affected before you upgrade.

You are affected by this vulnerability if your instance has the BlueOcean Credentials provider installed and enabled and have any credentials stored in a user credential store. Additionally, you are affected by the security fix if you created jobs using the wizard. To check this, you can run the following script from the Manage Jenkins -> Script Console on your instance. This script confirms if any of your jobs are impacted and provides the list of affected jobs:

import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
import jenkins.scm.api.SCMSource;
import java.lang.reflect.Method;
import io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanCredentialsProvider;
import java.lang.reflect.InvocationTargetException;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import org.apache.commons.lang.StringUtils;
import hudson.security.ACL;
import hudson.security.ACLContext;
import org.acegisecurity.userdetails.UsernameNotFoundException;


println("Checking if your instance contains the Blue Ocean plugin...")

def isPresent = Jenkins.instance.pluginManager.plugins.stream().anyMatch {it.displayName == "Blue Ocean"}

if (isPresent){
    println("Blue Ocean plugin found. Searching for pipelines...")
    def output = findPipelines()
    println(output)
} else {
    println("Blue Ocean plugin not found.")
}

def findPipelines() {
    def total = 0
    def output = ""

    def projects = Jenkins.get().getAllItems( WorkflowMultiBranchProject.class );
    for (WorkflowMultiBranchProject project : projects) {
      for (SCMSource scmSource : project.getSCMSources()) {
        try {
          Method getCredentialsIdMethod =
            scmSource.getClass().getDeclaredMethod( "getCredentialsId", null );
          String credentialsId = (String) getCredentialsIdMethod.invoke( scmSource, null );
          BlueOceanCredentialsProvider.FolderPropertyImpl property =
            project.getProperties().get( BlueOceanCredentialsProvider.FolderPropertyImpl.class );
          if (property != null) {
            // property id is same as credentials and this is a credentials stored at USER scope
            if(StringUtils.equals(property.getId(), credentialsId)
               && findCredentials(property.getUser(), credentialsId) != null) {
              output += "- "+( project.getName() + "," + project.getUrl()) + "\n";
              total++
            }
          }
        } catch (NoSuchMethodException e) {
          println( "SCMSource: " + scmSource.getClass().getName() + " do not have method getCredentialsId");
        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
          println( "SCMSource: '" + scmSource.getClass().getName() + "' cannot retrieve credentialId via getCredentialsId", e);
        }
      }
    }
    if (output.length() > 0){
        output = "Pipelines found.\nPipeline list:\n" + output + "Total found: " + total
    } else {
    	output = "No affected pipelines found."
    }

    return output
}
  
def findCredentials(userId, credentialsId) {
  try {
    User proxyUser = User.get(userId, false, Collections.emptyMap());
    ACL.as( proxyUser.impersonate()).with { res ->
      try {
        StandardCredentials standardCredentials = CredentialsMatchers.firstOrNull(
          CredentialsProvider.lookupCredentials(StandardCredentials.class, Jenkins.get(),
                                              Jenkins.getAuthentication(),
                                              (List<DomainRequirement>) null),
          CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId),
                                  CredentialsMatchers.withScope(CredentialsScope.USER)));
      
        return standardCredentials;
      } finally {
        res.close()
      }  
    }
  } catch (UsernameNotFoundException e) {
	return null;
  }   
}

**Note that if you do not have any of the below plugins installed in your instance, the script output will fail indicating that some of the classes are missing, which is expected:

  • blueocean
  • blueocean-pipeline-scm-api
  • blueocean-pipeline-api-impl
  • blueocean-pipeline-editor
  • blueocean-github-pipeline
  • blueocean-git-pipeline
  • blueocean-events
  • blueocean-bitbucket-pipeline

The script output provides the list of affected pipelines in your instance. After you receive the output, you can confirm if any of your jobs were affected. You will receive one of the following messages:

  • Blue Ocean plugin not found: No actions are required from your side.
  • Blue Ocean plugin is installed but no affected pipelines found: You can safely upgrade CBCI to fix the issue without impacting any pipelines.
  • Blue Ocean plugin found affected pipelines: You must migrate the credentials used by these jobs to a nonuser scoped store. This might impact your pipelines behavior and it may cause them to fail as these credentials will be missing, and it will also break the pipeline editor.

NOTE: You can create a Cluster Operation if you want to automate the script execution in multiple controllers. Refer to Cluster Operation article for more details about this approach.

Credentials migration

You need to recreate the credentials outside of the user scope, so you can create new ones under a store of your choice; for example, at a general or folder level. It is recommended that you use a service account to ensure that no personal access tokens are used.

After you have finished, you must configure the affected jobs to use the new credentials.

NOTE: After the old credentials are deleted, you cannot use the pipeline editor for this job. The pipeline editor is used to commit changes to the SCM using these credentials.

Credential migration cannot be automated as you have to choose where the credentials will be moved in each case.

Workaround - Re-enabling the Blue Ocean Credentials Provider##

If you need to avoid migrating these credentials or have any blockers, you can re-enable the Blue Ocean Credential Provider after upgrading your instance to version .**.., so the jobs that are created with the Blue Ocean Wizard can use these credentials.

NOTE: Your instance is still affected by the security vulnerability, so this is a temporary solution if you cannot migrate the credentials. The ability to enable the Blue Ocean Credential Provider will be completely removed in an upcoming release.

To continue with this process, set up the following property as a startup argument in your instance. This requires a restart for the change to occur.

`-Dio.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanCredentialsProvider.enabled=true`

For more information on how to add startup arguments, refer to How to add Java arguments to Jenkins.

Alternatively, this property can be set up through the Manage Jenkins -> Script Console from your instance. You can use a Cluster Operation to enable or disable this property in multiple controllers at one time. Use the following scripts:

Enabling the property:

System.setProperty(io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanCredentialsProvider.class.getName() + ".enabled", "true")

Disabling the property:

System.clearProperty(io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanCredentialsProvider.class.getName() + ".enabled")

This process does not require a restart, but the change does not persist if you restart the instance. Choose the most appropriate option for your use case.

Proceed with the regular instance upgrade

Once all the necessary actions have been taken, you can proceed with the instance upgrade so you get the new Blue Ocean plugin version.

References

Have more questions?

1 Comments

  • 1
    Avatar
    Antonio Petricca

    Hi, we use Jenkins for complex company development processes.

    We keep it updated regularly. Unfortunately this upgrade broke our systems, so we had to restore it by the most recent backup.

    Our pipelines use SSH generated keys, by the builtin credential folder "BlueOcean Folder Credentials".

    After upgrading Jenkins that folder disappeared, and we could not select anything else.

    In order to let us, and I suppose many other companies, to upgrade Jenkins, we kindly ask you to consider one of the following proposals:

    1. Don't remove the plugin in the future and let it enabled by default. Unfortunately we cannot recreate the Jenkins container in order to pass the option to enable again the provider. This is the reason why we keep it upgraded by the upgrade Jenkins menù option.
    2. Provide a way to migrate automagically the pipelines to the new security model without any production breaking.

    In the meanwhile we don't go further with next upgrades! :(

    Best regards,
    Antonio Petricca

Please sign in to leave a comment.