Skip to content

Commit a07b590

Browse files
nre-abletonemmetog
authored andcommitted
Improve authentication mechanisms (#50)
* Configure files before plugins This is necessary because certain files, such as user config.xml files, may contain API tokens which are needed for authenticated plugin installation. Therefore these files must be in place before attempting to start up Jenkins for the first time. * Install custom plugins first Since the custom plugins need to be installed by Jenkins when it starts up, this minor optimization saves a bit of time during plugin installation. * Add jenkins_auth variable to determine auth type This change is the first step in flexibly supporting multiple authentication types to use when deploying Jenkins. In a future commit, support for API tokens will be added. This change also renames jenkins_token to jenkins_crumb_token, in order to be more explicit about the token type. * Save the web session cookie along with the crumb As of Jenkins 2.176.2, Jenkins' CSRF issuer requires a corresponding web session ID to be submitted along with the crumb. For more info, see: https://jenkins.io/security/advisory/2019-07-17/#SECURITY-626 * Support API token authentication * Add a molecule scenario test for API tokens This is hopefully the second of many additional non-sanity tests that will be added to ensure that different behaviors of this role are working properly. * Add some tasks to do basic sanity checking of vars This could probably be expanded in the future for other states that we need to verify before beginning a deployment. * Add documentation on authentication and security
1 parent d7c96e8 commit a07b590

File tree

18 files changed

+462
-22
lines changed

18 files changed

+462
-22
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ services:
77
install:
88
- pip install -r requirements.txt
99
script:
10-
- molecule test
10+
- molecule test --all

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,72 @@ instance and deploying using docker you probably
167167
want to set the `jenkins_docker_expose_port` var to false so that the
168168
port is not exposed on the host, only to the reverse proxy.
169169

170+
Authentication and Security
171+
---------------------------
172+
173+
This role supports the following authentication mechanisms for Jenkins:
174+
175+
1. API token-based authentication (recommended, requires at least Jenkins 2.96)
176+
2. Crumb-based authentication with the [Strict Crumb Issuer
177+
plugin](https://plugins.jenkins.io/strict-crumb-issuer) (required if _not_
178+
using API tokens and Jenkins 2.176.2 or newer)
179+
3. No security (not recommended)
180+
181+
*API token-based authentication*
182+
183+
API token-based authentication is recommended, but requires a bit of extra
184+
effort to configure. The advantage of API tokens is that they can be easily
185+
revoked in Jenkins, and their usage is also tracked. API tokens also do not
186+
require getting a crumb token, which has become more difficult since Jenkins
187+
version 2.172.2 (see [this security
188+
bulletin](https://jenkins.io/security/advisory/2019-07-17/#SECURITY-626).
189+
190+
To create an API token, you'll need to do the following:
191+
192+
1. All API tokens must belong to a specific user. So either create a special
193+
user for deployments, or log in as the administrator or another account.
194+
2. In the user's configuration page, click the "Add new Token" button.
195+
3. Save the token value, preferably in an Ansible vault.
196+
4. Define the following variables in your playbook:
197+
- `jenkins_auth: "api"`
198+
- `jenkins_api_token: "(defined in the Anible vault)"`
199+
- `jenkins_api_username: "(defined in the Ansible vault)"`
200+
5. Create a backup of the file `$JENKINS_HOME/users/the_username/config.xml`,
201+
where `the_username` corresponds to the user which owns the API token you
202+
just created.
203+
6. Add this file to your control host, and make sure that is deployed to Jenkins
204+
in the `jenkins_custom_files` list, like so:
205+
206+
```
207+
jenkins_custom_files:
208+
- src: "users/the_username/config.xml"
209+
dest: "users/ci/config.xml"
210+
```
211+
212+
Note that you may need to change the `src` value, depending on where you save
213+
the file on the control machine relative to the playbook.
214+
215+
*Crumb-based authentication*
216+
217+
Crumb-based authentication can be used to prevent cross-site request forgery
218+
attacks and is recommended if API tokens are impractical. However, it can also
219+
be a bit tricky to configure this due to security fixes in Jenkins. To configure
220+
CSRF, you'll need to do the following:
221+
222+
1. If you are using Jenkins >= 2.176.2, you'll need to install the
223+
Strict Crumb Issuer plugin. This can be done by this role by adding the
224+
`strict-crumb-issuer` ID to the `jenkins_plugins` list.
225+
2. In Jenkins, click on "Manage Jenkins" -> "Configure Global Security"
226+
3. In the "CSRF Protection" section, enable "Prevent Cross Site Request Forgery
227+
exploits", and then select "Strict Crumb Issuer" if using Jenkins >= 2.176.2,
228+
or otherwise "Default Crumb Issuer". Note that to see this option, you'll
229+
need to have the Strict Crumb Issuer plugin installed. Afterwards, you'll
230+
also need to backup the main Jenkins `config.xml` file to the control host.
231+
232+
Likewise, for the above to work, you'll need at least Ansible 2.9.0pre5 or 2.10
233+
(which are, at the time of this writing, both in development. See [this Ansible
234+
issue](https://github.com/ansible/ansible/issues/61672) for more details).
235+
170236
Jenkins Configs
171237
---------------
172238

defaults/main.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,24 @@ jenkins_plugin_timeout: 300
5454
# List of sources of custom jenkins plugins to install
5555
jenkins_custom_plugins: []
5656

57+
#######################
58+
# Authentication vars #
59+
#######################
60+
61+
# Mechanism to use when authenticating to Jenkins. Must be one of the following values:
62+
# - api: Use an API token which belongs to a specific user
63+
# - crumb: Use anonymous crumb-based authentication
64+
# - none: No security (not recommended)
65+
# For more information, please refer to the "Authentication and Security" section of the
66+
# README.
67+
jenkins_auth: "crumb"
68+
69+
# When defined, use this API token instead of getting a crumb from the system. Requires
70+
# jenkins_api_username.
71+
jenkins_api_token: ""
72+
# Username which owns the above API token.
73+
jenkins_api_username: ""
74+
5775
###################################################
5876
# Docker vars: apply to deploying via docker only #
5977
###################################################

molecule/api-token/Dockerfile.j2

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Molecule managed
2+
3+
{% if item.registry is defined %}
4+
FROM {{ item.registry.url }}/{{ item.image }}
5+
{% else %}
6+
FROM {{ item.image }}
7+
{% endif %}
8+
9+
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 648ACFD622F3D138 && \
10+
apt-get update && \
11+
apt-get install -y apt-transport-https aptitude bash ca-certificates sudo python \
12+
python-apt && \
13+
apt-get clean
14+
15+
RUN useradd -G sudo molecule
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version='1.1' encoding='UTF-8'?>
2+
<hudson>
3+
<disabledAdministrativeMonitors/>
4+
<version>2.190.1</version>
5+
<installStateName>RESTART</installStateName>
6+
<numExecutors>1</numExecutors>
7+
<mode>EXCLUSIVE</mode>
8+
<useSecurity>true</useSecurity>
9+
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
10+
<securityRealm class="hudson.security.HudsonPrivateSecurityRealm">
11+
<disableSignup>false</disableSignup>
12+
<enableCaptcha>false</enableCaptcha>
13+
</securityRealm>
14+
<disableRememberMe>false</disableRememberMe>
15+
<projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
16+
<workspaceDir>${JENKINS_HOME}/workspace/${ITEM_FULLNAME}</workspaceDir>
17+
<buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
18+
<markupFormatter class="hudson.markup.EscapedMarkupFormatter"/>
19+
<jdks/>
20+
<viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
21+
<myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
22+
<clouds/>
23+
<quietPeriod>0</quietPeriod>
24+
<scmCheckoutRetryCount>0</scmCheckoutRetryCount>
25+
<views>
26+
<hudson.model.AllView>
27+
<owner class="hudson" reference="../../.."/>
28+
<name>all</name>
29+
<filterExecutors>false</filterExecutors>
30+
<filterQueue>false</filterQueue>
31+
<properties class="hudson.model.View$PropertyList"/>
32+
</hudson.model.AllView>
33+
</views>
34+
<primaryView>all</primaryView>
35+
<slaveAgentPort>0</slaveAgentPort>
36+
<disabledAgentProtocols>
37+
<string>JNLP-connect</string>
38+
<string>JNLP2-connect</string>
39+
</disabledAgentProtocols>
40+
<label>master</label>
41+
<crumbIssuer class="hudson.security.csrf.DefaultCrumbIssuer">
42+
<excludeClientIPFromCrumb>false</excludeClientIPFromCrumb>
43+
</crumbIssuer>
44+
<nodeProperties/>
45+
<globalNodeProperties/>
46+
</hudson>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version='1.1' encoding='UTF-8'?>
2+
<project>
3+
<keepDependencies>false</keepDependencies>
4+
<properties/>
5+
<scm class="hudson.scm.NullSCM"/>
6+
<canRoam>true</canRoam>
7+
<disabled>false</disabled>
8+
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
9+
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
10+
<triggers/>
11+
<concurrentBuild>false</concurrentBuild>
12+
<builders/>
13+
<publishers/>
14+
<buildWrappers/>
15+
</project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?xml version='1.1' encoding='UTF-8'?>
2+
<user>
3+
<version>10</version>
4+
<id>molecule</id>
5+
<fullName>Molecule Test User</fullName>
6+
<description></description>
7+
<properties>
8+
<jenkins.security.ApiTokenProperty>
9+
<tokenStore>
10+
<tokenList>
11+
<jenkins.security.apitoken.ApiTokenStore_-HashedToken>
12+
<uuid>bdf647eb-10f3-46e3-8a2d-13d10962247f</uuid>
13+
<name>Deployment Token</name>
14+
<creationDate>2019-10-23 15:27:12.108 UTC</creationDate>
15+
<value>
16+
<version>11</version>
17+
<hash>bf8962734f66b3a0e122d6c96cae2e85dfceb6d8b9b4cdc0333de5d73c956313</hash>
18+
</value>
19+
</jenkins.security.apitoken.ApiTokenStore_-HashedToken>
20+
</tokenList>
21+
</tokenStore>
22+
</jenkins.security.ApiTokenProperty>
23+
<io.jenkins.blueocean.autofavorite.user.FavoritingUserProperty plugin="blueocean-autofavorite@1.2.4">
24+
<autofavoriteEnabled>true</autofavoriteEnabled>
25+
</io.jenkins.blueocean.autofavorite.user.FavoritingUserProperty>
26+
<com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty plugin="credentials@2.3.0">
27+
<domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash"/>
28+
</com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty>
29+
<hudson.tasks.Mailer_-UserProperty plugin="mailer@1.29">
30+
<emailAddress>test@example.com</emailAddress>
31+
</hudson.tasks.Mailer_-UserProperty>
32+
<hudson.plugins.favorite.user.FavoriteUserProperty plugin="favorite@2.3.2">
33+
<data class="concurrent-hash-map"/>
34+
</hudson.plugins.favorite.user.FavoriteUserProperty>
35+
<jenkins.security.LastGrantedAuthoritiesProperty>
36+
<roles>
37+
<string>authenticated</string>
38+
</roles>
39+
<timestamp>1571844399911</timestamp>
40+
</jenkins.security.LastGrantedAuthoritiesProperty>
41+
<hudson.model.MyViewsProperty>
42+
<primaryViewName></primaryViewName>
43+
<views>
44+
<hudson.model.AllView>
45+
<owner class="hudson.model.MyViewsProperty" reference="../../.."/>
46+
<name>all</name>
47+
<filterExecutors>false</filterExecutors>
48+
<filterQueue>false</filterQueue>
49+
<properties class="hudson.model.View$PropertyList"/>
50+
</hudson.model.AllView>
51+
</views>
52+
</hudson.model.MyViewsProperty>
53+
<org.jenkinsci.plugins.displayurlapi.user.PreferredProviderUserProperty plugin="display-url-api@2.3.2">
54+
<providerId>default</providerId>
55+
</org.jenkinsci.plugins.displayurlapi.user.PreferredProviderUserProperty>
56+
<hudson.model.PaneStatusProperties>
57+
<collapsed/>
58+
</hudson.model.PaneStatusProperties>
59+
<hudson.security.HudsonPrivateSecurityRealm_-Details>
60+
<passwordHash>#jbcrypt:$2a$10$AwJxJ.RV0mv7nzROLZN1O.XarAZfFdxaYXM.MRbdnr/pgSp5m608i</passwordHash>
61+
</hudson.security.HudsonPrivateSecurityRealm_-Details>
62+
<org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
63+
<authorizedKeys></authorizedKeys>
64+
</org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
65+
<jenkins.security.seed.UserSeedProperty>
66+
<seed>ae4a45c415ddc43f</seed>
67+
</jenkins.security.seed.UserSeedProperty>
68+
<hudson.search.UserSearchProperty>
69+
<insensitiveSearch>true</insensitiveSearch>
70+
</hudson.search.UserSearchProperty>
71+
<jenkins.plugins.slack.user.SlackUserProperty plugin="slack@2.34">
72+
<userId></userId>
73+
<disableNotifications>false</disableNotifications>
74+
</jenkins.plugins.slack.user.SlackUserProperty>
75+
</properties>
76+
</user>

molecule/api-token/molecule.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
dependency:
3+
name: galaxy
4+
driver:
5+
name: docker
6+
lint:
7+
name: yamllint
8+
platforms:
9+
- name: instance
10+
image: ubuntu:16.04
11+
privileged: true
12+
exposed_ports:
13+
- 8080/tcp
14+
published_ports:
15+
- 0.0.0.0:8080:8080/tcp
16+
env:
17+
JENKINS_HOME: /jenkins
18+
provisioner:
19+
name: ansible
20+
log: true
21+
lint:
22+
name: ansible-lint
23+
options:
24+
# E602: Don't compare to empty string
25+
# All workarounds for this are uglier than just comparing to empty strings. See:
26+
# https://github.com/ansible/ansible-lint/issues/457
27+
x: ['602']
28+
scenario:
29+
test_sequence:
30+
- destroy
31+
- create
32+
- converge
33+
# The idempotence check must be disabled for this scenario, because we are copying
34+
# custom files for user accounts. These files (or rather, their parent directories)
35+
# are renamed by Jenkins when it starts up in order to avoid naming conflicts. So
36+
# "users/molecule/config.xml" becomes "users/molecule_7942252632599620805/config.xml",
37+
# with a randomly-generated number.
38+
# - idempotence
39+
- lint
40+
- verify
41+
verifier:
42+
name: testinfra
43+
lint:
44+
name: flake8

molecule/api-token/playbook.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
- name: Converge
3+
hosts: all
4+
vars:
5+
jenkins_auth: "api"
6+
jenkins_api_username: "molecule"
7+
# NOTE: Do not store actual API tokens in plain-text in your playbooks. Always use an
8+
# Ansible vault for such data.
9+
jenkins_api_token: "110cd5010cc081551972181446639ba99f"
10+
jenkins_config_owner: "jenkins"
11+
jenkins_config_group: "jenkins"
12+
jenkins_home: "/jenkins"
13+
jenkins_install_via: "apt"
14+
jenkins_custom_files:
15+
- src: "users/molecule/config.xml"
16+
dest: "users/molecule/config.xml"
17+
jenkins_include_custom_files: true
18+
jenkins_jobs:
19+
- test_job
20+
jenkins_plugins:
21+
- git
22+
jenkins_version: "2.190.1"
23+
roles:
24+
- ansible-jenkins

molecule/api-token/tests/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__/
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import os
2+
3+
import testinfra.utils.ansible_runner
4+
5+
from jenkins import Jenkins
6+
7+
8+
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
9+
os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
10+
11+
12+
def test_jenkins_installed(host):
13+
package = host.package('jenkins')
14+
15+
assert package.is_installed
16+
17+
18+
def test_jenkins_version():
19+
master = Jenkins('http://127.0.0.1:8080')
20+
version = master.get_version()
21+
22+
assert version == '2.190.1'
23+
24+
25+
def test_jenkins_plugins():
26+
master = Jenkins('http://127.0.0.1:8080')
27+
plugins = master.get_plugins()
28+
29+
assert plugins['git']['active']
30+
assert plugins['git']['enabled']
31+
32+
33+
def test_jenkins_jobs():
34+
master = Jenkins('http://127.0.0.1:8080')
35+
test_job = master.get_job_info('test_job')
36+
37+
assert test_job['name'] == 'test_job'
38+
assert test_job['buildable']

molecule/default/molecule.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ provisioner:
2020
log: true
2121
lint:
2222
name: ansible-lint
23+
options:
24+
# E602: Don't compare to empty string
25+
# All workarounds for this are uglier than just comparing to empty strings. See:
26+
# https://github.com/ansible/ansible-lint/issues/457
27+
x: ['602']
2328
verifier:
2429
name: testinfra
2530
lint:

0 commit comments

Comments
 (0)