I am currently using Chef to deploy a Jenkins instance on a managed node. I am using the following public supermarket cookbook: https://supermarket.chef.io/cookbooks/jenkins .
I am using the following code in my recipe file to enable authentication:
jenkins_script 'activate global security' do
command <<-EOH.gsub(/^ {4}/, '')
import jenkins.model.*
import hudson.security.*
def instance = Jenkins.getInstance()
def hudsonRealm = new HudsonPrivateSecurityRealm(false)
hudsonRealm.createAccount("Administrator","Password")
instance.setSecurityRealm(hudsonRealm)
instance.save()
def strategy = new GlobalMatrixAuthorizationStrategy()
strategy.add(Jenkins.ADMINISTER, "Administrator")
instance.setAuthorizationStrategy(strategy)
instance.save()
EOH
end
This works great to setup security on the instance the first time the recipe is run on the managed node. It creates an administrator user with administrator permissions on the Jenkins server. In addition to enabling security on the Jenkins instance, plugins are also installed using this recipe.
Once security has been enabled, installation of plugins which do not yet exist (but are specified to be installed), fail:
ERROR: anonymous is missing the Overall/Read permission
I assume this is an error related to the newly created administrator account, and Chef attempting to install the plugins using the anonymous user as opposed to the administrator user. Is there anything that should be set in my recipe file in order to work around this permissions issue?
The goal here is that in the event a plugin is upgraded to an undesired version or uninstalled completely, running the recipe will reinstall / rollback any plugin changes. Currently this does not appear to be possible if I also have security enabled on the Jenkins instance.
EDIT It should also be noted that currently each time I need to repair plugins in this way, I have to disable security then run the entire recipe (plugin installation + security enable).
Thanks for any help!
The jenkins_plugin
resource doesn't appear to expose any authentication options so you'll probably need to build your own resource. If you dive in to the code you'll see that the underlying executor layer in the cookbook does support auth (and a whole bunch of other stuff) so it might be easy to do in a copy-fork (and send us a patch) of just that resource.
We ran into this because we had previously been defining :jenkins_username
and :jenkins_password
, but those only work with the remoting
protocol which is being deprecated in favor of the REST API being accessed via SSH or HTTPS and in newer releases defaults to DISABLED.
We ended up combining the logic from @StephenKing's cookbook and the information from chef-cookbooks/jenkins and this GitHub issue comment on that repo to get our plugin installation working after enabling authentication via Active Directory on our instances (we used SSH).
We basically pulled the example from https://github.com/TYPO3-cookbooks/jenkins-chefci/blob/e1b82e679074e96de5d6e668b0f10549c48b58d1/recipes/_jenkins_chef_user.rb and removed the portion that automatically generated the key if it didn't exist (our instances stick around and need to be mostly deterministic) and replaced the File.read with a lookup in our encrypted databag (or functional equivalent).
recipes/authentication.rb
require 'aws-sdk'
require 'net/ssh'
require 'openssl'
ssm = Aws::SSM::Client.new(region: 'us-west-2')
unless node.run_state[:jenkins_private_key]
key_contents = ssm.get_parameter(name: node['jenkins_wrapper']['secrets']['chefjenkins']['id_rsa']['path'], with_decryption: true).parameter.value
key_path = node['jenkins_wrapper']['secrets']['chefjenkins']['id_rsa']['path']
key = OpenSSL::PKey::RSA.new key_contents
# We use `log` here so we can assert the correct path was queried without exposing or hardcoding the secret in our tests
log 'Successfully read existing private key from ' + key_path
public_key = [key.ssh_type, [key.to_blob].pack('m0'), 'auto-generated key'].join(' ')
# Create the Chef Jenkins user with the public key
jenkins_user 'chefjenkins' do
id 'chefjenkins' # This also matches up with an Active Directory user
full_name 'Chef Client'
public_keys [public_key]
end
# Set the private key on the Jenkins executor
node.run_state[:jenkins_private_key] = key.to_pem
end
# This was our previous implementation that stopped working recently
# jenkins_password = ssm.get_parameter(name: node['jenkins_wrapper']['secrets']['chefjenkins']['path'], with_decryption: true).parameter.value
# node.run_state[:jenkins_username] = 'chefjenkins' # ~FC001
# node.run_state[:jenkins_password] = jenkins_password # ~FC001
recipes/enable_jenkins_sshd.rb
port = node['jenkins']['ssh']['port']
jenkins_script 'configure_sshd_access' do
command <<-EOH.gsub(/^ {4}/, '')
import jenkins.model.*
def instance = Jenkins.getInstance()
def sshd = instance.getDescriptor("org.jenkinsci.main.modules.sshd.SSHD")
def currentPort = sshd.getActualPort()
def expectedPort = #{port}
if (currentPort != expectedPort) {
sshd.setPort(expectedPort)
}
EOH
not_if "grep #{port} /var/lib/jenkins/org.jenkinsci.main.modules.sshd.SSHD.xml"
notifies :execute, 'jenkins_command[safe-restart]', :immediately
end
attributes/default.rb
# Enable/disable SSHd.
# If the port is 0, Jenkins will serve SSHd on a random port
# If the port is > 0, Jenkins will serve SSHd on that port specifically
# If the port is is -1 turns off SSHd.
default['jenkins']['ssh']['port'] = 8222
# This happens to be our lookup path in AWS SSM, but
# this could be a local file on Jenkins or in databag or wherever
default['jenkins_wrapper']['secrets']['chefjenkins']['id_rsa']['path'] = 'jenkins_wrapper.users.chefjenkins.id_rsa'