Jenkins as Code (OLD)

May 24, 2018

OK, there are many articles on the web on how to set up Jenkins as Code images, there is also a lot of post that describes how to seed job into Jenkins using DSL. But in my world ALL of them are half done. My requirements to such a setup are much higher. My requirements are as followed:

@Deprecation warning:

Since I have written this, the great community of Jenkins have been starting the work on JEP-201, which is going to remove the use of init file, the work however is not at all done, so until it is, this article is still valid.

That Jenkins pre-installed with all plugin’s. The system is pre-configured, so that all plugin’s and system setting is set. All pipeline jobs is scripted. All the configurations should be scripted (NO XML, if need YAML or JSON) This post is about, how to achieve all of these three things in on go. The first requirements are quite simple and described many times before, so i will simple just state my Dockerfile.

FROM jenkins/jenkins:2.95-alpine
MAINTAINER Jes Struck "mail@jesstruck.dk"
#Install plugin's
RUN /usr/local/bin/install-plugins.sh \
    blueocean:1.3.4 \
    disk-usage:0.28 \
    greenballs:1.15 \
    jenkinslint:0.14.0 \
    jobConfigHistory:2.18 \
    ldap:1.18 \
    locale:1.2 \
    monitoring:1.70.0 \
    nested-view:1.14 \
    saferestart:0.3 \
    slack:2.3 \
    tasks:4.52
# To avoid jenkins to run setup wizard
ENV JAVA_OPTS=”-Djenkins.install.runSetupWizard=false”
 
#Copy in Jenkins masters private public key
COPY key-pair/* /root/.ssh/
 
# Environment setup
COPY conf.groovy /usr/share/jenkins/ref/init.groovy.d/conf.groovy
 
# Job Seeding
COPY jobs.groovy /usr/share/jenkins/ref/init.groovy.d/jobs.groovy

As an alternative for installing plugins you can also, pass in a file to install-plugins.sh see Docker preinstalling_plugins ex. Personally I like the first approach, because having it in the file allows you, update the file and rerun install-plugins.sh on the running environment, which is considered BAD practice, not matter how much you have to do :(.

Configure Jenkins as Code

This section of course is very much dependent on which plugins you selected in the previous part, but I have chosen some very likely plugins, and scenarios, and described them, for your personal requirement, I suggest google “Jenkins groovy bootstrap [your-plugin/property]” Right next to the Dockerfile, create a new file called conf.groovy. This is where all the System configurations of Jenkins will be placed.

I would have preferred that we could have created a folder called conf and in that created a main.groovy and all of our classes, but I could simply not get the classloader to find does classes so I had to hack all classes in one file :(, if anyone have a suggestion to improve this I gladly update this guide.

Hardening Jenkins

Whenever you fire Jenkins up these days, and browse the /config page you’ll get some security warnings, the following class will remove these warnings.

class JenkinsConfigure{
    def instance = Jenkins.getInstance()
 
    def conf(){
        locale()
        execution()
        securityVulnerabilities()
        createAdminUser()
        instance.save()
    }
 
    def execution(){
        instance.setNumExecutors(0)
        instance.setSlaveAgentPort([55000])
    }
 
    def securityVulnerabilities(){
        //disables cli -remoting - google it
        jenkins.CLI.get().setEnabled(false)
    
        //enforces login, but all logged in users can do anything
        def strategy = new hudson.security.FullControlOnceLoggedInAuthorizationStrategy()
        strategy.setAllowAnonymousRead(false)
        instance.setAuthorizationStrategy(strategy)
        
        //CSRF
        instance.setCrumbIssuer(new DefaultCrumbIssuer(true))
        
        //Agent to master security subsystem
        instance.getInjector().getInstance(AdminWhitelistRule.class).setMasterKillSwitch(false)
        
        //Disable JNLP, unless you are using that with your windows slaves
        jenkins.setSlaveAgentPort(-1)
        
        /** Agent protocols ("Java Web Start Agent Protocol/1", Java Web Start Agent Protocol/2 )
        * Disable old Non-Encrypted protocols
        * This still gives a warning in log during start up, because the startup test is done before this groovy hook is
        executed
        */
        instance.setAgentProtocols( (Set<String>) ['JNLP4-connect', 'Ping'])
    }
 
    def createAdminUser(){
        def hudsonRealm = new HudsonPrivateSecurityRealm(false)
        //find your own secure admin username / password combo (you will not get mine :))
        hudsonRealm.createAccount("admin","admin")
        instance.setSecurityRealm(hudsonRealm
    }
    
    def locale(){
        def plugin = instance.getPluginManager().getPlugin('locale').getPlugin()
        plugin.setSystemLocale('en')
        plugin.ignoreAcceptLanguage = true
    }
}

Creating credentials

The next big thing, in setting your new Jenkins server up, is handling all your credentials in Jenkins. The following class shows how to handle three types of credentials, username/password, username/ssh-key and security-tokens.

class Credentials{
    def conf(){
        def credentials_store = Jenkins.instance.getExtensionList('com.cloudbees.plugins.credentials.SystemCredentialsProvider')[0].getStore()
        def global_domain = Domain.global()
    
        //Ex, on how to add username and password
        usernameAndPassword = new UsernamePasswordCredentialsImpl(
        CredentialsScope.GLOBAL, "jenkins-slave-password", "Jenkis Slave with Password Configuration","root", "jenkins" )
        credentials_store.addCredentials(global_domain, usernameAndPassword)
        
        // Create a global credential to work with git, this is needed to access the git related jobs.
        // Which means all the jobs, since all our jobs are somewhat working with git 🙂
        // This assumes there is a ssh private key in /root/.ssh/ which i actually added in dockerfile, no need to worry
        def key_credentials = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, "Jenkins2Gitlab", "jenkins", new BasicSSHUserPrivateKey.UsersPrivateKeySource(), "", "Used for Jenkins to access Gitlab server")
        credentials_store.addCredentials(global_domain, key_credentials)
        //Ex, on how to handle security-tokens
        def secretText = new StringCredentialsImpl(CredentialsScope.GLOBAL,
                                                    "SlackIntegration", //ID
                                                    "", //description
                                                    Secret.fromString("your-secret-slack-token"))
        credentials_store.addCredentials(global_domain, secretText)
    }
}

Configuring LDAP

In the conf.groovy create a new class called LDAP.

class Ldap{
    def conf(){
        println ''
        //Setting up ldap
        String server = 'ldap://1.2.3.4'
        String rootDN = 'dc=foo,dc=com'
        String userSearchBase = 'cn=users,cn=accounts'
        String userSearch = ''
        String groupSearchBase = ''
        String managerDN = 'uid=serviceaccount,cn=users,cn=accounts,dc=foo,dc=com'</code>
        
        //Todo must encrypted
        String managerPassword = 'password'
        boolean inhibitInferRootDN = false
        SecurityRealm ldap_realm = new LDAPSecurityRealm(server, rootDN, userSearchBase, userSearch, groupSearchBase,
        managerDN, managerPassword, inhibitInferRootDN)
        Jenkins.instance.setSecurityRealm(ldap_realm)
        Jenkins.instance.save()
    }
}

Configuring Slack

Create class slack in conf.groovy, as

class Slack {
    def conf() {
        // Setting up Slack
        JSONObject formData = ['slack': ['tokenCredentialId': 'SlackIntegration']] as JSONObject
        def slack = Jenkins.instance.getExtensionList(jenkins.plugins.slack.SlackNotifier.DescriptorImpl.class)[0]
        //valid tokens for testing in the jenkins-slack-plugin-test instance of slack.com
        def params = [
        slackBaseUrl: new jenkins.model.JenkinsLocationConfiguration().getUrl(),
        slackTeamDomain: 'team-name',
        slackToken: '',
        slackBotUser: 'true',
        slackRoom: 'jenkins',
        slackSendAs: 'Jenkins',
        slackNotifySuccess: 'false']
        def req = [ getParameter: { name -&gt; params[name] }] as org.kohsuke.stapler.StaplerRequest
        slack.configure(req, formData)
        slack.save()
        println 'Slack configured!'
    }
}

Notice in

JSONObject formData = ['slack': ['tokenCredentialId': 'SlackIntegration']] as JSONObject

that we are using the SlackIntegration Credential we created previously.

So just to summarize what we have just done,

We have,

Hardened the Jenkins installation following the guiding principles from the community. Created those credentials that Jenkins will need to know for integrating ex. with Git, Slack etc. Set up a Jenkins installation to allow login for people the correct privileges in the domain. Setup integration between Jenkins and Slack so that the team/organisation can be notified. Configured Jenkins as Code. If you want to script your job definitions, i can highly recommend using jenkins-job-builder

if you want to get more info on for Jenkins ninjas please see all my post on Jenkins

see the code at jenkins-docker@github